diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..5ec5201
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,42 @@
+#/**
+# * Contains shared programming interfaces.
+# * All iptables "communication" is handled by this class.
+# *
+# * Copyright (C) 2007-2008 The Android Open Source Project
+# * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro
+# * Copyright (C) 2011-2012 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 .
+# *
+# * @author Rodrigo Zechin Rosauro, Umakanthan Chandran
+# * @version 1.1
+# */
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := afwall
+LOCAL_CERTIFICATE := platform
+
+# Builds against the public SDK
+# LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
+
+# This finds and builds the test apk as well, so a single make does both.
+include $(call all-makefiles-under,$(LOCAL_PATH))
\ No newline at end of file
diff --git a/Changelog.md b/Changelog.md
new file mode 100644
index 0000000..3e0931c
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,922 @@
+AFWall+ Changelog
+==================
+
+AFWall+ v4.0.0
+
+🚀 Major Features & Enhancements
+
+🎯 Rule Management & Stability
+
+- Fixed critical rule application issues - Resolved iptables command failures and cascade errors
+- Improved error handling - Smart selective error handling prevents unnecessary fallbacks
+- Enhanced chain management - Better synchronization prevents race conditions
+- Owner module compatibility - Automatic detection and fallback for devices without owner
+ iptables module
+
+🎨 Material Design Overhaul
+
+- Modernized UI - Material Design enhancements for rules, help, and custom scripts views
+- Revamped help section - Complete redesign with better organization and moved legends
+- Visual widget indicators - Added pulse animations and visual feedback for toggle widgets
+- Improved layouts - Fixed layout issues for devices with merged status bars
+
+📊 Enhanced Logging System
+
+- Better log details view - Enhanced display with allow/deny address information
+- Improved UID detection - Better handling of special UIDs (including uid -100) and bug fixes
+- Seamless log target switching - Dynamic switching between LOG and NFLOG in preferences
+- NFLOG improvements - Better NFLOG and LOG handling with updated binaries
+
+🔒 Security Enhancements
+
+- Upgraded encryption - Migrated from DES to AES for better security
+- Enhanced security utilities - New SecureCrypto and SecurityUtil classes
+- Input validation improvements - Better validation and security checks
+
+🛠 Platform & Compatibility
+
+📱 Android Support
+
+- Android 16 preparation - Updated build configuration for future Android support
+- Binary updates - Cross-compiled binaries: busybox v1.36.1, iptables v1.8.10
+- Architecture support - Added ARM64 binaries and improved architecture detection
+- GitHub Actions CI - Automated binary builds and improved CI/CD pipeline
+
+🔧 Bug Fixes & Stability
+
+- Export/Import fixes - Resolved bugs when handling large numbers of files (#1399, #1401)
+- Build error fixes - Fixed app:tint and other build-related issues
+- Service leak fixes - Improved thread safety and fixed service connection leaks
+- USB tethering support - Added auto-detection and support for USB tethering
+- DNS forwarding - Improved DNS handling for tethering scenarios
+
+🔧 Technical Improvements
+
+⚡ Performance & Threading
+
+- Thread safety - Improved synchronization and thread-safe operations
+- Better exception handling - More robust error handling and recovery
+- Optimized rule processing - Faster and more reliable rule application
+
+🛠 Developer Experience
+
+- Code cleanup - Extensive refactoring and code organization improvements
+- CI/CD enhancements - Updated GitHub Actions and automated workflows
+- Binary management - Automated cross-compilation and binary distribution
+
+📋 Specific Issue Fixes
+
+- #1386 - Default chain rules only applied when necessary (with smart revert)
+- #1410 - Fallback on default commands when needed
+- #1423, #1382 - Various stability and functionality fixes
+- #1400 - Layout fixes for merged status bar screens
+- #1399 - Export only enabled rule types
+- #1169 - Export/import rule improvements
+
+⚠️ Breaking Changes
+
+- Security upgrade - DES encryption deprecated in favor of AES
+- Removed legacy views - Old unsupported view components removed
+- Binary updates - Requires newer binaries for optimal performance
+
+🙏 Contributors
+
+- @getgo-nobugs - Syntax fixes and improvements
+- @NeroProtagonist - CI/CD updates (upload-artifact@v4)
+- @Fry-kun - Multiple fixes: typos, layout improvements, export fixes, ARM64 NFLOG binary (initial version)
+
+Version 3.6.0
+
+* Updated libraries and SDK (33)
+* Fixes:
+ - Chinese language not working
+ - Added Sinhala language
+ - libsu memory leak
+ - Optimizing copying binaries during install
+ - Log freezing on few devices
+ - Work profile fix for android 12
+* Add support for recent versions of Android.
+* Code optimizations.
+
+
+Version 3.5.3
+
+* New: Enable delay when applying rules (Required for Android 11+ on some devices)
+* Fixes
+ Applying rules with ipv6 error
+ #1101 DNS leak when using external dns client.
+ #1280 work profile apps not shown on Android 11+
+ Bluetooth tether
+ import/export hanging
+* Use libsu for root detection.
+* Chinese menu issue.
+* Dropped xposed support! will be provided as separate module
+* Updated libraries and SDK (31)
+
+Version 3.5.2
+
+* Fix: Fail to Import from older versions.
+* IPv6 issues on few devices when enabled.
+* Chinese lang issue
+* Crash fixes.
+* Removed run_pie binaries which are no longer used.
+
+
+Version 3.5.1
+* Feature: Cloning of profiles
+* Bug: PrivateDNS changes on boot
+* Bug: Log target missing on few scenarios
+* Bug: Import/Export rules missing on A11
+
+
+Version 3.5.0
+Features
+ - Show installed apps without internet permission
+ - Default setting for installed app - Donate Only
+ - Private DNS support
+ - Better support for Multi Profile/Island/Work Profile - Thanks to @n90p
+ - Firewall logs engine rewritten with notification support
+ - Support for android 10 and 11
+Bug Fixes:
+ - Lots of logs related issues
+ - Export rules with mode
+ - Language option issue
+ - device rotation issue
+* Update support tools and build libraries(AndroidX)
+
+Version 3.4.0
+* Feature: Bluetooth,USB tethering as separate rules - Thanks @nxzero
+* Feature: Clone columns (Copy rules from one column to other column)
+* Feature: Selectable Log target (LOG/NFLOG)
+* Fix: Log related issues
+* Fix: Notification related issues/Option removed!!
+* Main screen UI update - Thanks @vvimjam
+* Crash fixes and performance improvements
+* Translation updates.
+
+
+Version 3.4.0-BETA2
+* Main UI update only when more than 4 controls
+* Notification related fixes
+* UI related fixes.
+* Crash fixes
+
+Version 3.4.0-BETA1
+* Feature: Bluetooth,USB tethering as separate rules - Thanks @nxzero
+* Feature: Clone columns (Copy rules from one column to other column)
+* Feature: Selectable Log target (LOG/NFLOG)
+* Main screen UI update - Thanks @vvimjam
+* Crash fixes and performance improvements
+
+Version 3.3.1
+
+* Bug: Firewall not blocking on oneplus devices
+* Minor UI fixes
+
+Version 3.3.0
+
+* Bug Fixes:
+ - MAJOR: Rules not applied properly (v4/v6)
+ - Theme related bug with logs
+ - Log related bugs on many devices
+ - Boot rules are not applied on Pie and above
+ - Disable/Enable of firewall issue
+ - Crash on applying rules on fewer devices
+ - Notification bug on fewer devices
+
+Version 3.2.0
+* Integrate basic themes (Dark/Light or Black (donate version only!)
+* Preferences now showing selected values
+* Updated libraries
+* Bug Fixes:
+ - AFWall's Logservice stops after sometime
+ - Crash on LOS 16 due to permission
+ - Notification not getting cleared upon opening
+ - Tasker settings not applied bug due to crash
+ - Root progress showing on main screen (dismiss button added incase)
+ - Log service process bug
+ - Applying dialog issue
+ - kernel in the logs even whitelisted
+ - Statusbar notification update on firewall status
+ - Possible memory leaks in async
+ - mDNS and CLAT on core apps (Android 10)
+ - Additional startup leak path for supersu
+
+
+Version 3.1.0
+* Performance: IPv4 & IPv6 rules applying time reduced by half
+* IPv6 is enabled by default - Disable if not (under preferences -> rules)
+* Fix: Tasker plugin issue after profile migration
+* Fix: Widget crashing issue
+* Fix: New installed app notification issue
+* Fix: Device boot rules issue
+* Fix: Duplicate app issue on oneplus devices
+* Fix: Tor related bug
+* Fix: Xposed module unable to download allowed apps
+
+Version 3.0.4
+* Fix: Domain names are now been correctly resolved
+* Fix: Removed notification dot on all notifications
+* Fix: Inbound option caused AFWall+ to disable its functionality
+* Fix: Hang issue on log detail when ping/resolve
+* Removed: SUPER_USER permission which is not relevant anymore
+
+Version 3.0.3
+
+* Fix: Disable firewall issue
+* Fix: Traffic stats always zero in app details
+* Rewritten: Filter logic for main screen apps
+* Xposed: Plugin wasn't able to read preference
+
+
+Version 3.0.2
+* Fix: Issue with Pixel C devices
+* Support for Magisk 18.0 and startup leak
+* Fix: Notification sound issue on some devices
+* Fix: Duplicate name appears on main screen
+
+
+Version 3.0.1
+
+* Fix: Status toggle widget 1x1
+* Fix: Ability to hide ongoing notification (Stop firewall and restart to hide after disable it in preferences)
+* Fix: Firewall error notification on oreo and above
+* Security: Tile toggle checks for password
+* User reported crashes
+* Updated translations
+
+Version 3.0.0
+
+Features:
+* Better support for nougat/oreo and pie
+* Firewall toggle tile
+* Adaptive Icons
+* Notification channels
+* Tor support
+Bugs:
+* Language selection bug
+* Filter selection bug
+* Compatible with magisk 17.x
+* Better handling of background process
+* Drops support for 4.x devices
+* Update languages
+* Updated libraries
+
+Version 2.9.9
+
+* Support for dual apps (experimental)
+* DNS Hostname option on log toast(donate feature)
+* Multiple memory leaks across screens
+* Block log notification now moved under individual app detail screen
+* Enabled log cache for faster load
+* Widgets now ask for password if enabled (except status widget)
+
+Bugs:
+* Shortcut open rules & preference screen without password
+* fingerprint related issues
+* selinux deny for startup script
+* User reported crash fixes
+* Updated string translations
+* Added default system language option
+* Log notification name issue
+
+Version 2.9.8
+
+* Option to disable notification when applying rules - Recommended to turn on !
+* Added magisk related information in the error report
+* Fixed storage permission on export from rules
+* Pixel 2 bug on netfilter error on start
+* Rare preference crash on some devices
+* Upgraded runtime to Java 8
+* Updated support libraries
+* Fixed crashes upon loading
+* Fixed user reported bugs
+* Removed buggy quick apply -- Sincere Apologies! will add it after testing with various usecases
+
+Version 2.9.7
+
+Features:
+* Control default chains for IPv4 & IPv6 (preference)
+* Quick apply from main UI - using floating apply button - Donate Version Only
+* Showing rules count on apply
+* Search using UID
+* Improved detection for su binaries
+* Ability to choose init.d path
+* Removed storage permission from start (used only when export/import)
+* Xposed plugin updated to Nougat
+
+Bug Fixes:
+* Added mount applet for busybox to fix mount issue for init.d
+* Widgets & tasker toggle issue for profiles
+* Possible fix for starup rule
+* Library updates
+* Out of memory crashes when enabled logging
+* Runtime crash fixes due to incompatible libraries
+* Lots of minor bug fixes and underhood changes to introduce new features
+
+
+Version 2.9.6.1
+
+* Removed BIND_ADMIN permission and related device admin feature as per Google - Will be introduced after sometime
+* ANR issues due to busybox detection
+* Minor UI changes in tasker plugin
+* Tasker plugin related bugs related to disable/enable firewall
+* User reported crash fixes
+
+
+Version 2.9.6
+* LAN/Data leak issues on 7.x
+* Application list refresh issue
+* Minor fix in startupscript - Thanks Peake
+* Updated support libraries
+* Reported crash fixes
+* Updated translations
+
+Version 2.9.5
+* Mobile data support for newer devices
+* Custom interval for startup delay
+* Custom interval for ping timeout
+* Low priority notification
+* Fixed issue with import preference crashes Log/Experimental preference
+* User reported crashes
+
+Version 2.9.4
+* Fix log notification stops after sometime
+* Fix crash on log preferences.
+* mDNS notification can be turned-off
+* Bug in notification filterering
+* Added back x86/mips support for built-in binaries
+* Minor UI change for UID
+* Updated support Libraries
+* Updated translations
+
+Version 2.9.3 [24 Feb, 2017]
+* Fix crash on log preferences.
+* Traffic stats always shown empty.
+* Zoom icons.
+
+Version 2.9.2 [22 Feb, 2017]
+Features
+* Log- Network options-Donate(TX @vzool)
+* Fingerprint support (TX @vzool)
+* Preserve Zoom size(+/-)
+* Profiles-delete/rename profiles(+)
+* Hardware search key
+
+Bugs
+* LAN issue on Nougat
+* Boot rules issue
+* Logservice start issue
+* Notification glitch on profile switch
+* Log notification filter button not showing
+* Optimizing logTarget detection
+* Removed toybox check due to ANR
+ + etc.
+Misc
+* Updated: Busybox to latest version
+* Removed: x86 & mips support
+* Updated libraries
+* Translation: Small updates
+
+Version 2.9.1 [Nov 27, 2016]
+* Boot/Connectivity change rules hung on some devices
+* Widget always display errors
+* Optimized number of su calls
+* Log Service does not work on some devices (Reboot or enable/disable logservice after install)
+* Update material dialog library
+* Updated translations
+
+Version 2.9.0 [Nov 25, 2016]
+* Bug: Sometimes connection gets blocked
+* Bug: Sometimes logservice does not work after service terminated
+* Bug: Hang issue with phh super user
+* Bug: Applying rules dialog issue
+* Bug: Old logview clear issue
+* Ground work on notification filter
+* Code cleanup and removed older API related reference
+* Accessibility improvements
+* Feature: Block IPv6 - block all default chains of IPv6 to prevent leak
+* Feature: Preference enhancements related to rules
+* Feature: Application Shortcuts - Nougat 7.1
+* User reported crashes
+* Updated Translations
+
+Version 2.8.0 [Nov 8, 2016]
+* Bug: Boot rules are not working on some devices
+* Bug: Logs without active rules issue
+* Bug: Rules are blocked completely on some devices
+* Bug: Removed notification while applying rules
+* Feature: New UI menu icons to support themes
+* Upgraded support library to API 25
+* User Reported crash fixes
+* Updated Translations
+
+Version 2.7.0 [Nov 1, 2016]
+* Bug: Random hang issue with various superuser (phh,cm root)
+* Bug: Added option to add delay applying rules on startup
+* Bug: Minor fixes on preferences on change
+* Bug: Fix for newly installed app on top on few scenarios
+* Bug: Startup script related bugs
+* Bug: Possible tether issue from last version
+* Request: Extra space on notification text! It took lot of time.
+* Fix: User reported crash fixes
+* Updated: Translations
+
+Version 2.6.0.1 [Oct 1, 2016]
+* Bug: Frequent crash while logservice enabled
+* Bug: Persistent notification not shown after reboot
+* Translation updates
+
+Version 2.6.0 [Sep 28, 2016]
+* CRITICAL: Connection leak when LAN option is enabled
+* CRITICAL: Multiple su process (su leak) when logservice enabled
+* Feature: Nougat Support
+* Feature: Split screen of Activity (Nougat+)
+* Feature: Ability is use device pin/password/+ for app using Android API (Donate)
+* Feature: Ability to fetch logs using busybox/toybox/system
+* Feature: Improve auto apply rules for specific preference changes
+* Using NDK10 for building binary
+* UI: Log toast with warning icon
+* Bug: Improved log storing logic to avoid cpu/battery/hang/crash issues
+* Bug: Connectivity change hang notification issue
+* Bug: Xposed related and user reported fixes
+* Reduced overall APK size
+
+
+Version 2.5.2 [Aug 14, 2016]
+* New Feature: Auto-trim log database
+* Enhancement: Logs should load much faster
+* Change: Removed lock screen notification from Xposed module, since it was draining battery
+* Bug Fix: Fix switch log view bug
+* Bug Fix: Fix crash on export and import
+* Update: Translation updates
+
+Version 2.5.1 [Aug 10, 2016]
+
+* Enhancement: Added Switchback option to Old Log view - Listen to users
+* New Feature (Pro): Donate / Donate Key users can view the ipaddress/src/dst in details view (clicking on from new logview)
+* Bug fix: Fixed iptables entries for uninstalled apps not resetting
+
+Version 2.5.0 [Aug 9, 2016]
+* Features
+ - Xposed module - Download manager leak with notification - Application can bypaas AFWall+ by using Download Manager API to download from network. This module helps to block applications from using this API to get around not being allowed to access the internet with AFWall+
+ - New Log UI with History - Log service now stores the blocked information in database. Current UI only shows how many times its blocked. Future versions will have more details screens with all ipaddress with DNS Lookup. Also you can start blocking ipaddress directly from Logs in future versions
+ - Webview filter (applist) - Another possible way apps can use webview to access internet. So now there is a separate system level application for webviews. Please whitelist/blocklist this app accordingly.
+ - Xposed module - Hide lockscreen notification - This will hide ongoing notification in lockscreen. Due to android restrictions it uses Xposed to hide it
+ - Log toast position - Now you can customize the position of app block notification
+ - Toybox support (system level) - CM13/12 and even stock android uses toybox instead of busybox. If AFwall+ does not find busybox, it will look for toybox. If toybox not found, it will use built-in busybox.
+* Bugs Fixed
+ - su leak issues - This issue was related to log service was not able to close properly.
+ - Random block issue - Now by default AFWall+ sets all default chains to ACCEPT state
+ - Fix toast related crashes
+ - All Log related issues/Removed klogripper - klogripper was causing lot of issues on multiple devices. its been replaced with stock dmesg.
+ - Widget crashes,bug in app lock
+ - Fix rules export issue
+ - Kingroot issue - AFWall+ removes the chains used by kingroot now. So, kingroot should not be able to use internet if blocked by AFWall+
+ - Improved init.d/su.d related bugs - ipv6 support is improved now, Thanks to F-i-f
+* UI
+ - Rearrange preferences - Language now moved to new group along with Tasker & Xposed module settings.
+ - Added legends - More detail about the icons used by AFWall+
+ - Firewall mode - Since the dropdown was not visible on multiple devices, its moved to ActionBar
+ - Helper notification on preference change - Some preference changes require repply of firewall rules (DNS for example). AFWall+ notify the user when those changes happen
+* Misc
+ - Updated support libraries
+ - Updated Translations and cleanup - Huge thanks for Gitoffthelawn
+* Thanks to F-i-f and Gitoffthelawn
+
+Version 2.2.3 [Mar 15, 2016]
+* Allow kingroot users to continue with warning message until I find proper solution for kingroot problem.
+* AFWall+ will now show in recent apps list
+* Removed highly experimental feature added in the last version for now.
+* Reported crash issues
+* Updated Translations
+
+Version 2.2.2 [Mar 11, 2016]
+* Fix: Issue with auto IPv6 from preference
+* Fix: afwall su.d script removal on uncheck preference & Added support for systemless su
+* Fix: additional steps to kill klogripper process.
+* Disable AFWall+ if KingRoot is detected. AFWall+ will no longer work with KingRoot, see here for more details: https://github.com/ukanth/afwall/issues/501
+* Added highly experimental feature - Keep only AFWall+ chains on connectivity change.
+* Updated translations.
+
+Version 2.2.1 [Mar 3, 2016]
+* Fix: Delete su.d script if unchecked from preference
+* Fix: Startup hang issue while applying rules
+* Fix: Widget size issue
+* Added missing translation for Romania - Thanks to @ASebastian/mysterys3by
+
+Version 2.2.0 [Feb 27, 2016]
+* Auto IPv6 support
+* Support for Android 6 runtime permission (external storage - import/export)
+* Widget alignment issue - Support to manual adjustment
+* Device startup rules with 3 sec delay to apply rules properly on some devices
+* Fixed random disable issue of firewall
+* Fixed random complete blocking issue of firewall
+* su.d support(supported by supersu and alternate for init.d to prevent startup leak)
+* Mobile data issue for new devices
+* Logs should show for more devices
+* Application not shown after search completed
+* Fixed sort option position on start
+* Additional check for netfilter support on startup
+* Added additional information for error report for better understanding
+* Initial support to store logs to db for history. New UI will be in the next version with History.
+* Fixed lot of reported crash issues
+* Fix: Language not switching for few languages
+* Updated Translations with new language support (pt-BR)
+* Library updates: androidlockpattern,material dialogs
+
+Version 2.1.3 [Nov 24, 2015]
+* Fix: Missing data Interfaces for new devices
+* UI: Sort option as radio button
+* UI: New Languages (Catalan/Bengali)
+* Minor UI Improvements and About/FAQ link click issue
+* Fixed: Reported crashes fixed
+
+Version 2.1.2 [Oct 6, 2015]
+* UI: Sorting now in main page
+* UI: Main screen width issue
+* Possible fix for Widget alignment(not tested!)
+* Reported Crash fixes
+* UI: Status icon fix for Lollipop
+* Added proper widget preview
+* UI: Profile switch Bug (change of profile)
+
+Version 2.1.1 [Sep 28, 2015]
+* FIX: Rules not saving when profiles are used.
+
+Version 2.1.0 [Sep 26, 2015]
+* Fixed rules not saving on some devices
+* Revert Filter from dropdown to radio (UI)
+* Kingo superuser issue
+* Menu key doesn't work (UI)
+* User reported crashes and bug fixes
+* lock pattern improvements (UI)
+* Additional checks for system busybox
+* Log service process leak fix.
+* Widget alignment issue on some devices
+* Fix F-Droid builds using NDK-r10e
+
+Version 2.0.0 [Sep 7, 2015]
+* Initial Material design
+* Support for 5.x Lollipop
+* Revamped UI (pull to refresh), Preferences and Icons
+* Enhanced security password protection
+* Enhanced Import/Export with File Picker
+* New Profile Management
+* Performance Improvements and optimizations
+* Experimental - Added sort apps by uid/install date
+* Added/Updated Translations.
+* And lots of changes...
+
+Version 1.3.4 [Aug 3, 2014]
+* Feature: Added permanent notification on firewall status (optional)
+* Bug: Modified init.d script to support system iptables
+* Bug: Fixed FC on multiple devices when enable/disable
+* Bug: Fixed issue with widget when password protected
+* Minor widget enhancement for old android devices
+* Updated Translations
+
+Version 1.3.3 [Jul 18, 2014]
+* Added export & import for preferences/profiles including custom profiles (Donate version only)
+* Custom Script for each Profile
+* New combined dialog for import and export
+* Encryption for application password - Also resets the old passwords. Please set password again!
+* Fix for LogService FC issues on 4.4
+* Fix for new apps not showing on top when profiles are enabled
+* Fix for Possible SU leak
+* Improved notification text
+* Improved search filter/profile validation logic
+* Updated libs
+
+Version 1.3.2 [May 31, 2014]
+* Added back the old profile switch widget till the new widget gets stable.
+* Fix: process leak with log and nflog service. Please do a clean install if it does not work after update.
+* Fix: filter application's not working for block notifications.
+* Fix: multiple tasker issues (profile 2 applies - profile 3 and rules are not applying when using tasker)
+* Fix: profile status not getting reflected on main view when changed using tasker/widget
+* Fix: new widget not applying rules properly for profiles.
+* Fix: Import rules fails when package not found.
+* Fix: User reported NPE & Force close issues.
+
+Version 1.3.1.1 [Apr 25, 2014]
+* Revert Target SDK to 16 to fix issue with boot rules
+
+Version 1.3.1 [Apr 23, 2014]
+* Added experimental filter for block notifications.
+* Error report FC
+* ip6tables log/toggle issues on most devices
+* Widget display issue on some devices #265
+* Better root detection and error display when no root
+* Fixed FC on experimental Preferences #270
+* Widget name issues and better icons
+* Performance improvement for multiple profiles
+* Reuse of rootshell on new logservice
+* Widget profile switch issue
+* Apply rules on boot fix for some devices
+
+Version 1.3.0.2 [Apr 3, 2014]
+* Bug fixes on 1.3.x
+
+Version 1.3.0.1 [Apr 1, 2014]
+* Old toggle widget is back - Hate you guys :)
+* "Allow All Application" option is back - Again hate you guys :)
+
+Version 1.3.0 [Mar 30, 2014]
+* New Widget with support for multiple profiles (single widget)
+* Updated lockpattern - stealth mode/max retry count
+* DNS Proxy to Preferences (By default UDP 53 will be blocked on <4.3)
+* More Log information (PORT/PROTOCOL)
+* Fixed application list load performance issues
+* Fixed bug in preferences
+* Support for Wifi-only tab (auto hide data column)
+* Block packet notification (exp!) - Log service
+* New Icon,User reported bug fixes including tasker plugin
+* Translations updated - Indonesian (thx mirulumam)
+
+Version 1.2.9 [Feb 8, 2014]
+* Feature: Column level select/invert/unselect
+* Feature: New Import/Export (with backward compatiblity)
+* Feature: Filter by All/Core/System/User applications
+* BugFix: Fixed issue with Multiuser iptables rules
+* BugFix: Fixed issue with Tasker plugin (enable/disable/Profile switch)
+* UI: Revamped About and added FAQ page.
+* User reported bug fixes.
+
+Version 1.2.8 [Jan 19, 2014]
+* Traffic stats + App detail View (Long press on App Label) Note: A minimal stats and not a complete statistics of traffic details.
+* Add/Remove Additional Profiles
+* Multiuser support for Tablets (Experimental)
+* Custom rules file support (. /path/to/file)
+* Fixed VPN issue with KitKat & Updated libsuperuser library
+* Many minor UI enhancements and performance improvements
+* Bug Fixes: #154, sdcard mount on startup, user reported crash fixes
+
+Version 1.2.7 [Nov 23, 2013]
+* Improved search functionality & select confirmation.
+* Added built-in ip6tables support
+* Support for x86/MIPS/ARM devices.
+* Built-in iptables is upgraded to the latest version.
+* Various user reported crash/bug fixes.
+* Build scripts updated for F-Droid and developer friendly builds (ant)
+* Added Hungarian/Turkish Translations and updated other translations
+
+Version 1.2.6.2 [Sep 9, 2013]
+* NGLog fixes for various devices including nexus.
+
+Version 1.2.6.1
+* LOGS should work now on newer devices which has NFLOG chain.
+* New RootShell Service to keep AFWall+ active.
+* RootShell - Retry on exit 4 while switching from 3G to WiFi or viceversa.
+* Removed alternate startup service from experimental- It works without it.
+* Improved Caching Logic, Uninstall apps will now remove cache along with rules.
+* Reload applications will remove unused cache.
+* New Help page - Added another developer (@cernekee)
+* Fixed too many prompts when password is enabled.
+* Rewritten logic to detect LOG chain.
+* Added "error send report" option to help beta testers with more diagnostics information.
+* Special UID for DNS Proxy and NTP (4.3 users)
+* Added support for custom rules from 1.2.5.2 (it was broken because of afwall iptables chain change)
+* Better tether status check
+* Keep alive RootShell on some devices.
+
+Version 1.2.6 [Aug 16, 2013]
+* Lots of Refactor to bring stability and performance. Fixed many issues along with it
+ (HUGE THANKS TO cernekee!)
+ New option -> Now enable/disable log from preferences
+* New option -> Apply rules on Switch Profiles
+* New option -> Active Rules is now optional (But required when using LAN/Roaming)
+* New option -> Enable inbound connections (Required for sshd, AirDroid etc)
+* New busybox binary for ifconfig
+* Rules Log has more information
+* NFLOG support for newer devices
+
+Version 1.2.5.2 [May 30, 2013]
+* Fixed issue with Wifi blocked on whitelist for couple of devices
+
+Version 1.2.5.1
+* Improved search functionality.
+* Frequent connectivity change rules will now work only on roaming/lan. this should reduce the number of
+ superuser prompts and reduce lag on some devices.
+* Fixed issue with application list not showing.
+* Fixed issue with logs where it used to work before.
+* Fixed issue with default language (default is set to English, please change it in preference)
+* Added translations.
+
+Version 1.2.5 [May 25, 2013]
+* Added Tether support. (Thanks to cernekee)
+* Added LAN/WAN support. (Thanks to cernekee)
+* Added Import from DroidWall (from Donate Version!)
+* Fixed issue with special applications not showing in different color(system apps) (Thanks to cernekee)
+* Fixed issue with preferences for defauly system application picker (Thanks to cernekee)
+* Fixed issue with Language preferences default (Thanks to cernekee)
+* Lots of code refactor/bug fixes (Thanks to cernekee!)
+* Fixed issue with multiline in search text.
+* Minor UI changes on the application list.
+* Added selectable iptables/busybox binary
+* Added new translations (Chinese, Greek, more)
+
+Version 1.2.4.1 [Apr 27, 2013]
+* Fixed issue with cleanup afwall rules on disable
+* Fixed issue with OUTPUT chain not removed for afwall on disable
+
+Version 1.2.4 (bump version to match Donate version) [Apr 23, 2013]
+* Support IPv6 (Enable it in preference)
+* Tasker support enable/disable of AFWall+
+* Improved performance of applying rules and application list.
+* Improved application loading progress dialog.
+* Show keyboard automatically on password protected dialogs
+* Fixed issue with custom script hangs.
+* Improved translations strings.
+* Fixed issue with multiple password request (in beta testing)
+* Improved detection logic for data leak prevention script (Thanks GermainZ)
+* Improved multiple profile performance while loading applications. It will no longer apply rules on switching
+ profiles. You need to manually apply rules after profile switch.
+* Added translations for Greek and Produguese languages.
+
+Version 1.2.1 [Mar 11, 2013]
+* Minor issue fixed for "Media Server" not apply properly after reboot
+* Fixed iptables rules which breaks wifi/Mobile data limit.
+* Updated translations for German/Chineese
+* Added Swedish Translation - Many Thanks to CreepyLinguist@Crowdin
+
+Version 1.2.0 [Mar 3, 2013]
+* Added change app language from the preferences (default is system lang)
+* Added device admin feature - Extra protection to AFWall+, so that it can't be uninstalled from any other app.
+* Added Tasker/Locale plugin (from donate version) with bug fixes.
+* Added VPN Support (enable/disable it preferences) - Tested with DroidVPN and works fine!
+* Added new widget with quick toggle (enable/disable/profiles)
+* Added option to import from DroidWall (only for Donate version for now!)
+* Added Active defense (Make sure only AFWall+ able to control the internet) - Not optional!
+* Added new super user permission ( koush's superuser permission)
+* Added ability to enable/disable roaming feature
+* New logic to apply rules - Performance improvement
+* Removed deprecated API's for Notification. Going forward this will be improved for ICS/JB
+* Improved preferences - Added summary for each preferences and rearranged order
+* New menu icons (white icons !)
+* Removed all inline style alert messages and alert boxes. Now it just display toast messages.
+* Fixed data leak on boot for devices REQUIRES init.d support/S-OFF (enable it in preferences - EXPERIMENTAL!)
+(to enable init.d support use this app -> https://play.google.com/store/apps/d...k.initdtoggler)
+* New log rule to get the logs from dmesg and enable logs by default
+* Enable/Disable logs now from "Firewall Logs" menu.
+* Fixed issue with iptable rules are not applying after reboot, mainly CM 10.1 devices (Enable it in preferences - EXPERIMENTAL !)
+* Various UI glitches in multi profiles/icons & UID
+* Fixed hang/rules issue on startup
+* Fixed issue with profiles where the default profile is applied after restart instead of selected one.
+* FC issue when using app menu (ActionBarSherlock - NPE)
+* Fixed issue with Media Server/VPN not applying properly.
+* Simplified Chinese Translation - Many thanks to wufei0513 & tianchaoren@Crowdin
+* Czech Translation - Many thanks to syk3s@Crowdin
+* Turkish Translation - Many thanks to metah@Crowdin
+* Ukrainian Translation - Many thanks to andriykopanytsia,igor@Crowdin
+
+Version 1.1.9 [Jan 23, 2013]
+* Added invert selection for apps (useful when switching whitelist <-> blacklist)
+* Fixed issue with special apps (root/shell/media server) not applying
+* Fixed issue with new lockpattern not working properly.
+* Added MDPI images for icons.
+* Code cleanup, mainly strings.xml (removed version from strings.xml etc.)
+
+Version 1.1.8
+* Fixed FC on new lockpattern
+
+Version 1.1.7
+* Added lockpattern (you can still use the old style password protection) with SHA1 protection
+* Fixed force close issue while adding system apps.
+* Fixed issue with select All/none. it wroks properly and doesn't require scroll. Thanks to Pragma!
+* Significant improvements while loading applications (hope not a placebo)
+* Fixed issue with search case sensitive and expand search will show the keyboard (no more two press!)
+* Disable notification when the firewall is disabled.
+* Added new language translations
+ - Spanish translation by spezzino@crowdin
+ - Dutch translation by DutchWaG@crowdin
+ - Japanese translation by nnnn@crowdin
+ - Ukrainian translation by andriykopanytsia@crowdin
+
+Version 1.1.6 [Jan 3, 2013]
+* Back to Chainfire's SU library. More stable but little slower compare to RootTools. Performance will be improved going forward.
+ I'm planning to rewrite the entire code to make it faster and stable. But for now, it will be continue as it is.
+* Fixed issue with rules were not applied after system reboot for couple of devices.
+* Fixed issue with custom rules were broken completely.
+* Fixed issue with Notification icon size is huge.
+* Fixed Force Close of some devices when alert message is displayed.
+
+Version 1.1.5 [Dec 27, 2012]
+* New Busybox binary (at least I feel little faster loading on logs) compiled from latest busybox source. This is packed with handpicked additional and useful busybox commands which will be used in the future versions of AFWall+ to build more advance features! Stay tuned.
+* Fixed issue with widget size 1x1 on newer devices
+* Fixed issue with firewall rules not applying before shutdown to prevent leak.
+* Fixed Force close on many devices while opening application.
+* Fixed Force close on some devices when alert message is displayed.
+
+Version 1.1.4
+* Replace su library with RootTools, much faster and stable!
+* Improved detection logic for iptables for ICS/JS devices and removed EXPERIMENTAL option from preferences.
+* Now disable icons will free up space on the main view
+* Added option to show UID for applications (like DroidWall)
+* Fixed Issue with tf201 devices with su permisssions.
+* Fixed constant force close on some devices while applying rules.
+* Fixes issue with packages reset to root when importing.
+* Improved Russian Translations - Many thanks to Kirhe@xda!
+* Fixed issue with custom script not applying properly after uid (github issue #89)
+* Removed Disable 3G when USB connected preference because of some bugs. I'll put that back after fixing it.
+
+Version 1.1.3 [Dec 20, 2012]
+* Critical bugfix: Rules were not applied after every system reboot!
+
+Version 1.1.2
+* Minor bug fix for Forceclose on alerts!
+
+Version 1.1.1
+* Feature : Tasker/Locate Plugin! (Only for donate version for now)
+* Feature : Now allow customize names for profiles.(from preferences)
+* Feature : Replace alert/toasts with appmsg(displays within the app) - enable it in preference
+* Feature : Initial simple improvements for view logs. It will be improved further!
+* Preference : Added new preference to enable confirmbox on firewall disable
+* FC : Replaced old style deprecated Thread implementation with AsyncTask.(faster and safer)
+* FC : NullPointer exception while reading preferences.
+* Bug Fix : Shutdown Custom Rule doesn't work.
+* Bug Fix : Refresh issue of mode on the multiple profiles switching.
+* Bug Fix : Fixed two identical profile names on multiple profiles.
+* i18n : Completed french/Germen translations.
+* i18n : Added russian language support (Thanks: Google translator toolkit)
+* and many small fixes
+
+Version 1.1.0 [Dec 7, 2012]
+* Initial Play Store Version
+
+Version 1.0.7a
+* New icon for AFWall+ (Thanks for hush66!)
+* Multiple Profiles (Currently limited to 4)
+* Added support for Epic 4G Touch (Thanks to JoshMiers!)
+* Unified Preferences (https://github.com/saik0/UnifiedPreference/)
+* Translations added: French and German
+* Fixed multiple menu on ActionBar with staticbar ( no more two menu items on some devices )
+* Enable/Disable logs now moved to preferences and log menu will be hidden if disabled
+* Bug Fix: Update of application packages will not be notified with AFwall.
+* Bug Fix: Uninstalling app will reset rule for root application to default.
+
+Version: 1.0.6.1a [25 Nov 2012]
+* Bug Fix : Rules for spcial application were not applied after application restart.
+
+Version: 1.0.6a [25 Nov 2012]
+* Now uses Chainfire's SU library, This will get rid of old shell script approach. I feel it's faster and better approach and helps to enable profiles!
+ Please Note: If you use afwall.sh outside, starting this version it will not work!
+* Improved menubar and confirmation dialogs
+* Fixed bug with logging
+* Fixed bug on some ICS/JB devices
+* Added new EXPERIMENTAL option for ICS/JB devices (uses extra rules)
+* Enabled fast scrolling on lists (main list)
+* German Translation (Thanks to CHEF-KOCH!)
+* Support for 4.2 JB
+
+Version 1.0.5a [10 Nov 2012]
+* Enhanced Rules view with additional actions like copy, flush, export to sdcard and network interfaces.
+* Moved flush rules from main menu to enhanced rules view
+* Enhanced Log view with additional copy & clear action
+* Moved clear log from main menu to enhanced log view
+* Fixed FC issues from 1.0.4.x
+
+Version 1.0.4.1a [9 Nov 2012]
+* Fixed force close on viewlog and view rules pages.
+
+Version 1.0.4a [9 Nov 2012]
+* Import/Export Rules (for now it's just a single import & export to external storage)
+* Integrated search bar (application search)
+* Revamped Log & IPTables rules view (you can now view the logs and rules in a clear view and copy them!)
+* Added reenter password confirmation dialog.
+* Added additional ifaces to support more devices (working on another solution which will identity interfaces on the particular device)
+* Fixed force close when scrolling for some devices
+* And more!
+
+Version 1.0.3a
+* Fix for some apps can "bypass" the firewall by just using UDP port 53. Disable port 53
+* Added 3g ifaces to support more devices (should solve issues with firewall for some devices)
+* Fixed Widget on/off issue (First enable firewall and then add the widget will do the trick!)
+* Fixed Widget size for 4.0+ devices
+* Prepared for i18n Support
+* Prepared support for XHDPI devices
+Note: If you have any issue, please clean the rules via menu and apply again.
+
+Version 1.0.2a (Please note, if you upgrade from 1.0.1a, rules will be reset!)
+* Roaming Option (not tested !)
+* Added Shutdown broadcast and applied rule to block all the connections (this should solve the leakage
+ when phone is rebooted/started before AFwall+ can start!!!) - Not tested!
+* Added option to disable application icon (faster loading on slow devices)
+* Added option to disable 3g rules when connected via Wired USB (Droidwall issue)
+* Added support for more ifaces for 3G (support multiple devices)
+* Added clear Rules option in menu (now the iptables will be saved as afwall-3g,afwall-wifi, to solve the issue when both Droidwall & AFWall+ installed )
+* Fixed bug in reload applications
+* Fixed bug in applying rules in clear/select all
+* Fixed the issue with save/discard rules when press back button.
+
+Version 1.0.1a
+* Improved install notification (only notify when app has internet permission)
+* Select All Wifi / 3G or Clean All option! (HUGE FIX) - No Invert select this time. just click on the 3G/WiFi icons will do the trick!
+* Fixed dangerous file permissions issues (reported in original Droidwall as an issue)
+
+Version 1.0.0a
+* Initial release based on [DroidWall](http://code.google.com/p/droidwall/) 1.5.7
+* ICS style menubar and theme
+* New install notifications
+* New preferences options
+* Force reload Applications
+* Highlight System applications using custom color from preferences
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ef7e7ef
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {one line to give the program's name and a brief idea of what it does.}
+ Copyright (C) {year} {name of author}
+
+ 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 .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ {project} Copyright (C) {year} {fullname}
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.md b/README.md
index 03777ba..213f5e2 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,385 @@
-# af-wall
+# AFWall+ (Android Firewall+)
-Android Firewall and IP tables
\ No newline at end of file
+[](https://github.com/ukanth/afwall/actions) [](https://crowdin.net/project/afwall)    
+
+> **Your Privacy, Your Control** - AFWall+ gives you complete control over which apps can access the internet on your Android device.
+
+---
+
+## 💝 Support AFWall+ Development
+
+AFWall+ is developed and maintained by volunteers in their free time. If you find it useful, consider supporting the project:
+
+### 💰 **Making Donations**
+
+**Why Donate?** AFWall+ is completely free and open-source. Your donations help:
+- 🔧 **Continue development** - Fund new features and maintenance
+- 🐛 **Bug fixes and testing** - Keep the app stable and secure
+- 📱 **Device compatibility** - Support more Android versions and devices
+- 🌍 **Community support** - Help users and maintain documentation
+
+**Donation Options:**
+- **PayPal**: [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6E4VZTULRB8GU)
+- **Google Play**: Purchase the [unlocker key](https://play.google.com/store/apps/details?id=dev.ukanth.ufirewall.donate) for additional features
+- **Amazon Gift Cards**: `cumakt+amazon@gmail.com`
+- **Bitcoin**: `bc1q54nf3y9zmdcpasxx9sywkprd6309rfhav3mape`
+- **Ethereum**: `0x5e65649C2B26eD816fCeD25a8E507C90D4b1D697`
+
+### 🌟 **Other Ways to Help**
+- ⭐ Star this repository
+- 🐛 Report bugs and test new features
+- 🌐 Contribute translations on [Crowdin](http://crowdin.net/project/afwall)
+- 📝 Improve documentation
+- 💬 Help other users in forums
+
+---
+
+
+
+
+
+## 🔥 What is AFWall+?
+
+**AFWall+ (Android Firewall+)** is a powerful, open-source firewall application for rooted Android devices. Built on Linux's robust `iptables` framework, AFWall+ provides **granular network control** at the system level - something impossible with standard Android permissions.
+
+### 🎯 **Core Purpose**
+- **Block unwanted network access** by apps, even when they have internet permission
+- **Prevent data leaks** and unauthorized background connections
+- **Monitor network activity** with comprehensive logging
+- **Save battery and data** by controlling which apps can connect when
+- **Enhance privacy** by blocking tracking and analytics
+
+### 🛡️ **How It Works**
+AFWall+ operates at the **Linux kernel level** using `iptables` rules to:
+1. **Intercept all network requests** before they leave your device
+2. **Apply custom firewall rules** based on your preferences
+3. **Allow or block connections** per app, per network type (WiFi, mobile, VPN)
+4. **Log blocked attempts** for monitoring and analysis
+
+This approach is **far more powerful** than app-level solutions because it works regardless of how apps try to connect to the internet.
+
+---
+
+## 📥 Download
+
+
+
+📋 **Release Notes**: Check the [changelog](https://github.com/ukanth/afwall/blob/beta/Changelog.md) for what's new in each version.
+
+---
+
+## 🌟 Key Features
+
+### 🔐 **Granular Control**
+- **Per-app network rules** - Allow/block individual apps
+- **Network type filtering** - Different rules for WiFi, mobile data, VPN, tethering
+- **IPv4 & IPv6 support** - Complete protocol coverage
+- **Custom rule scripting** - Advanced users can write custom iptables rules
+
+### 🎛️ **User Experience**
+- **Clean, intuitive interface** - Easy to understand app list with clear allow/block controls
+- **Quick search & filtering** - Find apps instantly, sort by name, install date, or permissions
+- **Bulk operations** - Enable/disable rules for multiple apps at once
+- **Profile management** - Switch between different rule sets (home, work, travel)
+
+### 📊 **Monitoring & Logging**
+- **Real-time network monitoring** - See which apps are trying to connect
+- **Detailed connection logs** - Track blocked attempts with timestamps and destinations
+- **Notification system** - Get alerts for blocked connection attempts
+- **Export/import rules** - Backup your configuration or share with others
+
+### 🔧 **Advanced Features**
+- **Boot protection** - Apply rules before apps start (prevents data leaks during startup)
+- **Startup delay management** - Robust boot rule application with network change handling
+- **Multi-user support** - Different profiles for different Android users
+- **Tasker/Locale integration** - Automate firewall based on conditions
+- **Password protection** - Secure your firewall settings
+- **Tor and VPN detection** - Special handling for privacy networks
+
+### 🌐 **Network Types Supported**
+- 📶 **Mobile Data** (3G/4G/5G) - including roaming detection
+- 📡 **WiFi** - home, work, public hotspots
+- 🔗 **VPN** - all VPN types and providers
+- 🔄 **Tethering** - WiFi hotspot, USB, Bluetooth
+- 🧅 **Tor** - onion routing support
+- 🏠 **LAN** - local network access
+
+---
+
+## 📋 System Requirements
+
+### ✅ **Compatibility**
+- **Android versions**: 5.0 (API 21) to 14+ (actively maintained)
+ - Legacy support: Android 4.x (version 2.9.9), Android 2.x (version 1.3.4.1)
+- **Root access**: Required (Magisk, SuperSU, LineageOS su)
+- **Architectures**: ARM, ARM64, x86, x86_64
+- **Storage**: ~15MB app + ~5MB for binaries
+
+### 🔧 **Root Methods Supported**
+- ✅ **Magisk** (recommended)
+- ✅ **LineageOS built-in su**
+- ✅ **SuperSU** (legacy)
+- ✅ **KingRoot** (not recommended)
+
+### 🚫 **Limitations**
+- **Requires root access** - No root = no functionality
+- **Not an antivirus** - Doesn't scan files for malware
+- **Not an ad-blocker** - Blocks network access, not ads within allowed connections
+- **VPN conflicts** - Some VPN apps may interfere with firewall rules
+- **System-level apps** - Some system processes may bypass rules if they have root access
+
+---
+
+## 🚀 Quick Start Guide
+
+### 1. **Pre-Installation**
+```bash
+# Verify root access
+su -c "id"
+# Should return: uid=0(root) gid=0(root)
+```
+
+### 2. **Installation**
+- Install AFWall+ from your preferred source
+- Grant root permission when prompted
+- Enable firewall in main screen
+
+### 3. **Basic Configuration**
+1. **Enable the firewall** - Toggle the main switch
+2. **Configure apps** - Tap apps to allow WiFi (green) or mobile data (orange)
+3. **Apply rules** - Tap the apply button (firewall icon)
+4. **Test connectivity** - Verify apps work as expected
+
+### 4. **Essential Settings**
+- **Boot startup delay**: Prevents rule conflicts during boot
+- **Notification settings**: Control alert behavior
+- **Log settings**: Enable if you want connection monitoring
+
+---
+
+## 🔧 Advanced Configuration
+
+### 📝 **Custom Rules**
+AFWall+ supports custom iptables rules for advanced users:
+
+```bash
+# Example: Allow specific IP range
+-A afwall-wifi -d 192.168.1.0/24 -j ACCEPT
+
+# Example: Block specific port
+-A afwall -p tcp --dport 443 -j REJECT
+```
+
+### 🔄 **Profiles**
+Create different rule sets for different scenarios:
+- **Home**: Relaxed rules for trusted network
+- **Work**: Restrictive rules for corporate network
+- **Public**: Maximum security for public WiFi
+- **Travel**: Balanced rules for mobile use
+
+### 📊 **Logging Configuration**
+- **Packet logging**: Uses nflog for detailed connection tracking
+- **Log rotation**: Automatic cleanup of old logs
+- **Export options**: Save logs for external analysis
+
+---
+
+## 🌍 Language Support
+
+AFWall+ is available in **40+ languages** thanks to our community translators:
+
+🇺🇸 English • 🇪🇸 Español • 🇫🇷 Français • 🇩🇪 Deutsch • 🇮🇹 Italiano • 🇷🇺 Русский • 🇨🇳 中文 • 🇯🇵 日本語 • 🇰🇷 한국어 • 🇵🇹 Português • 🇳🇱 Nederlands • 🇵🇱 Polski • 🇹🇷 Türkçe • 🇸🇦 العربية • 🇮🇳 हिंदी • And many more!
+
+**Want to help translate?** Join our [Crowdin translation project](http://crowdin.net/project/afwall).
+
+---
+
+## 🛠️ Development
+
+### 🏗️ **Building from Source**
+
+#### **Prerequisites**
+- Android SDK (API level 21+)
+- Java 17+
+- Git
+- Android NDK (for native binaries)
+
+#### **Quick Build**
+```bash
+git clone https://github.com/ukanth/afwall.git
+cd afwall
+./gradlew clean assembleDebug
+```
+
+#### **Native Binaries**
+To compile iptables, busybox, and other native components:
+```bash
+# Requires Android NDK
+export NDK=/opt/android-ndk-r25
+make -C external NDK=$NDK
+```
+
+### 📁 **Project Structure**
+```
+afwall/
+├── app/src/main/java/dev/ukanth/ufirewall/
+│ ├── Api.java # Core iptables interface
+│ ├── MainActivity.java # Main UI
+│ ├── InterfaceTracker.java # Network state monitoring
+│ ├── util/BootRuleManager.java # Boot rule application
+│ ├── service/ # Background services
+│ ├── broadcast/ # System event receivers
+│ └── log/ # Logging subsystem
+├── app/src/main/res/raw/ # Native binaries (iptables, busybox)
+├── external/ # Native binary sources
+└── scripts/ # Build scripts
+```
+
+### 🧪 **Testing**
+```bash
+# Run lint checks
+./gradlew lint
+
+# Run unit tests
+./gradlew test
+
+# Install debug build
+./gradlew installDebug
+```
+
+---
+
+## 🤝 Contributing
+
+We welcome contributions! Here's how you can help:
+
+### 🐛 **Bug Reports**
+- Check [existing issues](https://github.com/ukanth/afwall/issues) first
+- Follow our [bug report guide](https://github.com/ukanth/afwall/wiki/HOWTO-Report-Bug)
+- Include device info, Android version, and logs
+
+### 💡 **Feature Requests**
+- Open an issue with the "enhancement" label
+- Describe the use case and expected behavior
+- Consider if it fits AFWall+'s scope and philosophy
+
+### 👨💻 **Code Contributions**
+```bash
+# Standard GitHub workflow
+1. Fork the repository
+2. Create a feature branch: git checkout -b feature-name
+3. Make your changes and test thoroughly
+4. Submit a pull request with clear description
+```
+
+### 🌐 **Translations**
+- Join our [Crowdin project](http://crowdin.net/project/afwall)
+- No technical knowledge required
+- Help make AFWall+ accessible worldwide
+
+---
+
+## 📞 Community & Support
+
+### 💬 **Discussion Forums**
+- **XDA Thread**: [Official community discussion](http://forum.xda-developers.com/showthread.php?t=1957231)
+- **GitHub Issues**: Technical problems and feature requests
+- **Wiki**: [Comprehensive documentation](https://github.com/ukanth/afwall/wiki)
+
+### ❓ **Frequently Asked Questions**
+Before reporting issues, check our [FAQ](https://github.com/ukanth/afwall/wiki/FAQ) for common solutions.
+
+### 🆘 **Getting Help**
+1. Check the FAQ and wiki
+2. Search existing GitHub issues
+3. Ask on XDA forums
+4. Create a new GitHub issue (last resort)
+
+---
+
+## 📖 Technical Details
+
+### 🔧 **Architecture**
+AFWall+ uses a **layered architecture**:
+
+1. **UI Layer**: Android activities and fragments for user interaction
+2. **Service Layer**: Background services for rule application and monitoring
+3. **Core Layer**: iptables rule generation and management
+4. **System Layer**: Native binaries and root shell interface
+
+### 🏗️ **Key Components**
+- **BootRuleManager**: Robust boot-time rule application with race condition prevention
+- **InterfaceTracker**: Network interface monitoring and change detection
+- **Api.java**: Central iptables command generation and execution
+- **FirewallService**: Background service for continuous monitoring
+- **LogService**: Network packet logging and analysis
+
+### 📱 **Android Integration**
+- **Broadcast Receivers**: Monitor system events (boot, network changes, app installs)
+- **Content Providers**: Share configuration data securely
+- **Notification System**: User alerts for blocked connections
+- **Quick Settings Tile**: Fast firewall toggle (Android 7+)
+
+---
+
+## 🏆 Acknowledgements
+
+AFWall+ builds upon the work of many open-source projects and contributors:
+
+### 🌟 **Origins**
+- **Original concept**: Derived from [DroidWall](http://code.google.com/p/droidwall) by Rodrigo Rosauro
+- **Current maintainer**: [Umakanthan Chandran](https://github.com/ukanth)
+
+### 📚 **Libraries & Dependencies**
+| Component | License | Purpose |
+|-----------|---------|---------|
+| [iptables](http://netfilter.org/projects/iptables/) | GPL v2 | Linux firewall framework |
+| [BusyBox](http://www.busybox.net) | GPL v2 | Unix utilities |
+| [libsuperuser](https://github.com/Chainfire/libsuperuser) | Apache 2.0 | Root access management |
+| [libsu](https://github.com/topjohnwu/libsu) | Apache 2.0 | Modern root interface |
+| [Material Dialogs](https://github.com/afollestad/material-dialogs) | MIT | UI components |
+| [DBFlow](https://github.com/Raizlabs/DBFlow) | MIT | Database ORM |
+| [PrettyTime](https://github.com/ocpsoft/prettytime) | Apache 2.0 | Human-readable timestamps |
+
+### 👥 **Contributors**
+Thanks to all contributors who have helped improve AFWall+ over the years!
+
+---
+
+## 📄 License
+
+AFWall+ is released under the **GNU General Public License v3.0**.
+
+```
+Copyright (C) 2009-2011 Rodrigo Zechin Rosauro
+Copyright (C) 2011-2024 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.
+```
+
+**Full license text**: [LICENSE](LICENSE) file or [gnu.org/licenses/gpl-3.0](https://www.gnu.org/licenses/gpl-3.0.html)
+
+---
+
+
+ Made with ❤️ for Android privacy and security
+ AFWall+ - Your Network, Your Rules
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..01f6a61
--- /dev/null
+++ b/app/build.gradle
@@ -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"
+
+}
diff --git a/app/lint.xml b/app/lint.xml
new file mode 100644
index 0000000..ee0eead
--- /dev/null
+++ b/app/lint.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..58ba74c
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -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** { *; }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f12256d
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,366 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/rules.json b/app/src/main/assets/rules.json
new file mode 100644
index 0000000..309794c
--- /dev/null
+++ b/app/src/main/assets/rules.json
@@ -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"
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..df4d315
Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/ic_launcher_donate-web.png b/app/src/main/ic_launcher_donate-web.png
new file mode 100644
index 0000000..38c060c
Binary files /dev/null and b/app/src/main/ic_launcher_donate-web.png differ
diff --git a/app/src/main/java/com/stericson/rootshell/NativeJavaClass.java b/app/src/main/java/com/stericson/rootshell/NativeJavaClass.java
new file mode 100755
index 0000000..63d61e3
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/NativeJavaClass.java
@@ -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();
+ }
+
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/stericson/rootshell/RootShell.java b/app/src/main/java/com/stericson/rootshell/RootShell.java
new file mode 100644
index 0000000..66a47fe
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/RootShell.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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 boolean 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 result = new ArrayList();
+
+ 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 final_result = new ArrayList(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 List containing the locations the binary was found at.
+ */
+ public static List findBinary(String binaryName, boolean singlePath) {
+ return findBinary(binaryName, null, singlePath);
+ }
+
+ /**
+ * @param binaryName String that represent the binary to find.
+ * @param searchPaths List which contains the paths to search for this binary in.
+ * @param singlePath boolean that represents whether to return a single path or multiple.
+ *
+ * @return List containing the locations the binary was found at.
+ */
+ public static List findBinary(final String binaryName, List searchPaths, boolean singlePath) {
+
+ final List foundPaths = new ArrayList();
+
+ 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 String to Indicate the path to the shell that you want to open.
+ * @param timeout an int 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 List A List of Strings representing the environment variable $PATH
+ */
+ public static List 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 boolean to Indicate whether or not you want to open a root shell or a standard shell
+ * @param timeout an int 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 int 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 boolean to Indicate whether or not you want to open a root shell or a standard shell
+ * @param timeout an int 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 boolean 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 boolean to Indicate whether or not you want to open a root shell or a standard shell
+ * @param timeout an int 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 boolean 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 true 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 ID = new HashSet();
+ 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 true if BusyBox or Toybox was found.
+ */
+ public static boolean isBusyboxAvailable()
+ {
+ return (findBinary("busybox", true)).size() > 0;
+ }
+
+ /**
+ * @return true 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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)
+ *
+ * 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.
+ *
+ * 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();
+ }
+ }
+
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java b/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java
new file mode 100755
index 0000000..f2a572a
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java
@@ -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 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;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/rootshell/containers/RootClass.java b/app/src/main/java/com/stericson/rootshell/containers/RootClass.java
new file mode 100644
index 0000000..239812a
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/containers/RootClass.java
@@ -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 classFiles;
+
+ public AnnotationsFinder() throws IOException {
+ System.out.println("Discovering root class annotations...");
+ classFiles = new ArrayList();
+ 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 al = new ArrayList();
+ 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 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);
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java b/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java
new file mode 100644
index 0000000..4ee3849
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java
@@ -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);
+ }
+}
diff --git a/app/src/main/java/com/stericson/rootshell/execution/Command.java b/app/src/main/java/com/stericson/rootshell/execution/Command.java
new file mode 100644
index 0000000..f4b5ec9
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/execution/Command.java
@@ -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;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java b/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java
new file mode 100644
index 0000000..4614e21
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java
@@ -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
+ }
+}
diff --git a/app/src/main/java/com/stericson/rootshell/execution/Shell.java b/app/src/main/java/com/stericson/rootshell/execution/Shell.java
new file mode 100644
index 0000000..f5a522c
--- /dev/null
+++ b/app/src/main/java/com/stericson/rootshell/execution/Shell.java
@@ -0,0 +1,1029 @@
+/*
+ * 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 com.stericson.rootshell.RootShell;
+import com.stericson.rootshell.exceptions.RootDeniedException;
+
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+public class Shell {
+
+ public enum ShellType {
+ NORMAL,
+ ROOT,
+ CUSTOM
+ }
+
+ //this is only used with root shells
+ public enum ShellContext {
+ NORMAL("normal"), //The normal context...
+ SHELL("u:r:shell:s0"), //unprivileged shell (such as an adb shell)
+ SYSTEM_SERVER("u:r:system_server:s0"), // system_server, u:r:system:s0 on some firmwares
+ SYSTEM_APP("u:r:system_app:s0"), // System apps
+ PLATFORM_APP("u:r:platform_app:s0"), // System apps
+ UNTRUSTED_APP("u:r:untrusted_app:s0"), // Third-party apps
+ RECOVERY("u:r:recovery:s0"); //Recovery
+
+ private final String value;
+
+ ShellContext(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ }
+
+ //Statics -- visible to all
+ private static final String token = "F*D^W@#FGF";
+
+ private static Shell rootShell = null;
+
+ private static Shell shell = null;
+
+ private static Shell customShell = null;
+
+ private static final String[] suVersion = new String[]{
+ null, null
+ };
+
+ //the default context for root shells...
+ public static ShellContext defaultContext = ShellContext.NORMAL;
+
+ //per shell
+ private int shellTimeout = 25000;
+
+ private ShellType shellType = null;
+
+ private ShellContext shellContext = ShellContext.NORMAL;
+
+ private String error = "";
+
+ private final Process proc;
+
+ private final BufferedReader inputStream;
+
+ private final BufferedReader errorStream;
+
+ private final OutputStreamWriter outputStream;
+
+ private final List commands = new ArrayList();
+
+ //indicates whether or not to close the shell
+ private boolean close = false;
+
+ private Boolean isSELinuxEnforcing = null;
+
+ public boolean isExecuting = false;
+
+ public boolean isReading = false;
+
+ public boolean isClosed = false;
+
+ private final int maxCommands = 5000;
+
+ private int read = 0;
+
+ private int write = 0;
+
+ private int totalExecuted = 0;
+
+ private int totalRead = 0;
+
+ private boolean isCleaning = false;
+
+ private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException {
+
+ RootShell.log("Starting shell: " + cmd);
+ RootShell.log("Context: " + shellContext.getValue());
+ RootShell.log("Timeout: " + shellTimeout);
+
+ this.shellType = shellType;
+ this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout;
+ this.shellContext = shellContext;
+
+ if (this.shellContext == ShellContext.NORMAL) {
+ this.proc = Runtime.getRuntime().exec(cmd);
+ } else {
+ String display = getSuVersion(false);
+ String internal = getSuVersion(true);
+
+ //only done for root shell...
+ //Right now only SUPERSU supports the --context switch
+ if (isSELinuxEnforcing() &&
+ (display != null) &&
+ (internal != null) &&
+ (display.endsWith("SUPERSU")) &&
+ (Integer.valueOf(internal) >= 190)) {
+ cmd += " --context " + this.shellContext.getValue();
+ } else {
+ RootShell.log("Su binary --context switch not supported!");
+ RootShell.log("Su binary display version: " + display);
+ RootShell.log("Su binary internal version: " + internal);
+ RootShell.log("SELinuxEnforcing: " + isSELinuxEnforcing());
+ }
+
+ this.proc = Runtime.getRuntime().exec(cmd);
+
+ }
+
+ this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), StandardCharsets.UTF_8));
+ this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), StandardCharsets.UTF_8));
+ this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), StandardCharsets.UTF_8);
+
+ /**
+ * Thread responsible for carrying out the requested operations
+ */
+ Worker worker = new Worker(this);
+ worker.start();
+
+ try {
+ /**
+ * The flow of execution will wait for the thread to die or wait until the
+ * given timeout has expired.
+ *
+ * The result of the worker, which is determined by the exit code of the worker,
+ * will tell us if the operation was completed successfully or it the operation
+ * failed.
+ */
+ worker.join(this.shellTimeout);
+
+ /**
+ * The operation could not be completed before the timeout occurred.
+ */
+ if (worker.exit == -911) {
+
+ try {
+ this.proc.destroy();
+ } catch (Exception e) {
+ }
+
+ closeQuietly(this.inputStream);
+ closeQuietly(this.errorStream);
+ closeQuietly(this.outputStream);
+
+ throw new TimeoutException(this.error);
+ }
+ /**
+ * Root access denied?
+ */
+ else if (worker.exit == -42) {
+
+ try {
+ this.proc.destroy();
+ } catch (Exception e) {
+ }
+
+ closeQuietly(this.inputStream);
+ closeQuietly(this.errorStream);
+ closeQuietly(this.outputStream);
+
+ throw new RootDeniedException("Root Access Denied");
+ }
+ /**
+ * Normal exit
+ */
+ else {
+ /**
+ * The shell is open.
+ *
+ * Start two threads, one to handle the input and one to handle the output.
+ *
+ * input, and output are runnables that the threads execute.
+ */
+ Thread si = new Thread(this.input, "Shell Input");
+ si.setPriority(Thread.NORM_PRIORITY);
+ si.start();
+
+ Thread so = new Thread(this.output, "Shell Output");
+ so.setPriority(Thread.NORM_PRIORITY);
+ so.start();
+ }
+ } catch (InterruptedException ex) {
+ worker.interrupt();
+ Thread.currentThread().interrupt();
+ throw new TimeoutException();
+ }
+ }
+
+
+ public Command add(Command command) throws IOException {
+ if (this.close) {
+ throw new IllegalStateException(
+ "Unable to add commands to a closed shell");
+ }
+
+ if(command.used) {
+ //The command has been used, don't re-use...
+ throw new IllegalStateException(
+ "This command has already been executed. (Don't re-use command instances.)");
+ }
+
+ while (this.isCleaning) {
+ //Don't add commands while cleaning
+ }
+
+ this.commands.add(command);
+
+ this.notifyThreads();
+
+ return command;
+ }
+
+ public final void useCWD(Context context) throws IOException, TimeoutException, RootDeniedException {
+ add(
+ new Command(
+ -1,
+ false,
+ "cd " + context.getApplicationInfo().dataDir)
+ );
+ }
+
+ private void cleanCommands() {
+ this.isCleaning = true;
+ int toClean = Math.abs(this.maxCommands - (this.maxCommands / 4));
+ RootShell.log("Cleaning up: " + toClean);
+
+ this.commands.subList(0, toClean).clear();
+
+ this.read = this.commands.size() - 1;
+ this.write = this.commands.size() - 1;
+ this.isCleaning = false;
+ }
+
+ private void closeQuietly(final Reader input) {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (Exception ignore) {
+ }
+ }
+
+ private void closeQuietly(final Writer output) {
+ try {
+ if (output != null) {
+ output.close();
+ }
+ } catch (Exception ignore) {
+ }
+ }
+
+ public void close() throws IOException {
+ RootShell.log("Request to close shell!");
+
+ int count = 0;
+ while (isExecuting) {
+ RootShell.log("Waiting on shell to finish executing before closing...");
+ count++;
+
+ //fail safe
+ if (count > 10000) {
+ break;
+ }
+
+ }
+
+ synchronized (this.commands) {
+ /**
+ * instruct the two threads monitoring input and output
+ * of the shell to close.
+ */
+ this.close = true;
+ this.notifyThreads();
+ }
+
+ RootShell.log("Shell Closed!");
+
+ if (this == Shell.rootShell) {
+ Shell.rootShell = null;
+ } else if (this == Shell.shell) {
+ Shell.shell = null;
+ } else if (this == Shell.customShell) {
+ Shell.customShell = null;
+ }
+ }
+
+ public static void closeCustomShell() throws IOException {
+ RootShell.log("Request to close custom shell!");
+
+ if (Shell.customShell == null) {
+ return;
+ }
+
+ Shell.customShell.close();
+ }
+
+ public static void closeRootShell() throws IOException {
+ RootShell.log("Request to close root shell!");
+
+ if (Shell.rootShell == null) {
+ return;
+ }
+ Shell.rootShell.close();
+ }
+
+ public static void closeShell() throws IOException {
+ RootShell.log("Request to close normal shell!");
+
+ if (Shell.shell == null) {
+ return;
+ }
+ Shell.shell.close();
+ }
+
+ public static void closeAll() throws IOException {
+ RootShell.log("Request to close all shells!");
+
+ Shell.closeShell();
+ Shell.closeRootShell();
+ Shell.closeCustomShell();
+ }
+
+ public int getCommandQueuePosition(Command cmd) {
+ return this.commands.indexOf(cmd);
+ }
+
+ public String getCommandQueuePositionString(Command cmd) {
+ return "Command is in position " + getCommandQueuePosition(cmd) + " currently executing command at position " + this.write + " and the number of commands is " + commands.size();
+ }
+
+ public static Shell getOpenShell() {
+ if (Shell.customShell != null) {
+ return Shell.customShell;
+ } else if (Shell.rootShell != null) {
+ return Shell.rootShell;
+ } else {
+ return Shell.shell;
+ }
+ }
+
+ /**
+ * From libsuperuser.
+ *
+ *
+ * Detects the version of the su binary installed (if any), if supported
+ * by the binary. Most binaries support two different version numbers,
+ * the public version that is displayed to users, and an internal
+ * version number that is used for version number comparisons. Returns
+ * null if su not available or retrieving the version isn't supported.
+ *
+ *
+ * Note that su binary version and GUI (APK) version can be completely
+ * different.
+ *
+ *
+ * This function caches its result to improve performance on multiple
+ * calls
+ *
+ *
+ * @param internal Request human-readable version or application
+ * internal version
+ * @return String containing the su version or null
+ */
+ private synchronized String getSuVersion(boolean internal) {
+ int idx = internal ? 0 : 1;
+ if (suVersion[idx] == null) {
+ String version = null;
+
+ // Replace libsuperuser:Shell.run with manual process execution
+ Process process;
+ try {
+ process = Runtime.getRuntime().exec(internal ? "su -V" : "su -v", null);
+ process.waitFor();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ // From libsuperuser:StreamGobbler
+ List stdout = new ArrayList();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ try {
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ stdout.add(line);
+ }
+ } catch (IOException e) {
+ }
+ // make sure our stream is closed and resources will be freed
+ try {
+ reader.close();
+ } catch (IOException e) {
+ }
+
+ process.destroy();
+
+ if (stdout != null) {
+ for (String line : stdout) {
+ if (!internal) {
+ if (line.contains(".")) {
+ version = line;
+ break;
+ }
+ } else {
+ try {
+ if (Integer.parseInt(line) > 0) {
+ version = line;
+ break;
+ }
+ } catch (NumberFormatException e) {
+ }
+ }
+ }
+ }
+
+ suVersion[idx] = version;
+ }
+ return suVersion[idx];
+ }
+
+ public static boolean isShellOpen() {
+ return Shell.shell == null;
+ }
+
+ public static boolean isCustomShellOpen() {
+ return Shell.customShell == null;
+ }
+
+ public static boolean isRootShellOpen() {
+ return Shell.rootShell == null;
+ }
+
+ public static boolean isAnyShellOpen() {
+ return Shell.shell != null || Shell.rootShell != null || Shell.customShell != null;
+ }
+
+ /**
+ * From libsuperuser.
+ *
+ * Detect if SELinux is set to enforcing, caches result
+ *
+ * @return true if SELinux set to enforcing, or false in the case of
+ * permissive or not present
+ */
+ public synchronized boolean isSELinuxEnforcing() {
+ if (isSELinuxEnforcing == null) {
+ Boolean enforcing = null;
+
+ // First known firmware with SELinux built-in was a 4.2 (17)
+ // leak
+ if (android.os.Build.VERSION.SDK_INT >= 17) {
+
+ // Detect enforcing through sysfs, not always present
+ File f = new File("/sys/fs/selinux/enforce");
+ if (f.exists()) {
+ try {
+ InputStream is = new FileInputStream("/sys/fs/selinux/enforce");
+ try {
+ enforcing = (is.read() == '1');
+ } finally {
+ is.close();
+ }
+ } catch (Exception e) {
+ }
+ }
+
+ // 4.4+ builds are enforcing by default, take the gamble
+ if (enforcing == null) {
+ enforcing = (android.os.Build.VERSION.SDK_INT >= 19);
+ }
+ }
+
+ if (enforcing == null) {
+ enforcing = false;
+ }
+
+ isSELinuxEnforcing = enforcing;
+ }
+ return isSELinuxEnforcing;
+ }
+
+ /**
+ * Runnable to write commands to the open shell.
+ *
+ * When writing commands we stay in a loop and wait for new
+ * commands to added to "commands"
+ *
+ * The notification of a new command is handled by the method add in this class
+ */
+ private final Runnable input = new Runnable() {
+ public void run() {
+
+ try {
+ while (true) {
+
+ synchronized (commands) {
+ /**
+ * While loop is used in the case that notifyAll is called
+ * and there are still no commands to be written, a rare
+ * case but one that could happen.
+ */
+ while (!close && write >= commands.size()) {
+ isExecuting = false;
+ commands.wait();
+ }
+ }
+
+ if (write >= maxCommands) {
+
+ /**
+ * wait for the read to catch up.
+ */
+ while (read != write) {
+ RootShell.log("Waiting for read and write to catch up before cleanup.");
+ }
+ /**
+ * Clean up the commands, stay neat.
+ */
+ cleanCommands();
+ }
+
+ /**
+ * Write the new command
+ *
+ * We write the command followed by the token to indicate
+ * the end of the command execution
+ */
+ if (write < commands.size()) {
+ isExecuting = true;
+ Command cmd = commands.get(write);
+ cmd.startExecution();
+ RootShell.log("Executing: " + cmd.getCommand() + " with context: " + shellContext);
+
+ //write the command
+ outputStream.write(cmd.getCommand());
+ outputStream.flush();
+
+ //write the token...
+ String line = "\necho " + token + " " + totalExecuted + " $?\n";
+ outputStream.write(line);
+ outputStream.flush();
+
+ write++;
+ totalExecuted++;
+ } else if (close) {
+ /**
+ * close the thread, the shell is closing.
+ */
+ isExecuting = false;
+ outputStream.write("\nexit 0\n");
+ outputStream.flush();
+ RootShell.log("Closing shell");
+ return;
+ }
+ }
+ } catch (IOException | InterruptedException e) {
+ RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
+ }
+ finally {
+ write = 0;
+ closeQuietly(outputStream);
+ }
+ }
+ };
+
+ protected void notifyThreads() {
+ Thread t = new Thread() {
+ public void run() {
+ synchronized (commands) {
+ commands.notifyAll();
+ }
+ }
+ };
+
+ t.start();
+ }
+
+ /**
+ * Runnable to monitor the responses from the open shell.
+ *
+ * This include the output and error stream
+ */
+ private final Runnable output = new Runnable() {
+ public void run() {
+ try {
+ Command command = null;
+
+ //as long as there is something to read, we will keep reading.
+ while (!close || inputStream.ready() || read < commands.size()) {
+ isReading = false;
+ String outputLine = inputStream.readLine();
+ isReading = true;
+
+ /**
+ * If we receive EOF then the shell closed?
+ */
+ if (outputLine == null) {
+ break;
+ }
+
+ if (command == null) {
+ if (read >= commands.size()) {
+ if (close) {
+ break;
+ }
+
+ continue;
+ }
+
+ command = commands.get(read);
+ }
+
+ /**
+ * trying to determine if all commands have been completed.
+ *
+ * if the token is present then the command has finished execution.
+ */
+ int pos = -1;
+
+ pos = outputLine.indexOf(token);
+
+ if (pos == -1) {
+ /**
+ * send the output for the implementer to process
+ */
+ command.output(command.id, outputLine);
+ } else if (pos > 0) {
+ /**
+ * token is suffix of output, send output part to implementer
+ */
+ RootShell.log("Found token, line: " + outputLine);
+ command.output(command.id, outputLine.substring(0, pos));
+ }
+
+ if (pos >= 0) {
+ outputLine = outputLine.substring(pos);
+ String[] fields = outputLine.split(" ");
+
+ if (fields.length >= 2 && fields[1] != null) {
+ int id = 0;
+
+ try {
+ id = Integer.parseInt(fields[1]);
+ } catch (NumberFormatException e) {
+ }
+
+ int exitCode = -1;
+
+ try {
+ exitCode = Integer.parseInt(fields[2]);
+ } catch (NumberFormatException e) {
+ }
+
+ if (id == totalRead) {
+ processErrors(command);
+
+
+ /**
+ * wait for output to be processed...
+ *
+ */
+ int iterations = 0;
+ while (command.totalOutput > command.totalOutputProcessed) {
+
+ if(iterations == 0)
+ {
+ iterations++;
+ RootShell.log("Waiting for output to be processed. " + command.totalOutputProcessed + " Of " + command.totalOutput);
+ }
+
+ try {
+
+ synchronized (this)
+ {
+ this.wait(2000);
+ }
+ } catch (Exception e) {
+ RootShell.log(e.getMessage());
+ }
+ }
+
+ RootShell.log("Read all output");
+
+ command.setExitCode(exitCode);
+ command.commandFinished();
+
+ command = null;
+
+ read++;
+ totalRead++;
+ continue;
+ }
+ }
+ }
+ }
+
+ try {
+ proc.waitFor();
+ proc.destroy();
+ } catch (Exception e) {
+ }
+
+ while (read < commands.size()) {
+ if (command == null) {
+ command = commands.get(read);
+ }
+
+ if(command.totalOutput < command.totalOutputProcessed)
+ {
+ command.terminated("All output not processed!");
+ command.terminated("Did you forget the super.commandOutput call or are you waiting on the command object?");
+ }
+ else
+ {
+ command.terminated("Unexpected Termination.");
+ }
+
+ command = null;
+ read++;
+ }
+
+ read = 0;
+
+ } catch (IOException e) {
+ RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
+ } finally {
+ closeQuietly(outputStream);
+ closeQuietly(errorStream);
+ closeQuietly(inputStream);
+
+ RootShell.log("Shell destroyed");
+ isClosed = true;
+ isReading = false;
+ }
+ }
+ };
+
+ public void processErrors(Command command) {
+ try {
+ while (errorStream.ready() && command != null) {
+ String line = errorStream.readLine();
+
+ /**
+ * If we recieve EOF then the shell closed?
+ */
+ if (line == null) {
+ break;
+ }
+
+ /**
+ * send the output for the implementer to process
+ */
+ command.output(command.id, line);
+ }
+ } catch (Exception e) {
+ RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
+ }
+ }
+
+ public static Command runRootCommand(Command command) throws IOException, TimeoutException, RootDeniedException {
+ return Shell.startRootShell().add(command);
+ }
+
+ public static Command runCommand(Command command) throws IOException, TimeoutException {
+ return Shell.startShell().add(command);
+ }
+
+ public static Shell startRootShell() throws IOException, TimeoutException, RootDeniedException {
+ return Shell.startRootShell(0, 3);
+ }
+
+ public static Shell startRootShell(int timeout) throws IOException, TimeoutException, RootDeniedException {
+ return Shell.startRootShell(timeout, 3);
+ }
+
+ public static Shell startRootShell(int timeout, int retry) throws IOException, TimeoutException, RootDeniedException {
+ return Shell.startRootShell(timeout, Shell.defaultContext, retry);
+ }
+
+ public static Shell startRootShell(int timeout, ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException {
+ // keep prompting the user until they accept for x amount of times...
+ int retries = 0;
+
+ if (Shell.rootShell == null) {
+
+ RootShell.log("Starting Root Shell!");
+ String cmd = "su";
+ while (Shell.rootShell == null) {
+ try {
+ RootShell.log("Trying to open Root Shell, attempt #" + retries);
+ Shell.rootShell = new Shell(cmd, ShellType.ROOT, shellContext, timeout);
+ } catch (IOException e) {
+ if (retries++ >= retry) {
+ RootShell.log("IOException, could not start shell");
+ throw e;
+ }
+ } catch (RootDeniedException e) {
+ if (retries++ >= retry) {
+ RootShell.log("RootDeniedException, could not start shell");
+ throw e;
+ }
+ } catch (TimeoutException e) {
+ if (retries++ >= retry) {
+ RootShell.log("TimeoutException, could not start shell");
+ throw e;
+ }
+ }
+ }
+ } else if (Shell.rootShell.shellContext != shellContext) {
+ try {
+ RootShell.log("Context is different than open shell, switching context... " + Shell.rootShell.shellContext + " VS " + shellContext);
+ Shell.rootShell.switchRootShellContext(shellContext);
+ } catch (IOException e) {
+ if (retries++ >= retry) {
+ RootShell.log("IOException, could not switch context!");
+ throw e;
+ }
+ } catch (RootDeniedException e) {
+ if (retries++ >= retry) {
+ RootShell.log("RootDeniedException, could not switch context!");
+ throw e;
+ }
+ } catch (TimeoutException e) {
+ if (retries++ >= retry) {
+ RootShell.log("TimeoutException, could not switch context!");
+ throw e;
+ }
+ }
+ } else {
+ RootShell.log("Using Existing Root Shell!");
+ }
+
+ return Shell.rootShell;
+ }
+
+ public static Shell startCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException {
+ return Shell.startCustomShell(shellPath, 0);
+ }
+
+ public static Shell startCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException {
+
+ if (Shell.customShell == null) {
+ RootShell.log("Starting Custom Shell!");
+ Shell.customShell = new Shell(shellPath, ShellType.CUSTOM, ShellContext.NORMAL, timeout);
+ } else {
+ RootShell.log("Using Existing Custom Shell!");
+ }
+
+ return Shell.customShell;
+ }
+
+ public static Shell startShell() throws IOException, TimeoutException {
+ return Shell.startShell(0);
+ }
+
+ public static Shell startShell(int timeout) throws IOException, TimeoutException {
+
+ try {
+ if (Shell.shell == null) {
+ RootShell.log("Starting Shell!");
+ Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout);
+ } else {
+ RootShell.log("Using Existing Shell!");
+ }
+ return Shell.shell;
+ } catch (RootDeniedException e) {
+ //Root Denied should never be thrown.
+ throw new IOException();
+ }
+ }
+
+ public Shell switchRootShellContext(ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
+ if (this.shellType == ShellType.ROOT) {
+ try {
+ Shell.closeRootShell();
+ } catch (Exception e) {
+ RootShell.log("Problem closing shell while trying to switch context...");
+ }
+
+ //create new root shell with new context...
+
+ return Shell.startRootShell(this.shellTimeout, shellContext, 3);
+ } else {
+ //can only switch context on a root shell...
+ RootShell.log("Can only switch context on a root shell!");
+ return this;
+ }
+ }
+
+ protected static class Worker extends Thread {
+
+ public int exit = -911;
+
+ public Shell shell;
+
+ private Worker(Shell shell) {
+ this.shell = shell;
+ }
+
+ public void run() {
+
+ /**
+ * Trying to open the shell.
+ *
+ * We echo "Started" and we look for it in the output.
+ *
+ * If we find the output then the shell is open and we return.
+ *
+ * If we do not find it then we determine the error and report
+ * it by setting the value of the variable exit
+ */
+ try {
+ shell.outputStream.write("echo Started\n");
+ shell.outputStream.flush();
+
+ while (true) {
+ String line = shell.inputStream.readLine();
+
+ if (line == null) {
+ throw new EOFException();
+ } else if ("".equals(line)) {
+ continue;
+ } else if ("Started".equals(line)) {
+ this.exit = 1;
+ setShellOom();
+ break;
+ }
+
+ shell.error = "unknown error occurred.";
+ }
+ } catch (IOException e) {
+ exit = -42;
+ if (e.getMessage() != null) {
+ shell.error = e.getMessage();
+ } else {
+ shell.error = "RootAccess denied?.";
+ }
+ }
+
+ }
+
+ /*
+ * setOom for shell processes (sh and su if root shell) and discard outputs
+ * Negative values make the process LESS likely to be killed in an OOM situation
+ * Positive values make the process MORE likely to be killed in an OOM situation
+ */
+ private void setShellOom() {
+ try {
+ Class> processClass = shell.proc.getClass();
+ Field field;
+ try {
+ field = processClass.getDeclaredField("pid");
+ } catch (NoSuchFieldException e) {
+ field = processClass.getDeclaredField("id");
+ }
+ field.setAccessible(true);
+ int pid = (Integer) field.get(shell.proc);
+ shell.outputStream.write("(echo -17 > /proc/" + pid + "/oom_adj) &> /dev/null\n");
+ shell.outputStream.write("(echo -17 > /proc/$$/oom_adj) &> /dev/null\n");
+ shell.outputStream.flush();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/Constants.java b/app/src/main/java/com/stericson/roottools/Constants.java
new file mode 100644
index 0000000..f15ee7e
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/Constants.java
@@ -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;
+
+}
diff --git a/app/src/main/java/com/stericson/roottools/RootTools.java b/app/src/main/java/com/stericson/roottools/RootTools.java
new file mode 100644
index 0000000..6b80873
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/RootTools.java
@@ -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.
+ *
+ * All methods and Variables that the developer may need to have access to should be here.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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 boolean 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 List containing the paths the binary was found at.
+ */
+ public static List 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.
+ *
+ *
+ * @return null If we cannot return the list of applets.
+ */
+ public static List getBusyBoxApplets() throws Exception {
+ return RootTools.getBusyBoxApplets("");
+ }
+
+ /**
+ * This will return an List of Strings. Each string represents an applet available from BusyBox.
+ *
+ *
+ * @param path Path to the busybox binary that you want the list of applets from.
+ * @return null If we cannot return the list of applets.
+ */
+ public static List 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 String to Indicate the path to the shell that you want to open.
+ * @param timeout an int 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 String 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
+ *
+ * These will provide you with any information you need to work with the mount points.
+ *
+ * @return ArrayList an ArrayList of the class Mount.
+ * @throws Exception if we cannot return the mount points.
+ */
+ public static ArrayList getMounts() throws Exception {
+ return getInternals().getMounts();
+ }
+
+ /**
+ * This will tell you how the specified mount is mounted. rw, ro, etc...
+ *
+ *
+ * @param path The mount you want to check
+ * @return String 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 List A List of Strings representing the environment variable $PATH
+ */
+ public static List 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 boolean to Indicate whether or not you want to open a root shell or a standard shell
+ * @param timeout an int 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 int 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 boolean to Indicate whether or not you want to open a root shell or a standard shell
+ * @param timeout an int 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 boolean 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 boolean to Indicate whether or not you want to open a root shell or a standard shell
+ * @param timeout an int 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 boolean 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.
+ *
+ *
+ * @param file path to the file to get the Symlink for. (must have absolute path)
+ * @return String 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
+ *
+ * These will provide you with any Symlinks in the given path.
+ *
+ * @param path path to search for Symlinks.
+ * @return ArrayList an ArrayList of the class Symlink.
+ * @throws Exception if we cannot return the Symlinks.
+ */
+ public static ArrayList 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 true if the Update will fit on SDCard, false if not enough
+ * space on SDCard. Will also return false, 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 Context
+ * @param sourceId resource id; typically R.raw.id
+ * @param destName destination file name; appended to /data/data/app.package/files/
+ * @param mode chmod value for this file
+ * @return a boolean 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 Context
+ * @param sourceId resource id; typically R.raw.id
+ * @param binaryName destination file name; appended to /data/data/app.package/files/
+ * @return a boolean 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 Context
+ * @param binaryName binary file name; appended to /data/data/app.package/files/
+ * @return a boolean 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
+ *
+ *
+ * @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 true 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
+ *
+ *
+ * @param applet The applet to check for.
+ * @return true if applet is available, false otherwise.
+ */
+ public static boolean isAppletAvailable(String applet) {
+ return RootTools.isAppletAvailable(applet, "");
+ }
+ /**
+ * @return true 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 true 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 true 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 true 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 true 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 true 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.
+ *
+ * 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 boolean 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 Context
+ * @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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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)
+ *
+ * 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.
+ *
+ * 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;
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java b/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java
new file mode 100644
index 0000000..9d36748
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java
@@ -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 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;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/containers/Mount.java b/app/src/main/java/com/stericson/roottools/containers/Mount.java
new file mode 100644
index 0000000..ce455f2
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/containers/Mount.java
@@ -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 mFlags;
+
+ public Mount(File device, File path, String type, String flagsStr)
+ {
+ mDevice = device;
+ mMountPoint = path;
+ mType = type;
+ mFlags = new LinkedHashSet(Arrays.asList(flagsStr.split(",")));
+ }
+
+ public File getDevice()
+ {
+ return mDevice;
+ }
+
+ public File getMountPoint()
+ {
+ return mMountPoint;
+ }
+
+ public String getType()
+ {
+ return mType;
+ }
+
+ public Set getFlags()
+ {
+ return mFlags;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s on %s type %s %s", mDevice, mMountPoint, mType, mFlags);
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/containers/Permissions.java b/app/src/main/java/com/stericson/roottools/containers/Permissions.java
new file mode 100644
index 0000000..51aae5f
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/containers/Permissions.java
@@ -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;
+ }
+
+
+}
diff --git a/app/src/main/java/com/stericson/roottools/containers/Symlink.java b/app/src/main/java/com/stericson/roottools/containers/Symlink.java
new file mode 100644
index 0000000..b811b1a
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/containers/Symlink.java
@@ -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;
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/internal/Installer.java b/app/src/main/java/com/stericson/roottools/internal/Installer.java
new file mode 100644
index 0000000..54d57ae
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/internal/Installer.java
@@ -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 R.raw.id
+ * @param destName destination file name; appended to /data/data/app.package/files/
+ * @param mode chmod value for this file
+ * @return a boolean 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());
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java b/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java
new file mode 100644
index 0000000..6b52e20
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java
@@ -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 mounts;
+ protected static ArrayList 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);
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/internal/Remounter.java b/app/src/main/java/com/stericson/roottools/internal/Remounter.java
new file mode 100644
index 0000000..f054962
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/internal/Remounter.java
@@ -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.
+ *
+ * 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 boolean 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 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();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java b/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java
new file mode 100644
index 0000000..f5b8257
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java
@@ -0,0 +1,1342 @@
+/*
+ * 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.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.StatFs;
+import android.util.Log;
+
+import com.stericson.rootshell.RootShell;
+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 com.stericson.roottools.containers.Permissions;
+import com.stericson.roottools.containers.Symlink;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+
+public final class RootToolsInternalMethods {
+
+ // --------------------
+ // # Internal methods #
+ // --------------------
+
+ protected RootToolsInternalMethods() {
+ }
+
+ public static void getInstance() {
+ //this will allow RootTools to be the only one to get an instance of this class.
+ RootTools.setRim(new RootToolsInternalMethods());
+ }
+
+ public Permissions getPermissions(String line) {
+
+ String[] lineArray = line.split(" ");
+ String rawPermissions = lineArray[0];
+
+ if (rawPermissions.length() == 10
+ && (rawPermissions.charAt(0) == '-'
+ || rawPermissions.charAt(0) == 'd' || rawPermissions
+ .charAt(0) == 'l')
+ && (rawPermissions.charAt(1) == '-' || rawPermissions.charAt(1) == 'r')
+ && (rawPermissions.charAt(2) == '-' || rawPermissions.charAt(2) == 'w')) {
+ RootTools.log(rawPermissions);
+
+ Permissions permissions = new Permissions();
+
+ permissions.setType(rawPermissions.substring(0, 1));
+
+ RootTools.log(permissions.getType());
+
+ permissions.setUserPermissions(rawPermissions.substring(1, 4));
+
+ RootTools.log(permissions.getUserPermissions());
+
+ permissions.setGroupPermissions(rawPermissions.substring(4, 7));
+
+ RootTools.log(permissions.getGroupPermissions());
+
+ permissions.setOtherPermissions(rawPermissions.substring(7, 10));
+
+ RootTools.log(permissions.getOtherPermissions());
+
+ StringBuilder finalPermissions = new StringBuilder();
+ finalPermissions.append(parseSpecialPermissions(rawPermissions));
+ finalPermissions.append(parsePermissions(permissions.getUserPermissions()));
+ finalPermissions.append(parsePermissions(permissions.getGroupPermissions()));
+ finalPermissions.append(parsePermissions(permissions.getOtherPermissions()));
+
+ permissions.setPermissions(Integer.parseInt(finalPermissions.toString()));
+
+ return permissions;
+ }
+
+ return null;
+ }
+
+ public int parsePermissions(String permission) {
+ permission = permission.toLowerCase(Locale.US);
+ int tmp;
+ if (permission.charAt(0) == 'r') {
+ tmp = 4;
+ } else {
+ tmp = 0;
+ }
+
+ RootTools.log("permission " + tmp);
+ RootTools.log("character " + permission.charAt(0));
+
+ if (permission.charAt(1) == 'w') {
+ tmp += 2;
+ } else {
+ tmp += 0;
+ }
+
+ RootTools.log("permission " + tmp);
+ RootTools.log("character " + permission.charAt(1));
+
+ if (permission.charAt(2) == 'x' || permission.charAt(2) == 's'
+ || permission.charAt(2) == 't') {
+ tmp += 1;
+ } else {
+ tmp += 0;
+ }
+
+ RootTools.log("permission " + tmp);
+ RootTools.log("character " + permission.charAt(2));
+
+ return tmp;
+ }
+
+ public int parseSpecialPermissions(String permission) {
+ int tmp = 0;
+ if (permission.charAt(2) == 's') {
+ tmp += 4;
+ }
+
+ if (permission.charAt(5) == 's') {
+ tmp += 2;
+ }
+
+ if (permission.charAt(8) == 't') {
+ tmp += 1;
+ }
+
+ RootTools.log("special permissions " + tmp);
+
+ return tmp;
+ }
+
+ /**
+ * 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 boolean copyFile(String source, String destination, boolean remountAsRw,
+ boolean preserveFileAttributes) {
+
+ Command command = null;
+ boolean result = true;
+
+ try {
+ // mount destination as rw before writing to it
+ if (remountAsRw) {
+ RootTools.remount(destination, "RW");
+ }
+
+ // if cp is available and has appropriate permissions
+ if (checkUtil("cp")) {
+ RootTools.log("cp command is available!");
+
+ if (preserveFileAttributes) {
+ command = new Command(0, false, "cp -fp " + source + " " + destination);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ //ensure that the file was copied, an exitcode of zero means success
+ result = command.getExitCode() == 0;
+
+ } else {
+ command = new Command(0, false, "cp -f " + source + " " + destination);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ //ensure that the file was copied, an exitcode of zero means success
+ result = command.getExitCode() == 0;
+
+ }
+ } else {
+ if (checkUtil("busybox") && hasUtil("cp", "busybox")) {
+ RootTools.log("busybox cp command is available!");
+
+ if (preserveFileAttributes) {
+ command = new Command(0, false, "busybox cp -fp " + source + " " + destination);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ } else {
+ command = new Command(0, false, "busybox cp -f " + source + " " + destination);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ }
+ } else { // if cp is not available use cat
+ // if cat is available and has appropriate permissions
+ if (checkUtil("cat")) {
+ RootTools.log("cp is not available, use cat!");
+
+ int filePermission = -1;
+ if (preserveFileAttributes) {
+ // get permissions of source before overwriting
+ Permissions permissions = getFilePermissionsSymlinks(source);
+ filePermission = permissions.getPermissions();
+ }
+
+ // copy with cat
+ command = new Command(0, false, "cat " + source + " > " + destination);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ if (preserveFileAttributes) {
+ // set premissions of source to destination
+ command = new Command(0, false, "chmod " + filePermission + " " + destination);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+ }
+ } else {
+ result = false;
+ }
+ }
+ }
+
+ // mount destination back to ro
+ if (remountAsRw) {
+ RootTools.remount(destination, "RO");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ result = false;
+ }
+
+ if (command != null) {
+ //ensure that the file was copied, an exitcode of zero means success
+ result = command.getExitCode() == 0;
+ }
+
+ return result;
+ }
+
+ /**
+ * 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 boolean checkUtil(String util) {
+ List foundPaths = RootShell.findBinary(util, true);
+ if (foundPaths.size() > 0) {
+
+ for (String path : foundPaths) {
+ Permissions permissions = RootTools
+ .getFilePermissionsSymlinks(path + "/" + util);
+
+ if (permissions != null) {
+ String permission;
+
+ if (Integer.toString(permissions.getPermissions()).length() > 3) {
+ permission = Integer.toString(permissions.getPermissions()).substring(1);
+ } else {
+ permission = Integer.toString(permissions.getPermissions());
+ }
+
+ if (permission.equals("755") || permission.equals("777")
+ || permission.equals("775")) {
+ RootTools.utilPath = path + "/" + util;
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+
+ }
+
+ /**
+ * 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 boolean deleteFileOrDirectory(String target, boolean remountAsRw) {
+ boolean result = true;
+
+ try {
+ // mount destination as rw before writing to it
+ if (remountAsRw) {
+ RootTools.remount(target, "RW");
+ }
+
+ if (hasUtil("rm", "toolbox")) {
+ RootTools.log("rm command is available!");
+
+ Command command = new Command(0, false, "rm -r " + target);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ if (command.getExitCode() != 0) {
+ RootTools.log("target not exist or unable to delete file");
+ result = false;
+ }
+ } else {
+ if (checkUtil("busybox") && hasUtil("rm", "busybox")) {
+ RootTools.log("busybox rm command is available!");
+
+ Command command = new Command(0, false, "busybox rm -rf " + target);
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ if (command.getExitCode() != 0) {
+ RootTools.log("target not exist or unable to delete file");
+ result = false;
+ }
+ }
+ }
+
+ // mount destination back to ro
+ if (remountAsRw) {
+ RootTools.remount(target, "RO");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ result = false;
+ }
+
+ return result;
+ }
+
+ /**
+ * 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 void fixUtil(String util, String utilPath) {
+ try {
+ RootTools.remount("/system", "rw");
+
+ List foundPaths = RootShell.findBinary(util, true);
+
+ if (foundPaths.size() > 0) {
+ for (String path : foundPaths) {
+ Command command = new Command(0, false, utilPath + " rm " + path + "/" + util);
+ RootShell.getShell(true).add(command);
+ commandWait(RootShell.getShell(true), command);
+
+ }
+
+ Command command = new Command(0, false, utilPath + " ln -s " + utilPath + " /system/bin/" + util, utilPath + " chmod 0755 /system/bin/" + util);
+ RootShell.getShell(true).add(command);
+ commandWait(RootShell.getShell(true), command);
+
+ }
+
+ RootTools.remount("/system", "ro");
+ } catch (Exception e) {
+ }
+ }
+
+ /**
+ * 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 boolean fixUtils(String[] utils) throws Exception {
+
+ for (String util : utils) {
+ if (!checkUtil(util)) {
+ if (checkUtil("busybox")) {
+ if (hasUtil(util, "busybox")) {
+ fixUtil(util, RootTools.utilPath);
+ }
+ } else {
+ if (checkUtil("toolbox")) {
+ if (hasUtil(util, "toolbox")) {
+ fixUtil(util, RootTools.utilPath);
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * This will return an List of Strings. Each string represents an applet available from BusyBox.
+ *
+ *
+ * @param path Path to the busybox binary that you want the list of applets from.
+ * @return null If we cannot return the list of applets.
+ */
+ public List getBusyBoxApplets(String path) throws Exception {
+
+ if (path != null && !path.endsWith("/") && !path.equals("")) {
+ path += "/";
+ } else if (path == null) {
+ //Don't know what the user wants to do...what am I pshycic?
+ throw new Exception("Path is null, please specifiy a path");
+ }
+
+ final List results = new ArrayList();
+
+ Command command = new Command(Constants.BBA, false, path + "busybox --list") {
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.BBA) {
+ if (!line.trim().equals("") && !line.trim().contains("not found") && !line.trim().contains("file busy")) {
+ results.add(line);
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+
+ //try without root first...
+ RootShell.getShell(false).add(command);
+ commandWait(RootShell.getShell(false), command);
+
+ if (results.size() <= 0) {
+ //try with root...
+
+ command = new Command(Constants.BBA, false, path + "busybox --list") {
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.BBA) {
+ if (!line.trim().equals("") && !line.trim().contains("not found") && !line.trim().contains("file busy")) {
+ results.add(line);
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+
+ RootShell.getShell(true).add(command);
+ commandWait(RootShell.getShell(true), command);
+ }
+
+ return results;
+ }
+
+ /**
+ * @return BusyBox version if found, "" if not found.
+ */
+ public String getBusyBoxVersion(String path) {
+
+ final StringBuilder version = new StringBuilder();
+
+ if (!path.equals("") && !path.endsWith("/")) {
+ path += "/";
+ }
+
+ try {
+ Command command = new Command(Constants.BBV, false, path + "busybox") {
+ @Override
+ public void commandOutput(int id, String line) {
+ line = line.trim();
+
+ boolean foundVersion = false;
+
+ if (id == Constants.BBV) {
+ RootTools.log("Version Output: " + line);
+
+ String[] temp = line.split(" ");
+
+ if (temp.length > 1 && temp[1].contains("v1.") && !foundVersion) {
+ foundVersion = true;
+ version.append(temp[1]);
+ RootTools.log("Found Version: " + version.toString());
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+
+ //try without root first
+ RootTools.log("Getting BusyBox Version without root");
+ Shell shell = RootTools.getShell(false);
+ shell.add(command);
+ commandWait(shell, command);
+
+ if (version.length() <= 0) {
+
+ command = new Command(Constants.BBV, false, path + "busybox") {
+ @Override
+ public void commandOutput(int id, String line) {
+ line = line.trim();
+
+ boolean foundVersion = false;
+
+ if (id == Constants.BBV) {
+ RootTools.log("Version Output: " + line);
+
+ String[] temp = line.split(" ");
+
+ if (temp.length > 1 && temp[1].contains("v1.") && !foundVersion) {
+ foundVersion = true;
+ version.append(temp[1]);
+ RootTools.log("Found Version: " + version.toString());
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+
+ RootTools.log("Getting BusyBox Version with root");
+ Shell rootShell = RootTools.getShell(true);
+ //Now look for it...
+ rootShell.add(command);
+ commandWait(rootShell, command);
+ }
+
+ } catch (Exception e) {
+ RootTools.log("BusyBox was not found, more information MAY be available with Debugging on.");
+ return "";
+ }
+
+ RootTools.log("Returning found version: " + version.toString());
+ return version.toString();
+ }
+
+ /**
+ * @return long Size, converted to kilobytes (from xxx or xxxm or xxxk etc.)
+ */
+ public long getConvertedSpace(String spaceStr) {
+ try {
+ double multiplier = 1.0;
+ char c;
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < spaceStr.length(); i++) {
+ c = spaceStr.charAt(i);
+ if (!Character.isDigit(c) && c != '.') {
+ if (c == 'm' || c == 'M') {
+ multiplier = 1024.0;
+ } else if (c == 'g' || c == 'G') {
+ multiplier = 1024.0 * 1024.0;
+ }
+ break;
+ }
+ sb.append(spaceStr.charAt(i));
+ }
+ return (long) Math.ceil(Double.valueOf(sb.toString()) * multiplier);
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ /**
+ * 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 String getInode(String file) {
+ try {
+ Command command = new Command(Constants.GI, false, "/data/local/ls -i " + file) {
+
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.GI) {
+ if (!line.trim().equals("") && Character.isDigit(line.trim().substring(0, 1).toCharArray()[0])) {
+ InternalVariables.inode = line.trim().split(" ")[0];
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ return InternalVariables.inode;
+ } catch (Exception ignore) {
+ return "";
+ }
+ }
+
+ public boolean isNativeToolsReady(int nativeToolsId, Context context) {
+ RootTools.log("Preparing Native Tools");
+ InternalVariables.nativeToolsReady = false;
+
+ Installer installer;
+ try {
+ installer = new Installer(context);
+ } catch (IOException ex) {
+ if (RootTools.debugMode) {
+ ex.printStackTrace();
+ }
+ return false;
+ }
+
+ if (installer.isBinaryInstalled("nativetools")) {
+ InternalVariables.nativeToolsReady = true;
+ } else {
+ InternalVariables.nativeToolsReady = installer.installBinary(nativeToolsId,
+ "nativetools", "700");
+ }
+ return InternalVariables.nativeToolsReady;
+ }
+
+ /**
+ * @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 Permissions getFilePermissionsSymlinks(String file) {
+ RootTools.log("Checking permissions for " + file);
+ if (RootTools.exists(file)) {
+ RootTools.log(file + " was found.");
+ try {
+
+ Command command = new Command(
+ Constants.FPS, false, "ls -l " + file,
+ "busybox ls -l " + file,
+ "/system/bin/failsafe/toolbox ls -l " + file,
+ "toolbox ls -l " + file) {
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.FPS) {
+ String symlink_final = "";
+
+ String[] lineArray = line.split(" ");
+ if (lineArray[0].length() != 10) {
+ super.commandOutput(id, line);
+ return;
+ }
+
+ RootTools.log("Line " + line);
+
+ try {
+ String[] symlink = line.split(" ");
+ if (symlink[symlink.length - 2].equals("->")) {
+ RootTools.log("Symlink found.");
+ symlink_final = symlink[symlink.length - 1];
+ }
+ } catch (Exception e) {
+ }
+
+ try {
+ InternalVariables.permissions = getPermissions(line);
+ if (InternalVariables.permissions != null) {
+ InternalVariables.permissions.setSymlink(symlink_final);
+ }
+ } catch (Exception e) {
+ RootTools.log(e.getMessage());
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ RootShell.getShell(true).add(command);
+ commandWait(RootShell.getShell(true), command);
+
+ return InternalVariables.permissions;
+
+ } catch (Exception e) {
+ RootTools.log(e.getMessage());
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * This will return an ArrayList of the class Mount. The class mount contains the following
+ * property's: device mountPoint type flags
+ *
+ * These will provide you with any information you need to work with the mount points.
+ *
+ * @return ArrayList an ArrayList of the class Mount.
+ * @throws Exception if we cannot return the mount points.
+ */
+ public ArrayList getMounts() throws Exception {
+
+ InternalVariables.mounts = new ArrayList<>();
+
+ if(null == InternalVariables.mounts || InternalVariables.mounts.isEmpty()) {
+ Shell shell = RootTools.getShell(true);
+
+ Command cmd = new Command(Constants.GET_MOUNTS,
+ false,
+ "cat /proc/mounts") {
+
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.GET_MOUNTS) {
+ RootTools.log(line);
+
+ String[] fields = line.split(" ");
+ InternalVariables.mounts.add(new Mount(new File(fields[0]), // device
+ new File(fields[1]), // mountPoint
+ fields[2], // fstype
+ fields[3] // flags
+ ));
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ shell.add(cmd);
+ this.commandWait(shell, cmd);
+ }
+
+ return InternalVariables.mounts;
+ }
+
+ /**
+ * This will tell you how the specified mount is mounted. rw, ro, etc...
+ *
+ *
+ * @param path mount you want to check
+ * @return String What the mount is mounted as.
+ * @throws Exception if we cannot determine how the mount is mounted.
+ */
+ public String getMountedAs(String path) throws Exception {
+ InternalVariables.mounts = getMounts();
+ String mp;
+ if (InternalVariables.mounts != null) {
+ for (Mount mount : InternalVariables.mounts) {
+
+ mp = mount.getMountPoint().getAbsolutePath();
+
+ if (mp.equals("/")) {
+ if (path.equals("/")) {
+ return (String) mount.getFlags().toArray()[0];
+ } else {
+ continue;
+ }
+ }
+
+ if (path.equals(mp) || path.startsWith(mp + "/")) {
+ RootTools.log((String) mount.getFlags().toArray()[0]);
+ return (String) mount.getFlags().toArray()[0];
+ }
+ }
+
+ throw new Exception();
+ } else {
+ throw new Exception();
+ }
+ }
+
+ /**
+ * 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 long getSpace(String path) {
+ InternalVariables.getSpaceFor = path;
+ boolean found = false;
+ RootTools.log("Looking for Space");
+ try {
+ final Command command = new Command(Constants.GS, false, "df " + path) {
+
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.GS) {
+ if (line.contains(InternalVariables.getSpaceFor.trim())) {
+ InternalVariables.space = line.split(" ");
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ } catch (Exception e) {
+ }
+
+ if (InternalVariables.space != null) {
+ RootTools.log("First Method");
+
+ for (String spaceSearch : InternalVariables.space) {
+
+ RootTools.log(spaceSearch);
+
+ if (found) {
+ return getConvertedSpace(spaceSearch);
+ } else if (spaceSearch.equals("used,")) {
+ found = true;
+ }
+ }
+
+ // Try this way
+ int count = 0, targetCount = 3;
+
+ RootTools.log("Second Method");
+
+ if (InternalVariables.space[0].length() <= 5) {
+ targetCount = 2;
+ }
+
+ for (String spaceSearch : InternalVariables.space) {
+
+ RootTools.log(spaceSearch);
+ if (spaceSearch.length() > 0) {
+ RootTools.log(spaceSearch + ("Valid"));
+ if (count == targetCount) {
+ return getConvertedSpace(spaceSearch);
+ }
+ count++;
+ }
+ }
+ }
+ RootTools.log("Returning -1, space could not be determined.");
+ return -1;
+ }
+
+ /**
+ * This will return a String that represent the symlink for a specified file.
+ *
+ *
+ * @param file file to get the Symlink for. (must have absolute path)
+ * @return String a String that represent the symlink for a specified file or an
+ * empty string if no symlink exists.
+ */
+ public String getSymlink(String file) {
+ RootTools.log("Looking for Symlink for " + file);
+
+ try {
+ final List results = new ArrayList();
+
+ Command command = new Command(Constants.GSYM, false, "ls -l " + file) {
+
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.GSYM) {
+ if (!line.trim().equals("")) {
+ results.add(line);
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ String[] symlink = results.get(0).split(" ");
+ if (symlink.length > 2 && symlink[symlink.length - 2].equals("->")) {
+ RootTools.log("Symlink found.");
+
+ String final_symlink;
+
+ if (!symlink[symlink.length - 1].equals("") && !symlink[symlink.length - 1].contains("/")) {
+ //We assume that we need to get the path for this symlink as it is probably not absolute.
+ List paths = RootShell.findBinary(symlink[symlink.length - 1], true);
+ if (paths.size() > 0) {
+ //We return the first found location.
+ final_symlink = paths.get(0) + symlink[symlink.length - 1];
+ } else {
+ //we couldnt find a path, return the symlink by itself.
+ final_symlink = symlink[symlink.length - 1];
+ }
+ } else {
+ final_symlink = symlink[symlink.length - 1];
+ }
+
+ return final_symlink;
+ }
+ } catch (Exception e) {
+ if (RootTools.debugMode) {
+ e.printStackTrace();
+ }
+ }
+
+ RootTools.log("Symlink not found");
+ return "";
+ }
+
+ /**
+ * This will return an ArrayList of the class Symlink. The class Symlink contains the following
+ * property's: path SymplinkPath
+ *
+ * These will provide you with any Symlinks in the given path.
+ *
+ * @param path path to search for Symlinks.
+ * @return ArrayList an ArrayList of the class Symlink.
+ * @throws Exception if we cannot return the Symlinks.
+ */
+ public ArrayList getSymlinks(String path) throws Exception {
+
+ // this command needs find
+ if (!checkUtil("find")) {
+ throw new Exception();
+ }
+
+ InternalVariables.symlinks = new ArrayList<>();
+
+ Command command = new Command(0, false, "find " + path + " -type l -exec ls -l {} \\;") {
+ @Override
+ public void commandOutput(int id, String line) {
+ if (id == Constants.GET_SYMLINKS) {
+ RootTools.log(line);
+
+ String[] fields = line.split(" ");
+ InternalVariables.symlinks.add(new Symlink(new File(fields[fields.length - 3]), // file
+ new File(fields[fields.length - 1]) // SymlinkPath
+ ));
+
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ Shell.startRootShell().add(command);
+ commandWait(Shell.startRootShell(), command);
+
+ if (InternalVariables.symlinks != null) {
+ return InternalVariables.symlinks;
+ } else {
+ throw new Exception();
+ }
+ }
+
+ /**
+ * 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 String getWorkingToolbox() {
+ if (RootTools.checkUtil("busybox")) {
+ return "busybox";
+ } else if (RootTools.checkUtil("toolbox")) {
+ return "toolbox";
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Checks if there is enough Space on SDCard
+ *
+ * @param updateSize size to Check (long)
+ * @return true if the Update will fit on SDCard, false if not enough
+ * space on SDCard. Will also return false, if the SDCard is not mounted as
+ * read/write
+ */
+ @SuppressWarnings("deprecation")
+ public boolean hasEnoughSpaceOnSdCard(long updateSize) {
+ RootTools.log("Checking SDcard size and that it is mounted as RW");
+ String status = Environment.getExternalStorageState();
+ if (!status.equals(Environment.MEDIA_MOUNTED)) {
+ return false;
+ }
+ File path = Environment.getExternalStorageDirectory();
+ StatFs stat = new StatFs(path.getPath());
+ long blockSize = 0;
+ long availableBlocks = 0;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ blockSize = stat.getBlockSize();
+ availableBlocks = stat.getAvailableBlocks();
+ } else {
+ blockSize = stat.getBlockSizeLong();
+ availableBlocks = stat.getAvailableBlocksLong();
+ }
+ return (updateSize < availableBlocks * blockSize);
+ }
+
+ /**
+ * 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 boolean hasUtil(final String util, final String box) {
+
+ InternalVariables.found = false;
+
+ // only for busybox and toolbox
+ if (!(box.endsWith("toolbox") || box.endsWith("busybox"))) {
+ return false;
+ }
+
+ try {
+
+ Command command = new Command(0, false, box.endsWith("toolbox") ? box + " " + util : box + " --list") {
+
+ @Override
+ public void commandOutput(int id, String line) {
+ if (box.endsWith("toolbox")) {
+ if (!line.contains("no such tool")) {
+ InternalVariables.found = true;
+ }
+ } else if (box.endsWith("busybox")) {
+ // go through all lines of busybox --list
+ if (line.contains(util)) {
+ RootTools.log("Found util!");
+ InternalVariables.found = true;
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ RootTools.getShell(true).add(command);
+ commandWait(RootTools.getShell(true), command);
+
+ if (InternalVariables.found) {
+ RootTools.log("Box contains " + util + " util!");
+ return true;
+ } else {
+ RootTools.log("Box does not contain " + util + " util!");
+ return false;
+ }
+ } catch (Exception e) {
+ RootTools.log(e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 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 Context
+ * @param sourceId resource id; typically R.raw.id
+ * @param destName destination file name; appended to /data/data/app.package/files/
+ * @param mode chmod value for this file
+ * @return a boolean which indicates whether or not we were able to create the new
+ * file.
+ */
+ public boolean installBinary(Context context, int sourceId, String destName, String mode) {
+ Installer installer;
+
+ try {
+ installer = new Installer(context);
+ } catch (IOException ex) {
+ if (RootTools.debugMode) {
+ ex.printStackTrace();
+ }
+ return false;
+ }
+
+ return (installer.installBinary(sourceId, destName, mode));
+ }
+
+ /**
+ * This method checks whether a binary is installed.
+ *
+ * @param context the current activity's Context
+ * @param binaryName binary file name; appended to /data/data/app.package/files/
+ * @return a boolean which indicates whether or not
+ * the binary already exists.
+ */
+ public boolean isBinaryAvailable(Context context, String binaryName) {
+ Installer installer;
+
+ try {
+ installer = new Installer(context);
+ } catch (IOException ex) {
+ if (RootTools.debugMode) {
+ ex.printStackTrace();
+ }
+ return false;
+ }
+
+ return (installer.isBinaryInstalled(binaryName));
+ }
+
+ /**
+ * This will let you know if an applet is available from BusyBox
+ *
+ *
+ * @param applet The applet to check for.
+ * @return true if applet is available, false otherwise.
+ */
+ public boolean isAppletAvailable(String applet, String binaryPath) {
+ try {
+ for (String aplet : getBusyBoxApplets(binaryPath)) {
+ if (aplet.equals(applet)) {
+ return true;
+ }
+ }
+ return false;
+ } catch (Exception e) {
+ RootTools.log(e.toString());
+ return false;
+ }
+ }
+
+ /**
+ * This method can be used to to check if a process is running
+ *
+ * @param processName name of process to check
+ * @return true if process was found
+ * @throws TimeoutException (Could not determine if the process is running)
+ */
+ public boolean isProcessRunning(final String processName) {
+
+ RootTools.log("Checks if process is running: " + processName);
+
+ InternalVariables.processRunning = false;
+
+ try {
+ Command command = new Command(0, false, "ps") {
+ @Override
+ public void commandOutput(int id, String line) {
+ if (line.contains(processName)) {
+ InternalVariables.processRunning = true;
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ RootTools.getShell(true).add(command);
+ commandWait(RootTools.getShell(true), command);
+
+ } catch (Exception e) {
+ RootTools.log(e.getMessage());
+ }
+
+ return InternalVariables.processRunning;
+ }
+
+ /**
+ * This method can be used to kill a running process
+ *
+ * @param processName name of process to kill
+ * @return true if process was found and killed successfully
+ */
+ public boolean killProcess(final String processName) {
+ RootTools.log("Killing process " + processName);
+
+ InternalVariables.pid_list = "";
+
+ //Assume that the process is running
+ InternalVariables.processRunning = true;
+
+ try {
+
+ Command command = new Command(0, false, "ps") {
+ @Override
+ public void commandOutput(int id, String line) {
+ if (line.contains(processName)) {
+ Matcher psMatcher = InternalVariables.psPattern.matcher(line);
+
+ try {
+ if (psMatcher.find()) {
+ String pid = psMatcher.group(1);
+
+ InternalVariables.pid_list += " " + pid;
+ InternalVariables.pid_list = InternalVariables.pid_list.trim();
+
+ RootTools.log("Found pid: " + pid);
+ } else {
+ RootTools.log("Matching in ps command failed!");
+ }
+ } catch (Exception e) {
+ RootTools.log("Error with regex!");
+ e.printStackTrace();
+ }
+ }
+
+ super.commandOutput(id, line);
+ }
+ };
+ RootTools.getShell(true).add(command);
+ commandWait(RootTools.getShell(true), command);
+
+ // get all pids in one string, created in process method
+ String pids = InternalVariables.pid_list;
+
+ // kill processes
+ if (!pids.equals("")) {
+ try {
+ // example: kill -9 1234 1222 5343
+ command = new Command(0, false, "kill -9 " + pids);
+ RootTools.getShell(true).add(command);
+ commandWait(RootTools.getShell(true), command);
+
+ return true;
+ } catch (Exception e) {
+ RootTools.log(e.getMessage());
+ }
+ } else {
+ //no pids match, must be dead
+ return true;
+ }
+ } catch (Exception e) {
+ RootTools.log(e.getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * This will launch the Android market looking for BusyBox
+ *
+ * @param activity pass in your Activity
+ */
+ public void offerBusyBox(Activity activity) {
+ RootTools.log("Launching Market for BusyBox");
+ Intent i = new Intent(Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=stericson.busybox"));
+ activity.startActivity(i);
+ }
+
+ /**
+ * 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 Intent offerBusyBox(Activity activity, int requestCode) {
+ RootTools.log("Launching Market for BusyBox");
+ Intent i = new Intent(Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=stericson.busybox"));
+ activity.startActivityForResult(i, requestCode);
+ return i;
+ }
+
+ /**
+ * This will launch the Play Store looking for SuperUser
+ *
+ * @param activity pass in your Activity
+ */
+ public void offerSuperUser(Activity activity) {
+ RootTools.log("Launching Play Store for SuperSU");
+ Intent i = new Intent(Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=eu.chainfire.supersu"));
+ activity.startActivity(i);
+ }
+
+ /**
+ * This will launch the Play Store looking for SuperSU, 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 Intent offerSuperUser(Activity activity, int requestCode) {
+ RootTools.log("Launching Play Store for SuperSU");
+ Intent i = new Intent(Intent.ACTION_VIEW,
+ Uri.parse("market://details?id=eu.chainfire.supersu"));
+ activity.startActivityForResult(i, requestCode);
+ return i;
+ }
+
+ private void commandWait(Shell shell, Command cmd) throws Exception {
+
+ while (!cmd.isFinished()) {
+
+ RootTools.log(Constants.TAG, shell.getCommandQueuePositionString(cmd));
+ RootTools.log(Constants.TAG, "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) {
+ Log.e(Constants.TAG, "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) {
+ Log.e(Constants.TAG, "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 {
+ Log.e(Constants.TAG, "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();
+ }
+ }
+
+ }
+ }
+}
diff --git a/app/src/main/java/com/stericson/roottools/internal/Runner.java b/app/src/main/java/com/stericson/roottools/internal/Runner.java
new file mode 100644
index 0000000..4221fca
--- /dev/null
+++ b/app/src/main/java/com/stericson/roottools/internal/Runner.java
@@ -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();
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/twofortyfouram/locale/BreadCrumber.java b/app/src/main/java/com/twofortyfouram/locale/BreadCrumber.java
new file mode 100644
index 0000000..539c4e5
--- /dev/null
+++ b/app/src/main/java/com/twofortyfouram/locale/BreadCrumber.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012 two forty four a.m. LLC
+ *
+ * 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
+ *
+ *
+ * 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.
+ *
+ * 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$
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/twofortyfouram/locale/Constants.java b/app/src/main/java/com/twofortyfouram/locale/Constants.java
new file mode 100644
index 0000000..39da5bd
--- /dev/null
+++ b/app/src/main/java/com/twofortyfouram/locale/Constants.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012 two forty four a.m. LLC
+ *
+ * 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
+ *
+ *
+ * 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$
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/twofortyfouram/locale/Intent.java b/app/src/main/java/com/twofortyfouram/locale/Intent.java
new file mode 100644
index 0000000..40df3a7
--- /dev/null
+++ b/app/src/main/java/com/twofortyfouram/locale/Intent.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2012 two forty four a.m. LLC
+ *
+ * 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
+ *
+ *
+ * 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).
+ *
+ * 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).
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * 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}.
+ *
+ * The extra {@link #EXTRA_ACTIVITY} MUST be included, otherwise Locale will ignore this {@code Intent}.
+ *
+ * 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}
+ *
+ * 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}
+ *
+ * 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}.
+ *
+ * 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}
+ *
+ * Maps to a {@code Bundle} that contains all of a plug-in's extras.
+ *
+ * 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.
+ *
+ * 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}
+ *
+ * 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$
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/twofortyfouram/locale/PackageUtilities.java b/app/src/main/java/com/twofortyfouram/locale/PackageUtilities.java
new file mode 100644
index 0000000..a1ef768
--- /dev/null
+++ b/app/src/main/java/com/twofortyfouram/locale/PackageUtilities.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2012 two forty four a.m. LLC
+ *
+ * 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
+ *
+ *
+ * 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 rather than a String[], in order to enforce immutability.
+ */
+ private static final Set COMPATIBLE_PACKAGES = constructPackageSet();
+
+ /**
+ * @return a list wrapped in {@link Collections#unmodifiableList(List)} that represents the set of
+ * Locale-compatible packages.
+ */
+ private static Set constructPackageSet()
+ {
+ final HashSet packages = new HashSet();
+
+ 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.
+ *
+ * Note: A TOCTOU error exists, due to the fact that the package could be uninstalled at any time.
+ *
+ * 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 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;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/ukanth/ufirewall/Api.java b/app/src/main/java/dev/ukanth/ufirewall/Api.java
new file mode 100644
index 0000000..1f52351
--- /dev/null
+++ b/app/src/main/java/dev/ukanth/ufirewall/Api.java
@@ -0,0 +1,4419 @@
+/**
+ * All iptables "communication" is handled by this class.
+ *
+ * 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 .
+ *
+ * @author Rodrigo Zechin Rosauro, Umakanthan Chandran
+ * @version 1.2
+ */
+
+package dev.ukanth.ufirewall;
+
+import static dev.ukanth.ufirewall.util.G.ctx;
+import static dev.ukanth.ufirewall.util.G.ipv4Fwd;
+import static dev.ukanth.ufirewall.util.G.ipv4Input;
+import static dev.ukanth.ufirewall.util.G.ipv6Fwd;
+import static dev.ukanth.ufirewall.util.G.ipv6Input;
+import static dev.ukanth.ufirewall.util.G.showAllApps;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.SparseArray;
+import android.widget.Toast;
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.TaskStackBuilder;
+
+import com.afollestad.materialdialogs.DialogAction;
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.raizlabs.android.dbflow.sql.language.Delete;
+import com.raizlabs.android.dbflow.sql.language.SQLite;
+import com.raizlabs.android.dbflow.sql.language.Select;
+import com.stericson.roottools.RootTools;
+import com.topjohnwu.superuser.Shell;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESKeySpec;
+
+import dev.ukanth.ufirewall.MainActivity.GetAppList;
+import dev.ukanth.ufirewall.log.Log;
+import dev.ukanth.ufirewall.log.LogData;
+import dev.ukanth.ufirewall.log.LogData_Table;
+import dev.ukanth.ufirewall.preferences.DefaultConnectionPref;
+import dev.ukanth.ufirewall.preferences.DefaultConnectionPref_Table;
+import dev.ukanth.ufirewall.profiles.ProfileData;
+import dev.ukanth.ufirewall.profiles.ProfileHelper;
+import dev.ukanth.ufirewall.service.FirewallService;
+import dev.ukanth.ufirewall.service.RootCommand;
+import dev.ukanth.ufirewall.util.G;
+import dev.ukanth.ufirewall.util.JsonHelper;
+import dev.ukanth.ufirewall.util.UidResolver;
+import dev.ukanth.ufirewall.widget.StatusWidget;
+
+/**
+ * Contains shared programming interfaces.
+ * All iptables "communication" is handled by this class.
+ */
+public final class Api {
+ /**
+ * application logcat tag
+ */
+ public static final String TAG = "AFWall";
+
+ /**
+ * special application UID used to indicate "any application"
+ */
+ public static final int SPECIAL_UID_ANY = -10;
+ /**
+ * special application UID used to indicate the Linux Kernel
+ */
+ public static final int SPECIAL_UID_KERNEL = -11;
+ /**
+ * special application UID used for dnsmasq DHCP/DNS
+ */
+ public static final int SPECIAL_UID_TETHER = -12;
+
+ /**
+ * special application UID used for NTP
+ */
+ public static final int SPECIAL_UID_NTP = -14;
+
+ public static final int NOTIFICATION_ID = 1;
+ public static final String PREF_FIREWALL_STATUS = "AFWallStatus";
+ public static final String DEFAULT_PREFS_NAME = "AFWallPrefs";
+ //for import/export rules
+ //revertback to old approach for performance
+ public static final String PREF_3G_PKG_UIDS = "AllowedPKG3G_UIDS";
+ public static final String PREF_WIFI_PKG_UIDS = "AllowedPKGWifi_UIDS";
+ public static final String PREF_ROAMING_PKG_UIDS = "AllowedPKGRoaming_UIDS";
+ public static final String PREF_VPN_PKG_UIDS = "AllowedPKGVPN_UIDS";
+ public static final String PREF_TETHER_PKG_UIDS = "AllowedPKGTether_UIDS";
+ public static final String PREF_LAN_PKG_UIDS = "AllowedPKGLAN_UIDS";
+ public static final String PREF_TOR_PKG_UIDS = "AllowedPKGTOR_UIDS";
+ public static final String PREF_CUSTOMSCRIPT = "CustomScript";
+ public static final String PREF_CUSTOMSCRIPT2 = "CustomScript2"; // Executed on shutdown
+ public static final String PREF_MODE = "BlockMode";
+ public static final String PREF_ENABLED = "Enabled";
+ // Modes
+ public static final String MODE_WHITELIST = "whitelist";
+ public static final String MODE_BLACKLIST = "blacklist";
+ public static final String STATUS_CHANGED_MSG = "dev.ukanth.ufirewall.intent.action.STATUS_CHANGED";
+ public static final String TOGGLE_REQUEST_MSG = "dev.ukanth.ufirewall.intent.action.TOGGLE_REQUEST";
+ public static final String CUSTOM_SCRIPT_MSG = "dev.ukanth.ufirewall.intent.action.CUSTOM_SCRIPT";
+ // Message extras (parameters)
+ public static final String STATUS_EXTRA = "dev.ukanth.ufirewall.intent.extra.STATUS";
+ public static final String SCRIPT_EXTRA = "dev.ukanth.ufirewall.intent.extra.SCRIPT";
+ public static final String SCRIPT2_EXTRA = "dev.ukanth.ufirewall.intent.extra.SCRIPT2";
+ public static final int ERROR_NOTIFICATION_ID = 9;
+ private static final int WIFI_EXPORT = 0;
+ private static final int DATA_EXPORT = 1;
+ private static final int ROAM_EXPORT = 2;
+ // Messages
+ private static final int VPN_EXPORT = 3;
+ private static final int TETHER_EXPORT = 6;
+ private static final int LAN_EXPORT = 4;
+ private static final int TOR_EXPORT = 5;
+ private static final String[] ITFS_WIFI = InterfaceTracker.ITFS_WIFI;
+ private static final String[] ITFS_3G = InterfaceTracker.ITFS_3G;
+ private static final String[] ITFS_VPN = InterfaceTracker.ITFS_VPN;
+ private static final String[] ITFS_TETHER = InterfaceTracker.ITFS_TETHER;
+ // iptables can exit with status 4 if two processes tried to update the same table
+ private static final int IPTABLES_TRY_AGAIN = 4;
+ private static final String[] dynChains = {"-3g-postcustom", "-3g-fork", "-wifi-postcustom", "-wifi-fork"};
+ private static final String[] natChains = {"", "-tor-check", "-tor-filter"};
+ private static final String[] staticChains = {"", "-input", "-3g", "-wifi", "-reject", "-vpn", "-3g-tether", "-3g-home", "-3g-roam", "-wifi-tether", "-wifi-wan", "-wifi-lan", "-usb-tether", "-tor", "-tor-reject", "-tether", "-3g-home-reject", "-3g-roam-reject", "-wifi-wan-reject", "-wifi-lan-reject", "-vpn-reject", "-tether-reject"};
+ private static volatile boolean globalStatus = false;
+
+ private static final Object GLOBAL_STATUS_LOCK = new Object();
+
+ public static List getListOfUids() {
+ return listOfUids;
+ }
+
+ private static List listOfUids = new ArrayList<>();
+
+
+ private static Map uidToApplicationInfoMap = null;
+
+
+ private static final Pattern dual_pattern = Pattern.compile("package:(.*) uid:(.*)", Pattern.MULTILINE);
+
+ /**
+ * @brief Special user/group IDs that aren't associated with
+ * any particular app.
+ *
+ * The accounts listed below are the only ones from
+ * android_filesystem_config.h that are known to be used as
+ * the UID of a process that uses the network. The other
+ * accounts in that .h file are either:
+ * * used as supplemental group IDs for granting extra
+ * privileges to apps,
+ * * used as UIDs of processes that don't need the network,
+ * or
+ * * have not yet been reported by users as needing the
+ * network.
+ *
+ * The list is sorted in ascending UID order.
+ */
+ private static final String[] specialAndroidAccounts = {
+ "root",
+ "adb",
+ "media",
+ "vpn",
+ "drm",
+ "gps",
+ "shell"
+ };
+ private static final Pattern p = Pattern.compile("UserHandle\\{(.*)\\}");
+ // Preferences
+ public static String PREFS_NAME = "AFWallPrefs";
+ // Cached applications
+ public static List applications = null;
+ public static Set recentlyInstalled = new HashSet<>();
+ //for custom scripts
+ //public static String ipPath = null;
+ public static String bbPath = null;
+ private static final String charsetName = "UTF8";
+ private static final String algorithm = "DES";
+ private static final int base64Mode = Base64.DEFAULT;
+ //private static volatile String AFWALL_CHAIN_NAME = "afwall";
+ private static final Object CHAIN_NAME_LOCK = new Object();
+ private static Map specialApps = null;
+ private static volatile boolean rulesUpToDate = false;
+ private static final Object RULES_LOCK = new Object();
+ public static void setRulesUpToDate(boolean rulesUpToDate) {
+ synchronized (RULES_LOCK) {
+ Api.rulesUpToDate = rulesUpToDate;
+ }
+ }
+ public static boolean getRulesUpToDate() {
+ synchronized (RULES_LOCK) {
+ return Api.rulesUpToDate;
+ }
+ }
+
+
+ // returns c.getString(R.string._item)
+ public static String getSpecialDescription(Context ctx, String acct) {
+ try {
+ int rid = ctx.getResources().getIdentifier(acct + "_item", "string", ctx.getPackageName());
+ return ctx.getString(rid);
+ } catch (Resources.NotFoundException exception) {
+ return null;
+ }
+ }
+
+ public static String getSpecialDescriptionSystem(Context ctx, String packageName) {
+ switch (packageName) {
+ case "any":
+ return ctx.getString(R.string.all_item);
+ case "kernel":
+ return ctx.getString(R.string.kernel_item);
+ case "tether":
+ return ctx.getString(R.string.tethering_item);
+ case "ntp":
+ return ctx.getString(R.string.ntp_item);
+ }
+ return "";
+ }
+
+ /**
+ * Display a simple alert box
+ *
+ * @param ctx context
+ * @param msgText message
+ */
+ public static void toast(final Context ctx, final CharSequence msgText) {
+ if (ctx != null) {
+ Handler mHandler = new Handler(Looper.getMainLooper());
+ mHandler.post(() -> Toast.makeText(G.getContext(), msgText, Toast.LENGTH_SHORT).show());
+ }
+ }
+
+ public static void toast(final Context ctx, final CharSequence msgText, final int toastlen) {
+ if (ctx != null) {
+ Handler mHandler = new Handler(Looper.getMainLooper());
+ mHandler.post(() -> Toast.makeText(G.getContext(), msgText, toastlen).show());
+ }
+ }
+
+ public static String getBinaryPath(Context ctx, boolean setv6) {
+ String ip_path = G.ip_path();
+ String binaryName = setv6 ? "ip6tables" : "iptables";
+
+ // If built-in binaries have previously failed with exit 126, prefer system binaries
+ if (G.isBuiltinIptablesFailed() && !ip_path.equals("builtin")) {
+ Log.i(TAG, "Built-in iptables previously failed, preferring system binary for " + binaryName);
+ String systemBinaryPath = findSystemBinary(binaryName);
+ if (systemBinaryPath != null) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return systemBinaryPath;
+ }
+ Log.w(TAG, "System binary " + binaryName + " not found despite previous built-in failure");
+ }
+
+ // First priority: check system binary if preference is "system" or "auto"
+ if (ip_path.equals("system") || ip_path.equals("auto")) {
+ String systemBinaryPath = findSystemBinary(binaryName);
+ if (systemBinaryPath != null) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return systemBinaryPath;
+ }
+
+ // If system binary not found and preference is "system", log warning
+ if (ip_path.equals("system")) {
+ Log.w(TAG, "System binary " + binaryName + " not found, falling back to built-in");
+ }
+ }
+
+ // Second priority: use built-in binary
+ // Check if built-in binary exists for current architecture
+ String builtinDir = ctx.getDir("bin", 0).getAbsolutePath() + "/";
+ String builtinPath = builtinDir + binaryName;
+
+ File builtinFile = new File(builtinPath);
+ if (builtinFile.exists() && builtinFile.canExecute()) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return builtinPath;
+ }
+
+ // Fallback: try to install built-in binaries if they don't exist
+ Log.w(TAG, "Built-in binary " + binaryName + " not found, attempting to install binaries");
+ if (assertBinaries(ctx, false)) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return builtinPath;
+ }
+
+ // Last resort: return the path even if binary doesn't exist (will likely fail at runtime)
+ Log.e(TAG, "No working " + binaryName + " binary found, returning built-in path anyway");
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return builtinPath;
+ }
+
+ /**
+ * Find system binary by checking common system paths
+ *
+ * @param binaryName the name of the binary to find
+ * @return full path to the binary if found, null otherwise
+ */
+ public static String findSystemBinary(String binaryName) {
+ // Common paths where system iptables/ip6tables binaries are located
+ String[] systemPaths = {
+ "/system/bin/" + binaryName,
+ "/system/xbin/" + binaryName,
+ "/vendor/bin/" + binaryName,
+ "/sbin/" + binaryName,
+ "/usr/bin/" + binaryName,
+ "/bin/" + binaryName
+ };
+
+ for (String path : systemPaths) {
+ File binaryFile = new File(path);
+ if (binaryFile.exists() && binaryFile.canExecute()) {
+ Log.i(TAG, "Found system binary: " + path);
+ return path;
+ }
+ }
+
+ // Also try using 'which' command if available
+ try {
+ Shell.Result result = Shell.cmd("which " + binaryName).exec();
+ if (result.isSuccess() && !result.getOut().isEmpty()) {
+ String whichPath = result.getOut().get(0).trim();
+ File whichFile = new File(whichPath);
+ if (whichFile.exists() && whichFile.canExecute()) {
+ Log.i(TAG, "Found system binary via 'which': " + whichPath);
+ return whichPath;
+ }
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Unable to use 'which' command to find " + binaryName + ": " + e.getMessage());
+ }
+
+ Log.d(TAG, "System binary " + binaryName + " not found in any standard location");
+ return null;
+ }
+
+ /**
+ * Determine toybox/busybox or built in
+ *
+ * @param ctx
+ * @param considerSystem
+ * @return
+ */
+ public static String getBusyBoxPath(Context ctx, boolean considerSystem) {
+ String bb_path = G.bb_path();
+
+ // First priority: check system busybox if preference is "system" or "auto" and considerSystem is true
+ if (considerSystem && (bb_path.equals("system") || bb_path.equals("auto"))) {
+ String systemBusybox = findSystemBinary("busybox");
+ if (systemBusybox != null) {
+ return systemBusybox + " ";
+ }
+
+ // If system busybox not found and preference is "system", log warning and fall back
+ if (bb_path.equals("system")) {
+ Log.w(TAG, "System busybox not found, falling back to built-in");
+ }
+ }
+
+ // Second priority: use built-in busybox
+ String dir = ctx.getDir("bin", 0).getAbsolutePath();
+ String builtinPath = dir + "/busybox";
+
+ File builtinFile = new File(builtinPath);
+ if (builtinFile.exists() && builtinFile.canExecute()) {
+ return builtinPath + " ";
+ }
+
+ // Fallback: return built-in path even if it doesn't exist yet (may be installed later)
+ if (!builtinFile.exists()) {
+ Log.w(TAG, "Built-in busybox not found at " + builtinPath + ", returning path anyway");
+ } else {
+ Log.w(TAG, "Built-in busybox exists but not executable at " + builtinPath + ", permissions: " +
+ (builtinFile.canRead() ? "R" : "-") +
+ (builtinFile.canWrite() ? "W" : "-") +
+ (builtinFile.canExecute() ? "X" : "-"));
+ }
+ return builtinPath + " ";
+ }
+
+ /**
+ * Get NFLog Path - Enhanced version with fallback support
+ *
+ * @param ctx Context
+ * @return path to best available nflog binary
+ */
+ public static String getNflogPath(Context ctx) {
+ String dir = ctx.getDir("bin", 0).getAbsolutePath();
+ String originalPath = dir + "/nflog";
+ File originalFile = new File(originalPath);
+
+ if (!originalFile.exists()) {
+ Log.w(TAG, "No NFLOG binary found at: " + originalPath);
+ return null;
+ }
+
+ if (!originalFile.canExecute()) {
+ Log.w(TAG, "NFLOG binary not executable at: " + originalPath);
+ // Try to make it executable
+ try {
+ originalFile.setExecutable(true);
+ if (!originalFile.canExecute()) {
+ Log.e(TAG, "Failed to make nflog executable");
+ return null;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to make nflog executable: " + e.getMessage());
+ return null;
+ }
+ }
+
+ Log.i(TAG, "Using original NFLOG binary");
+ return originalPath;
+ }
+
+ /**
+ * Get enhanced NFLOG command with optimized parameters
+ *
+ * @param ctx Context
+ * @param queueNum NFLOG queue number
+ * @return complete command string with optimizations
+ */
+ public static String getEnhancedNflogCommand(Context ctx, int queueNum) {
+ String nflogPath = getNflogPath(ctx);
+ if (nflogPath == null) {
+ return null;
+ }
+
+ // Use standard nflog command with queue number
+ return nflogPath + " " + queueNum;
+ }
+
+
+ /**
+ * Copies a raw resource file, given its ID to the given location
+ *
+ * @param ctx context
+ * @param resid resource id
+ * @param file destination file
+ * @param mode file permissions (E.g.: "755")
+ * @throws IOException on error
+ * @throws InterruptedException when interrupted
+ */
+ private static void copyRawFile(Context ctx, int resid, File file, String mode) throws IOException, InterruptedException {
+ final String abspath = file.getAbsolutePath();
+ // Write the iptables binary
+ final FileOutputStream out = new FileOutputStream(file);
+ final InputStream is = ctx.getResources().openRawResource(resid);
+ byte[] buf = new byte[1024];
+ int len;
+ while ((len = is.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ out.close();
+ is.close();
+ // Change the permissions
+
+ executeSecureCommand(new String[]{"chmod", mode, abspath});
+ }
+
+ /**
+ * Execute system commands securely using ProcessBuilder to prevent command injection
+ *
+ * @param command Array of command and arguments (prevents shell interpretation)
+ * @throws IOException if command execution fails
+ * @throws InterruptedException if command is interrupted
+ */
+ private static void executeSecureCommand(String[] command) throws IOException, InterruptedException {
+ if (command == null || command.length == 0) {
+ throw new IllegalArgumentException("Command cannot be null or empty");
+ }
+
+ // Validate command and arguments don't contain dangerous characters
+ for (String arg : command) {
+ if (arg == null || arg.contains("\n") || arg.contains("\r") ||
+ arg.contains(";") || arg.contains("&") || arg.contains("|") ||
+ arg.contains("`") || arg.contains("$")) {
+ Log.w(TAG, "Rejecting command with potentially dangerous characters: " + java.util.Arrays.toString(command));
+ throw new SecurityException("Command contains illegal characters");
+ }
+ }
+
+ ProcessBuilder pb = new ProcessBuilder(command);
+ pb.environment().clear(); // Clear environment to prevent injection via env vars
+ Process process = pb.start();
+ int exitCode = process.waitFor();
+
+ if (exitCode != 0) {
+ // For chmod commands, permission denied is expected on Android - don't fail the installation
+ if (command.length > 0 && "chmod".equals(command[0])) {
+ Log.w(TAG, "chmod command failed (expected on Android without root): exit code " + exitCode + " for " + java.util.Arrays.toString(command));
+ return; // Don't throw exception for chmod failures
+ }
+ Log.w(TAG, "Command failed with exit code " + exitCode + ": " + java.util.Arrays.toString(command));
+ throw new IOException("Command execution failed with exit code: " + exitCode);
+ }
+ }
+
+ /**
+ * Look up uid for each user by name, and if he exists, append an iptables rule.
+ *
+ * @param listCommands current list of iptables commands to execute
+ * @param users list of users to whom the rule applies
+ * @param prefix "iptables" command and the portion of the rule preceding "-m owner --uid-owner X"
+ * @param suffix the remainder of the iptables rule, following "-m owner --uid-owner X"
+ */
+ private static void addRuleForUsers(List listCommands, String[] users, String prefix, String suffix) {
+ for (String user : users) {
+ int uid = android.os.Process.getUidForName(user);
+ if (uid != -1)
+ listCommands.add(prefix + " -m owner --uid-owner " + uid + " " + suffix);
+ }
+ }
+
+ private static void addRulesForUidlist(List cmds, List uids, String chain, boolean whitelist) {
+ String action = whitelist ? " -j RETURN" : " -j " + chain + "-reject";
+
+ if (uids.contains(SPECIAL_UID_ANY)) {
+ if (!whitelist) {
+ cmds.add("-A " + chain + action);
+ } else {
+ cmds.add("-A " + chain + " -j RETURN");
+ }
+ } else {
+ for (Integer uid : uids) {
+ if (uid != null && uid >= 0) {
+ cmds.add("-A " + chain + " -m owner --uid-owner " + uid + action);
+ }
+ }
+
+ /*// netd runs as root, and on Android 4.3+ it handles all DNS queries
+ if (uids.indexOf(SPECIAL_UID_DNSPROXY) >= 0) {
+ addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p udp --dport 53", action);
+ }*/
+
+ if (whitelist) {
+ addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p udp --dport 53", " -j RETURN");
+ addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p tcp --dport 53", " -j RETURN");
+ } else {
+ addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p udp --dport 53", " -j RETURN");
+ addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p tcp --dport 53", " -j RETURN");
+ }
+
+
+ // NTP service runs as "system" user
+ if (uids.contains(SPECIAL_UID_NTP)) {
+ addRuleForUsers(cmds, new String[]{"system"}, "-A " + chain + " -p udp --dport 123", action);
+ }
+
+
+ if (G.getPrivateDnsStatus()) {
+ cmds.add("-A " + chain + " -p tcp --dport 853" + " -j ACCEPT");
+ // disabling HTTPS over DNS
+ //cmds.add("-A " + chain + " -p tcp --dport 443" + " -j ACCEPT");
+ }
+
+ boolean kernel_checked = uids.contains(SPECIAL_UID_KERNEL);
+
+ if (whitelist) {
+ if (kernel_checked) {
+ // reject any other UIDs, but allow the kernel through
+ // Use fallback rule if owner module is not available
+ if (G.hasOwnerModule()) {
+ Log.d(TAG, "Adding whitelist kernel rule with owner module for chain " + chain);
+ cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j " + chain + "-reject");
+ } else {
+ Log.w(TAG, "Owner module not available, using fallback rule for chain " + chain);
+ cmds.add("-A " + chain + " -j " + chain + "-reject");
+ }
+ } else {
+ // kernel is blocked so reject everything
+ String rejectRule = "-A " + chain + " -j " + chain + "-reject";
+ cmds.add(rejectRule);
+ }
+ } else {
+ if (kernel_checked) {
+ // allow any other UIDs, but block the kernel
+ if (G.hasOwnerModule()) {
+ cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j RETURN");
+ cmds.add("-A " + chain + " -j " + chain + "-reject");
+ } else {
+ Log.w(TAG, "Owner module not available, using fallback rule for chain " + chain);
+ cmds.add("-A " + chain + " -j " + chain + "-reject");
+ }
+ }
+ }
+
+ //add 1052 for LAN
+ if(G.enableLAN() && G.hasOwnerModule()) {
+ cmds.add("-A " + "afwall-wifi-lan" + " -m owner --uid-owner 1052 -j RETURN");
+ }
+
+ if (G.hasOwnerModule()) {
+ cmds.add("-A " + "afwall-wifi-wan" + " -m owner --uid-owner 1052 -j RETURN");
+ }
+ }
+ }
+
+ private static void addRejectRules(List cmds, String chainName) {
+ // set up reject chain to log or not log
+ // this can be changed dynamically through the Firewall Logs activity
+
+ if (G.enableLogService()) {
+ if (G.logTarget().trim().equals("LOG")) {
+ //cmds.add("-A " + chainName + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL-ALLOW}\" --log-level 4 --log-uid");
+ String logRule = "-A " + chainName + "-reject" + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL}\" --log-level 4 --log-uid --log-tcp-options --log-ip-options";
+ Log.d(TAG, "Adding LOG rule to reject chain: " + logRule);
+ cmds.add(logRule);
+ } else if (G.logTarget().trim().equals("NFLOG")) {
+ //cmds.add("-A " + chainName + " -j NFLOG --nflog-prefix \"{AFL-ALLOW}\" --nflog-group 40");
+ String nflogRule = "-A " + chainName + "-reject" + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40";
+ Log.d(TAG, "Adding NFLOG rule to reject chain: " + nflogRule);
+ cmds.add(nflogRule);
+ }
+ }
+ String rejectRule = "-A " + chainName + "-reject" + " -j REJECT";
+ Log.d(TAG, "Adding final REJECT rule: " + rejectRule);
+ cmds.add(rejectRule);
+
+ // Also populate individual reject chains that are used by whitelist mode
+ String[] rejectChainSuffixes = {"-3g-home-reject", "-3g-roam-reject", "-wifi-wan-reject",
+ "-wifi-lan-reject", "-vpn-reject", "-tether-reject"};
+ for (String suffix : rejectChainSuffixes) {
+ String individualRejectChain = chainName + suffix;
+ Log.d(TAG, "Populating individual reject chain: " + individualRejectChain);
+ if (G.enableLogService() && G.logTarget().trim().equals("NFLOG")) {
+ String nflogRule = "-A " + individualRejectChain + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40";
+ Log.d(TAG, "Adding NFLOG to individual reject chain: " + nflogRule);
+ cmds.add(nflogRule);
+ }
+ String individualRejectRule = "-A " + individualRejectChain + " -j REJECT";
+ Log.d(TAG, "Adding REJECT to individual reject chain: " + individualRejectRule);
+ cmds.add(individualRejectRule);
+ }
+ }
+
+ private static void addTorRules(List cmds, List uids, Boolean whitelist, Boolean ipv6, String chainName) {
+ for (Integer uid : uids) {
+ if (uid != null && uid >= 0) {
+ if (G.enableInbound() || ipv6) {
+ cmds.add("-A " + chainName + "-tor-reject -m owner --uid-owner " + uid + " -j " + chainName + "-reject");
+ }
+ if (!ipv6) {
+ cmds.add("-t nat -A " + chainName + "-tor-check -m owner --uid-owner " + uid + " -j " + chainName + "-tor-filter");
+ }
+ }
+ }
+ if (ipv6) {
+ cmds.add("-A " + chainName + " -j " + chainName + "-tor-reject");
+ } else {
+ Integer socks_port = 9050;
+ Integer http_port = 8118;
+ Integer dns_port = 5400;
+ Integer tcp_port = 9040;
+ cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + socks_port + " -j RETURN");
+ cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + http_port + " -j RETURN");
+ cmds.add("-t nat -A " + chainName + "-tor-filter -p udp --dport 53 -j REDIRECT --to-ports " + dns_port);
+ cmds.add("-t nat -A " + chainName + "-tor-filter -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j REDIRECT --to-ports " + tcp_port);
+ cmds.add("-t nat -A " + chainName + "-tor-filter -j MARK --set-mark 0x500");
+ cmds.add("-t nat -A " + chainName + " -j " + chainName + "-tor-check");
+ cmds.add("-A " + chainName + "-tor -m mark --mark 0x500 -j " + chainName + "-reject");
+ cmds.add("-A " + chainName + " -j " + chainName + "-tor");
+ }
+ if (G.enableInbound()) {
+ cmds.add("-A " + chainName + "-input -j " + chainName + "-tor-reject");
+ }
+ }
+
+ private static String sanitizeRule(String rule) {
+ // Remove potentially dangerous characters and commands
+ if (rule.contains("&&") || rule.contains("||") || rule.contains(";") ||
+ rule.contains("|") || rule.contains("`") || rule.contains("$") ||
+ rule.contains("rm ") || rule.contains("dd ") || rule.contains("chmod ") ||
+ rule.contains("chown ") || rule.contains("su ") || rule.contains("sudo ")) {
+ Log.w(TAG, "Rejecting potentially dangerous custom rule: " + rule);
+ return null;
+ }
+
+ // Only allow basic iptables/ip6tables commands
+ if (!rule.startsWith("iptables ") && !rule.startsWith("ip6tables ") &&
+ !rule.startsWith("-A ") && !rule.startsWith("-I ") &&
+ !rule.startsWith("-D ") && !rule.startsWith("-F ") &&
+ !rule.startsWith("-P ") && !rule.startsWith("-N ")) {
+ Log.w(TAG, "Rejecting non-iptables rule: " + rule);
+ return null;
+ }
+
+ return rule;
+ }
+
+ private static void addCustomRules(String prefName, List cmds) {
+ String customRulesStr = G.pPrefs.getString(prefName, "");
+ if (customRulesStr.isEmpty()) return;
+
+ String[] customRules = customRulesStr.split("[\\r\\n]+");
+ for (String rule : customRules) {
+ if (rule.matches(".*\\S.*")) {
+ // Sanitize the rule to prevent command injection
+ String sanitizedRule = sanitizeRule(rule.trim());
+ if (sanitizedRule != null && !sanitizedRule.isEmpty()) {
+ cmds.add("#LITERAL# " + sanitizedRule);
+ }
+ }
+ }
+ }
+
+ /**
+ * Reconfigure the firewall rules based on interface changes seen at runtime: tethering
+ * enabled/disabled, IP address changes, etc. This should only affect a small number of
+ * rules; we want to avoid calling applyIptablesRulesImpl() too often since applying
+ * 100+ rules is expensive.
+ *
+ * @param ctx application context
+ * @param cmds command list
+ */
+ private static void addInterfaceRouting(Context ctx, List cmds, boolean ipv6, String chainName) {
+ try {
+ //force only for v4
+ final InterfaceDetails cfg = InterfaceTracker.getCurrentCfg(ctx, !ipv6);
+ final boolean whitelist = G.pPrefs.getString(PREF_MODE, MODE_WHITELIST).equals(MODE_WHITELIST);
+ for (String s : dynChains) {
+ cmds.add("-F " + chainName + s);
+ }
+
+ if (whitelist) {
+ // always allow the DHCP client full wifi access
+ addRuleForUsers(cmds, new String[]{"dhcp", "wifi"}, "-A " + chainName + "-wifi-postcustom", "-j RETURN");
+ }
+
+ if (cfg.isWifiTethered || cfg.isUsbTethered) {
+ if (cfg.isWifiTethered) {
+ cmds.add("-A " + chainName + "-wifi-postcustom -j " + chainName + "-wifi-tether");
+ } else {
+ cmds.add("-A " + chainName + "-wifi-postcustom -j " + chainName + "-wifi-fork");
+ }
+
+ if (cfg.isUsbTethered) {
+ cmds.add("-A " + chainName + "-3g-postcustom -j " + chainName + "-usb-tether");
+ } else {
+ cmds.add("-A " + chainName + "-3g-postcustom -j " + (cfg.isWifiTethered ? chainName + "-3g-tether" : chainName + "-3g-fork"));
+ }
+ } else {
+ cmds.add("-A " + chainName + "-wifi-postcustom -j " + chainName + "-wifi-fork");
+ cmds.add("-A " + chainName + "-3g-postcustom -j " + chainName + "-3g-fork");
+ }
+
+ if (G.enableLAN() && !cfg.isWifiTethered) {
+ if (ipv6) {
+ if (!cfg.lanMaskV6.equals("")) {
+ cmds.add("-A " + chainName + "-wifi-fork -d " + cfg.lanMaskV6 + " -j " + chainName + "-wifi-lan");
+ cmds.add("-A " + chainName + "-wifi-fork '!' -d " + cfg.lanMaskV6 + " -j " + chainName + "-wifi-wan");
+ } else {
+ Log.i(TAG, "no ipv6 found: " + G.enableIPv6() + "," + cfg.lanMaskV6);
+ }
+ } else {
+ if (!cfg.lanMaskV4.equals("")) {
+ cmds.add("-A " + chainName + "-wifi-fork -d " + cfg.lanMaskV4 + " -j " + chainName + "-wifi-lan");
+ cmds.add("-A " + chainName + "-wifi-fork '!' -d " + cfg.lanMaskV4 + " -j " + chainName + "-wifi-wan");
+ } else {
+ Log.i(TAG, "no ipv4 found:" + G.enableIPv6() + "," + cfg.lanMaskV4);
+ }
+ }
+ if (cfg.lanMaskV4.equals("") && cfg.lanMaskV6.equals("")) {
+ Log.i(TAG, "No ipaddress found for LAN");
+ // lets find one more time
+ //atleast allow internet - don't block completely
+ cmds.add("-A " + chainName + "-wifi-fork -j " + chainName + "-wifi-wan");
+ }
+ } else {
+ cmds.add("-A " + chainName + "-wifi-fork -j " + chainName + "-wifi-wan");
+ }
+
+ if (G.enableRoam() && cfg.isRoaming) {
+ cmds.add("-A " + chainName + "-3g-fork -j " + chainName + "-3g-roam");
+ } else {
+ cmds.add("-A " + chainName + "-3g-fork -j " + chainName + "-3g-home");
+ }
+
+
+ } catch (Exception e) {
+ Log.i(TAG, "Exception while applying shortRules " + e.getMessage());
+ }
+
+ }
+
+ public static String getSpecialAppName(int uid) {
+ // First, try special apps (AFWall+ specific entries)
+ List packageInfoData = getSpecialData();
+ for (PackageInfoData infoData : packageInfoData) {
+ if (infoData.uid == uid) {
+ return infoData.names.get(0);
+ }
+ }
+
+ // If not found in special apps, use comprehensive UID resolver
+ return UidResolver.resolveUid(ctx, uid);
+ }
+
+
+ private static void applyShortRules(Context ctx, List cmds, boolean ipv6) {
+ Log.i(TAG, "Setting OUTPUT chain to DROP");
+ cmds.add("-P OUTPUT DROP");
+ /*FIXME: Adding custom rules might increase the time */
+ Log.i(TAG, "Applying custom rules");
+ addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds);
+ String chainName = getThreadSafeChainName();
+ addInterfaceRouting(ctx, cmds, ipv6, chainName);
+ Log.i(TAG, "Setting OUTPUT chain to ACCEPT");
+ cmds.add("-P OUTPUT ACCEPT");
+ }
+
+
+ /**
+ * Purge and re-add all rules (internal implementation).
+ *
+ * @param ctx application context (mandatory)
+ * @param showErrors indicates if errors should be alerted
+ */
+ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet ruleDataSet, final boolean showErrors,
+ List out, boolean ipv6) {
+ return applyIptablesRulesImpl(ctx, ruleDataSet, showErrors, out, ipv6, null);
+ }
+
+ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet ruleDataSet, final boolean showErrors,
+ List out, boolean ipv6, String threadSafeChainName) {
+ if (ctx == null) {
+ return false;
+ }
+
+ assertBinaries(ctx, showErrors);
+
+ final InterfaceDetails cfg = InterfaceTracker.getCurrentCfg(ctx, !ipv6);
+
+ // Use thread-safe chain name if provided, otherwise determine it safely
+ final String chainName;
+ if (threadSafeChainName != null) {
+ chainName = threadSafeChainName;
+ } else {
+ chainName = getThreadSafeChainName();
+ }
+ final boolean whitelist = G.pPrefs.getString(PREF_MODE, MODE_WHITELIST).equals(MODE_WHITELIST);
+
+ List cmds = new ArrayList();
+
+ Log.i(TAG, "Constructing rules for " + (ipv6 ? "v6": "v4"));
+
+ //check before make them ACCEPT state
+ if (ipv4Input() || (ipv6 && ipv6Input())) {
+ cmds.add("-P INPUT ACCEPT");
+ }
+
+ if (ipv4Fwd() || (ipv6 && ipv6Fwd())) {
+ cmds.add("-P FORWARD ACCEPT");
+ }
+
+ try {
+ // prevent data leaks due to incomplete rules
+ cmds.add("-P OUTPUT DROP");
+
+ // Create and flush all chains first to ensure they exist
+ // Use NOCHK to avoid errors if chain already exists, then flush to ensure clean state
+ for (String s : staticChains) {
+ cmds.add("#NOCHK# -N " + chainName + s);
+ cmds.add("#NOCHK# -F " + chainName + s);
+ }
+ for (String s : dynChains) {
+ cmds.add("#NOCHK# -N " + chainName + s);
+ cmds.add("#NOCHK# -F " + chainName + s);
+ }
+
+
+ cmds.add("#NOCHK# -D OUTPUT -j " + chainName);
+ cmds.add("-I OUTPUT 1 -j " + chainName);
+
+
+ if (G.enableInbound()) {
+ cmds.add("#NOCHK# -D INPUT -j " + chainName + "-input");
+ cmds.add("-I INPUT 1 -j " + chainName + "-input");
+ }
+
+ if (G.enableTor() && !ipv6) {
+ for (String s : natChains) {
+ cmds.add("#NOCHK# -t nat -N " + chainName + s);
+ cmds.add("-t nat -F " + chainName + s);
+ }
+ cmds.add("#NOCHK# -t nat -D OUTPUT -j " + chainName);
+ cmds.add("-t nat -I OUTPUT 1 -j " + chainName);
+ }
+
+ // custom rules in afwall-{3g,wifi,reject} supersede everything else
+ addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds);
+
+ cmds.add("-A " + chainName + "-3g -j " + chainName + "-3g-postcustom");
+ cmds.add("-A " + chainName + "-wifi -j " + chainName + "-wifi-postcustom");
+ addRejectRules(cmds, chainName);
+
+ if (G.enableInbound()) {
+ // we don't have any rules in the INPUT chain prohibiting inbound traffic, but
+ // local processes can't reply to half-open connections without this rule
+ cmds.add("-A " + chainName + " -m state --state ESTABLISHED -j RETURN");
+ cmds.add("-A " + chainName + "-input -m state --state ESTABLISHED -j RETURN");
+ }
+
+ addInterfaceRouting(ctx, cmds, ipv6, chainName);
+
+ // send wifi, 3G, VPN packets to the appropriate dynamic chain based on interface
+ if (G.enableVPN()) {
+ // if !enableVPN then we ignore those interfaces (pass all traffic)
+ for (final String itf : ITFS_VPN) {
+ cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-vpn");
+ }
+ // KitKat policy based routing - see:
+ // http://forum.xda-developers.com/showthread.php?p=48703545
+ // This covers mark range 0x3c - 0x47. The official range is believed to be
+ // 0x3c - 0x45 but this is close enough.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ cmds.add("-A " + chainName + " -m mark --mark 0x3c/0xfffc -g " + chainName + "-vpn");
+ cmds.add("-A " + chainName + " -m mark --mark 0x40/0xfff8 -g " + chainName + "-vpn");
+ }
+ }
+
+ if (G.enableTether()) {
+ for (final String itf : ITFS_TETHER) {
+ cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-tether");
+ }
+ }
+
+ for (final String itf : ITFS_WIFI) {
+ cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-wifi");
+ }
+
+ for (final String itf : ITFS_3G) {
+ cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-3g");
+ }
+
+ // special rules to allow tethering
+ // note that this can only blacklist DNS/DHCP services, not all tethered traffic
+ String[] users_dhcp = {"root", "nobody", "network_stack"};
+ String[] users_dns = {"root", "nobody", "dns_tether"};
+ String action = " -j " + (whitelist ? "RETURN" : chainName + "-reject");
+
+ if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER)) {
+ // DHCP replies to client
+ addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-wifi-tether", "-p udp --sport=67 --dport=68" + action);
+ // DNS replies to client
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p udp --sport=53" + action);
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p tcp --sport=53" + action);
+
+ }
+
+ // USB tethering rules
+ if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER) || containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) {
+ // DHCP replies to USB tethered client
+ addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-usb-tether", "-p udp --sport=67 --dport=68" + action);
+ // DNS replies to USB tethered client
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p udp --sport=53" + action);
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p tcp --sport=53" + action);
+ }
+ if (containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) {
+ // DHCP replies to client
+ addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-tether", "-p udp --sport=67 --dport=68" + action);
+ // DNS replies to client
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p udp --sport=53" + action);
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p tcp --sport=53" + action);
+ }
+
+ // DNS requests to upstream servers - support all connection types
+ if (containsUidOrAny(ruleDataSet.dataList, SPECIAL_UID_TETHER)) {
+ // Define all tethering chains that need DNS upstream access
+ String[] tetherChains = {"-3g-tether", "-wifi-tether", "-usb-tether", "-tether"};
+
+ for (String chain : tetherChains) {
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + chain, "-p udp --dport=53" + action);
+ addRuleForUsers(cmds, users_dns, "-A " + chainName + chain, "-p tcp --dport=53" + action);
+ }
+ }
+
+ // if tethered, try to match the above rules (if enabled). no match -> fall through to the
+ // normal 3G/wifi rules
+ cmds.add("-A " + chainName + "-wifi-tether -j " + chainName + "-wifi-fork");
+ cmds.add("-A " + chainName + "-3g-tether -j " + chainName + "-3g-fork");
+
+ // NOTE: we still need to open a hole to let WAN-only UIDs talk to a DNS server
+ // on the LAN - use specific DNS servers instead of opening to all LAN hosts
+ if (whitelist) {
+ // Add rules for specific DNS servers instead of all LAN hosts
+ addDnsServerRules(cmds, cfg, chainName + "-wifi-lan", ipv6);
+
+ // Fallback: if no specific DNS servers found, use the old broad rule
+ if (cfg.dnsServersV4.isEmpty() && cfg.dnsServersV6.isEmpty()) {
+ cmds.add("-A " + chainName + "-wifi-lan -p udp --dport 53 -j RETURN");
+ cmds.add("-A " + chainName + "-wifi-lan -p tcp --dport 53 -j RETURN");
+ }
+
+ //bug fix allow dns to be open on Pie for all connection type
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ cmds.add("-A " + chainName + "-wifi-wan" + " -p udp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-3g-home" + " -p udp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-3g-roam" + " -p udp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-vpn" + " -p udp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-tether" + " -p udp --dport 53" + " -j RETURN");
+
+ cmds.add("-A " + chainName + "-wifi-wan" + " -p tcp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-3g-home" + " -p tcp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-3g-roam" + " -p tcp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-vpn" + " -p tcp --dport 53" + " -j RETURN");
+ cmds.add("-A " + chainName + "-tether" + " -p tcp --dport 53" + " -j RETURN");
+ }
+ }
+ // now add the per-uid rules for 3G home, 3G roam, wifi WAN, wifi LAN, VPN
+ // in whitelist mode the last rule in the list routes everything else to afwall-reject
+ addRulesForUidlist(cmds, ruleDataSet.dataList, chainName + "-3g-home", whitelist);
+ addRulesForUidlist(cmds, ruleDataSet.roamList, chainName + "-3g-roam", whitelist);
+ addRulesForUidlist(cmds, ruleDataSet.wifiList, chainName + "-wifi-wan", whitelist);
+ addRulesForUidlist(cmds, ruleDataSet.lanList, chainName + "-wifi-lan", whitelist);
+ addRulesForUidlist(cmds, ruleDataSet.vpnList, chainName + "-vpn", whitelist);
+ addRulesForUidlist(cmds, ruleDataSet.tetherList, chainName + "-tether", whitelist);
+ if (G.enableTor()) {
+ addTorRules(cmds, ruleDataSet.torList, whitelist, ipv6, chainName);
+ }
+ cmds.add("-P OUTPUT ACCEPT");
+ } catch (Exception e) {
+ Log.e(e.getClass().getName(), e.getMessage(), e);
+ }
+
+ iptablesCommands(cmds, out, ipv6);
+ return true;
+ }
+
+ /**
+ * Checks if a collection contains specified uid or {@code SPECIAL_UID_ANY}
+ *
+ * @param uidList collection of uids
+ * @param uidToCheck uid to check
+ * @return true if {@code uidList} contains {@code SPECIAL_UID_ANY} or {@code uidToCheck}
+ */
+ private static boolean containsUidOrAny(Collection uidList, int uidToCheck) {
+ return uidList.contains(SPECIAL_UID_ANY) || uidList.contains(uidToCheck);
+ }
+
+ /**
+ * Add the repetitive parts (ipPath and such) to an iptables command list
+ *
+ * @param in Commands in the format: "-A foo ...", "#NOCHK# -A foo ...", or "#LITERAL# "
+ * @param out A list of UNIX commands to execute
+ */
+ private static void iptablesCommands(List in, List out, boolean ipv6) {
+ String ipPath = getBinaryPath(G.ctx, ipv6);
+
+ String waitTime = "";
+ if(G.ip_path().equals("system")) {
+ // Always use wait flag with system iptables to prevent lock contention
+ waitTime = " -w 5";
+ }
+ boolean firstLit = true;
+ for (String s : in) {
+ s = s + waitTime;
+ if (s.matches("#LITERAL# .*")) {
+ if (firstLit) {
+ // export vars for the benefit of custom scripts
+ // "true" is a dummy command which needs to return success
+ firstLit = false;
+ out.add("export IPTABLES=\"" + ipPath + "\"; "
+ + "export BUSYBOX=\"" + bbPath + "\"; "
+ + "export IPV6=" + (ipv6 ? "1" : "0") + "; "
+ + "true");
+ }
+ out.add(s.replaceFirst("^#LITERAL# ", ""));
+ } else if (s.matches("#NOCHK# .*")) {
+ out.add(s.replaceFirst("^#NOCHK# ", "#NOCHK# " + ipPath + " "));
+ } else {
+ out.add(ipPath + " " + s);
+ }
+ }
+ }
+
+ private static void fixupLegacyCmds(List cmds) {
+ for (int i = 0; i < cmds.size(); i++) {
+ String s = cmds.get(i);
+ if (s.matches("#NOCHK# .*")) {
+ s = s.replaceFirst("^#NOCHK# ", "");
+ } else {
+ s += " || exit";
+ }
+ cmds.set(i, s);
+ }
+ }
+
+ /**
+ * Get thread-safe chain name, handling multi-user scenarios safely
+ */
+ private static String getThreadSafeChainName() {
+ synchronized (CHAIN_NAME_LOCK) {
+ if (G.isMultiUser() && G.getMultiUserId() > 0) {
+ return "afwall" + G.getMultiUserId();
+ }
+ return "afwall";
+ }
+ }
+
+ public static void waitAndTerminate(ExecutorService executorService) {
+ executorService.shutdown();
+ try {
+ if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
+ executorService.shutdownNow();
+ }
+ } catch (InterruptedException ex) {
+ executorService.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public static void applySavedIptablesRules(Context ctx, boolean showErrors, RootCommand callback) {
+ synchronized (GLOBAL_STATUS_LOCK) {
+ if(!globalStatus) {
+ globalStatus = true;
+
+ try {
+ RuleDataSet dataSet = getDataSet();
+ List ipv4cmds = new ArrayList<>();
+ List ipv6cmds = new ArrayList<>();
+
+ // Create thread-safe chain name for this execution
+ final String chainName = getThreadSafeChainName();
+
+ // Apply IPv4 rules first (sequentially)
+ try {
+ Log.i(TAG, "Applying IPv4 rules");
+ applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv4cmds, false, chainName);
+ applySavedIp4tablesRules(ctx, ipv4cmds, callback);
+ Log.i(TAG, "Successfully applied IPv4 rules");
+ } catch (Exception e) {
+ Log.e(TAG, "Error applying IPv4 rules", e);
+ throw new RuntimeException(e);
+ }
+
+ // Apply IPv6 rules second (sequentially after IPv4)
+ if (G.enableIPv6()) {
+ try {
+ Log.i(TAG, "Applying IPv6 rules");
+ applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv6cmds, true, chainName);
+ applySavedIp6tablesRules(ctx, ipv6cmds, new RootCommand());
+ Log.i(TAG, "Successfully applied IPv6 rules");
+ } catch (Exception e) {
+ Log.e(TAG, "Error applying IPv6 rules", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ Log.i(TAG, "Successfully applied all firewall rules");
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error applying rules", e);
+ } finally {
+ globalStatus = false;
+ setRulesUpToDate(true);
+ }
+ } else {
+ Log.i(TAG, "ignore applySavedIptablesRules as existing thread running");
+ }
+ }
+ }
+
+
+ private static RuleDataSet getDataSet() {
+ initSpecial();
+
+ final String savedPkg_wifi_uid = G.pPrefs.getString(PREF_WIFI_PKG_UIDS, "");
+ final String savedPkg_3g_uid = G.pPrefs.getString(PREF_3G_PKG_UIDS, "");
+ final String savedPkg_roam_uid = G.pPrefs.getString(PREF_ROAMING_PKG_UIDS, "");
+ final String savedPkg_vpn_uid = G.pPrefs.getString(PREF_VPN_PKG_UIDS, "");
+ final String savedPkg_tether_uid = G.pPrefs.getString(PREF_TETHER_PKG_UIDS, "");
+ final String savedPkg_lan_uid = G.pPrefs.getString(PREF_LAN_PKG_UIDS, "");
+ final String savedPkg_tor_uid = G.pPrefs.getString(PREF_TOR_PKG_UIDS, "");
+
+
+ List wifiList = getListFromPref(savedPkg_wifi_uid);
+ List dataList = getListFromPref(savedPkg_3g_uid);
+
+
+ // Warn if no applications are configured - this means no blocking will occur
+ if (wifiList.isEmpty() && dataList.isEmpty()) {
+ Log.w(TAG, "WARNING: No applications configured for firewall rules - firewall will not block any traffic!");
+ Log.w(TAG, "Please configure applications in AFWall+ main screen and apply rules.");
+ }
+
+ return new RuleDataSet(wifiList,
+ dataList,
+ getListFromPref(savedPkg_roam_uid),
+ getListFromPref(savedPkg_vpn_uid),
+ getListFromPref(savedPkg_tether_uid),
+ getListFromPref(savedPkg_lan_uid),
+ getListFromPref(savedPkg_tor_uid));
+
+ }
+
+ /**
+ * Purge and re-add all saved rules (not in-memory ones).
+ * This is much faster than just calling "applyIptablesRules", since it don't need to read installed applications.
+ *
+ * @param ctx application context (mandatory)
+ * @param callback If non-null, use a callback instead of blocking the current thread
+ */
+ public static boolean applySavedIp4tablesRules(Context ctx, List cmds, RootCommand callback) {
+ if (ctx == null) {
+ return false;
+ }
+ try {
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, cmds);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while applying IPv4 rules: " + e.getMessage(), e);
+ // Only apply default chains if it's a critical failure
+ // Avoid overriding user chain preferences unnecessarily
+ if (e.getMessage() != null && !e.getMessage().contains("Chain") && !e.getMessage().contains("policy")) {
+ Log.w(TAG, "Applying default chains due to rule application failure");
+ applyDefaultChains(ctx, callback);
+ } else {
+ Log.w(TAG, "Skipping default chains application to preserve user chain preferences");
+ }
+ return false;
+ }
+ }
+
+
+ public static boolean applySavedIp6tablesRules(Context ctx, List cmds, RootCommand callback) {
+ if (ctx == null) {
+ return false;
+ }
+ try {
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, cmds,true);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while applying IPv6 rules: " + e.getMessage(), e);
+ // Only apply default chains if it's a critical failure
+ // Avoid overriding user chain preferences unnecessarily
+ if (e.getMessage() != null && !e.getMessage().contains("Chain") && !e.getMessage().contains("policy")) {
+ Log.w(TAG, "Applying default chains due to rule application failure");
+ applyDefaultChains(ctx, callback);
+ } else {
+ Log.w(TAG, "Skipping default chains application to preserve user chain preferences");
+ }
+ return false;
+ }
+ }
+
+
+ public static boolean fastApply(Context ctx, RootCommand callback) {
+ try {
+ if (!getRulesUpToDate()) {
+ Log.i(TAG, "Using full Apply");
+ applySavedIptablesRules(ctx, true, callback);
+ } else {
+ Log.i(TAG, "Using fastApply");
+ List out = new ArrayList();
+ List cmds;
+ cmds = new ArrayList();
+ applyShortRules(ctx, cmds, false);
+ iptablesCommands(cmds, out, false);
+ if (G.enableIPv6()) {
+ cmds = new ArrayList();
+ applyShortRules(ctx, cmds, true);
+ iptablesCommands(cmds, out, true);
+ }
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception in fastApply: " + e.getMessage(), e);
+ // Only apply default chains if it's a critical failure
+ // Avoid overriding user chain preferences unnecessarily
+ if (e.getMessage() != null && !e.getMessage().contains("Chain") && !e.getMessage().contains("policy")) {
+ Log.w(TAG, "Applying default chains due to fastApply failure");
+ applyDefaultChains(ctx, callback);
+ } else {
+ Log.w(TAG, "Skipping default chains application in fastApply to preserve user chain preferences");
+ }
+ }
+ setRulesUpToDate(true);
+ return true;
+ }
+
+ /**
+ * Save current rules using the preferences storage.
+ *
+ * @param ctx application context (mandatory)
+ */
+ public static RuleDataSet generateRules(Context ctx, List apps, boolean store) {
+
+ setRulesUpToDate(false);
+ RuleDataSet dataSet = null;
+
+ if (apps != null) {
+ // Builds a pipe-separated list of names
+ HashSet newpkg_wifi = new HashSet();
+ HashSet newpkg_3g = new HashSet();
+ HashSet newpkg_roam = new HashSet();
+ HashSet newpkg_vpn = new HashSet();
+ HashSet newpkg_tether = new HashSet();
+ HashSet newpkg_lan = new HashSet();
+ HashSet newpkg_tor = new HashSet();
+
+ for (int i = 0; i < apps.size(); i++) {
+ if (apps.get(i) != null) {
+ if (apps.get(i).selected_wifi) {
+ newpkg_wifi.add(apps.get(i).uid);
+ } else {
+ if (!store) newpkg_wifi.add(-apps.get(i).uid);
+ }
+ if (apps.get(i).selected_3g) {
+ newpkg_3g.add(apps.get(i).uid);
+ } else {
+ if (!store) newpkg_3g.add(-apps.get(i).uid);
+ }
+ if (G.enableRoam()) {
+ if (apps.get(i).selected_roam) {
+ newpkg_roam.add(apps.get(i).uid);
+ } else {
+ if (!store) newpkg_roam.add(-apps.get(i).uid);
+ }
+ }
+ if (G.enableVPN()) {
+ if (apps.get(i).selected_vpn) {
+ newpkg_vpn.add(apps.get(i).uid);
+ } else {
+ if (!store) newpkg_vpn.add(-apps.get(i).uid);
+ }
+ }
+ if (G.enableTether()) {
+ if (apps.get(i).selected_tether) {
+ newpkg_tether.add(apps.get(i).uid);
+ } else {
+ if (!store) newpkg_tether.add(-apps.get(i).uid);
+ }
+ }
+ if (G.enableLAN()) {
+ if (apps.get(i).selected_lan) {
+ newpkg_lan.add(apps.get(i).uid);
+ } else {
+ if (!store) newpkg_lan.add(-apps.get(i).uid);
+ }
+ }
+ if (G.enableTor()) {
+ if (apps.get(i).selected_tor) {
+ newpkg_tor.add(apps.get(i).uid);
+ } else {
+ if (!store) newpkg_tor.add(-apps.get(i).uid);
+ }
+ }
+ }
+ }
+
+ String wifi = android.text.TextUtils.join("|", newpkg_wifi);
+ String data = android.text.TextUtils.join("|", newpkg_3g);
+ String roam = android.text.TextUtils.join("|", newpkg_roam);
+ String vpn = android.text.TextUtils.join("|", newpkg_vpn);
+ String tether = android.text.TextUtils.join("|", newpkg_tether);
+ String lan = android.text.TextUtils.join("|", newpkg_lan);
+ String tor = android.text.TextUtils.join("|", newpkg_tor);
+ // save the new list of UIDs
+ if (store) {
+ SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ Editor edit = prefs.edit();
+ edit.putString(PREF_WIFI_PKG_UIDS, wifi);
+ edit.putString(PREF_3G_PKG_UIDS, data);
+ edit.putString(PREF_ROAMING_PKG_UIDS, roam);
+ edit.putString(PREF_VPN_PKG_UIDS, vpn);
+ edit.putString(PREF_TETHER_PKG_UIDS, tether);
+ edit.putString(PREF_LAN_PKG_UIDS, lan);
+ edit.putString(PREF_TOR_PKG_UIDS, tor);
+ edit.apply();
+ } else {
+ dataSet = new RuleDataSet(new ArrayList<>(newpkg_wifi),
+ new ArrayList<>(newpkg_3g),
+ new ArrayList<>(newpkg_roam),
+ new ArrayList<>(newpkg_vpn),
+ new ArrayList<>(newpkg_tether),
+ new ArrayList<>(newpkg_lan),
+ new ArrayList<>(newpkg_tor));
+ }
+ }
+ return dataSet;
+
+ }
+
+ /**
+ * Purge all iptables rules.
+ *
+ * @param ctx mandatory context
+ * @param showErrors indicates if errors should be alerted
+ * @param callback If non-null, use a callback instead of blocking the current thread
+ * @return true if the rules were purged
+ */
+ public static void purgeIptables(Context ctx, boolean showErrors, RootCommand callback) {
+ String chainName = getThreadSafeChainName();
+
+ List cmds = new ArrayList<>();
+ List cmdsv4 = new ArrayList<>();
+ List out = new ArrayList<>();
+
+ for (String s : staticChains) {
+ cmds.add("-F " + chainName + s);
+ }
+ for (String s : dynChains) {
+ cmds.add("-F " + chainName + s);
+ }
+ if (G.enableTor()) {
+ for (String s : natChains) {
+ cmdsv4.add("-t nat -F " + chainName + s);
+ }
+ cmdsv4.add("#NOCHK# -t nat -D OUTPUT -j " + chainName);
+ } else {
+ cmdsv4.add("#NOCHK# -D OUTPUT -j " + chainName);
+ }
+
+ //make sure reset the OUTPUT chain to accept state.
+ cmds.add("-P OUTPUT ACCEPT");
+
+ //Delete only when the afwall chain exist !
+ //cmds.add("-D OUTPUT -j " + chainName);
+
+ if (G.enableInbound()) {
+ cmds.add("-D INPUT -j " + chainName + "-input");
+ }
+
+ addCustomRules(Api.PREF_CUSTOMSCRIPT2, cmds);
+
+ // Execute the purge commands and call the callback
+ Log.i(TAG, "Executing purge commands for IPv4");
+ cmds.addAll(cmdsv4);
+ iptablesCommands(cmds, out, false);
+
+ if (G.enableIPv6()) {
+ Log.i(TAG, "Executing purge commands for IPv6");
+ List cmdsv6 = new ArrayList<>();
+ for (String s : staticChains) {
+ cmdsv6.add("-F " + chainName + s);
+ }
+ for (String s : dynChains) {
+ cmdsv6.add("-F " + chainName + s);
+ }
+ cmdsv6.add("#NOCHK# -D OUTPUT -j " + chainName);
+ cmdsv6.add("-P OUTPUT ACCEPT");
+ if (G.enableInbound()) {
+ cmdsv6.add("-D INPUT -j " + chainName + "-input");
+ }
+ iptablesCommands(cmdsv6, out, true);
+ }
+
+ Log.i(TAG, "Purge completed, calling callback");
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
+ }
+
+ /**
+ * Add DNS-specific iptables rules for identified DNS servers instead of broad LAN access
+ */
+ private static void addDnsServerRules(List cmds, InterfaceDetails cfg, String chain, boolean ipv6) {
+ String protocol = ipv6 ? "ip6tables" : "iptables";
+ java.util.List dnsServers = ipv6 ? cfg.dnsServersV6 : cfg.dnsServersV4;
+
+ for (String dnsServer : dnsServers) {
+ if (dnsServer != null && !dnsServer.isEmpty()) {
+ // Add rules for both UDP and TCP DNS traffic to specific servers
+ cmds.add("-A " + chain + " -d " + dnsServer + " -p udp --dport 53 -j RETURN");
+ cmds.add("-A " + chain + " -d " + dnsServer + " -p tcp --dport 53 -j RETURN");
+ }
+ }
+ }
+
+
+ /**
+ * Retrieve the current set of IPv4 or IPv6 rules and pass it to a callback
+ *
+ * @param ctx application context
+ * @param callback callback to receive rule list
+ * @param useIPV6 true to list IPv6 rules, false to list IPv4 rules
+ */
+ public static void fetchIptablesRules(Context ctx, boolean useIPV6, RootCommand callback) {
+ List cmds = new ArrayList<>();
+ List out = new ArrayList<>();
+ cmds.add("-n -v -L");
+ iptablesCommands(cmds, out, false);
+ if (useIPV6) {
+ iptablesCommands(cmds, out, true);
+ }
+ callback.run(ctx, out);
+ }
+
+ /**
+ * Run a list of commands with both iptables and ip6tables
+ *
+ * @param ctx application context
+ * @param cmds list of commands to run
+ * @param callback callback for completion
+ */
+ public static void apply46(Context ctx, List cmds, RootCommand callback) {
+ List out = new ArrayList();
+ iptablesCommands(cmds, out, false);
+
+ if (G.enableIPv6()) {
+ iptablesCommands(cmds, out, true);
+ }
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
+ }
+
+ public static void applyIPv6Quick(Context ctx, List cmds, RootCommand callback) {
+ List out = new ArrayList();
+ ////setBinaryPath(ctx, true);
+ iptablesCommands(cmds, out, true);
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
+ }
+
+ public static void applyQuick(Context ctx, List cmds, RootCommand callback) {
+ List out = new ArrayList();
+
+ //setBinaryPath(ctx, false);
+ iptablesCommands(cmds, out, false);
+
+ //related to #511, disable ipv6 but use startup leak.
+ if (G.enableIPv6() || G.fixLeak()) {
+ //setBinaryPath(ctx, true);
+ iptablesCommands(cmds, out, true);
+ }
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
+ }
+
+ /**
+ * Delete all kingroot firewall rules. For diagnostic purposes only.
+ *
+ * @param ctx application context
+ * @param callback callback for completion
+ */
+ public static void flushAllRules(Context ctx, RootCommand callback) {
+ List cmds = new ArrayList();
+ cmds.add("-F");
+ cmds.add("-X");
+ apply46(ctx, cmds, callback);
+ }
+
+ /**
+ * Enable or disable logging by rewriting the afwall-reject chain. Logging
+ * will be enabled or disabled based on the preference setting.
+ *
+ * @param ctx application context
+ * @param callback callback for completion
+ */
+ public static void updateLogRules(Context ctx, RootCommand callback) {
+ if (!isEnabled(ctx)) {
+ return;
+ }
+ String chainName = getThreadSafeChainName();
+ List cmds = new ArrayList();
+ cmds.add("#NOCHK# -N " + chainName + "-reject");
+ cmds.add("-F " + chainName + "-reject");
+ addRejectRules(cmds, chainName);
+ apply46(ctx, cmds, callback);
+ }
+
+
+ //purge 2 hour data
+ public static void purgeOldLog() {
+ long purgeInterval = System.currentTimeMillis() - 7200000;
+ long count = new Select(com.raizlabs.android.dbflow.sql.language.Method.count()).from(LogData.class).count();
+ //records are more
+ if(count > 5000) {
+ new Delete().from(LogData.class).where(LogData_Table.timestamp.lessThan(purgeInterval)).async().execute();
+ }
+ }
+
+ /**
+ * Fetch kernel logs via busybox dmesg. This will include {AFL} lines from
+ * logging rejected packets.
+ *
+ * @return true if logging is enabled, false otherwise
+ */
+ public static List fetchLogs() {
+ //load hour data due to performance issue with old view
+ long loadInterval = System.currentTimeMillis() - 3600000;
+ List log = SQLite.select()
+ .from(LogData.class)
+ .where(LogData_Table.timestamp.greaterThan(loadInterval))
+ .orderBy(LogData_Table.timestamp, true)
+ .queryList();
+ purgeOldLog();
+ //fetch last 100 records
+ if (log.size() > 100) {
+ return log.subList((log.size() - 100), log.size());
+ } else {
+ return log;
+ }
+ }
+
+ /**
+ * List all interfaces via "ifconfig -a"
+ *
+ * @param ctx application context
+ * @param callback Callback for completion status
+ */
+ public static void runIfconfig(Context ctx, RootCommand callback) {
+ // Try system ifconfig first, then busybox for all versions
+ callback.run(ctx, "ifconfig -a || " + getBusyBoxPath(ctx, true) + " ifconfig -a");
+ }
+
+ public static void runNetworkInterface(Context ctx, RootCommand callback) {
+ // Try Android API method first for all versions
+ try {
+ StringBuilder result = new StringBuilder();
+ java.util.Enumeration interfaces = java.net.NetworkInterface.getNetworkInterfaces();
+ while (interfaces.hasMoreElements()) {
+ java.net.NetworkInterface networkInterface = interfaces.nextElement();
+ result.append(networkInterface.getName()).append("\n");
+ }
+ if (result.length() > 0) {
+ // Create a mock RootCommand with API results
+ RootCommand apiResult = new RootCommand();
+ apiResult.res = result;
+ apiResult.exitCode = 0;
+ apiResult.done = true;
+ if (callback.cb != null) {
+ callback.cb.cbFunc(apiResult);
+ }
+ return;
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Android API network interface detection failed: " + e.getMessage());
+ }
+
+ // Fallback to shell commands with multiple options
+ String cmd = "ls /sys/class/net 2>/dev/null || " +
+ getBusyBoxPath(ctx, true) + " ls /sys/class/net 2>/dev/null || " +
+ "ip link show 2>/dev/null";
+ callback.run(ctx, cmd);
+ }
+
+
+ public static void fixFolderPermissionsAsync(Context mContext) {
+ AsyncTask.execute(() -> {
+ try {
+ mContext.getFilesDir().setExecutable(true, false);
+ mContext.getFilesDir().setReadable(true, false);
+ File sharedPrefsFolder = new File(mContext.getFilesDir().getAbsolutePath()
+ + "/../shared_prefs");
+ sharedPrefsFolder.setExecutable(true, false);
+ sharedPrefsFolder.setReadable(true, false);
+ } catch (Exception e) {
+ Log.e(Api.TAG, e.getMessage(), e);
+ }
+ });
+ }
+
+ /**
+ * @param ctx application context (mandatory)
+ * @return a list of applications
+ */
+ public static List getApps(Context ctx, GetAppList appList) {
+
+ initSpecial();
+ if (applications != null && applications.size() > 0) {
+ // return cached instance
+ return applications;
+ }
+
+ SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+
+ String savedPkg_wifi_uid = prefs.getString(PREF_WIFI_PKG_UIDS, "");
+ String savedPkg_3g_uid = prefs.getString(PREF_3G_PKG_UIDS, "");
+ String savedPkg_roam_uid = prefs.getString(PREF_ROAMING_PKG_UIDS, "");
+ String savedPkg_vpn_uid = prefs.getString(PREF_VPN_PKG_UIDS, "");
+ String savedPkg_tether_uid = prefs.getString(PREF_TETHER_PKG_UIDS, "");
+ String savedPkg_lan_uid = prefs.getString(PREF_LAN_PKG_UIDS, "");
+ String savedPkg_tor_uid = prefs.getString(PREF_TOR_PKG_UIDS, "");
+
+ List selected_wifi;
+ List selected_3g;
+ List selected_roam = new ArrayList<>();
+ List selected_vpn = new ArrayList<>();
+ List selected_tether = new ArrayList<>();
+ List selected_lan = new ArrayList<>();
+ List selected_tor = new ArrayList<>();
+
+
+ selected_wifi = getListFromPref(savedPkg_wifi_uid);
+ selected_3g = getListFromPref(savedPkg_3g_uid);
+
+ if (G.enableRoam()) {
+ selected_roam = getListFromPref(savedPkg_roam_uid);
+ }
+ if (G.enableVPN()) {
+ selected_vpn = getListFromPref(savedPkg_vpn_uid);
+ }
+ if (G.enableTether()) {
+ selected_tether = getListFromPref(savedPkg_tether_uid);
+ }
+ if (G.enableLAN()) {
+ selected_lan = getListFromPref(savedPkg_lan_uid);
+ }
+ if (G.enableTor()) {
+ selected_tor = getListFromPref(savedPkg_tor_uid);
+ }
+ //revert back to old approach
+
+ //always use the defaul preferences to store cache value - reduces the application usage size
+ SharedPreferences cachePrefs = ctx.getSharedPreferences(DEFAULT_PREFS_NAME, Context.MODE_PRIVATE);
+
+ int count = 0;
+ try {
+ listOfUids = new ArrayList<>();
+ //this code will be executed on devices running ICS or later
+ final UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
+ List list = um.getUserProfiles();
+
+ for (UserHandle user : list) {
+ Matcher m = p.matcher(user.toString());
+ if (m.find() && m.groupCount() > 0) {
+ int id = Integer.parseInt(m.group(1));
+ if (id > 0) {
+ listOfUids.add(id);
+ }
+ }
+ }
+ //use pm list packages -f -U --user 10
+ int pkgManagerFlags = PackageManager.GET_META_DATA;
+ // it's useless to iterate over uninstalled packages if we don't support multi-profile apps
+ if (G.supportDual()) {
+ pkgManagerFlags |= PackageManager.GET_UNINSTALLED_PACKAGES;
+ }
+ PackageManager pkgmanager = ctx.getPackageManager();
+ List installed = pkgmanager.getInstalledApplications(pkgManagerFlags);
+ SparseArray syncMap = new SparseArray<>();
+ Editor edit = cachePrefs.edit();
+ boolean changed = false;
+ String name;
+ String cachekey;
+ String cacheLabel = "cache.label.";
+ PackageInfoData app;
+ ApplicationInfo apinfo;
+
+ Date install = new Date();
+ install.setTime(System.currentTimeMillis() - (180000));
+
+ SparseArray multiUserAppsMap = new SparseArray<>();
+ HashMap packagesForUser = new HashMap<>();
+ if(G.supportDual()) {
+ packagesForUser = getPackagesForUser(listOfUids);
+ }
+
+ for (int i = 0; i < installed.size(); i++) {
+ //for (ApplicationInfo apinfo : installed) {
+ count = count + 1;
+ apinfo = installed.get(i);
+
+ if (appList != null) {
+ appList.doProgress(count);
+ }
+
+ boolean firstseen = false;
+ app = syncMap.get(apinfo.uid);
+ // filter applications which are not allowed to access the Internet
+ if (app == null && PackageManager.PERMISSION_GRANTED != pkgmanager.checkPermission(Manifest.permission.INTERNET, apinfo.packageName) && !showAllApps()) {
+ continue;
+ }
+ // try to get the application label from our cache - getApplicationLabel() is horribly slow!!!!
+ cachekey = cacheLabel + apinfo.packageName;
+ name = prefs.getString(cachekey, "");
+ if (name.length() == 0 || isRecentlyInstalled(apinfo.packageName)) {
+ // get label and put on cache
+ name = pkgmanager.getApplicationLabel(apinfo).toString();
+ edit.putString(cachekey, name);
+ changed = true;
+ firstseen = true;
+ }
+ if (app == null) {
+ app = new PackageInfoData();
+ app.uid = apinfo.uid;
+ app.installTime = new File(apinfo.sourceDir).lastModified();
+ app.names = new ArrayList();
+ app.names.add(name);
+ app.appinfo = apinfo;
+ if (app.appinfo != null && (app.appinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ //user app
+ app.appType = 1;
+ } else {
+ //system app
+ app.appType = 0;
+ }
+ app.pkgName = apinfo.packageName;
+ if ((apinfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0)
+ syncMap.put(apinfo.uid, app);
+ } else {
+ app.names.add(name);
+ }
+
+ app.firstseen = firstseen;
+ // check if this application is selected
+ if (!app.selected_wifi && Collections.binarySearch(selected_wifi, app.uid) >= 0) {
+ app.selected_wifi = true;
+ }
+ if (!app.selected_3g && Collections.binarySearch(selected_3g, app.uid) >= 0) {
+ app.selected_3g = true;
+ }
+ if (G.enableRoam() && !app.selected_roam && Collections.binarySearch(selected_roam, app.uid) >= 0) {
+ app.selected_roam = true;
+ }
+ if (G.enableVPN() && !app.selected_vpn && Collections.binarySearch(selected_vpn, app.uid) >= 0) {
+ app.selected_vpn = true;
+ }
+ if (G.enableTether() && !app.selected_tether && Collections.binarySearch(selected_tether, app.uid) >= 0) {
+ app.selected_tether = true;
+ }
+ if (G.enableLAN() && !app.selected_lan && Collections.binarySearch(selected_lan, app.uid) >= 0) {
+ app.selected_lan = true;
+ }
+ if (G.enableTor() && !app.selected_tor && Collections.binarySearch(selected_tor, app.uid) >= 0) {
+ app.selected_tor = true;
+ }
+ if (G.supportDual()) {
+ checkPartOfMultiUser(apinfo, name, listOfUids, packagesForUser, multiUserAppsMap);
+ }
+ }
+
+ if (G.supportDual()) {
+ //run through multi user map
+ for (int i = 0; i < multiUserAppsMap.size(); i++) {
+ app = multiUserAppsMap.valueAt(i);
+ if (!app.selected_wifi && Collections.binarySearch(selected_wifi, app.uid) >= 0) {
+ app.selected_wifi = true;
+ }
+ if (!app.selected_3g && Collections.binarySearch(selected_3g, app.uid) >= 0) {
+ app.selected_3g = true;
+ }
+ if (G.enableRoam() && !app.selected_roam && Collections.binarySearch(selected_roam, app.uid) >= 0) {
+ app.selected_roam = true;
+ }
+ if (G.enableVPN() && !app.selected_vpn && Collections.binarySearch(selected_vpn, app.uid) >= 0) {
+ app.selected_vpn = true;
+ }
+ if (G.enableTether() && !app.selected_tether && Collections.binarySearch(selected_tether, app.uid) >= 0) {
+ app.selected_tether = true;
+ }
+ if (G.enableLAN() && !app.selected_lan && Collections.binarySearch(selected_lan, app.uid) >= 0) {
+ app.selected_lan = true;
+ }
+ if (G.enableTor() && !app.selected_tor && Collections.binarySearch(selected_tor, app.uid) >= 0) {
+ app.selected_tor = true;
+ }
+ syncMap.put(app.uid, app);
+ }
+ }
+
+ List specialData = getSpecialData();
+
+ if (specialApps == null) {
+ specialApps = new HashMap();
+ }
+ for (int i = 0; i < specialData.size(); i++) {
+ app = specialData.get(i);
+ //core apps
+ app.appType = 2;
+ specialApps.put(app.pkgName, app.uid);
+ //default DNS/NTP
+ if (app.uid != -1 && syncMap.get(app.uid) == null) {
+ // check if this application is allowed
+ if (!app.selected_wifi && Collections.binarySearch(selected_wifi, app.uid) >= 0) {
+ app.selected_wifi = true;
+ }
+ if (!app.selected_3g && Collections.binarySearch(selected_3g, app.uid) >= 0) {
+ app.selected_3g = true;
+ }
+ if (G.enableRoam() && !app.selected_roam && Collections.binarySearch(selected_roam, app.uid) >= 0) {
+ app.selected_roam = true;
+ }
+ if (G.enableVPN() && !app.selected_vpn && Collections.binarySearch(selected_vpn, app.uid) >= 0) {
+ app.selected_vpn = true;
+ }
+ if (G.enableTether() && !app.selected_tether && Collections.binarySearch(selected_tether, app.uid) >= 0) {
+ app.selected_tether = true;
+ }
+ if (G.enableLAN() && !app.selected_lan && Collections.binarySearch(selected_lan, app.uid) >= 0) {
+ app.selected_lan = true;
+ }
+ if (G.enableTor() && !app.selected_tor && Collections.binarySearch(selected_tor, app.uid) >= 0) {
+ app.selected_tor = true;
+ }
+ syncMap.put(app.uid, app);
+ }
+ }
+
+ if (changed) {
+ edit.apply();
+ }
+ /* convert the map into an array */
+ applications = Collections.synchronizedList(new ArrayList());
+ for (int i = 0; i < syncMap.size(); i++) {
+ applications.add(syncMap.valueAt(i));
+ }
+ return applications;
+ } catch (Exception e) {
+ Log.i(TAG, "Exception in getting app list", e);
+ }
+ return new ArrayList<>();
+ }
+
+ /* public boolean isSuPackage(PackageManager pm, String suPackage) {
+ boolean found = false;
+ try {
+ PackageInfo info = pm.getPackageInfo(suPackage, 0);
+ if (info.applicationInfo != null) {
+ found = true;
+ }
+ //found = s + " v" + info.versionName;
+ } catch (NameNotFoundException e) {
+ }
+ return found;
+ }*/
+
+ public static List getSpecialData() {
+ List specialData = new ArrayList<>();
+ specialData.add(new PackageInfoData(SPECIAL_UID_ANY, ctx.getString(R.string.all_item), "dev.afwall.special.any"));
+ specialData.add(new PackageInfoData(SPECIAL_UID_KERNEL, ctx.getString(R.string.kernel_item), "dev.afwall.special.kernel"));
+ specialData.add(new PackageInfoData(SPECIAL_UID_TETHER, ctx.getString(R.string.tethering_item), "dev.afwall.special.tether"));
+ specialData.add(new PackageInfoData(SPECIAL_UID_NTP, ctx.getString(R.string.ntp_item), "dev.afwall.special.ntp"));
+
+
+ specialData.add(new PackageInfoData(1020, ctx.getString(R.string.mdnslabel), "dev.afwall.special.mdnsr"));
+
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ specialData.add(new PackageInfoData(1029, ctx.getString(R.string.clat), "dev.afwall.special.clat"));
+ }
+
+ /*if (additional) {
+ specialData.add(new PackageInfoData(1020, "mDNS", "dev.afwall.special.mDNS"));
+ }*/
+ for (String acct : specialAndroidAccounts) {
+ String dsc = getSpecialDescription(ctx, acct);
+ if (dsc != null) {
+ String pkg = "dev.afwall.special." + acct;
+ specialData.add(new PackageInfoData(acct, dsc, pkg));
+ }
+ }
+ return specialData;
+ }
+
+ private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, List uid1, HashMap pkgs, SparseArray syncMap) {
+ try {
+ for (Integer integer : uid1) {
+ int appUid = Integer.parseInt(integer + "" + apinfo.uid + "");
+ try{
+ //String[] pkgs = pkgmanager.getPackagesForUid(appUid);
+ if (packagesExistForUserUid(pkgs, appUid)) {
+ PackageInfoData app = new PackageInfoData();
+ app.uid = appUid;
+ app.installTime = new File(apinfo.sourceDir).lastModified();
+ app.names = new ArrayList();
+ app.names.add(name + "(M)");
+ app.appinfo = apinfo;
+ if (app.appinfo != null && (app.appinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ //user app
+ app.appType = 1;
+ } else {
+ //system app
+ app.appType = 0;
+ }
+ app.pkgName = apinfo.packageName;
+ syncMap.put(appUid, app);
+ }
+ }catch (Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+
+ private static boolean packagesExistForUserUid(HashMap pkgs, int appUid) {
+ if(pkgs.containsKey(appUid)){
+ return true;
+ }
+ return false;
+ }
+
+ public static HashMap getPackagesForUser(List userProfile) {
+ HashMap listApps = new HashMap<>();
+ for(Integer integer: userProfile) {
+ try {
+ Shell.Result result = Shell.cmd("pm list packages -U --user " + integer).exec();
+ List out = result.getOut();
+ Matcher matcher;
+ for (String item : out) {
+ matcher = dual_pattern.matcher(item);
+ if (matcher.find() && matcher.groupCount() > 0) {
+ String packageName = matcher.group(1);
+ String packageId = matcher.group(2);
+ Log.i(TAG, packageId + " " + packageName);
+ listApps.put(Integer.parseInt(packageId), packageName);
+ }
+ }
+ } catch (java.util.concurrent.RejectedExecutionException e) {
+ Log.w(TAG, "Package listing rejected for user " + integer + ": " + e.getMessage());
+ break; // Stop processing other users if execution rejected
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to list packages for user " + integer + ": " + e.getMessage());
+ // Continue with next user on other errors
+ }
+ }
+ return listApps.size() > 0 ? listApps : null;
+ }
+
+ private static boolean isRecentlyInstalled(String packageName) {
+ boolean isRecent = false;
+ if (recentlyInstalled != null && recentlyInstalled.contains(packageName)) {
+ isRecent = true;
+ recentlyInstalled.remove(packageName);
+ }
+ return isRecent;
+ }
+
+ private static List getListFromPref(String savedPkg_uid) {
+ StringTokenizer tok = new StringTokenizer(savedPkg_uid, "|");
+ List listUids = new ArrayList<>();
+ while (tok.hasMoreTokens()) {
+ String uid = tok.nextToken();
+ if (!uid.equals("")) {
+ listUids.add(Integer.parseInt(uid));
+ }
+ }
+ // Sort the array to allow using "Arrays.binarySearch" later
+ Collections.sort(listUids);
+ return listUids;
+ }
+
+ /*public static boolean isAppAllowed(Context context, ApplicationInfo applicationInfo, SharedPreferences sharedPreferences, SharedPreferences pPrefs) {
+ InterfaceDetails details = InterfaceTracker.getCurrentCfg(context, true);
+ //allow webview to download since webview requires INTERNET permission
+ if (applicationInfo.packageName.equals("com.android.webview") || applicationInfo.packageName.equals("com.google.android.webview")) {
+ return true;
+ }
+ if (details != null && details.netEnabled) {
+ String mode = pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST);
+ Log.i(TAG, "Calling isAppAllowed method from DM with Mode: " + mode);
+ switch ((details.netType)) {
+ case ConnectivityManager.TYPE_WIFI:
+ String savedPkg_wifi_uid = pPrefs.getString(PREF_WIFI_PKG_UIDS, "");
+ if (savedPkg_wifi_uid.isEmpty()) {
+ savedPkg_wifi_uid = sharedPreferences.getString(PREF_WIFI_PKG_UIDS, "");
+ }
+ Log.i(TAG, "DM check for UID: " + applicationInfo.uid);
+ Log.i(TAG, "DM allowed UIDs: " + savedPkg_wifi_uid);
+ if (mode.equals(Api.MODE_WHITELIST) && savedPkg_wifi_uid.contains(applicationInfo.uid + "")) {
+ return true;
+ } else return mode.equals(Api.MODE_BLACKLIST) && !savedPkg_wifi_uid.contains(applicationInfo.uid + "");
+
+ case ConnectivityManager.TYPE_MOBILE:
+ String savedPkg_3g_uid = pPrefs.getString(PREF_3G_PKG_UIDS, "");
+ if (details.isRoaming) {
+ savedPkg_3g_uid = pPrefs.getString(PREF_ROAMING_PKG_UIDS, "");
+ }
+ Log.i(TAG, "DM check for UID: " + applicationInfo.uid);
+ Log.i(TAG, "DM allowed UIDs: " + savedPkg_3g_uid);
+ if (mode.equals(Api.MODE_WHITELIST) && savedPkg_3g_uid.contains(applicationInfo.uid + "")) {
+ return true;
+ } else return mode.equals(Api.MODE_BLACKLIST) && !savedPkg_3g_uid.contains(applicationInfo.uid + "");
+ }
+ }
+
+ return true;
+ }*/
+
+ /**
+ * Get Default Chain status
+ *
+ * @param ctx
+ * @param callback
+ */
+ public static void getChainStatus(Context ctx, RootCommand callback) {
+ List cmds = new ArrayList();
+ cmds.add("-S INPUT");
+ cmds.add("-S OUTPUT");
+ cmds.add("-S FORWARD");
+ List out = new ArrayList<>();
+
+ iptablesCommands(cmds, out, false);
+
+ ArrayList base = new ArrayList();
+ base.add("-S INPUT");
+ base.add("-S OUTPUT");
+ cmds.add("-S FORWARD");
+ iptablesCommands(base, out, true);
+
+ callback.run(ctx, out);
+ }
+
+ /**
+ * Apply single rule
+ *
+ * @param ctx
+ * @param rule
+ * @param isIpv6
+ * @param callback
+ */
+ public static void applyRule(Context ctx, String rule, boolean isIpv6, RootCommand callback) {
+ List cmds = new ArrayList();
+ cmds.add(rule);
+ //setBinaryPath(ctx, isIpv6);
+ List out = new ArrayList<>();
+ iptablesCommands(cmds, out, isIpv6);
+ callback.run(ctx, out);
+ }
+
+ /**
+ * Runs a script as root (multiple commands separated by "\n")
+ *
+ * @param ctx mandatory context
+ * @param script the script to be executed
+ * @param res the script output response (stdout + stderr)
+ * @return the script exit code
+ * @throws IOException on any error executing the script, or writing it to disk
+ */
+ public static int runScriptAsRoot(Context ctx, List script, StringBuilder res) throws IOException {
+ int returnCode = -1;
+
+ if ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())) {
+ Log.e(TAG, "runScriptAsRoot should not be called from the main thread\nCall Trace:\n");
+ for (StackTraceElement e : new Throwable().getStackTrace()) {
+ Log.e(TAG, e.toString());
+ }
+ }
+
+ try {
+ RunCommand runCommand = new RunCommand();
+ returnCode = runCommand.execute(script, res, ctx).get();
+ } catch (RejectedExecutionException r) {
+ Log.w(TAG, "Shell execution rejected, likely due to app shutdown: " + r.getLocalizedMessage());
+ returnCode = -1;
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Shell execution was interrupted: " + e.getLocalizedMessage());
+ Thread.currentThread().interrupt(); // Restore interrupted status
+ returnCode = -1;
+ } catch (java.util.concurrent.ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof java.io.InterruptedIOException) {
+ Log.w(TAG, "Shell execution interrupted (IO): " + cause.getMessage());
+ } else if (cause instanceof java.util.concurrent.RejectedExecutionException) {
+ Log.w(TAG, "Shell execution rejected in wrapped exception: " + cause.getMessage());
+ } else {
+ Log.e(TAG, "Shell execution failed with ExecutionException: " + e.getLocalizedMessage());
+ }
+ returnCode = -1;
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected error during shell execution: " + e.getLocalizedMessage());
+ returnCode = -1;
+ }
+
+ return returnCode;
+ }
+
+ private static boolean installBinary(Context ctx, int resId, String filename) {
+ try {
+ File binDir = ctx.getDir("bin", 0);
+ File f = new File(binDir, filename);
+
+ Log.d(TAG, "Installing binary: " + filename + " to " + f.getAbsolutePath());
+
+ if (f.exists()) {
+ Log.d(TAG, "Removing existing binary: " + filename);
+ if (!f.delete()) {
+ Log.w(TAG, "Failed to delete existing binary: " + filename);
+ }
+ }
+
+ copyRawFile(ctx, resId, f, "0755");
+
+ // Verify the binary was installed correctly
+ if (!f.exists()) {
+ Log.e(TAG, "Binary installation failed - file does not exist: " + filename);
+ return false;
+ }
+
+ if (!f.canExecute()) {
+ Log.w(TAG, "Binary installed but not executable: " + filename);
+ // Try to fix permissions manually
+ try {
+ f.setExecutable(true, false);
+ Log.d(TAG, "Fixed permissions for: " + filename);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to fix permissions for: " + filename + " - " + e.getMessage());
+ }
+ }
+
+ Log.d(TAG, "Successfully installed binary: " + filename +
+ " (size: " + f.length() + " bytes, executable: " + f.canExecute() + ")");
+ return true;
+
+ } catch (Exception e) {
+ Log.e(TAG, "installBinary failed for " + filename + ": " + e.getClass().getSimpleName() +
+ " - " + e.getLocalizedMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * Install binary if the resource exists, using reflection to check for resource availability
+ * @param ctx Context
+ * @param resourceName Name of the resource (e.g., "busybox_arm64")
+ * @param filename Target filename
+ * @return true if installed successfully or resource doesn't exist, false on installation error
+ */
+ private static boolean installBinaryIfExists(Context ctx, String resourceName, String filename) {
+ try {
+ // Use reflection to check if the resource exists
+ Class> rawClass = R.raw.class;
+ java.lang.reflect.Field field = rawClass.getDeclaredField(resourceName);
+ int resId = field.getInt(null);
+
+ // Resource exists, try to install it
+ return installBinary(ctx, resId, filename);
+ } catch (NoSuchFieldException e) {
+ // Resource doesn't exist - this is expected when binaries are not yet added
+ Log.d(TAG, "Resource " + resourceName + " not found - this is expected if binary is not yet available");
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Error checking/installing binary " + resourceName + ": " + e.getMessage());
+ return false;
+ }
+ }
+
+ private static boolean installBinariesX86(Context ctx) {
+ if (!installBinary(ctx, R.raw.busybox_x86, "busybox")) return false;
+ if (!installBinary(ctx, R.raw.iptables_x86, "iptables")) return false;
+ if (!installBinary(ctx, R.raw.ip6tables_x86, "ip6tables")) return false;
+ if (!installBinary(ctx, R.raw.nflog_x86, "nflog")) return false;
+
+
+ return true;
+ }
+
+
+ private static boolean installBinariesArm64(Context ctx) {
+ if (!installBinary(ctx, R.raw.busybox_arm64, "busybox")) return false;
+ if (!installBinary(ctx, R.raw.iptables_arm64, "iptables")) return false;
+ if (!installBinary(ctx, R.raw.ip6tables_arm64, "ip6tables")) return false;
+ if (!installBinary(ctx, R.raw.nflog_arm64, "nflog")) return false;
+
+
+ return true;
+ }
+
+ private static boolean installBinariesArm(Context ctx) {
+ if (!installBinary(ctx, R.raw.busybox_arm, "busybox")) return false;
+ if (!installBinary(ctx, R.raw.iptables_arm, "iptables")) return false;
+ if (!installBinary(ctx, R.raw.ip6tables_arm, "ip6tables")) return false;
+ if (!installBinary(ctx, R.raw.nflog_arm, "nflog")) return false;
+
+
+ return true;
+ }
+
+ private static boolean installBinariesForAbi(Context ctx, String abi) {
+ if (abi.startsWith("x86")) {
+ return installBinariesX86(ctx);
+ } else if (abi.startsWith("arm64")) {
+ return installBinariesArm64(ctx);
+ } else {
+ return installBinariesArm(ctx);
+ }
+ }
+
+ private static int getPackageVersion(Context ctx) {
+ try {
+ return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionCode;
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Can't determine the package version!");
+ return -1;
+ }
+ }
+
+ private static String getAbi() {
+ if (Build.VERSION.SDK_INT > 21) {
+ return Build.SUPPORTED_ABIS[0];
+ } else {
+ return Build.CPU_ABI;
+ }
+ }
+
+ // Static lock object for synchronizing binary installation
+ private static final Object BINARY_INSTALL_LOCK = new Object();
+
+ /**
+ * Asserts that the binary files are installed in the cache directory.
+ *
+ * @param ctx context
+ * @param showErrors indicates if errors should be alerted
+ * @return false if the binary files could not be installed
+ */
+ public static boolean assertBinaries(Context ctx, boolean showErrors) {
+ synchronized (BINARY_INSTALL_LOCK) {
+ Log.d(TAG, "assertBinaries() called - Entry point");
+
+ int currentVer = getPackageVersion(ctx);
+ boolean wasAlreadyInstalled = (G.appVersion() == currentVer);
+ Log.d(TAG, "assertBinaries() - currentVer=" + currentVer + ", storedVer=" + G.appVersion() + ", wasAlreadyInstalled=" + wasAlreadyInstalled);
+
+ if (wasAlreadyInstalled) {
+ // The version hasn't changed: Check if binaries are still functional
+ Log.d(TAG, "assertBinaries() - Verifying existing binaries...");
+ if (verifyBinaries(ctx)) {
+ Log.d(TAG, "assertBinaries() - Verification passed, returning true (no reinstall needed)");
+ return true;
+ } else {
+ Log.w(TAG, "Binaries verification failed, forcing reinstallation");
+ }
+ }
+
+ String abi = getAbi();
+
+ Log.d(TAG, "Installing binaries for " + abi + " (currentVer=" + currentVer +
+ ", storedVer=" + G.appVersion() + ", wasAlreadyInstalled=" + wasAlreadyInstalled + ")...");
+
+ if (!installBinariesForAbi(ctx, abi))
+ {
+ Log.e(TAG, "Installation of the binaries for " + abi + " failed!");
+ toast(ctx, ctx.getString(R.string.error_binary), Toast.LENGTH_LONG);
+ return false;
+ }
+
+ // Arch-independent scripts:
+ if (!installBinary(ctx, R.raw.afwallstart, "afwallstart"))
+ {
+ Log.e(TAG, "Installation of the arch-independent binaries failed!");
+ toast(ctx, ctx.getString(R.string.error_binary));
+ return false;
+ }
+
+ Log.d(TAG, "Installed binaries for " + abi + ".");
+
+ // Only show toast for actual new installations (not verification failures)
+ if (!wasAlreadyInstalled) {
+ Log.d(TAG, "New installation completed - showing toast");
+ toast(ctx, ctx.getString(R.string.toast_bin_installed), Toast.LENGTH_SHORT);
+ } else {
+ Log.d(TAG, "Binaries reinstalled (wasAlreadyInstalled=true) - no toast shown");
+ }
+
+ G.appVersion(currentVer); // This indicates that the installation of the binaries for this version was successful.
+
+ return true;
+ } // End synchronized block
+ }
+
+ /**
+ * Force reinstallation of binaries regardless of version
+ *
+ * @param ctx Context
+ * @param showErrors indicates if errors should be alerted
+ * @return true if installation successful
+ */
+ public static boolean forceReinstallBinaries(Context ctx, boolean showErrors) {
+ Log.i(TAG, "Forcing binary reinstallation...");
+
+ // Clear the version to force reinstallation
+ G.appVersion(-1);
+
+ return assertBinaries(ctx, showErrors);
+ }
+
+ /**
+ * Verify that installed binaries are functional
+ *
+ * @param ctx Context
+ * @return true if binaries are functional, false if they need reinstallation
+ */
+ private static boolean verifyBinaries(Context ctx) {
+ Log.d(TAG, "verifyBinaries() called - Starting verification");
+ String dir = ctx.getDir("bin", 0).getAbsolutePath();
+ Log.d(TAG, "verifyBinaries() - Binary directory: " + dir);
+
+ // Check if busybox exists and is executable
+ File busybox = new File(dir, "busybox");
+ boolean exists = busybox.exists();
+ boolean canExecute = busybox.canExecute();
+ boolean canRead = busybox.canRead();
+ long size = busybox.length();
+ Log.d(TAG, "verifyBinaries() - Checking busybox: exists=" + exists + ", canExecute=" + canExecute + ", canRead=" + canRead + ", size=" + size + " bytes");
+ if (!exists || !canExecute) {
+ Log.w(TAG, "Busybox binary missing or not executable");
+ return false;
+ }
+
+ // Test busybox functionality by running a simple command
+ // Note: On modern Android, binaries in app private directories may not be executable
+ // from the app context, but they will work when executed with root privileges
+ try {
+ Log.d(TAG, "verifyBinaries() - Testing busybox functionality with 'echo test'");
+ ProcessBuilder pb = new ProcessBuilder(busybox.getAbsolutePath(), "echo", "test");
+ pb.environment().clear();
+ Process process = pb.start();
+ int exitCode = process.waitFor();
+ Log.d(TAG, "verifyBinaries() - Busybox test exitCode: " + exitCode);
+
+ if (exitCode != 0) {
+ Log.w(TAG, "Busybox test command failed with exit code: " + exitCode);
+ return false;
+ }
+
+ // Read and verify output
+ java.util.Scanner scanner = new java.util.Scanner(process.getInputStream());
+ if (scanner.hasNextLine()) {
+ String output = scanner.nextLine().trim();
+ Log.d(TAG, "verifyBinaries() - Busybox test output: '" + output + "'");
+ scanner.close();
+ if (!"test".equals(output)) {
+ Log.w(TAG, "Busybox test output unexpected: " + output);
+ return false;
+ }
+ } else {
+ scanner.close();
+ Log.w(TAG, "Busybox test produced no output");
+ return false;
+ }
+
+ } catch (Exception e) {
+ String errorMsg = e.getMessage();
+ if (errorMsg != null && (errorMsg.contains("Permission denied") || errorMsg.contains("error=13"))) {
+ Log.w(TAG, "Busybox execution test failed due to Android security restrictions (expected behavior)");
+ Log.w(TAG, "Binary will be available for root execution. Skipping direct execution test.");
+ // Don't fail verification for permission denied - the binary will work with root
+ // Just log the issue and continue with other checks
+ } else {
+ Log.w(TAG, "Busybox verification failed: " + errorMsg);
+ return false;
+ }
+ }
+
+ // Check other critical binaries exist
+ Log.d(TAG, "verifyBinaries() - Checking other required binaries");
+ String[] requiredBinaries = {"iptables", "ip6tables"};
+ for (String binary : requiredBinaries) {
+ File binaryFile = new File(dir, binary);
+ Log.d(TAG, "verifyBinaries() - Checking " + binary + ": exists=" + binaryFile.exists() + ", canExecute=" + binaryFile.canExecute());
+ if (!binaryFile.exists() || !binaryFile.canExecute()) {
+ Log.w(TAG, "Required binary missing or not executable: " + binary);
+ return false;
+ }
+ }
+
+ Log.d(TAG, "Binary verification successful - All checks passed");
+ return true;
+ }
+
+ /**
+ * Check if the firewall is enabled
+ *
+ * @param ctx mandatory context
+ * @return boolean
+ */
+ public static boolean isEnabled(Context ctx) {
+ if (ctx == null) return false;
+ return ctx.getSharedPreferences(PREF_FIREWALL_STATUS, Context.MODE_PRIVATE).getBoolean(PREF_ENABLED, false);
+ }
+
+ /**
+ * Defines if the firewall is enabled and broadcasts the new status
+ *
+ * @param ctx mandatory context
+ * @param enabled enabled flag
+ */
+ public static void setEnabled(Context ctx, boolean enabled, boolean showErrors) {
+ if (ctx == null) return;
+ SharedPreferences prefs = ctx.getSharedPreferences(PREF_FIREWALL_STATUS, Context.MODE_PRIVATE);
+ if (prefs.getBoolean(PREF_ENABLED, false) == enabled) {
+ return;
+ }
+ setRulesUpToDate(false);
+
+ Editor edit = prefs.edit();
+ edit.putBoolean(PREF_ENABLED, enabled);
+ if (!edit.commit()) {
+ if (showErrors) toast(ctx, ctx.getString(R.string.error_write_pref));
+ return;
+ }
+
+ Intent myService = new Intent(ctx, FirewallService.class);
+ ctx.stopService(myService);
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ ctx.startForegroundService(myService);
+ } else {
+ ctx.startService(myService);
+ }
+
+ /* notify */
+ Intent message = new Intent(ctx, StatusWidget.class);
+ message.setAction(STATUS_CHANGED_MSG);
+ message.putExtra(Api.STATUS_EXTRA, enabled);
+ ctx.sendBroadcast(message);
+ }
+
+
+ public static void errorNotification(Context ctx) {
+
+ String NOTIFICATION_CHANNEL_ID = "firewall.error";
+ String channelName = ctx.getString(R.string.firewall_error_notify);
+
+ NotificationManager manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
+ manager.cancel(ERROR_NOTIFICATION_ID);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT);
+ notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
+ if (G.getNotificationPriority() == 0) {
+ notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
+ }
+ notificationChannel.setSound(null, null);
+ notificationChannel.setShowBadge(false);
+ notificationChannel.enableLights(false);
+ notificationChannel.enableVibration(false);
+
+ // Android 16+ specific notification channel configurations
+ if (Build.VERSION.SDK_INT >= 36) {
+ notificationChannel.setAllowBubbles(false);
+ }
+
+ manager.createNotificationChannel(notificationChannel);
+ }
+
+
+ Intent appIntent = new Intent(ctx, MainActivity.class);
+ appIntent.setAction(Intent.ACTION_MAIN);
+ appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ // Artificial stack so that navigating backward leads back to the Home screen
+ TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx)
+ .addParentStack(MainActivity.class)
+ .addNextIntent(new Intent(ctx, MainActivity.class));
+
+ PendingIntent notifyPendingIntent = PendingIntent.getActivity(ctx, 0, appIntent, PendingIntent.FLAG_IMMUTABLE);
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID);
+ notificationBuilder.setContentIntent(notifyPendingIntent);
+
+ Notification notification = notificationBuilder.setOngoing(false)
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ .setContentTitle(ctx.getString(R.string.error_notification_title))
+ .setContentText(ctx.getString(R.string.error_notification_text))
+ .setTicker(ctx.getString(R.string.error_notification_ticker))
+ .setSmallIcon(R.drawable.notification_warn)
+ .setAutoCancel(true)
+ .setContentIntent(notifyPendingIntent)
+ .build();
+
+ manager.notify(ERROR_NOTIFICATION_ID, notification);
+ }
+
+ public static void updateNotification(boolean status, Context ctx) {
+
+ String NOTIFICATION_CHANNEL_ID = "firewall.service";
+ String channelName = ctx.getString(R.string.firewall_service);
+
+ NotificationManager manager = (NotificationManager) ctx.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_PRIVATE);
+ if (G.getNotificationPriority() == 0) {
+ notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
+ }
+ notificationChannel.setSound(null, null);
+ notificationChannel.setShowBadge(false);
+ notificationChannel.enableLights(false);
+ notificationChannel.enableVibration(false);
+
+ // Android 16+ specific notification channel configurations
+ if (Build.VERSION.SDK_INT >= 36) {
+ notificationChannel.setAllowBubbles(false);
+ }
+
+ manager.createNotificationChannel(notificationChannel);
+ }
+
+ Intent appIntent = new Intent(ctx, MainActivity.class);
+ appIntent.setAction(Intent.ACTION_MAIN);
+ appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ int icon = status ? R.drawable.notification : R.drawable.notification_error;
+ String notificationText = status ? getNotificationText(ctx) : ctx.getString(R.string.inactive);
+
+ PendingIntent notifyPendingIntent = PendingIntent.getActivity(ctx, 0, appIntent, PendingIntent.FLAG_IMMUTABLE);
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID);
+ notificationBuilder.setContentIntent(notifyPendingIntent);
+
+ Notification notification = notificationBuilder.setOngoing(true)
+ .setContentTitle(ctx.getString(R.string.app_name))
+ .setTicker(ctx.getString(R.string.app_name))
+ .setSound(null)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ .setContentText(notificationText)
+ .setSmallIcon(icon)
+ .build();
+
+ notification.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_NO_CLEAR;
+ manager.notify(NOTIFICATION_ID, notification);
+ }
+
+ private static String getNotificationText(Context ctx) {
+ if (G.enableMultiProfile()) {
+ String storedProfile = G.storedProfile();
+ switch (storedProfile) {
+ case "AFWallPrefs":
+ return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("default", ctx.getString(R.string.defaultProfile)) + ")";
+ case "AFWallProfile1":
+ return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile1", ctx.getString(R.string.profile1)) + ")";
+ case "AFWallProfile2":
+ return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile2", ctx.getString(R.string.profile2)) + ")";
+ case "AFWallProfile3":
+ return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile3", ctx.getString(R.string.profile3)) + ")";
+ default:
+ return ctx.getString(R.string.active) + " (" + storedProfile + ")";
+ }
+ } else {
+ return ctx.getString(R.string.active);
+ }
+ }
+
+
+ private static boolean removePackageRef(Context ctx, String pkg, int pkgRemoved, SharedPreferences.Editor editor, String store) {
+ StringBuilder newUids = new StringBuilder();
+ StringTokenizer tokenizer = new StringTokenizer(pkg, "|");
+ boolean changed = false;
+ String uidStr = String.valueOf(pkgRemoved);
+
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken();
+ if (!uidStr.equals(token)) {
+ if (newUids.length() > 0) {
+ newUids.append('|');
+ }
+ newUids.append(token);
+ } else {
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ editor.putString(store, newUids.toString());
+ editor.apply();
+ }
+ return changed;
+ }
+
+
+ /**
+ * Remove the cache.label key from preferences, so that next time the app appears on the top
+ *
+ * @param pkgName
+ * @param ctx
+ */
+ public static void removeCacheLabel(String pkgName, Context ctx) {
+ SharedPreferences prefs = ctx.getSharedPreferences("AFWallPrefs", Context.MODE_PRIVATE);
+ try {
+ prefs.edit().remove("cache.label." + pkgName).commit();
+ } catch (Exception e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+ }
+
+ /**
+ * Cleansup the uninstalled packages from the cache - will have slight performance
+ *
+ * @param ctx
+ */
+ public static void removeAllUnusedCacheLabel(Context ctx) {
+ try {
+ SharedPreferences prefs = ctx.getSharedPreferences("AFWallPrefs", Context.MODE_PRIVATE);
+ final String cacheLabel = "cache.label.";
+ String pkgName;
+ String cacheKey;
+ PackageManager pm = ctx.getPackageManager();
+ Map allPrefs = prefs.getAll();
+
+ for (Map.Entry prefEntry : allPrefs.entrySet()) {
+ String key = prefEntry.getKey();
+ if (key.startsWith(cacheLabel)) {
+ cacheKey = key;
+ pkgName = key.replace(cacheLabel, "");
+ if (prefs.getString(cacheKey, "").length() > 0 && !isPackageExists(pm, pkgName)) {
+ prefs.edit().remove(cacheKey).apply();
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Handle the exception appropriately (e.g., log or print the stack trace)
+ }
+ }
+
+
+ /**
+ * Cleanup the cache from profiles - Improve performance.
+ *
+ * @param pm
+ * @param targetPackage
+ */
+
+ public static boolean isPackageExists(PackageManager pm, String targetPackage) {
+ try {
+ pm.getPackageInfo(targetPackage, PackageManager.GET_META_DATA);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public static PackageInfo getPackageDetails(Context ctx, HashMap listMaps, int uid) {
+ try {
+ final PackageManager pm = ctx.getPackageManager();
+ if (listMaps != null && listMaps.containsKey(uid)) {
+ return pm.getPackageInfo(listMaps.get(uid), PackageManager.GET_META_DATA);
+ } else {
+ return null;
+ }
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+
+
+ public static Drawable getApplicationIcon(Context context, int appUid) {
+ if (uidToApplicationInfoMap == null) {
+ PackageManager packageManager = context.getPackageManager();
+ List installedApplications = packageManager.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);
+ uidToApplicationInfoMap = new HashMap<>();
+ for (ApplicationInfo applicationInfo : installedApplications) {
+ if (!uidToApplicationInfoMap.containsKey(applicationInfo.uid)) {
+ uidToApplicationInfoMap.put(applicationInfo.uid, applicationInfo);
+ }
+ }
+ }
+
+ ApplicationInfo applicationInfo = uidToApplicationInfoMap.get(appUid);
+ if (applicationInfo != null) {
+ PackageManager packageManager = context.getPackageManager();
+ return applicationInfo.loadIcon(packageManager); // The application icon.
+ } else {
+ return context.getDrawable(R.drawable.ic_unknown); // The default icon.
+ }
+ }
+
+ /**
+ * Called when an application in removed (un-installed) from the system.
+ * This will look for that application in the selected list and update the persisted values if necessary
+ *
+ * @param ctx mandatory app context
+ */
+ public static void applicationRemoved(Context ctx, int pkgRemoved, RootCommand callback) {
+ SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ boolean isRuleChanged = false;
+
+ String[] prefKeys = {
+ PREF_WIFI_PKG_UIDS,
+ PREF_3G_PKG_UIDS,
+ PREF_ROAMING_PKG_UIDS,
+ PREF_VPN_PKG_UIDS,
+ PREF_TETHER_PKG_UIDS,
+ PREF_LAN_PKG_UIDS,
+ PREF_TOR_PKG_UIDS
+ };
+
+ String[] savedPackages = {
+ prefs.getString(PREF_WIFI_PKG_UIDS, ""),
+ prefs.getString(PREF_3G_PKG_UIDS, ""),
+ prefs.getString(PREF_ROAMING_PKG_UIDS, ""),
+ prefs.getString(PREF_VPN_PKG_UIDS, ""),
+ prefs.getString(PREF_TETHER_PKG_UIDS, ""),
+ prefs.getString(PREF_LAN_PKG_UIDS, ""),
+ prefs.getString(PREF_TOR_PKG_UIDS, "")
+ };
+
+ boolean[] ruleChanged = new boolean[savedPackages.length];
+
+ for (int i = 0; i < savedPackages.length; i++) {
+ ruleChanged[i] = removePackageRef(ctx, savedPackages[i], pkgRemoved, editor, prefKeys[i]);
+ if (ruleChanged[i]) {
+ isRuleChanged = true;
+ }
+ }
+
+ if (isRuleChanged) {
+ editor.apply();
+ if (isEnabled(ctx)) {
+ applySavedIptablesRules(ctx, false, new RootCommand());
+ }
+ }
+ }
+
+
+ public static void donateDialog(final Context ctx, boolean showToast) {
+ if (showToast) {
+ Toast.makeText(ctx, ctx.getText(R.string.donate_only), Toast.LENGTH_LONG).show();
+ } else {
+ try {
+ new MaterialDialog.Builder(ctx).cancelable(false)
+ .title(R.string.buy_donate)
+ .content(R.string.donate_only)
+ .positiveText(R.string.buy_donate)
+ .negativeText(R.string.close)
+ .icon(ctx.getResources().getDrawable(R.drawable.ic_launcher))
+ .onPositive(new MaterialDialog.SingleButtonCallback() {
+ @Override
+ public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse("market://search?q=pub:ukpriya"));
+ ctx.startActivity(intent);
+ }
+ })
+
+ .onNegative(new MaterialDialog.SingleButtonCallback() {
+ @Override
+ public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
+ dialog.cancel();
+ G.isDo(false);
+ }
+ })
+ .show();
+ } catch (Exception e) {
+ Toast.makeText(ctx, ctx.getText(R.string.donate_only), Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ public static void exportRulesToFileConfirm(final Context ctx) {
+ String fileName = "afwall-backup-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json";
+ if (exportRules(ctx, fileName)) {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_success) + " " + fileName);
+ } else {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_fail));
+ }
+ }
+
+ public static void exportAllPreferencesToFileConfirm(final Context ctx) {
+ String fileName = "afwall-backup-all-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json";
+ if (exportAll(ctx, fileName)) {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_success) + " " + fileName);
+ } else {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_fail));
+ }
+ }
+
+ public static void exportRulesToFileWithPicker(final Context ctx) {
+ showExportFileDialog(ctx, false);
+ }
+
+ public static void exportAllPreferencesToFileWithPicker(final Context ctx) {
+ showExportFileDialog(ctx, true);
+ }
+
+ private static void showExportFileDialog(final Context ctx, final boolean exportAll) {
+ try {
+ File defaultPath;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ defaultPath = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "/");
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ defaultPath = new File(ctx.getExternalFilesDir(null), "/");
+ } else {
+ defaultPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/afwall/");
+ defaultPath.mkdirs();
+ }
+
+ dev.ukanth.ufirewall.util.FileDialog fileDialog = new dev.ukanth.ufirewall.util.FileDialog((Activity) ctx, defaultPath, true);
+ fileDialog.setSelectDirectoryOption(true);
+ fileDialog.addDirectoryListener(directory -> {
+ String fileName = "afwall-backup" + (exportAll ? "-all" : "") + "-" +
+ new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json";
+ File fullPath = new File(directory, fileName);
+
+ boolean success;
+ if (exportAll) {
+ success = exportAllToFile(ctx, fullPath);
+ } else {
+ success = exportRulesToFile(ctx, fullPath);
+ }
+
+ if (success) {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_success) + " " + fullPath.getAbsolutePath());
+ } else {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_fail));
+ }
+ });
+ fileDialog.showDialog();
+ } catch (Exception e) {
+ // Fallback to original method if file dialog fails
+ if (exportAll) {
+ exportAllPreferencesToFileConfirm(ctx);
+ } else {
+ exportRulesToFileConfirm(ctx);
+ }
+ }
+ }
+
+ private static boolean exportRulesToFile(Context ctx, File file) {
+ boolean res = false;
+ try (FileOutputStream fOut = new FileOutputStream(file);
+ OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) {
+
+ JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx));
+ JSONArray jArray = new JSONArray("[" + obj.toString() + "]");
+ JSONObject exportObject = new JSONObject();
+ exportObject.put("rules", jArray);
+ String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST);
+ exportObject.put("mode", mode);
+
+ myOutWriter.write(exportObject.toString());
+ myOutWriter.flush(); // Ensure data is written
+ res = true;
+ Log.i(TAG, "Successfully exported rules to: " + file.getAbsolutePath());
+ } catch (Exception e) {
+ Log.e(TAG, "Error exporting rules to file: " + file.getAbsolutePath(), e);
+ }
+ return res;
+ }
+
+ private static boolean exportAllToFile(Context ctx, File file) {
+ boolean res = false;
+ try (FileOutputStream fOut = new FileOutputStream(file);
+ OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) {
+
+ JSONObject exportObject = new JSONObject();
+ if (G.enableMultiProfile()) {
+ if (!G.isProfileMigrated()) {
+ JSONObject profileObject = new JSONObject();
+ for (String profile : G.profiles) {
+ profileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile)));
+ }
+ exportObject.put("profiles", profileObject);
+
+ JSONObject addProfileObject = new JSONObject();
+ for (String profile : G.getAdditionalProfiles()) {
+ addProfileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile)));
+ }
+ exportObject.put("additional_profiles", addProfileObject);
+ } else {
+ JSONObject profileObject = new JSONObject();
+ String profileName = "AFWallPrefs";
+ profileObject.put(profileName, new JSONObject(getRulesForProfile(ctx, profileName)));
+
+ List profileDataList = ProfileHelper.getProfiles();
+ for (ProfileData profile : profileDataList) {
+ profileName = profile.getName();
+ if (profile.getIdentifier().startsWith("AFWallProfile")) {
+ profileName = profile.getIdentifier();
+ }
+ profileObject.put(profile.getName(), new JSONObject(getRulesForProfile(ctx, profileName)));
+ }
+ exportObject.put("_profiles", profileObject);
+ }
+ } else {
+ JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx));
+ exportObject.put("default", obj);
+ }
+
+ exportObject.put("prefs", getAllAppPreferences(ctx, G.gPrefs));
+ // Export profile-specific preferences (mode, custom rules, etc.)
+ if (G.pPrefs != null) {
+ exportObject.put("profilePrefs", getAllAppPreferences(ctx, G.pPrefs));
+ }
+
+ myOutWriter.write(exportObject.toString());
+ myOutWriter.flush(); // Ensure data is written
+ res = true;
+ Log.i(TAG, "Successfully exported all preferences to: " + file.getAbsolutePath());
+ } catch (Exception e) {
+ Log.e(TAG, "Error exporting all preferences to file: " + file.getAbsolutePath(), e);
+ }
+ return res;
+ }
+
+ private static void updateExportPackage(Map exportMap, String packageName, boolean isChecked, int identifier) throws JSONException {
+ if (!isChecked) {
+ return;
+ }
+ JSONObject obj;
+ if (packageName != null) {
+ if (exportMap.containsKey(packageName)) {
+ obj = exportMap.get(packageName);
+ obj.put(identifier + "", true);
+ } else {
+ obj = new JSONObject();
+ obj.put(identifier + "", true);
+ exportMap.put(packageName, obj);
+ }
+ }
+ }
+
+ private static void updatePackage(Context ctx, String savedPkg_uid, Map exportMap, int identifier) throws JSONException {
+ StringTokenizer tok = new StringTokenizer(savedPkg_uid, "|");
+ while (tok.hasMoreTokens()) {
+ String uid = tok.nextToken();
+ if (!uid.isEmpty()) {
+ String packageName = ctx.getPackageManager().getNameForUid(Integer.parseInt(uid));
+ updateExportPackage(exportMap, packageName, /*is_checked=*/ true, identifier);
+ }
+ }
+ }
+
+ private static Map getCurrentRulesAsMap(Context ctx) {
+ List apps = getApps(ctx, null);
+ Map exportMap = new HashMap<>();
+
+ try {
+ for (PackageInfoData app : apps) {
+ updateExportPackage(exportMap, app.pkgName, app.selected_wifi, WIFI_EXPORT);
+ updateExportPackage(exportMap, app.pkgName, app.selected_3g, DATA_EXPORT);
+ updateExportPackage(exportMap, app.pkgName, app.selected_roam, ROAM_EXPORT);
+ updateExportPackage(exportMap, app.pkgName, app.selected_vpn, VPN_EXPORT);
+ updateExportPackage(exportMap, app.pkgName, app.selected_tether, TETHER_EXPORT);
+ updateExportPackage(exportMap, app.pkgName, app.selected_lan, LAN_EXPORT);
+ updateExportPackage(exportMap, app.pkgName, app.selected_tor, TOR_EXPORT);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+ return exportMap;
+ }
+
+
+ public static boolean exportAll(Context ctx, final String fileName) {
+ boolean res = false;
+ try {
+ File file;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Android 11+ (API 30+): Use scoped storage
+ file = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10 (API 29): Use app-specific directory
+ file = new File(ctx.getExternalFilesDir(null), fileName);
+ } else {
+ // Android 9 and below: Use legacy external storage
+ File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "afwall");
+ dir.mkdirs();
+ file = new File(dir, fileName);
+ }
+
+ try (FileOutputStream fOut = new FileOutputStream(file);
+ OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) {
+
+ JSONObject exportObject = new JSONObject();
+ if (G.enableMultiProfile()) {
+ if (!G.isProfileMigrated()) {
+ JSONObject profileObject = new JSONObject();
+ for (String profile : G.profiles) {
+ profileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile)));
+ }
+ exportObject.put("profiles", profileObject);
+
+ JSONObject addProfileObject = new JSONObject();
+ for (String profile : G.getAdditionalProfiles()) {
+ addProfileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile)));
+ }
+ exportObject.put("additional_profiles", addProfileObject);
+ } else {
+ JSONObject profileObject = new JSONObject();
+ String profileName = "AFWallPrefs";
+ profileObject.put(profileName, new JSONObject(getRulesForProfile(ctx, profileName)));
+
+ List profileDataList = ProfileHelper.getProfiles();
+ for (ProfileData profile : profileDataList) {
+ profileName = profile.getName();
+ if (profile.getIdentifier().startsWith("AFWallProfile")) {
+ profileName = profile.getIdentifier();
+ }
+ profileObject.put(profile.getName(), new JSONObject(getRulesForProfile(ctx, profileName)));
+ }
+ exportObject.put("_profiles", profileObject);
+ }
+ } else {
+ JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx));
+ exportObject.put("default", obj);
+ }
+
+ exportObject.put("prefs", getAllAppPreferences(ctx, G.gPrefs));
+ // Export profile-specific preferences (mode, custom rules, etc.)
+ if (G.pPrefs != null) {
+ exportObject.put("profilePrefs", getAllAppPreferences(ctx, G.pPrefs));
+ }
+
+ String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST);
+ exportObject.put("mode", mode);
+
+ myOutWriter.append(exportObject.toString());
+ res = true;
+ }
+
+ } catch (Exception e) {
+ Log.d(TAG, e.getLocalizedMessage(), e);
+ }
+
+ return res;
+ }
+
+
+ private static Map getRulesForProfile(Context ctx, String profile) throws JSONException {
+ Map exportMap = new HashMap<>();
+ SharedPreferences prefs = ctx.getSharedPreferences(profile, Context.MODE_PRIVATE);
+ updatePackage(ctx, prefs.getString(PREF_WIFI_PKG_UIDS, ""), exportMap, WIFI_EXPORT);
+ updatePackage(ctx, prefs.getString(PREF_3G_PKG_UIDS, ""), exportMap, DATA_EXPORT);
+ updatePackage(ctx, prefs.getString(PREF_ROAMING_PKG_UIDS, ""), exportMap, ROAM_EXPORT);
+ updatePackage(ctx, prefs.getString(PREF_VPN_PKG_UIDS, ""), exportMap, VPN_EXPORT);
+ updatePackage(ctx, prefs.getString(PREF_TETHER_PKG_UIDS, ""), exportMap, TETHER_EXPORT);
+ updatePackage(ctx, prefs.getString(PREF_LAN_PKG_UIDS, ""), exportMap, LAN_EXPORT);
+ updatePackage(ctx, prefs.getString(PREF_TOR_PKG_UIDS, ""), exportMap, TOR_EXPORT);
+ return exportMap;
+ }
+
+ private static JSONArray getAllAppPreferences(Context ctx, SharedPreferences gPrefs) throws JSONException {
+ Map keys = gPrefs.getAll();
+ JSONArray arr = new JSONArray();
+ for (Map.Entry entry : keys.entrySet()) {
+ JSONObject obj = new JSONObject();
+ obj.put(entry.getKey(), entry.getValue().toString());
+ arr.put(obj);
+ }
+ return arr;
+ }
+
+ public static boolean exportRules(Context ctx, final String fileName) {
+ boolean res = false;
+
+ File file;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Android 11+ (API 30+): Use scoped storage
+ file = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10 (API 29): Use app-specific directory
+ file = new File(ctx.getExternalFilesDir(null), fileName);
+ } else {
+ // Android 9 and below: Use legacy external storage
+ File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/afwall/");
+ dir.mkdirs();
+ file = new File(dir, fileName);
+ }
+
+ try {
+
+ FileOutputStream fOut = new FileOutputStream(file);
+ OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut);
+
+ //default Profile - current one
+ JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx));
+ JSONArray jArray = new JSONArray("[" + obj.toString() + "]");
+
+ JSONObject exportObject = new JSONObject();
+ exportObject.put("rules", jArray);
+
+ String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST);
+ exportObject.put("mode", mode);
+
+ myOutWriter.append(exportObject.toString());
+ res = true;
+ myOutWriter.close();
+ fOut.close();
+
+
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ } catch (JSONException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ } catch (IOException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+
+ return res;
+ }
+
+
+ private static boolean importRulesRoot(Context ctx, File file, StringBuilder msg) {
+ boolean returnVal = false;
+ BufferedReader br = null;
+ try {
+ com.topjohnwu.superuser.Shell.Result result = com.topjohnwu.superuser.Shell.cmd("cat " + file.getAbsolutePath()).exec();
+ List out = result.getOut();
+ String data = TextUtils.join("", out);
+
+ try {
+ //old export format
+ JSONArray array = new JSONArray(data);
+ updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME);
+ } catch (JSONException e) {
+ //new exported format
+ JSONObject jsonObject = new JSONObject(data);
+ //save mode
+ if(jsonObject.get("mode") != null) {
+ G.pPrefs.edit().putString(PREF_MODE, jsonObject.getString("mode")).apply();
+ }
+ JSONArray array = (JSONArray) jsonObject.get("rules");
+ updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME);
+ }
+ returnVal = true;
+ } catch (java.util.concurrent.RejectedExecutionException e) {
+ Log.w(TAG, "Import rules file read rejected: " + e.getMessage());
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON parsing error during import: " + e.getLocalizedMessage());
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to import rules from file: " + e.getLocalizedMessage());
+ } finally {
+ if (br != null) {
+ try {
+ br.close();
+ } catch (IOException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+ }
+ }
+ return returnVal;
+ }
+ private static boolean importRules(Context ctx, File file, StringBuilder msg) {
+ boolean returnVal = false;
+
+ try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+ StringBuilder text = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ text.append(line);
+ }
+ String data = text.toString();
+ if (data.trim().isEmpty()) {
+ msg.append("Import file contains no data");
+ return false;
+ }
+
+ JSONObject jsonObject = new JSONObject(data);
+ if (jsonObject.has("mode")) {
+ G.pPrefs.edit().putString(PREF_MODE, jsonObject.getString("mode")).apply();
+ }
+ JSONArray array = jsonObject.optJSONArray("rules");
+ if (array != null) {
+ updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME);
+ } else {
+ updateRulesFromJson(ctx, jsonObject, PREFS_NAME);
+ }
+
+ returnVal = true;
+ } catch (FileNotFoundException e) {
+ if (e.getMessage().contains("EACCES")) {
+ return importRulesRoot(ctx, file, msg);
+ } else {
+ msg.append(ctx.getString(R.string.import_rules_missing));
+ }
+ } catch (IOException | JSONException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+
+ return returnVal;
+ }
+
+
+ private static void updateRulesFromJson(Context ctx, JSONObject object, String preferenceName) throws JSONException {
+ final StringBuilder[] uidBuilders = new StringBuilder[7];
+ uidBuilders[WIFI_EXPORT] = new StringBuilder();
+ uidBuilders[DATA_EXPORT] = new StringBuilder();
+ uidBuilders[ROAM_EXPORT] = new StringBuilder();
+ uidBuilders[VPN_EXPORT] = new StringBuilder();
+ uidBuilders[TETHER_EXPORT] = new StringBuilder();
+ uidBuilders[LAN_EXPORT] = new StringBuilder();
+ uidBuilders[TOR_EXPORT] = new StringBuilder();
+
+ Map json = JsonHelper.toMap(object);
+ final PackageManager pm = ctx.getPackageManager();
+
+ for (Map.Entry entry : json.entrySet()) {
+ String pkgName = entry.getKey();
+ if (pkgName.contains(":")) {
+ pkgName = pkgName.split(":")[0];
+ }
+
+ JSONObject jsonObj = (JSONObject) JsonHelper.toJSON(entry.getValue());
+ Iterator> keys = jsonObj.keys();
+ while (keys.hasNext()) {
+ String key = (String) keys.next();
+ int exportType = Integer.parseInt(key);
+ StringBuilder uidBuilder = uidBuilders[exportType];
+
+ if (uidBuilder.length() != 0) {
+ uidBuilder.append('|');
+ }
+
+ if (pkgName.startsWith("dev.afwall.special")) {
+ uidBuilder.append(specialApps.get(pkgName));
+ } else {
+ try {
+ uidBuilder.append(pm.getApplicationInfo(pkgName, 0).uid);
+ } catch (NameNotFoundException e) {
+ // Handle exception if needed
+ }
+ }
+ }
+ }
+
+ final SharedPreferences prefs = ctx.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
+ final Editor edit = prefs.edit();
+ edit.putString(PREF_WIFI_PKG_UIDS, uidBuilders[WIFI_EXPORT].toString());
+ edit.putString(PREF_3G_PKG_UIDS, uidBuilders[DATA_EXPORT].toString());
+ edit.putString(PREF_ROAMING_PKG_UIDS, uidBuilders[ROAM_EXPORT].toString());
+ edit.putString(PREF_VPN_PKG_UIDS, uidBuilders[VPN_EXPORT].toString());
+ edit.putString(PREF_TETHER_PKG_UIDS, uidBuilders[TETHER_EXPORT].toString());
+ edit.putString(PREF_LAN_PKG_UIDS, uidBuilders[LAN_EXPORT].toString());
+ edit.putString(PREF_TOR_PKG_UIDS, uidBuilders[TOR_EXPORT].toString());
+
+ edit.apply();
+ }
+
+ private static boolean shouldIgnoreKey(String key) {
+ String[] ignore = {"appVersion", "fixLeak", "enableLogService", "sort", "storedProfile", "hasRoot", "logChains", "kingDetect", "fingerprintEnabled"};
+ return Arrays.asList(ignore).contains(key);
+ }
+
+ private static boolean isIntType(String key) {
+ String[] intType = {"logPingTime", "customDelay", "patternMax", "widgetX", "widgetY", "notification_priority"};
+ return Arrays.asList(intType).contains(key);
+ }
+
+ private static void importProfiles(Context ctx, JSONObject profileObject) throws JSONException {
+ Iterator keys = profileObject.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ try {
+ JSONObject obj = profileObject.getJSONObject(key);
+ updateRulesFromJson(ctx, obj, key);
+ } catch (JSONException e) {
+ if (e.getMessage().contains("No value")) {
+ // continue;
+ }
+ }
+ }
+ }
+ private static boolean importAll(Context ctx, File file, StringBuilder msg) {
+ boolean returnVal = false;
+
+ try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+ StringBuilder text = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ text.append(line);
+ }
+ String data = text.toString();
+ if (data.trim().isEmpty()) {
+ msg.append("Import file contains no data");
+ return false;
+ }
+
+ JSONObject object = new JSONObject(data);
+ // Basic validation of expected JSON structure
+ if (!object.has("prefs") && !object.has("profiles") && !object.has("_profiles") && !object.has("default")) {
+ msg.append("Import file does not contain valid AFWall+ data");
+ Log.w(TAG, "Invalid import file structure - missing expected keys");
+ return false;
+ }
+
+ // Allow/deny rule
+ if (object.has("mode")) {
+ G.pPrefs.edit().putString(PREF_MODE, object.getString("mode")).apply();
+ }
+
+ JSONArray prefArray = object.getJSONArray("prefs");
+ for (int i = 0; i < prefArray.length(); i++) {
+ JSONObject prefObj = prefArray.getJSONObject(i);
+ Iterator keys = prefObj.keys();
+
+ while (keys.hasNext()) {
+ String key = keys.next();
+ String value = prefObj.getString(key);
+ if (shouldIgnoreKey(key)) {
+ continue;
+ }
+ if (value.equals("true") || value.equals("false")) {
+ G.gPrefs.edit().putBoolean(key, Boolean.parseBoolean(value)).apply();
+ } else {
+ try {
+ if (key.equals("multiUserId")) {
+ G.gPrefs.edit().putLong(key, Long.parseLong(value)).apply();
+ } else if (isIntType(key)) {
+ G.gPrefs.edit().putString(key, value).apply();
+ } else {
+ int intValue = Integer.parseInt(value);
+ G.gPrefs.edit().putInt(key, intValue).apply();
+ }
+ } catch (NumberFormatException e) {
+ G.gPrefs.edit().putString(key, value).apply();
+ }
+ }
+ }
+ }
+
+ // Import profile-specific preferences if available
+ if (object.has("profilePrefs")) {
+ JSONArray profilePrefArray = object.getJSONArray("profilePrefs");
+ for (int i = 0; i < profilePrefArray.length(); i++) {
+ JSONObject prefObj = profilePrefArray.getJSONObject(i);
+ Iterator keys = prefObj.keys();
+
+ while (keys.hasNext()) {
+ String key = keys.next();
+ String value = prefObj.getString(key);
+ if (shouldIgnoreKey(key)) {
+ continue;
+ }
+ if (value.equals("true") || value.equals("false")) {
+ G.pPrefs.edit().putBoolean(key, Boolean.parseBoolean(value)).apply();
+ } else {
+ try {
+ if (key.equals("multiUserId")) {
+ G.pPrefs.edit().putLong(key, Long.parseLong(value)).apply();
+ } else if (isIntType(key)) {
+ G.pPrefs.edit().putString(key, value).apply();
+ } else {
+ int intValue = Integer.parseInt(value);
+ G.pPrefs.edit().putInt(key, intValue).apply();
+ }
+ } catch (NumberFormatException e) {
+ G.pPrefs.edit().putString(key, value).apply();
+ }
+ }
+ }
+ }
+ }
+
+ if (G.enableMultiProfile()) {
+ if (G.isProfileMigrated()) {
+ JSONObject profileObject = object.getJSONObject("_profiles");
+ importProfiles(ctx, profileObject);
+ } else {
+ JSONObject profileObject = object.getJSONObject("profiles");
+ importProfiles(ctx, profileObject);
+ JSONObject customProfileObject = object.getJSONObject("additional_profiles");
+ importProfiles(ctx, customProfileObject);
+ }
+ } else {
+ JSONObject defaultRules = object.getJSONObject("default");
+ updateRulesFromJson(ctx, defaultRules, PREFS_NAME);
+ }
+ returnVal = true;
+ } catch (FileNotFoundException e) {
+ msg.append(ctx.getString(R.string.import_rules_missing));
+ } catch (IOException | JSONException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+
+ return returnVal;
+ }
+
+ public static boolean loadSharedPreferencesFromFile(Context ctx, StringBuilder builder, String fileName, boolean loadAll) {
+ boolean res = false;
+ File file = new File(fileName);
+ if (file.exists()) {
+ // Basic file validation
+ if (file.length() == 0) {
+ builder.append("Import file is empty");
+ Log.w(TAG, "Import file is empty: " + fileName);
+ return false;
+ }
+ if (file.length() > 50 * 1024 * 1024) { // 50MB limit
+ builder.append("Import file is too large (>50MB)");
+ Log.w(TAG, "Import file is too large: " + fileName + " (" + file.length() + " bytes)");
+ return false;
+ }
+
+ Log.i(TAG, "Importing from file: " + fileName + " (loadAll: " + loadAll + ")");
+ if (loadAll) {
+ res = importAll(ctx, file, builder);
+ } else {
+ res = importRules(ctx, file, builder);
+ }
+ } else {
+ builder.append("Import file does not exist: " + fileName);
+ Log.w(TAG, "Import file does not exist: " + fileName);
+ }
+ return res;
+ }
+
+ /**
+ * Probe log target
+ * @param ctx
+ */
+ public static void probeLogTarget(final Context ctx) {
+
+ }
+
+ @SuppressLint("InlinedApi")
+ public static void showInstalledAppDetails(Context context, String packageName) {
+ final String SCHEME = "package";
+ Intent intent = new Intent();
+ final int apiLevel = Build.VERSION.SDK_INT;
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Uri uri = Uri.fromParts(SCHEME, packageName, null);
+ intent.setData(uri);
+ context.startActivity(intent);
+ }
+
+ public static boolean isNetfilterSupported() {
+ boolean netfiler_exists = new File("/proc/net/netfilter").exists();
+ try {
+ Shell.Result result = Shell.cmd("cat /proc/net/ip_tables_targets").exec();
+ return netfiler_exists && result.isSuccess();
+ } catch (java.util.concurrent.RejectedExecutionException e) {
+ Log.w(TAG, "Netfilter check rejected: " + e.getMessage());
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to check netfilter support: " + e.getMessage());
+ return false;
+ }
+ }
+
+ private static void initSpecial() {
+ if (specialApps == null || specialApps.size() == 0) {
+ specialApps = new HashMap();
+ specialApps.put("dev.afwall.special.any", SPECIAL_UID_ANY);
+ specialApps.put("dev.afwall.special.kernel", SPECIAL_UID_KERNEL);
+ specialApps.put("dev.afwall.special.tether", SPECIAL_UID_TETHER);
+ //specialApps.put("dev.afwall.special.dnsproxy",SPECIAL_UID_DNSPROXY);
+ specialApps.put("dev.afwall.special.ntp", SPECIAL_UID_NTP);
+ for (String acct : specialAndroidAccounts) {
+ String pkg = "dev.afwall.special." + acct;
+ int uid = android.os.Process.getUidForName(acct);
+ specialApps.put(pkg, uid);
+ }
+ }
+ }
+
+ public static void updateLanguage(Context context, String lang) {
+ if (lang.equals("sys")) {
+ Locale defaultLocale = Resources.getSystem().getConfiguration().locale;
+ Locale.setDefault(defaultLocale);
+ Resources res = context.getResources();
+ Configuration conf = res.getConfiguration();
+ conf.locale = defaultLocale;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ context.createConfigurationContext(conf);
+ } else {
+ context.getResources().updateConfiguration(conf, context.getResources().getDisplayMetrics());
+ }
+ } else if (!"".equals(lang)) {
+ Locale locale = new Locale(lang);
+ if (lang.contains("_")) {
+ locale = new Locale(lang.split("_")[0], lang.split("_")[1]);
+ }
+ Locale.setDefault(locale);
+ Resources res = context.getResources();
+ Configuration conf = res.getConfiguration();
+ conf.locale = locale;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ context.createConfigurationContext(conf);
+ } else {
+ context.getResources().updateConfiguration(conf, context.getResources().getDisplayMetrics());
+ }
+ }
+ }
+
+ public static void setUserOwner(Context context) {
+ if (supportsMultipleUsers(context)) {
+ try {
+ Method getUserHandle = UserManager.class.getMethod("getUserHandle");
+ int userHandle = (Integer) getUserHandle.invoke(context.getSystemService(Context.USER_SERVICE));
+ G.setMultiUserId(userHandle);
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception on setUserOwner " + ex.getMessage());
+ }
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public static boolean supportsMultipleUsers(Context context) {
+ final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ try {
+ Method supportsMultipleUsers = UserManager.class.getMethod("supportsMultipleUsers");
+ return (Boolean) supportsMultipleUsers.invoke(um);
+ } catch (Exception ex) {
+ return false;
+ }
+ }
+
+ public static String loadData(final Context context,
+ final String resourceName) throws IOException {
+ int resourceIdentifier = context
+ .getApplicationContext()
+ .getResources()
+ .getIdentifier(resourceName, "raw",
+ context.getApplicationContext().getPackageName());
+ if (resourceIdentifier != 0) {
+ InputStream inputStream = context.getApplicationContext()
+ .getResources().openRawResource(resourceIdentifier);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ inputStream, StandardCharsets.UTF_8));
+ String line;
+ StringBuffer data = new StringBuffer();
+ while ((line = reader.readLine()) != null) {
+ data.append(line);
+ }
+ reader.close();
+ return data.toString();
+ }
+ return null;
+ }
+
+ /**
+ * Encrypt the password - DEPRECATED: Use SecureCrypto.encryptSecure() for new code
+ * This method is kept for backward compatibility only
+ *
+ * @param key
+ * @param data
+ * @return
+ * @deprecated Use SecureCrypto.encryptSecure() instead for better security
+ */
+ @Deprecated
+ public static String hideCrypt(String key, String data) {
+ if (key == null || data == null)
+ return null;
+ String encodeStr = null;
+ try {
+ DESKeySpec desKeySpec = new DESKeySpec(key.getBytes(charsetName));
+ SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
+ SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec);
+ byte[] dataBytes = data.getBytes(charsetName);
+ Cipher cipher = Cipher.getInstance(algorithm);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+ encodeStr = Base64.encodeToString(cipher.doFinal(dataBytes), base64Mode);
+
+ } catch (Exception e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+ return encodeStr;
+ }
+
+ /**
+ * Decrypt the password - DEPRECATED: Use SecureCrypto.decryptSecure() for new code
+ * This method is kept for backward compatibility only
+ *
+ * @param key
+ * @param data
+ * @return
+ * @deprecated Use SecureCrypto.decryptSecure() instead for better security
+ */
+ @Deprecated
+ public static String unhideCrypt(String key, String data) {
+ if (key == null || data == null)
+ return null;
+
+ String decryptStr = null;
+ try {
+ byte[] dataBytes = Base64.decode(data, base64Mode);
+ DESKeySpec desKeySpec = new DESKeySpec(key.getBytes(charsetName));
+ SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
+ SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec);
+ Cipher cipher = Cipher.getInstance(algorithm);
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+ byte[] dataBytesDecrypted = (cipher.doFinal(dataBytes));
+ decryptStr = new String(dataBytesDecrypted);
+ } catch (Exception e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+ return decryptStr;
+ }
+
+ public static boolean isMobileNetworkSupported(final Context ctx) {
+ boolean hasMobileData = true;
+ try {
+ ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm != null) {
+ if (cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) {
+ hasMobileData = false;
+ }
+ }
+ } catch (SecurityException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ return hasMobileData;
+ }
+
+ public static String getCurrentPackage(Context ctx) {
+ PackageInfo pInfo = null;
+ try {
+ pInfo = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
+ } catch (NameNotFoundException e) {
+ Log.e(Api.TAG, "Package not found", e);
+ }
+ return pInfo.packageName;
+ }
+
+ public static int getConnectivityStatus(Context context) {
+
+ ConnectivityManager cm = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ assert cm != null;
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+
+ if (null != activeNetwork) {
+
+ if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI)
+ return 1;
+
+ if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE)
+ return 2;
+
+ if (activeNetwork.getType() == ConnectivityManager.TYPE_BLUETOOTH)
+ return 3;
+ }
+ return 0;
+ }
+
+ /**
+ * Apply default chains based on preference
+ *
+ * @param ctx
+ */
+ public static void applyDefaultChains(Context ctx, RootCommand callback) {
+ List cmds = new ArrayList<>();
+ cmds.add(G.ipv4Input() ? "-P INPUT ACCEPT" : "-P INPUT DROP");
+ cmds.add(G.ipv4Fwd() ? "-P FORWARD ACCEPT" : "-P FORWARD DROP");
+ cmds.add(G.ipv4Output() ? "-P OUTPUT ACCEPT" : "-P OUTPUT DROP");
+ applyQuick(ctx, cmds, callback);
+ applyDefaultChainsv6(ctx, callback);
+ }
+
+ public static void applyDefaultChainsv6(Context ctx, RootCommand callback) {
+ if (G.controlIPv6()) {
+ List cmds = new ArrayList<>();
+ cmds.add(G.ipv6Input() ? "-P INPUT ACCEPT" : "-P INPUT DROP");
+ cmds.add(G.ipv6Fwd() ? "-P FORWARD ACCEPT" : "-P FORWARD DROP");
+ cmds.add(G.ipv6Output() ? "-P OUTPUT ACCEPT" : "-P OUTPUT DROP");
+ applyIPv6Quick(ctx, cmds, callback);
+ }
+ }
+
+ /**
+ * Delete all firewall rules. For diagnostic purposes only.
+ *
+ * @param ctx application context
+ * @param callback callback for completion
+ */
+ public static void flushOtherRules(Context ctx, RootCommand callback) {
+ List cmds = new ArrayList();
+ cmds.add("-F firewall");
+ cmds.add("-X firewall");
+ apply46(ctx, cmds, callback);
+ }
+
+ // Clipboard
+ public static void copyToClipboard(Context context, String val) {
+ ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("label", val);
+ clipboard.setPrimaryClip(clip);
+ }
+
+ public static void sendToastBroadcast(Context ctx, String message) {
+ Intent broadcastIntent = new Intent();
+ broadcastIntent.setAction("TOAST");
+ broadcastIntent.putExtra("MSG", message);
+ ctx.sendBroadcast(broadcastIntent);
+ }
+
+ public static String getFixLeakPath(String fileName) {
+ if (G.initPath() != null) {
+ return G.initPath() + "/" + fileName;
+ }
+ return null;
+ }
+
+ public static boolean isFixPathFileExist(String fileName) {
+ String path = getFixLeakPath(fileName);
+ if (path != null) {
+ File file = new File(path);
+ return file.exists();
+ }
+ return false;
+ }
+
+ public static boolean mountDir(Context context, String path, String mountType) {
+ if (path != null) {
+ String busyboxPath = Api.getBusyBoxPath(context, true);
+ if (!busyboxPath.trim().isEmpty()) {
+ return RootTools.remount(path, mountType, busyboxPath);
+ } else {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public static void checkAndCopyFixLeak(final Context context, final String fileName) {
+ if (G.initPath() != null && G.fixLeak() && !isFixPathFileExist(fileName)) {
+ final String srcPath = new File(ctx.getDir("bin", 0), fileName)
+ .getAbsolutePath();
+
+ new Thread(() -> {
+ String path = G.initPath();
+ if (path != null) {
+ File f = new File(path);
+ if (mountDir(context, getFixLeakPath(fileName), "RW")) {
+ //make sure it's executable
+ new RootCommand()
+ .setReopenShell(true)
+ .setLogging(true)
+ .run(ctx, "chmod 755 " + f.getAbsolutePath());
+ RootTools.copyFile(srcPath, (f.getAbsolutePath() + "/" + fileName),
+ true, false);
+ mountDir(context, getFixLeakPath(fileName), "RO");
+ }
+ }
+ }).start();
+ }
+ }
+
+ public static Context updateBaseContextLocale(Context context) {
+ String language = G.locale(); // Helper method to get saved language from SharedPreferences
+ Locale locale = new Locale(language);
+
+ if (language.equals("zh") || language.equals("zh_CN")) {
+ locale = Locale.SIMPLIFIED_CHINESE;
+ } else if (language.equals("zh_TW")) {
+ locale = Locale.TRADITIONAL_CHINESE;
+ }
+
+ Locale.setDefault(locale);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ return updateResourcesLocale(context, locale);
+ }
+ return updateResourcesLocaleLegacy(context, locale);
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private static Context updateResourcesLocale(Context context, Locale locale) {
+ Configuration configuration = context.getResources().getConfiguration();
+ configuration.setLocale(locale);
+ return context.createConfigurationContext(configuration);
+ }
+
+ private static Context updateResourcesLocaleLegacy(Context context, Locale locale) {
+ Resources resources = context.getResources();
+ Configuration configuration = resources.getConfiguration();
+ configuration.locale = locale;
+ resources.updateConfiguration(configuration, resources.getDisplayMetrics());
+ return context;
+ }
+
+ public static void setDefaultPermission(ApplicationInfo applicationInfo) {
+
+ boolean isModified = false;
+ SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ Editor edit = prefs.edit();
+
+ // Get the mode type
+ int modeType = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST).equals(Api.MODE_WHITELIST) ? 0 : 1;
+
+ // Get the preference list
+ List list = SQLite.select().from(DefaultConnectionPref.class)
+ .where(DefaultConnectionPref_Table.modeType.eq(modeType))
+ .queryList();
+
+ for (DefaultConnectionPref pref : list) {
+ if (pref.isState()) {
+ int uid = applicationInfo.uid;
+ switch (pref.getUid()) {
+ case 0:
+ edit.putString(PREF_LAN_PKG_UIDS, prefs.getString(PREF_LAN_PKG_UIDS, "") + "|" + uid);
+ isModified = true;
+ break;
+ case 1:
+ edit.putString(PREF_WIFI_PKG_UIDS, prefs.getString(PREF_WIFI_PKG_UIDS, "") + "|" + uid);
+ isModified = true;
+ break;
+ case 2:
+ edit.putString(PREF_3G_PKG_UIDS, prefs.getString(PREF_3G_PKG_UIDS, "") + "|" + uid);
+ isModified = true;
+ break;
+ case 3:
+ edit.putString(PREF_ROAMING_PKG_UIDS, prefs.getString(PREF_ROAMING_PKG_UIDS, "") + "|" + uid);
+ isModified = true;
+ break;
+ case 4:
+ edit.putString(PREF_TOR_PKG_UIDS, prefs.getString(PREF_TOR_PKG_UIDS, "") + "|" + uid);
+ isModified = true;
+ break;
+ case 5:
+ edit.putString(PREF_VPN_PKG_UIDS, prefs.getString(PREF_VPN_PKG_UIDS, "") + "|" + uid);
+ isModified = true;
+ break;
+ case 6:
+ edit.putString(PREF_TETHER_PKG_UIDS, prefs.getString(PREF_TETHER_PKG_UIDS, "") + "|" + uid);
+ isModified = true;
+ break;
+ }
+ }
+ }
+ if (isModified) {
+ edit.apply();
+ // Make sure rules are modified flag is set
+ Api.setRulesUpToDate(false);
+ fastApply(ctx, new RootCommand());
+ }
+ }
+
+ static class RuleDataSet {
+
+ List wifiList;
+ List dataList;
+ List lanList;
+ List roamList;
+ List vpnList;
+ List tetherList;
+ List torList;
+
+ RuleDataSet(List uidsWifi, List uids3g,
+ List uidsRoam, List uidsVPN, List uidsTether,
+ List uidsLAN, List uidsTor) {
+ this.wifiList = uidsWifi;
+ this.dataList = uids3g;
+ this.roamList = uidsRoam;
+ this.vpnList = uidsVPN;
+ this.tetherList = uidsTether;
+ this.lanList = uidsLAN;
+ this.torList = uidsTor;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((wifiList == null) ? 0 : dataList.hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ String builder = (wifiList != null ? android.text.TextUtils.join(",", wifiList) : "") +
+ (dataList != null ? android.text.TextUtils.join(",", dataList) : "") +
+ (lanList != null ? android.text.TextUtils.join(",", lanList) : "") +
+ (roamList != null ? android.text.TextUtils.join(",", roamList) : "") +
+ (vpnList != null ? android.text.TextUtils.join(",", vpnList) : "") +
+ (tetherList != null ? android.text.TextUtils.join(",", tetherList) : "") +
+ (torList != null ? android.text.TextUtils.join(",", torList) : "");
+ return builder.trim();
+ }
+ }
+
+ /**
+ * Safe shell command execution that handles library-level crashes
+ */
+ private static List executeSafeShellCommand(String command) {
+ // First try the primary libsu approach
+ try {
+ // Check if we can get a valid shell
+ if (Shell.getShell() == null || !Shell.getShell().isAlive()) {
+ Log.w(TAG, "Shell is not available or not alive, trying fallback");
+ return executeFallbackShellCommand(command);
+ }
+
+ // Execute with timeout and proper error handling
+ Shell.Result result = Shell.cmd(command).exec();
+ return result != null ? result.getOut() : null;
+
+ } catch (java.util.concurrent.RejectedExecutionException e) {
+ Log.w(TAG, "Shell execution rejected - trying fallback: " + e.getMessage());
+ return executeFallbackShellCommand(command);
+ } catch (RuntimeException e) {
+ // Check for wrapped ExecutionException with InterruptedIOException
+ Throwable cause = e.getCause();
+ if (cause instanceof java.util.concurrent.ExecutionException) {
+ java.util.concurrent.ExecutionException execEx = (java.util.concurrent.ExecutionException) cause;
+ if (execEx.getCause() instanceof java.io.InterruptedIOException) {
+ Log.w(TAG, "Shell execution interrupted at library level - trying fallback: " + execEx.getCause().getMessage());
+ return executeFallbackShellCommand(command);
+ }
+ }
+ // Re-throw if it's not a known interruption issue
+ throw e;
+ } catch (Exception e) {
+ Log.w(TAG, "Unexpected error in safe shell execution, trying fallback: " + e.getMessage());
+ return executeFallbackShellCommand(command);
+ }
+ }
+
+ /**
+ * Fallback shell execution using the legacy RootShell library
+ * This provides an alternative when libsu fails due to interruptions
+ */
+ private static List executeFallbackShellCommand(String command) {
+ try {
+ Log.d(TAG, "Using fallback shell execution for command: " + command);
+
+ // Use the legacy RootShell library as fallback
+ final java.util.List output = new java.util.ArrayList<>();
+ final boolean[] completed = {false};
+
+ com.stericson.rootshell.execution.Command cmd = new com.stericson.rootshell.execution.Command(0, command) {
+ @Override
+ public void commandCompleted(int id, int exitcode) {
+ super.commandCompleted(id, exitcode);
+ completed[0] = true;
+ }
+
+ @Override
+ public void commandOutput(int id, String line) {
+ super.commandOutput(id, line);
+ if (line != null) {
+ output.add(line);
+ }
+ }
+ };
+
+ // Execute with timeout
+ com.stericson.roottools.RootTools.getShell(true, 0).add(cmd);
+
+ // Wait for completion with timeout
+ long startTime = System.currentTimeMillis();
+ while (!completed[0] && (System.currentTimeMillis() - startTime) < 30000) {
+ Thread.sleep(100);
+ }
+
+ if (completed[0]) {
+ Log.d(TAG, "Fallback shell execution completed successfully");
+ return output;
+ } else {
+ Log.w(TAG, "Fallback shell execution timed out");
+ return null;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Fallback shell execution also failed: " + e.getMessage());
+ return null;
+ }
+ }
+
+ private static class RunCommand extends AsyncTask