Repo created

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

42
Android.mk Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
# *
# * @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))

922
Changelog.md Normal file
View file

@ -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

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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 <http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

386
README.md
View file

@ -1,3 +1,385 @@
# af-wall # AFWall+ (Android Firewall+)
Android Firewall and IP tables [![Android CI](https://github.com/ukanth/afwall/workflows/Android%20CI/badge.svg?branch=beta)](https://github.com/ukanth/afwall/actions) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/afwall/localized.png)](https://crowdin.net/project/afwall) ![License](https://img.shields.io/github/license/ukanth/afwall) ![F-Droid](https://img.shields.io/f-droid/v/dev.ukanth.ufirewall) ![Downloads](https://img.shields.io/github/downloads/ukanth/afwall/total) ![Repo Size](https://img.shields.io/github/repo-size/ukanth/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**: [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](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
---
<p align="center">
<img src="https://raw.githubusercontent.com/ukanth/afwall/0502e6f17ceda08069720ff2f260902690e65e9b/screenshots/Main_2.0.png" width="300" alt="AFWall+ Screenshot">
</p>
## 🔥 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
<p align="left">
<a href="https://play.google.com/store/apps/details?id=dev.ukanth.ufirewall">
<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="80">
</a>
<a href="https://f-droid.org/packages/dev.ukanth.ufirewall/">
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">
</a>
<a href="https://github.com/ukanth/afwall/releases">
<img src="https://img.shields.io/badge/GitHub-Releases-blue?style=for-the-badge&logo=github" alt="GitHub Releases" height="80">
</a>
</p>
📋 **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)
---
<p align="center">
<i>Made with ❤️ for Android privacy and security</i><br>
<strong>AFWall+ - Your Network, Your Rules</strong>
</p>

84
app/build.gradle Normal file
View file

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

3
app/lint.xml Normal file
View file

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

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

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

View file

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

View file

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

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,68 @@
/**
* Class to store wifi/3G/tethering status and LAN IP ranges.
*
* Copyright (C) 2013 Kevin Cernekee
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall;
public class InterfaceDetails {
// firewall policy
public boolean isRoaming = false;
public boolean isWifiTethered = false;
public boolean tetherWifiStatusKnown = false;
public boolean isBluetoothTethered = false;
public boolean tetherBluetoothStatusKnown = false;
public boolean isUsbTethered = false;
public boolean tetherUsbStatusKnown = false;
public String lanMaskV4 = "";
public String lanMaskV6 = "";
// DNS servers for targeted rules instead of opening port 53 to all LAN hosts
public java.util.List<String> dnsServersV4 = new java.util.ArrayList<>();
public java.util.List<String> dnsServersV6 = new java.util.ArrayList<>();
// supplementary info
String wifiName = "";
boolean netEnabled = false;
boolean noIP = false;
public int netType = -1;
public boolean equals(InterfaceDetails that) {
return this.isRoaming == that.isRoaming &&
this.isWifiTethered == that.isWifiTethered &&
this.tetherWifiStatusKnown == that.tetherWifiStatusKnown &&
this.isBluetoothTethered == that.isBluetoothTethered &&
this.tetherBluetoothStatusKnown == that.tetherBluetoothStatusKnown &&
this.isUsbTethered == that.isUsbTethered &&
this.tetherUsbStatusKnown == that.tetherUsbStatusKnown &&
this.lanMaskV4.equals(that.lanMaskV4) &&
this.lanMaskV6.equals(that.lanMaskV6) &&
this.dnsServersV4.equals(that.dnsServersV4) &&
this.dnsServersV6.equals(that.dnsServersV6) &&
this.wifiName.equals(that.wifiName) &&
this.netEnabled == that.netEnabled &&
this.netType == that.netType &&
this.noIP == that.noIP;
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,119 @@
/**
* Display/purge logs and toggle logging
* <p/>
* Copyright (C) 2011-2013 Kevin Cernekee
* <p/>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p/>
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p/>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.SubMenu;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.raizlabs.android.dbflow.config.FlowManager;
import java.util.List;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.LogData;
import dev.ukanth.ufirewall.log.LogDatabase;
import dev.ukanth.ufirewall.log.LogInfo;
import dev.ukanth.ufirewall.util.G;
public class OldLogActivity extends DataDumpActivity {
protected static final int MENU_CLEARLOG = 7;
protected static final int MENU_SWITCH_NEW = 70;
//protected static final int MENU_TOGGLE_LOG = 27;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(getString(R.string.showlog_title));
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
sdDumpFile = "iptables.log";
}
protected void parseAndSet(List<LogData> logDataList) {
String cooked = LogInfo.parseLog(OldLogActivity.this, logDataList);
if (cooked == null) {
setData(getString(R.string.log_parse_error));
} else {
setData(cooked);
}
}
protected void populateData(final Context ctx) {
parseAndSet(Api.fetchLogs());
}
protected void populateMenu(SubMenu sub) {
sub.add(0, MENU_CLEARLOG, 0, R.string.clear_log).setIcon(R.drawable.ic_clearlog);
sub.add(0, MENU_SWITCH_NEW, 0, R.string.switch_new).setIcon(R.drawable.ic_log);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final Context ctx = this;
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case MENU_SWITCH_NEW:
Intent i = new Intent(this, LogActivity.class);
G.oldLogView(false);
startActivity(i);
finish();
return true;
case MENU_CLEARLOG:
clearDatabase(OldLogActivity.this);
return true;
}
return super.onOptionsItemSelected(item);
}
private void clearDatabase(final Context ctx) {
new MaterialDialog.Builder(this)
.title(getApplicationContext().getString(R.string.clear_log) + " ?")
.cancelable(true)
.onPositive((dialog, which) -> {
//SQLite.delete(LogData_Table.class);
FlowManager.getDatabase(LogDatabase.NAME).reset();
Toast.makeText(ctx, ctx.getString(R.string.log_cleared), Toast.LENGTH_SHORT).show();
dialog.dismiss();
parseAndSet(Api.fetchLogs());
})
.onNegative((dialog, which) -> dialog.dismiss())
.positiveText(R.string.Yes)
.negativeText(R.string.No)
.show();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,133 @@
/**
* Circular log buffer.
* This provides timestamped logcat output for the "Rules" page, to aid
* in diagnosing failures.
*
* Copyright (C) 2013 Kevin Cernekee
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.log;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
public class Log {
private static final int MAX_ENTRIES = 512;
public static final int LOG_DEBUG = 0;
public static final int LOG_VERBOSE = 1;
public static final int LOG_INFO = 2;
public static final int LOG_WARNING = 3;
public static final int LOG_ERROR = 4;
public static final int LOG_WTF = 5;
public static class LogEntry {
Date timestamp;
int level;
String msg = "";
}
private static final LinkedList<LogEntry> circ = new LinkedList<LogEntry>();
private static synchronized void circLog(int level, String msg) {
LogEntry e = new LogEntry();
e.timestamp = new Date();
e.level = level;
e.msg = msg;
if (circ.size() >= MAX_ENTRIES) {
circ.removeFirst();
}
circ.addLast(e);
}
public static synchronized String getLog() {
StringBuilder ret = new StringBuilder();
for (int i = 0; i < circ.size(); i++) {
LogEntry e = circ.get(i);
String timestamp = new SimpleDateFormat("HH:mm:ss").format(e.timestamp);
ret.append(timestamp).append(" ").append(e.msg).append("\n");
}
return ret.toString();
}
public static int d(String tag, String msg) {
circLog(LOG_DEBUG, msg);
return android.util.Log.d(tag, msg);
}
public static int d(String tag, String msg, Exception e) {
circLog(LOG_DEBUG, msg);
return android.util.Log.d(tag, msg, e);
}
public static int v(String tag, String msg) {
circLog(LOG_VERBOSE, msg);
return android.util.Log.v(tag, msg);
}
public static int v(String tag, String msg, Exception e) {
circLog(LOG_VERBOSE, msg);
return android.util.Log.v(tag, msg, e);
}
public static int i(String tag, String msg) {
circLog(LOG_INFO, msg);
return android.util.Log.i(tag, msg);
}
public static int i(String tag, String msg, Exception e) {
circLog(LOG_INFO, msg);
return android.util.Log.i(tag, msg, e);
}
public static int w(String tag, String msg) {
circLog(LOG_WARNING, msg);
return android.util.Log.w(tag, msg);
}
public static int w(String tag, String msg, Exception e) {
circLog(LOG_WARNING, msg);
return android.util.Log.w(tag, msg, e);
}
public static int e(String tag, String msg) {
circLog(LOG_ERROR, msg);
return android.util.Log.e(tag, msg);
}
public static int e(String tag, String msg, Exception e) {
circLog(LOG_ERROR, msg);
return android.util.Log.e(tag, msg, e);
}
public static int wtf(String tag, String msg) {
circLog(LOG_WTF, msg);
return android.util.Log.wtf(tag, msg);
}
public static int wtf(String tag, String msg, Exception e) {
circLog(LOG_WTF, msg);
return android.util.Log.wtf(tag, msg, e);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,163 @@
/**
*
* Shell Command for stream blocked packets information from klogripper (/proc/kmsg)
*
* Copyright (C) 2014 Umakanthan Chandran
*
* Originally copied from NetworkLog (C) 2012 Pragmatic Software (MPL2.0)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Umakanthan Chandran
* @version 1.0
*/
package dev.ukanth.ufirewall.log;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ShellCommand {
Runtime rt;
String[] command;
String tag = "";
Process process;
BufferedReader stdout;
public String error;
public int exitval;
public ShellCommand(String[] command, String tag) {
this(command);
this.tag = tag;
}
public ShellCommand(String[] command) {
this.command = command;
rt = Runtime.getRuntime();
}
public void start(boolean waitForExit) {
exitval = -1;
error = null;
try {
process = new ProcessBuilder().command(command).redirectErrorStream(true).start();
stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
} catch (Exception e) {
error = e.getCause().getMessage();
return;
}
if (waitForExit) {
waitForExit();
}
}
public void waitForExit() {
while (!checkForExit()) {
if (stdoutAvailable()) {
Log.d("AFWALL", "ShellCommand waitForExit [" + tag
+ "] discarding read: " + readStdout());
} else {
try {
Thread.sleep(100);
} catch (Exception e) {
Log.d("AFWall", "waitForExit", e);
}
}
}
}
public void finish() {
try {
if (stdout != null) {
stdout.close();
}
} catch (Exception e) {
Log.e("AFWall", "Exception finishing [" + tag + "]", e);
}
if(process !=null) {
process.destroy();
}
process = null;
}
public boolean checkForExit() {
try {
if(process != null) {
exitval = process.exitValue();
} else {
finish();
}
} catch (IllegalThreadStateException e) {
return false;
}
finish();
return true;
}
public boolean stdoutAvailable() {
try {
return stdout.ready();
} catch (java.io.IOException e) {
Log.e("AFWall", "stdoutAvailable error", e);
return false;
}
}
public String readStdoutBlocking() {
String line;
if (stdout == null) {
return null;
}
try {
line = stdout.readLine();
} catch (Exception e) {
Log.e("AFWall", "readStdoutBlocking error", e);
return null;
}
if (line == null) {
return null;
} else {
return line + "\n";
}
}
public String readStdout() {
if (stdout == null) {
return null;
}
try {
if (stdout.ready()) {
String line = stdout.readLine();
if (line == null) {
return null;
} else {
return line + "\n";
}
} else {
return "";
}
} catch (Exception e) {
Log.e("AFWall", "readStdout error", e);
return null;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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