From ee0cddf35c9a5a61755fe2f36b3f23c00f730b67 Mon Sep 17 00:00:00 2001 From: Fr4nzD13trich Date: Thu, 20 Nov 2025 14:05:38 +0100 Subject: [PATCH] Repo created --- Android.mk | 42 + Changelog.md | 922 ++++ LICENSE | 674 +++ README.md | 386 +- app/build.gradle | 84 + app/lint.xml | 3 + app/proguard-rules.pro | 21 + app/src/main/AndroidManifest.xml | 366 ++ app/src/main/assets/rules.json | 78 + app/src/main/assets/xposed_init | 0 app/src/main/ic_launcher-web.png | Bin 0 -> 28000 bytes app/src/main/ic_launcher_donate-web.png | Bin 0 -> 27110 bytes .../stericson/rootshell/NativeJavaClass.java | 46 + .../com/stericson/rootshell/RootShell.java | 585 +++ .../rootshell/SanityCheckRootShell.java | 415 ++ .../rootshell/containers/RootClass.java | 328 ++ .../exceptions/RootDeniedException.java | 32 + .../rootshell/execution/Command.java | 325 ++ .../rootshell/execution/JavaCommand.java | 58 + .../stericson/rootshell/execution/Shell.java | 1029 ++++ .../com/stericson/roottools/Constants.java | 15 + .../com/stericson/roottools/RootTools.java | 848 ++++ .../roottools/SanityCheckRootTools.java | 459 ++ .../stericson/roottools/containers/Mount.java | 70 + .../roottools/containers/Permissions.java | 125 + .../roottools/containers/Symlink.java | 47 + .../roottools/internal/Installer.java | 300 ++ .../roottools/internal/InternalVariables.java | 62 + .../roottools/internal/Remounter.java | 238 + .../internal/RootToolsInternalMethods.java | 1342 +++++ .../stericson/roottools/internal/Runner.java | 98 + .../twofortyfouram/locale/BreadCrumber.java | 90 + .../com/twofortyfouram/locale/Constants.java | 48 + .../com/twofortyfouram/locale/Intent.java | 195 + .../locale/PackageUtilities.java | 123 + .../main/java/dev/ukanth/ufirewall/Api.java | 4419 +++++++++++++++++ .../ukanth/ufirewall/InterfaceDetails.java | 68 + .../ukanth/ufirewall/InterfaceTracker.java | 523 ++ .../dev/ukanth/ufirewall/MainActivity.java | 2723 ++++++++++ .../ufirewall/activity/AppDetailActivity.java | 232 + .../ufirewall/activity/BaseActivity.java | 21 + .../activity/CustomRulesActivity.java | 101 + .../activity/CustomScriptActivity.java | 159 + .../ufirewall/activity/DataDumpActivity.java | 480 ++ .../ufirewall/activity/HelpActivity.java | 72 + .../ufirewall/activity/LogActivity.java | 389 ++ .../ufirewall/activity/LogDetailActivity.java | 752 +++ .../ufirewall/activity/OldLogActivity.java | 119 + .../ufirewall/activity/ProfileActivity.java | 276 + .../ufirewall/activity/RulesActivity.java | 369 ++ .../ufirewall/activity/StartActivity.java | 27 + .../ufirewall/admin/AdminDeviceReceiver.java | 37 + .../broadcast/ConnectivityChangeReceiver.java | 83 + .../ufirewall/broadcast/OnBootReceiver.java | 70 + .../ufirewall/broadcast/PackageBroadcast.java | 192 + .../ufirewall/customrules/CustomRule.java | 75 + .../customrules/CustomRuleDatabase.java | 13 + .../ufirewall/events/LogChangeEvent.java | 17 + .../dev/ukanth/ufirewall/events/LogEvent.java | 21 + .../ukanth/ufirewall/events/RulesEvent.java | 17 + .../ufirewall/events/RxCommandEvent.java | 27 + .../dev/ukanth/ufirewall/events/RxEvent.java | 31 + .../java/dev/ukanth/ufirewall/log/Log.java | 133 + .../dev/ukanth/ufirewall/log/LogData.java | 164 + .../dev/ukanth/ufirewall/log/LogDatabase.java | 33 + .../log/LogDetailRecyclerViewAdapter.java | 273 + .../dev/ukanth/ufirewall/log/LogInfo.java | 345 ++ .../ukanth/ufirewall/log/LogPreference.java | 83 + .../ukanth/ufirewall/log/LogPreferenceDB.java | 15 + .../ufirewall/log/LogRecyclerViewAdapter.java | 127 + .../log/RecyclerItemClickListener.java | 9 + .../ukanth/ufirewall/log/ShellCommand.java | 163 + .../ufirewall/plugin/BundleScrubber.java | 77 + .../ukanth/ufirewall/plugin/FireReceiver.java | 301 ++ .../ukanth/ufirewall/plugin/LocaleEdit.java | 235 + .../ufirewall/plugin/PluginBundleManager.java | 92 + .../CustomBinaryPreferenceFragment.java | 15 + .../preferences/DefaultConnectionPref.java | 59 + .../preferences/DefaultConnectionPrefDB.java | 14 + .../preferences/ExpPreferenceFragment.java | 260 + .../LanguagePreferenceFragment.java | 62 + .../preferences/LogPreferenceFragment.java | 255 + .../MultiProfilePreferenceFragment.java | 80 + .../preferences/PreferencesActivity.java | 397 ++ .../preferences/RulesPreferenceFragment.java | 307 ++ .../preferences/SecPreferenceFragment.java | 514 ++ .../ufirewall/preferences/ShareContract.java | 19 + .../preferences/SharePreference.java | 294 ++ .../preferences/SharePreferenceProvider.java | 256 + .../preferences/ShareProfilePreference.java | 20 + .../ufirewall/preferences/ShareUtils.java | 64 + .../preferences/ThemePreferenceFragment.java | 81 + .../preferences/UIPreferenceFragment.java | 148 + .../preferences/WidgetPreferenceFragment.java | 47 + .../ufirewall/profiles/ProfileAdapter.java | 68 + .../ufirewall/profiles/ProfileData.java | 82 + .../ufirewall/profiles/ProfileHelper.java | 135 + .../ufirewall/profiles/ProfilesDatabase.java | 15 + .../ufirewall/service/FirewallService.java | 293 ++ .../ukanth/ufirewall/service/LogService.java | 742 +++ .../ukanth/ufirewall/service/RootCommand.java | 184 + .../ufirewall/service/RootShellService.java | 447 ++ .../ufirewall/service/RootShellService2.java | 420 ++ .../ufirewall/service/RulesApplyService.java | 41 + .../ufirewall/service/ToggleTileService.java | 117 + .../ufirewall/ui/about/AboutFragment.java | 154 + .../ufirewall/util/AppIconHelperV26.java | 56 + .../ufirewall/util/AppListArrayAdapter.java | 741 +++ .../ukanth/ufirewall/util/BiometricUtil.java | 394 ++ .../ufirewall/util/BootRuleManager.java | 215 + .../ukanth/ufirewall/util/CustomRuleOld.java | 93 + .../ufirewall/util/DataUsageParser.java | 280 ++ .../ukanth/ufirewall/util/DateComparator.java | 18 + .../dev/ukanth/ufirewall/util/FileDialog.java | 284 ++ .../ufirewall/util/FingerprintUtil.java | 397 ++ .../java/dev/ukanth/ufirewall/util/G.java | 1283 +++++ .../ukanth/ufirewall/util/HtmlTagHandler.java | 359 ++ .../dev/ukanth/ufirewall/util/ImportApi.java | 150 + .../ukanth/ufirewall/util/InputValidator.java | 318 ++ .../dev/ukanth/ufirewall/util/JsonHelper.java | 70 + .../ukanth/ufirewall/util/LocaleManager.java | 41 + .../dev/ukanth/ufirewall/util/LogNetUtil.java | 238 + .../ufirewall/util/PackageComparator.java | 36 + .../java/dev/ukanth/ufirewall/util/Rule.java | 65 + .../ukanth/ufirewall/util/SecureCrypto.java | 248 + .../ufirewall/util/SecurePasswordManager.java | 160 + .../ukanth/ufirewall/util/SecurityUtil.java | 202 + .../ufirewall/util/SlidingTabLayout.java | 310 ++ .../ufirewall/util/SlidingTabStrip.java | 154 + .../ukanth/ufirewall/util/UidCorrelator.java | 277 ++ .../ukanth/ufirewall/util/UidResolver.java | 667 +++ .../ufirewall/util/XPreferenceProvider.java | 12 + .../ufirewall/widget/RadialMenuWidget.java | 1188 +++++ .../ukanth/ufirewall/widget/StatusWidget.java | 207 + .../ukanth/ufirewall/widget/ToggleWidget.java | 57 + .../widget/ToggleWidgetActivity.java | 568 +++ .../ufirewall/widget/ToggleWidgetOld.java | 59 + .../widget/ToggleWidgetOldActivity.java | 486 ++ .../ukanth/ufirewall/xposed/XposedInit.java | 175 + .../colorpicker/AlphaPatternDrawable.java | 130 + .../colorpicker/ColorPickerDialog.java | 230 + .../colorpicker/ColorPickerPanelView.java | 174 + .../colorpicker/ColorPickerPreference.java | 318 ++ .../colorpicker/ColorPickerView.java | 945 ++++ app/src/main/res/anim/widget_pulse.xml | 20 + app/src/main/res/anim/widget_scale_in.xml | 19 + app/src/main/res/convert.properties | 47 + app/src/main/res/convert.sh | 15 + app/src/main/res/drawable-hdpi-v11/active.png | Bin 0 -> 686 bytes app/src/main/res/drawable-hdpi-v11/error.png | Bin 0 -> 628 bytes .../main/res/drawable-hdpi-v11/question.png | Bin 0 -> 590 bytes app/src/main/res/drawable-hdpi/ic_apply.png | Bin 0 -> 326 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 1271 bytes .../res/drawable-hdpi/ic_launcher_free.png | Bin 0 -> 1160 bytes .../main/res/drawable-hdpi/notification.png | Bin 0 -> 376 bytes .../res/drawable-hdpi/notification_error.png | Bin 0 -> 403 bytes .../res/drawable-hdpi/notification_quest.png | Bin 0 -> 372 bytes .../res/drawable-hdpi/notification_warn.png | Bin 0 -> 302 bytes ...twofortyfouram_locale_ic_menu_dontsave.png | Bin 0 -> 269 bytes .../twofortyfouram_locale_ic_menu_help.png | Bin 0 -> 322 bytes .../twofortyfouram_locale_ic_menu_save.png | Bin 0 -> 365 bytes app/src/main/res/drawable-mdpi-v11/active.png | Bin 0 -> 544 bytes app/src/main/res/drawable-mdpi-v11/error.png | Bin 0 -> 475 bytes .../main/res/drawable-mdpi-v11/question.png | Bin 0 -> 472 bytes app/src/main/res/drawable-mdpi/ic_apply.png | Bin 0 -> 245 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 990 bytes .../res/drawable-mdpi/ic_launcher_free.png | Bin 0 -> 948 bytes .../main/res/drawable-mdpi/notification.png | Bin 0 -> 299 bytes .../res/drawable-mdpi/notification_error.png | Bin 0 -> 328 bytes .../res/drawable-mdpi/notification_quest.png | Bin 0 -> 305 bytes .../res/drawable-mdpi/notification_warn.png | Bin 0 -> 265 bytes .../drawable-v21/ic_android_white_24dp.xml | 4 + .../main/res/drawable-xhdpi-v11/active.png | Bin 0 -> 784 bytes app/src/main/res/drawable-xhdpi-v11/error.png | Bin 0 -> 696 bytes .../main/res/drawable-xhdpi-v11/question.png | Bin 0 -> 650 bytes app/src/main/res/drawable-xhdpi/ic_apply.png | Bin 0 -> 456 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 1450 bytes .../res/drawable-xhdpi/ic_launcher_free.png | Bin 0 -> 1473 bytes .../main/res/drawable-xhdpi/notification.png | Bin 0 -> 440 bytes .../res/drawable-xhdpi/notification_error.png | Bin 0 -> 468 bytes .../res/drawable-xhdpi/notification_quest.png | Bin 0 -> 437 bytes .../res/drawable-xhdpi/notification_warn.png | Bin 0 -> 346 bytes .../main/res/drawable-xxhdpi-v11/active.png | Bin 0 -> 1153 bytes .../main/res/drawable-xxhdpi-v11/error.png | Bin 0 -> 876 bytes .../main/res/drawable-xxhdpi-v11/question.png | Bin 0 -> 830 bytes app/src/main/res/drawable-xxhdpi/ic_apply.png | Bin 0 -> 492 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 2360 bytes .../res/drawable-xxhdpi/ic_launcher_free.png | Bin 0 -> 2225 bytes .../main/res/drawable-xxhdpi/notification.png | Bin 0 -> 562 bytes .../drawable-xxhdpi/notification_error.png | Bin 0 -> 590 bytes .../drawable-xxhdpi/notification_quest.png | Bin 0 -> 549 bytes .../res/drawable-xxhdpi/notification_warn.png | Bin 0 -> 422 bytes .../main/res/drawable-xxxhdpi/ic_apply.png | Bin 0 -> 574 bytes app/src/main/res/drawable/card_border.xml | 9 + .../main/res/drawable/circle_background.xml | 5 + app/src/main/res/drawable/ic_action.xml | 5 + .../res/drawable/ic_action_fingerprint.xml | 9 + app/src/main/res/drawable/ic_allow.xml | 10 + .../res/drawable/ic_android_white_24dp.xml | 4 + app/src/main/res/drawable/ic_apply_menu.xml | 9 + .../res/drawable/ic_apply_notification.xml | 5 + .../main/res/drawable/ic_block_black_24dp.xml | 9 + app/src/main/res/drawable/ic_bluetooth.xml | 10 + app/src/main/res/drawable/ic_clean.xml | 5 + app/src/main/res/drawable/ic_clearlog.xml | 9 + app/src/main/res/drawable/ic_clone.xml | 5 + app/src/main/res/drawable/ic_copy.xml | 9 + app/src/main/res/drawable/ic_custom_rules.xml | 7 + app/src/main/res/drawable/ic_deny.xml | 7 + app/src/main/res/drawable/ic_exit.xml | 4 + app/src/main/res/drawable/ic_export.xml | 9 + app/src/main/res/drawable/ic_flow.xml | 10 + app/src/main/res/drawable/ic_help.xml | 9 + app/src/main/res/drawable/ic_import.xml | 9 + app/src/main/res/drawable/ic_invert.xml | 6 + app/src/main/res/drawable/ic_lan.xml | 8 + app/src/main/res/drawable/ic_legend.xml | 6 + app/src/main/res/drawable/ic_log.xml | 6 + app/src/main/res/drawable/ic_mail.xml | 9 + app/src/main/res/drawable/ic_menu_binary.xml | 8 + app/src/main/res/drawable/ic_menu_exp.xml | 6 + app/src/main/res/drawable/ic_menu_pref.xml | 8 + app/src/main/res/drawable/ic_menu_profile.xml | 8 + app/src/main/res/drawable/ic_menu_save.xml | 8 + app/src/main/res/drawable/ic_menu_secure.xml | 8 + .../main/res/drawable/ic_menu_translate.xml | 8 + app/src/main/res/drawable/ic_menu_widget.xml | 8 + app/src/main/res/drawable/ic_mobiledata.xml | 8 + .../ic_notifications_off_black_24dp.xml | 10 + .../ic_notifications_on_black_24dp.xml | 10 + .../drawable/ic_open_in_new_black_24dp.xml | 10 + app/src/main/res/drawable/ic_preference.xml | 9 + app/src/main/res/drawable/ic_refresh.xml | 9 + app/src/main/res/drawable/ic_roam.xml | 8 + app/src/main/res/drawable/ic_rules.xml | 10 + app/src/main/res/drawable/ic_script.xml | 10 + app/src/main/res/drawable/ic_search.xml | 10 + app/src/main/res/drawable/ic_settings.xml | 8 + app/src/main/res/drawable/ic_sort.xml | 10 + app/src/main/res/drawable/ic_tether.xml | 23 + app/src/main/res/drawable/ic_theme.xml | 9 + app/src/main/res/drawable/ic_tor.xml | 10 + app/src/main/res/drawable/ic_unknown.xml | 11 + app/src/main/res/drawable/ic_vpn.xml | 8 + app/src/main/res/drawable/ic_wifi.xml | 8 + app/src/main/res/drawable/list_divider.xml | 20 + app/src/main/res/drawable/plus.xml | 9 + app/src/main/res/drawable/preview_new.png | Bin 0 -> 3223 bytes app/src/main/res/drawable/preview_old.png | Bin 0 -> 2682 bytes app/src/main/res/drawable/preview_toggle.png | Bin 0 -> 2903 bytes app/src/main/res/drawable/question_widget.png | Bin 0 -> 3615 bytes .../res/drawable/text_background_rounded.xml | 9 + app/src/main/res/drawable/toast.xml | 9 + ...twofortyfouram_locale_ic_menu_dontsave.xml | 17 + .../twofortyfouram_locale_ic_menu_help.xml | 17 + .../twofortyfouram_locale_ic_menu_save.xml | 17 + app/src/main/res/drawable/widget_bg.xml | 6 + app/src/main/res/drawable/widget_bg_focus.png | Bin 0 -> 823 bytes .../main/res/drawable/widget_bg_pressed.png | Bin 0 -> 914 bytes .../res/drawable/widget_circle_enable.xml | 20 + .../main/res/drawable/widget_disabling.xml | 13 + app/src/main/res/drawable/widget_enabling.xml | 13 + app/src/main/res/drawable/widget_error.xml | 13 + app/src/main/res/drawable/widget_off.png | Bin 0 -> 4260 bytes app/src/main/res/drawable/widget_on.png | Bin 0 -> 3951 bytes app/src/main/res/drawable/widget_success.xml | 13 + app/src/main/res/drawable/zoomin.xml | 8 + app/src/main/res/drawable/zoomout.xml | 8 + .../res/layout-land/dialog_color_picker.xml | 82 + .../main/res/layout/activity_custom_rules.xml | 20 + app/src/main/res/layout/activity_prefs.xml | 16 + app/src/main/res/layout/app_detail.xml | 164 + app/src/main/res/layout/apply_view.xml | 47 + app/src/main/res/layout/custom_toast.xml | 34 + app/src/main/res/layout/customscript.xml | 214 + .../main/res/layout/dialog_color_picker.xml | 88 + app/src/main/res/layout/file_row.xml | 46 + app/src/main/res/layout/fingerprint.xml | 96 + app/src/main/res/layout/help_about.xml | 548 ++ .../main/res/layout/help_about_content.xml | 289 ++ .../main/res/layout/help_legend_section.xml | 607 +++ app/src/main/res/layout/legend.xml | 671 +++ .../main/res/layout/log_detail_summary.xml | 138 + app/src/main/res/layout/log_recycle_item.xml | 61 + app/src/main/res/layout/log_view.xml | 39 + .../res/layout/logdetail_recycle_item.xml | 199 + app/src/main/res/layout/logdetail_view.xml | 68 + app/src/main/res/layout/main.xml | 223 + app/src/main/res/layout/main_list.xml | 219 + app/src/main/res/layout/main_list_old.xml | 185 + app/src/main/res/layout/main_old.xml | 189 + app/src/main/res/layout/onoff_widget.xml | 15 + app/src/main/res/layout/profile_adddialog.xml | 25 + app/src/main/res/layout/profile_layout.xml | 12 + app/src/main/res/layout/profile_main.xml | 25 + app/src/main/res/layout/rules.xml | 38 + app/src/main/res/layout/rules_modern.xml | 326 ++ app/src/main/res/layout/searchbar.xml | 9 + app/src/main/res/layout/tasker_profile.xml | 90 + .../main/res/layout/toggle_widget_layout.xml | 14 + .../res/layout/toggle_widget_old_layout.xml | 15 + .../res/layout/toggle_widget_old_view.xml | 43 + .../main/res/layout/toggle_widget_view.xml | 10 + app/src/main/res/menu/menu_bar.xml | 124 + ...ofortyfouram_locale_help_save_dontsave.xml | 34 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_donate.xml | 5 + .../ic_launcher_donate_round.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4186 bytes .../res/mipmap-hdpi/ic_launcher_donate.png | Bin 0 -> 4060 bytes .../mipmap-hdpi/ic_launcher_donate_round.png | Bin 0 -> 4060 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 4128 bytes .../ic_launcher_foreground_donate.png | Bin 0 -> 4033 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4186 bytes .../main/res/mipmap-hdpi/round_launcher.png | Bin 0 -> 2029 bytes .../res/mipmap-hdpi/round_launcher_free.png | Bin 0 -> 2081 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2517 bytes .../res/mipmap-mdpi/ic_launcher_donate.png | Bin 0 -> 2469 bytes .../mipmap-mdpi/ic_launcher_donate_round.png | Bin 0 -> 2469 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2546 bytes .../ic_launcher_foreground_donate.png | Bin 0 -> 2467 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2517 bytes .../main/res/mipmap-mdpi/round_launcher.png | Bin 0 -> 1344 bytes .../res/mipmap-mdpi/round_launcher_free.png | Bin 0 -> 1382 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6067 bytes .../res/mipmap-xhdpi/ic_launcher_donate.png | Bin 0 -> 5906 bytes .../mipmap-xhdpi/ic_launcher_donate_round.png | Bin 0 -> 5906 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5970 bytes .../ic_launcher_foreground_donate.png | Bin 0 -> 5796 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6067 bytes .../main/res/mipmap-xhdpi/round_launcher.png | Bin 0 -> 2814 bytes .../res/mipmap-xhdpi/round_launcher_free.png | Bin 0 -> 2901 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 9788 bytes .../res/mipmap-xxhdpi/ic_launcher_donate.png | Bin 0 -> 9603 bytes .../ic_launcher_donate_round.png | Bin 0 -> 9603 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 10437 bytes .../ic_launcher_foreground_donate.png | Bin 0 -> 10145 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 9788 bytes .../main/res/mipmap-xxhdpi/round_launcher.png | Bin 0 -> 4554 bytes .../res/mipmap-xxhdpi/round_launcher_free.png | Bin 0 -> 4701 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14247 bytes .../res/mipmap-xxxhdpi/ic_launcher_donate.png | Bin 0 -> 13961 bytes .../ic_launcher_donate_round.png | Bin 0 -> 13961 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 15678 bytes .../ic_launcher_foreground_donate.png | Bin 0 -> 15282 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 14247 bytes .../res/mipmap-xxxhdpi/round_launcher.png | Bin 0 -> 6615 bytes .../mipmap-xxxhdpi/round_launcher_free.png | Bin 0 -> 6848 bytes app/src/main/res/raw/about.html | 305 ++ app/src/main/res/raw/afwallstart | 30 + app/src/main/res/raw/busybox_arm | Bin 0 -> 1828564 bytes app/src/main/res/raw/busybox_arm64 | Bin 0 -> 2295680 bytes app/src/main/res/raw/busybox_x86 | Bin 0 -> 2212344 bytes app/src/main/res/raw/faq.html | 117 + app/src/main/res/raw/ip6tables_arm | Bin 0 -> 395256 bytes app/src/main/res/raw/ip6tables_arm64 | Bin 0 -> 511736 bytes app/src/main/res/raw/ip6tables_x86 | Bin 0 -> 486464 bytes app/src/main/res/raw/iptables_arm | Bin 0 -> 395256 bytes app/src/main/res/raw/iptables_arm64 | Bin 0 -> 511736 bytes app/src/main/res/raw/iptables_x86 | Bin 0 -> 486464 bytes app/src/main/res/raw/nflog_arm | Bin 0 -> 373960 bytes app/src/main/res/raw/nflog_arm64 | Bin 0 -> 597936 bytes app/src/main/res/raw/nflog_x86 | Bin 0 -> 721768 bytes app/src/main/res/values-af/strings.xml | 236 + app/src/main/res/values-ar/strings.xml | 548 ++ app/src/main/res/values-ast-rES/strings.xml | 548 ++ app/src/main/res/values-az/strings.xml | 548 ++ app/src/main/res/values-bg/strings.xml | 548 ++ app/src/main/res/values-bi/strings.xml | 548 ++ app/src/main/res/values-bn/strings.xml | 548 ++ app/src/main/res/values-bs/strings.xml | 548 ++ app/src/main/res/values-ca/strings.xml | 548 ++ app/src/main/res/values-cs/strings.xml | 549 ++ app/src/main/res/values-da/strings.xml | 548 ++ app/src/main/res/values-de/strings.xml | 548 ++ app/src/main/res/values-el/strings.xml | 551 ++ app/src/main/res/values-es/strings.xml | 548 ++ app/src/main/res/values-eu/strings.xml | 548 ++ app/src/main/res/values-fa/strings.xml | 556 +++ app/src/main/res/values-fi/strings.xml | 548 ++ app/src/main/res/values-fr/strings.xml | 548 ++ app/src/main/res/values-he/strings.xml | 549 ++ app/src/main/res/values-hi/strings.xml | 549 ++ app/src/main/res/values-hr/strings.xml | 548 ++ app/src/main/res/values-hu/strings.xml | 548 ++ app/src/main/res/values-in/strings.xml | 548 ++ app/src/main/res/values-it/strings.xml | 548 ++ app/src/main/res/values-ja/strings.xml | 551 ++ app/src/main/res/values-kn/strings.xml | 548 ++ app/src/main/res/values-ko/strings.xml | 548 ++ app/src/main/res/values-ku/strings.xml | 548 ++ app/src/main/res/values-ky/strings.xml | 548 ++ app/src/main/res/values-ml-rIN/strings.xml | 548 ++ app/src/main/res/values-ms/strings.xml | 548 ++ app/src/main/res/values-nb/strings.xml | 548 ++ app/src/main/res/values-nl/strings.xml | 551 ++ app/src/main/res/values-pl/strings.xml | 548 ++ app/src/main/res/values-pt-rBR/strings.xml | 548 ++ app/src/main/res/values-pt/strings.xml | 548 ++ app/src/main/res/values-ro/strings.xml | 548 ++ app/src/main/res/values-ru/strings.xml | 549 ++ app/src/main/res/values-si/strings.xml | 548 ++ app/src/main/res/values-sk/strings.xml | 548 ++ app/src/main/res/values-sl/strings.xml | 548 ++ app/src/main/res/values-sr-rCS/strings.xml | 548 ++ app/src/main/res/values-sr/strings.xml | 548 ++ app/src/main/res/values-sv/strings.xml | 548 ++ app/src/main/res/values-ta/strings.xml | 548 ++ app/src/main/res/values-th/strings.xml | 548 ++ app/src/main/res/values-tr/strings.xml | 548 ++ app/src/main/res/values-uk/strings.xml | 548 ++ app/src/main/res/values-ur-rPK/strings.xml | 548 ++ app/src/main/res/values-v26/bools.xml | 4 + app/src/main/res/values-vi/strings.xml | 548 ++ app/src/main/res/values-w820dp/dimens.xml | 6 + app/src/main/res/values-w820dp/strings.xml | 4 + app/src/main/res/values-zh-rCN/strings.xml | 549 ++ app/src/main/res/values-zh-rTW/strings.xml | 548 ++ app/src/main/res/values-zh/strings.xml | 545 ++ app/src/main/res/values/arrays.xml | 220 + app/src/main/res/values/attrs.xml | 8 + app/src/main/res/values/bools.xml | 4 + app/src/main/res/values/colors.xml | 32 + app/src/main/res/values/dimens.xml | 12 + .../res/values/ic_launcher_background.xml | 4 + .../values/ic_launcher_background_donate.xml | 4 + app/src/main/res/values/id.xml | 27 + app/src/main/res/values/module_scopes.xml | 8 + app/src/main/res/values/strings.xml | 669 +++ .../main/res/values/strings_platform-en.xml | 34 + app/src/main/res/values/styles.xml | 110 + app/src/main/res/xml/device_admin.xml | 4 + .../main/res/xml/experimental_preferences.xml | 43 + app/src/main/res/xml/language_preferences.xml | 34 + app/src/main/res/xml/log_preferences.xml | 65 + app/src/main/res/xml/onoff_widget.xml | 9 + app/src/main/res/xml/preferences_headers.xml | 49 + app/src/main/res/xml/profiles_preferences.xml | 45 + app/src/main/res/xml/rules_preferences.xml | 115 + app/src/main/res/xml/security_preferences.xml | 39 + app/src/main/res/xml/shortcuts.xml | 44 + app/src/main/res/xml/theme_preference.xml | 37 + app/src/main/res/xml/toggle_widget.xml | 9 + app/src/main/res/xml/toggle_widget_old.xml | 9 + .../main/res/xml/ui_custom_preferences.xml | 21 + app/src/main/res/xml/ui_preferences.xml | 72 + app/src/main/res/xml/widget_preferences.xml | 20 + binaries/busybox_arm | Bin 0 -> 1828564 bytes binaries/busybox_arm64 | Bin 0 -> 2295680 bytes binaries/busybox_x86 | Bin 0 -> 2212344 bytes binaries/ip6tables_arm | Bin 0 -> 395256 bytes binaries/ip6tables_arm64 | Bin 0 -> 511736 bytes binaries/ip6tables_x86 | Bin 0 -> 486464 bytes binaries/iptables_arm | Bin 0 -> 395256 bytes binaries/iptables_arm64 | Bin 0 -> 511736 bytes binaries/iptables_x86 | Bin 0 -> 486464 bytes binaries/nflog_arm | Bin 0 -> 373960 bytes binaries/nflog_arm64 | Bin 0 -> 597936 bytes binaries/nflog_x86 | Bin 0 -> 721768 bytes build.gradle | 28 + external/.gitignore | 4 + external/Android.mk | 10 + external/Makefile | 161 + external/dist/busybox-config | 1077 ++++ external/dist/busybox-config_womount | 1069 ++++ external/dist/busybox-defconfig | 1010 ++++ .../busybox-patches/000-customMakefile.patch | 24 + .../busybox-patches/000-gcc-version.patch | 21 + .../dist/busybox-patches/001-mconf+lkc.patch | 26 + .../dist/busybox-patches/002-checklist.patch | 23 + .../003-a-mount-umount-fsck-df.patch | 323 ++ .../003-b-platform-mntent_r.patch | 38 + .../busybox-patches/004-telnet-telnetd.patch | 336 ++ .../dist/busybox-patches/005-rfkill.patch | 127 + external/dist/busybox-patches/006-eject.patch | 529 ++ .../busybox-patches/007-ifconfig_slip.patch | 82 + .../dist/busybox-patches/008-ifenslave.patch | 1096 ++++ .../dist/busybox-patches/009-watchdog.patch | 75 + external/dist/busybox-patches/010-ubiX.patch | 432 ++ .../dist/busybox-patches/011-ifplugd.patch | 171 + .../012-a-ether_ntoa-udhcpd.patch | 251 + .../dist/busybox-patches/012-b-arping.patch | 27 + .../busybox-patches/012-c-ether-wake.patch | 36 + .../013-loadfont-setfont-conspy.patch | 54 + .../014-nandwrite-nanddump.patch | 338 ++ external/dist/busybox-patches/015-zcip.patch | 108 + .../016-a-swaponoff-syscalls.patch | 32 + .../016-b-swapon-swapoff.patch | 32 + .../017-a-shmget-msgget-semget-syscalls.patch | 38 + .../017-b-msgctl-shmctl-syscalls.patch | 35 + .../017-c-semctl-syscall.patch | 83 + .../busybox-patches/017-d-ipcs-ipcrm.patch | 64 + .../017-e-semop-shmdt-syscalls.patch | 45 + .../017-f-syslogd-logread.patch | 204 + .../dist/busybox-patches/018-blkdiscard.patch | 29 + .../019-fsck.minix-mkfs.minix.patch | 46 + .../01_missing_interface_x86_patch.patch | 67 + .../dist/busybox-patches/020-microcom.patch | 187 + .../021-ipneigh-iproute-iprule.patch | 1249 +++++ external/dist/busybox-patches/022-ipv6.patch | 114 + external/dist/busybox-patches/024-hush.patch | 970 ++++ .../dist/busybox-patches/025-readahead.patch | 32 + .../026-modinfo-modprobe-without-utsrel.patch | 151 + .../027-depmod-parameter.patch | 31 + .../050-dietlibc_resolver-nslookup.patch | 1243 +++++ .../busybox-patches/051-ash-history.patch | 87 + ...s-correctly-reference-generated-file.patch | 33 + ...Fix-socklen_t-type-mismatch-on-Andro.patch | 45 + ...id-Don-t-include-conflicting-headers.patch | 32 + ...acklist-TCPOPTSTRIP-on-systems-that-.patch | 36 + ...es-to-talk-to-xt_IDLETIMER-version-1.patch | 113 + .../0006-ignore-SIGPIPES.patch | 39 + ...atest-libxt_quota2-code-from-AOSP-4..patch | 252 + ...ork-around-broken-Bionic-getaddrinfo.patch | 33 + ...-Use-consistent-exit-code-for-EAGAIN.patch | 33 + .../dist/old-pathches/busybox-022-ipv6.patch | 114 + external/nflog/Android.mk | 10 + external/nflog/attr.c | 753 +++ external/nflog/callback.c | 161 + external/nflog/config.h | 69 + external/nflog/internal.h | 12 + external/nflog/libmnl/libmnl.h | 203 + .../nflog/linux/netfilter/nfnetlink_log.h | 99 + external/nflog/linux/netlink.h | 148 + external/nflog/nflog.c | 672 +++ external/nflog/nlmsg.c | 573 +++ external/nflog/socket.c | 304 ++ external/run_pie/Android.mk | 9 + external/run_pie/run_pie.c | 56 + gradle.properties | 5 + gradle/gradle.iml | 13 + gradle/local.properties | 11 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 + gradlew.bat | 90 + playstore/feature.png | Bin 0 -> 79749 bytes playstore/playstore.png | Bin 0 -> 13126 bytes playstore/promo-graphic.png | Bin 0 -> 11359 bytes playstore/screenshots/Main_2.0.png | Bin 0 -> 288613 bytes playstore/screenshots/Settings_2.0.png | Bin 0 -> 200357 bytes scripts/convert.properties | 45 + scripts/convert.sh | 15 + scripts/make | 2 + scripts/makeclean | 2 + scripts/reverse.sh | 11 + settings.gradle | 1 + 548 files changed, 93129 insertions(+), 2 deletions(-) create mode 100644 Android.mk create mode 100644 Changelog.md create mode 100644 LICENSE create mode 100644 app/build.gradle create mode 100644 app/lint.xml create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/rules.json create mode 100644 app/src/main/assets/xposed_init create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/ic_launcher_donate-web.png create mode 100755 app/src/main/java/com/stericson/rootshell/NativeJavaClass.java create mode 100644 app/src/main/java/com/stericson/rootshell/RootShell.java create mode 100755 app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java create mode 100644 app/src/main/java/com/stericson/rootshell/containers/RootClass.java create mode 100644 app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java create mode 100644 app/src/main/java/com/stericson/rootshell/execution/Command.java create mode 100644 app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java create mode 100644 app/src/main/java/com/stericson/rootshell/execution/Shell.java create mode 100644 app/src/main/java/com/stericson/roottools/Constants.java create mode 100644 app/src/main/java/com/stericson/roottools/RootTools.java create mode 100644 app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java create mode 100644 app/src/main/java/com/stericson/roottools/containers/Mount.java create mode 100644 app/src/main/java/com/stericson/roottools/containers/Permissions.java create mode 100644 app/src/main/java/com/stericson/roottools/containers/Symlink.java create mode 100644 app/src/main/java/com/stericson/roottools/internal/Installer.java create mode 100644 app/src/main/java/com/stericson/roottools/internal/InternalVariables.java create mode 100644 app/src/main/java/com/stericson/roottools/internal/Remounter.java create mode 100644 app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java create mode 100644 app/src/main/java/com/stericson/roottools/internal/Runner.java create mode 100644 app/src/main/java/com/twofortyfouram/locale/BreadCrumber.java create mode 100644 app/src/main/java/com/twofortyfouram/locale/Constants.java create mode 100644 app/src/main/java/com/twofortyfouram/locale/Intent.java create mode 100644 app/src/main/java/com/twofortyfouram/locale/PackageUtilities.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/Api.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/InterfaceDetails.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/MainActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/AppDetailActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/BaseActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/CustomRulesActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/CustomScriptActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/DataDumpActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/HelpActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/LogActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/LogDetailActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/OldLogActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/ProfileActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/RulesActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/activity/StartActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/admin/AdminDeviceReceiver.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/broadcast/ConnectivityChangeReceiver.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/broadcast/OnBootReceiver.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/broadcast/PackageBroadcast.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRule.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRuleDatabase.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/events/LogChangeEvent.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/events/LogEvent.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/events/RulesEvent.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/events/RxCommandEvent.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/events/RxEvent.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/Log.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/LogData.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/LogDatabase.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/LogDetailRecyclerViewAdapter.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/LogPreference.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/LogPreferenceDB.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/LogRecyclerViewAdapter.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/RecyclerItemClickListener.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/log/ShellCommand.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/plugin/BundleScrubber.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/plugin/FireReceiver.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/plugin/LocaleEdit.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/CustomBinaryPreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPref.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPrefDB.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/ExpPreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/LanguagePreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/LogPreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/MultiProfilePreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/RulesPreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/ShareContract.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreference.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreferenceProvider.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/ShareProfilePreference.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/ShareUtils.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/ThemePreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/UIPreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/preferences/WidgetPreferenceFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileAdapter.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileData.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileHelper.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/profiles/ProfilesDatabase.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/service/FirewallService.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/service/LogService.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/service/RootCommand.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/service/RootShellService.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/service/RootShellService2.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/service/RulesApplyService.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/service/ToggleTileService.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/ui/about/AboutFragment.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/AppIconHelperV26.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/BiometricUtil.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/CustomRuleOld.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/DataUsageParser.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/DateComparator.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/FileDialog.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/G.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/HtmlTagHandler.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/ImportApi.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/InputValidator.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/JsonHelper.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/LocaleManager.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/LogNetUtil.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/Rule.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/SecureCrypto.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/SecurePasswordManager.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabLayout.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabStrip.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/UidCorrelator.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/UidResolver.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/util/XPreferenceProvider.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/widget/RadialMenuWidget.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/widget/StatusWidget.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidget.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOld.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java create mode 100644 app/src/main/java/dev/ukanth/ufirewall/xposed/XposedInit.java create mode 100644 app/src/main/java/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java create mode 100644 app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java create mode 100644 app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPanelView.java create mode 100644 app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java create mode 100644 app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java create mode 100644 app/src/main/res/anim/widget_pulse.xml create mode 100644 app/src/main/res/anim/widget_scale_in.xml create mode 100755 app/src/main/res/convert.properties create mode 100755 app/src/main/res/convert.sh create mode 100644 app/src/main/res/drawable-hdpi-v11/active.png create mode 100644 app/src/main/res/drawable-hdpi-v11/error.png create mode 100644 app/src/main/res/drawable-hdpi-v11/question.png create mode 100644 app/src/main/res/drawable-hdpi/ic_apply.png create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher_free.png create mode 100644 app/src/main/res/drawable-hdpi/notification.png create mode 100644 app/src/main/res/drawable-hdpi/notification_error.png create mode 100644 app/src/main/res/drawable-hdpi/notification_quest.png create mode 100644 app/src/main/res/drawable-hdpi/notification_warn.png create mode 100644 app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_dontsave.png create mode 100644 app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_help.png create mode 100644 app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_save.png create mode 100644 app/src/main/res/drawable-mdpi-v11/active.png create mode 100644 app/src/main/res/drawable-mdpi-v11/error.png create mode 100644 app/src/main/res/drawable-mdpi-v11/question.png create mode 100644 app/src/main/res/drawable-mdpi/ic_apply.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher_free.png create mode 100644 app/src/main/res/drawable-mdpi/notification.png create mode 100644 app/src/main/res/drawable-mdpi/notification_error.png create mode 100644 app/src/main/res/drawable-mdpi/notification_quest.png create mode 100644 app/src/main/res/drawable-mdpi/notification_warn.png create mode 100644 app/src/main/res/drawable-v21/ic_android_white_24dp.xml create mode 100644 app/src/main/res/drawable-xhdpi-v11/active.png create mode 100644 app/src/main/res/drawable-xhdpi-v11/error.png create mode 100644 app/src/main/res/drawable-xhdpi-v11/question.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_apply.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher_free.png create mode 100644 app/src/main/res/drawable-xhdpi/notification.png create mode 100644 app/src/main/res/drawable-xhdpi/notification_error.png create mode 100644 app/src/main/res/drawable-xhdpi/notification_quest.png create mode 100644 app/src/main/res/drawable-xhdpi/notification_warn.png create mode 100644 app/src/main/res/drawable-xxhdpi-v11/active.png create mode 100644 app/src/main/res/drawable-xxhdpi-v11/error.png create mode 100644 app/src/main/res/drawable-xxhdpi-v11/question.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_apply.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher_free.png create mode 100644 app/src/main/res/drawable-xxhdpi/notification.png create mode 100644 app/src/main/res/drawable-xxhdpi/notification_error.png create mode 100644 app/src/main/res/drawable-xxhdpi/notification_quest.png create mode 100644 app/src/main/res/drawable-xxhdpi/notification_warn.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_apply.png create mode 100644 app/src/main/res/drawable/card_border.xml create mode 100644 app/src/main/res/drawable/circle_background.xml create mode 100644 app/src/main/res/drawable/ic_action.xml create mode 100644 app/src/main/res/drawable/ic_action_fingerprint.xml create mode 100644 app/src/main/res/drawable/ic_allow.xml create mode 100644 app/src/main/res/drawable/ic_android_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_apply_menu.xml create mode 100644 app/src/main/res/drawable/ic_apply_notification.xml create mode 100644 app/src/main/res/drawable/ic_block_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_bluetooth.xml create mode 100644 app/src/main/res/drawable/ic_clean.xml create mode 100644 app/src/main/res/drawable/ic_clearlog.xml create mode 100644 app/src/main/res/drawable/ic_clone.xml create mode 100644 app/src/main/res/drawable/ic_copy.xml create mode 100644 app/src/main/res/drawable/ic_custom_rules.xml create mode 100644 app/src/main/res/drawable/ic_deny.xml create mode 100644 app/src/main/res/drawable/ic_exit.xml create mode 100644 app/src/main/res/drawable/ic_export.xml create mode 100644 app/src/main/res/drawable/ic_flow.xml create mode 100644 app/src/main/res/drawable/ic_help.xml create mode 100644 app/src/main/res/drawable/ic_import.xml create mode 100644 app/src/main/res/drawable/ic_invert.xml create mode 100644 app/src/main/res/drawable/ic_lan.xml create mode 100644 app/src/main/res/drawable/ic_legend.xml create mode 100644 app/src/main/res/drawable/ic_log.xml create mode 100644 app/src/main/res/drawable/ic_mail.xml create mode 100644 app/src/main/res/drawable/ic_menu_binary.xml create mode 100644 app/src/main/res/drawable/ic_menu_exp.xml create mode 100644 app/src/main/res/drawable/ic_menu_pref.xml create mode 100644 app/src/main/res/drawable/ic_menu_profile.xml create mode 100644 app/src/main/res/drawable/ic_menu_save.xml create mode 100644 app/src/main/res/drawable/ic_menu_secure.xml create mode 100644 app/src/main/res/drawable/ic_menu_translate.xml create mode 100644 app/src/main/res/drawable/ic_menu_widget.xml create mode 100644 app/src/main/res/drawable/ic_mobiledata.xml create mode 100644 app/src/main/res/drawable/ic_notifications_off_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_notifications_on_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_open_in_new_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_preference.xml create mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/drawable/ic_roam.xml create mode 100644 app/src/main/res/drawable/ic_rules.xml create mode 100644 app/src/main/res/drawable/ic_script.xml create mode 100644 app/src/main/res/drawable/ic_search.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_sort.xml create mode 100644 app/src/main/res/drawable/ic_tether.xml create mode 100644 app/src/main/res/drawable/ic_theme.xml create mode 100644 app/src/main/res/drawable/ic_tor.xml create mode 100644 app/src/main/res/drawable/ic_unknown.xml create mode 100644 app/src/main/res/drawable/ic_vpn.xml create mode 100644 app/src/main/res/drawable/ic_wifi.xml create mode 100644 app/src/main/res/drawable/list_divider.xml create mode 100644 app/src/main/res/drawable/plus.xml create mode 100644 app/src/main/res/drawable/preview_new.png create mode 100644 app/src/main/res/drawable/preview_old.png create mode 100644 app/src/main/res/drawable/preview_toggle.png create mode 100644 app/src/main/res/drawable/question_widget.png create mode 100644 app/src/main/res/drawable/text_background_rounded.xml create mode 100644 app/src/main/res/drawable/toast.xml create mode 100644 app/src/main/res/drawable/twofortyfouram_locale_ic_menu_dontsave.xml create mode 100644 app/src/main/res/drawable/twofortyfouram_locale_ic_menu_help.xml create mode 100644 app/src/main/res/drawable/twofortyfouram_locale_ic_menu_save.xml create mode 100644 app/src/main/res/drawable/widget_bg.xml create mode 100644 app/src/main/res/drawable/widget_bg_focus.png create mode 100644 app/src/main/res/drawable/widget_bg_pressed.png create mode 100644 app/src/main/res/drawable/widget_circle_enable.xml create mode 100644 app/src/main/res/drawable/widget_disabling.xml create mode 100644 app/src/main/res/drawable/widget_enabling.xml create mode 100644 app/src/main/res/drawable/widget_error.xml create mode 100644 app/src/main/res/drawable/widget_off.png create mode 100644 app/src/main/res/drawable/widget_on.png create mode 100644 app/src/main/res/drawable/widget_success.xml create mode 100644 app/src/main/res/drawable/zoomin.xml create mode 100644 app/src/main/res/drawable/zoomout.xml create mode 100644 app/src/main/res/layout-land/dialog_color_picker.xml create mode 100644 app/src/main/res/layout/activity_custom_rules.xml create mode 100644 app/src/main/res/layout/activity_prefs.xml create mode 100644 app/src/main/res/layout/app_detail.xml create mode 100644 app/src/main/res/layout/apply_view.xml create mode 100644 app/src/main/res/layout/custom_toast.xml create mode 100644 app/src/main/res/layout/customscript.xml create mode 100644 app/src/main/res/layout/dialog_color_picker.xml create mode 100755 app/src/main/res/layout/file_row.xml create mode 100644 app/src/main/res/layout/fingerprint.xml create mode 100644 app/src/main/res/layout/help_about.xml create mode 100644 app/src/main/res/layout/help_about_content.xml create mode 100644 app/src/main/res/layout/help_legend_section.xml create mode 100644 app/src/main/res/layout/legend.xml create mode 100644 app/src/main/res/layout/log_detail_summary.xml create mode 100644 app/src/main/res/layout/log_recycle_item.xml create mode 100644 app/src/main/res/layout/log_view.xml create mode 100644 app/src/main/res/layout/logdetail_recycle_item.xml create mode 100644 app/src/main/res/layout/logdetail_view.xml create mode 100644 app/src/main/res/layout/main.xml create mode 100644 app/src/main/res/layout/main_list.xml create mode 100644 app/src/main/res/layout/main_list_old.xml create mode 100644 app/src/main/res/layout/main_old.xml create mode 100644 app/src/main/res/layout/onoff_widget.xml create mode 100644 app/src/main/res/layout/profile_adddialog.xml create mode 100644 app/src/main/res/layout/profile_layout.xml create mode 100644 app/src/main/res/layout/profile_main.xml create mode 100644 app/src/main/res/layout/rules.xml create mode 100644 app/src/main/res/layout/rules_modern.xml create mode 100644 app/src/main/res/layout/searchbar.xml create mode 100644 app/src/main/res/layout/tasker_profile.xml create mode 100644 app/src/main/res/layout/toggle_widget_layout.xml create mode 100644 app/src/main/res/layout/toggle_widget_old_layout.xml create mode 100644 app/src/main/res/layout/toggle_widget_old_view.xml create mode 100644 app/src/main/res/layout/toggle_widget_view.xml create mode 100644 app/src/main/res/menu/menu_bar.xml create mode 100644 app/src/main/res/menu/twofortyfouram_locale_help_save_dontsave.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_donate.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_donate_round.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_donate.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_donate_round.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground_donate.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-hdpi/round_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/round_launcher_free.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_donate.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_donate_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground_donate.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/round_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/round_launcher_free.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_donate.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_donate_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground_donate.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/round_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/round_launcher_free.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_donate.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_donate_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground_donate.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/round_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/round_launcher_free.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_donate.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_donate_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground_donate.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/round_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/round_launcher_free.png create mode 100644 app/src/main/res/raw/about.html create mode 100644 app/src/main/res/raw/afwallstart create mode 100755 app/src/main/res/raw/busybox_arm create mode 100755 app/src/main/res/raw/busybox_arm64 create mode 100755 app/src/main/res/raw/busybox_x86 create mode 100644 app/src/main/res/raw/faq.html create mode 100755 app/src/main/res/raw/ip6tables_arm create mode 100755 app/src/main/res/raw/ip6tables_arm64 create mode 100755 app/src/main/res/raw/ip6tables_x86 create mode 100755 app/src/main/res/raw/iptables_arm create mode 100755 app/src/main/res/raw/iptables_arm64 create mode 100755 app/src/main/res/raw/iptables_x86 create mode 100755 app/src/main/res/raw/nflog_arm create mode 100755 app/src/main/res/raw/nflog_arm64 create mode 100755 app/src/main/res/raw/nflog_x86 create mode 100644 app/src/main/res/values-af/strings.xml create mode 100644 app/src/main/res/values-ar/strings.xml create mode 100644 app/src/main/res/values-ast-rES/strings.xml create mode 100644 app/src/main/res/values-az/strings.xml create mode 100644 app/src/main/res/values-bg/strings.xml create mode 100644 app/src/main/res/values-bi/strings.xml create mode 100644 app/src/main/res/values-bn/strings.xml create mode 100644 app/src/main/res/values-bs/strings.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-da/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-el/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-eu/strings.xml create mode 100644 app/src/main/res/values-fa/strings.xml create mode 100644 app/src/main/res/values-fi/strings.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-he/strings.xml create mode 100644 app/src/main/res/values-hi/strings.xml create mode 100644 app/src/main/res/values-hr/strings.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-in/strings.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-kn/strings.xml create mode 100644 app/src/main/res/values-ko/strings.xml create mode 100644 app/src/main/res/values-ku/strings.xml create mode 100644 app/src/main/res/values-ky/strings.xml create mode 100644 app/src/main/res/values-ml-rIN/strings.xml create mode 100644 app/src/main/res/values-ms/strings.xml create mode 100644 app/src/main/res/values-nb/strings.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-pl/strings.xml create mode 100644 app/src/main/res/values-pt-rBR/strings.xml create mode 100644 app/src/main/res/values-pt/strings.xml create mode 100644 app/src/main/res/values-ro/strings.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-si/strings.xml create mode 100644 app/src/main/res/values-sk/strings.xml create mode 100644 app/src/main/res/values-sl/strings.xml create mode 100644 app/src/main/res/values-sr-rCS/strings.xml create mode 100644 app/src/main/res/values-sr/strings.xml create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 app/src/main/res/values-ta/strings.xml create mode 100644 app/src/main/res/values-th/strings.xml create mode 100644 app/src/main/res/values-tr/strings.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-ur-rPK/strings.xml create mode 100644 app/src/main/res/values-v26/bools.xml create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values-w820dp/strings.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values-zh/strings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/bools.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/ic_launcher_background_donate.xml create mode 100644 app/src/main/res/values/id.xml create mode 100644 app/src/main/res/values/module_scopes.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/strings_platform-en.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/device_admin.xml create mode 100644 app/src/main/res/xml/experimental_preferences.xml create mode 100644 app/src/main/res/xml/language_preferences.xml create mode 100644 app/src/main/res/xml/log_preferences.xml create mode 100644 app/src/main/res/xml/onoff_widget.xml create mode 100644 app/src/main/res/xml/preferences_headers.xml create mode 100644 app/src/main/res/xml/profiles_preferences.xml create mode 100644 app/src/main/res/xml/rules_preferences.xml create mode 100644 app/src/main/res/xml/security_preferences.xml create mode 100644 app/src/main/res/xml/shortcuts.xml create mode 100644 app/src/main/res/xml/theme_preference.xml create mode 100644 app/src/main/res/xml/toggle_widget.xml create mode 100644 app/src/main/res/xml/toggle_widget_old.xml create mode 100644 app/src/main/res/xml/ui_custom_preferences.xml create mode 100644 app/src/main/res/xml/ui_preferences.xml create mode 100644 app/src/main/res/xml/widget_preferences.xml create mode 100644 binaries/busybox_arm create mode 100644 binaries/busybox_arm64 create mode 100644 binaries/busybox_x86 create mode 100644 binaries/ip6tables_arm create mode 100644 binaries/ip6tables_arm64 create mode 100644 binaries/ip6tables_x86 create mode 100644 binaries/iptables_arm create mode 100644 binaries/iptables_arm64 create mode 100644 binaries/iptables_x86 create mode 100644 binaries/nflog_arm create mode 100644 binaries/nflog_arm64 create mode 100644 binaries/nflog_x86 create mode 100644 build.gradle create mode 100644 external/.gitignore create mode 100644 external/Android.mk create mode 100644 external/Makefile create mode 100644 external/dist/busybox-config create mode 100644 external/dist/busybox-config_womount create mode 100644 external/dist/busybox-defconfig create mode 100644 external/dist/busybox-patches/000-customMakefile.patch create mode 100644 external/dist/busybox-patches/000-gcc-version.patch create mode 100644 external/dist/busybox-patches/001-mconf+lkc.patch create mode 100644 external/dist/busybox-patches/002-checklist.patch create mode 100644 external/dist/busybox-patches/003-a-mount-umount-fsck-df.patch create mode 100644 external/dist/busybox-patches/003-b-platform-mntent_r.patch create mode 100644 external/dist/busybox-patches/004-telnet-telnetd.patch create mode 100644 external/dist/busybox-patches/005-rfkill.patch create mode 100644 external/dist/busybox-patches/006-eject.patch create mode 100644 external/dist/busybox-patches/007-ifconfig_slip.patch create mode 100644 external/dist/busybox-patches/008-ifenslave.patch create mode 100644 external/dist/busybox-patches/009-watchdog.patch create mode 100644 external/dist/busybox-patches/010-ubiX.patch create mode 100644 external/dist/busybox-patches/011-ifplugd.patch create mode 100644 external/dist/busybox-patches/012-a-ether_ntoa-udhcpd.patch create mode 100644 external/dist/busybox-patches/012-b-arping.patch create mode 100644 external/dist/busybox-patches/012-c-ether-wake.patch create mode 100644 external/dist/busybox-patches/013-loadfont-setfont-conspy.patch create mode 100644 external/dist/busybox-patches/014-nandwrite-nanddump.patch create mode 100644 external/dist/busybox-patches/015-zcip.patch create mode 100644 external/dist/busybox-patches/016-a-swaponoff-syscalls.patch create mode 100644 external/dist/busybox-patches/016-b-swapon-swapoff.patch create mode 100644 external/dist/busybox-patches/017-a-shmget-msgget-semget-syscalls.patch create mode 100644 external/dist/busybox-patches/017-b-msgctl-shmctl-syscalls.patch create mode 100644 external/dist/busybox-patches/017-c-semctl-syscall.patch create mode 100644 external/dist/busybox-patches/017-d-ipcs-ipcrm.patch create mode 100644 external/dist/busybox-patches/017-e-semop-shmdt-syscalls.patch create mode 100644 external/dist/busybox-patches/017-f-syslogd-logread.patch create mode 100644 external/dist/busybox-patches/018-blkdiscard.patch create mode 100644 external/dist/busybox-patches/019-fsck.minix-mkfs.minix.patch create mode 100644 external/dist/busybox-patches/01_missing_interface_x86_patch.patch create mode 100644 external/dist/busybox-patches/020-microcom.patch create mode 100644 external/dist/busybox-patches/021-ipneigh-iproute-iprule.patch create mode 100644 external/dist/busybox-patches/022-ipv6.patch create mode 100644 external/dist/busybox-patches/024-hush.patch create mode 100644 external/dist/busybox-patches/025-readahead.patch create mode 100644 external/dist/busybox-patches/026-modinfo-modprobe-without-utsrel.patch create mode 100644 external/dist/busybox-patches/027-depmod-parameter.patch create mode 100644 external/dist/busybox-patches/050-dietlibc_resolver-nslookup.patch create mode 100644 external/dist/busybox-patches/051-ash-history.patch create mode 100644 external/dist/iptables-patches/0001-iptables-correctly-reference-generated-file.patch create mode 100644 external/dist/iptables-patches/0002-android-libiptc-Fix-socklen_t-type-mismatch-on-Andro.patch create mode 100644 external/dist/iptables-patches/0003-android-Don-t-include-conflicting-headers.patch create mode 100644 external/dist/iptables-patches/0004-android-build-Blacklist-TCPOPTSTRIP-on-systems-that-.patch create mode 100644 external/dist/iptables-patches/0005-Modify-iptables-to-talk-to-xt_IDLETIMER-version-1.patch create mode 100644 external/dist/iptables-patches/0006-ignore-SIGPIPES.patch create mode 100644 external/dist/iptables-patches/0007-android-Import-latest-libxt_quota2-code-from-AOSP-4..patch create mode 100644 external/dist/iptables-patches/0008-android-Work-around-broken-Bionic-getaddrinfo.patch create mode 100644 external/dist/iptables-patches/0009-ip6tables-Use-consistent-exit-code-for-EAGAIN.patch create mode 100644 external/dist/old-pathches/busybox-022-ipv6.patch create mode 100644 external/nflog/Android.mk create mode 100644 external/nflog/attr.c create mode 100644 external/nflog/callback.c create mode 100644 external/nflog/config.h create mode 100644 external/nflog/internal.h create mode 100644 external/nflog/libmnl/libmnl.h create mode 100644 external/nflog/linux/netfilter/nfnetlink_log.h create mode 100644 external/nflog/linux/netlink.h create mode 100644 external/nflog/nflog.c create mode 100644 external/nflog/nlmsg.c create mode 100644 external/nflog/socket.c create mode 100644 external/run_pie/Android.mk create mode 100644 external/run_pie/run_pie.c create mode 100644 gradle.properties create mode 100755 gradle/gradle.iml create mode 100755 gradle/local.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 playstore/feature.png create mode 100644 playstore/playstore.png create mode 100644 playstore/promo-graphic.png create mode 100644 playstore/screenshots/Main_2.0.png create mode 100644 playstore/screenshots/Settings_2.0.png create mode 100644 scripts/convert.properties create mode 100755 scripts/convert.sh create mode 100755 scripts/make create mode 100755 scripts/makeclean create mode 100755 scripts/reverse.sh create mode 100644 settings.gradle diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..5ec5201 --- /dev/null +++ b/Android.mk @@ -0,0 +1,42 @@ +#/** +# * Contains shared programming interfaces. +# * All iptables "communication" is handled by this class. +# * +# * Copyright (C) 2007-2008 The Android Open Source Project +# * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro +# * Copyright (C) 2011-2012 Umakanthan Chandran +# * +# * This program is free software: you can redistribute it and/or modify +# * it under the terms of the GNU General Public License as published by +# * the Free Software Foundation, either version 3 of the License, or +# * (at your option) any later version. +# * +# * This program is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# * GNU General Public License for more details. +# * +# * You should have received a copy of the GNU General Public License +# * along with this program. If not, see . +# * +# * @author Rodrigo Zechin Rosauro, Umakanthan Chandran +# * @version 1.1 +# */ + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := afwall +LOCAL_CERTIFICATE := platform + +# Builds against the public SDK +# LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) + +# This finds and builds the test apk as well, so a single make does both. +include $(call all-makefiles-under,$(LOCAL_PATH)) \ No newline at end of file diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..3e0931c --- /dev/null +++ b/Changelog.md @@ -0,0 +1,922 @@ +AFWall+ Changelog +================== + +AFWall+ v4.0.0 + +🚀 Major Features & Enhancements + +🎯 Rule Management & Stability + +- Fixed critical rule application issues - Resolved iptables command failures and cascade errors +- Improved error handling - Smart selective error handling prevents unnecessary fallbacks +- Enhanced chain management - Better synchronization prevents race conditions +- Owner module compatibility - Automatic detection and fallback for devices without owner + iptables module + +🎨 Material Design Overhaul + +- Modernized UI - Material Design enhancements for rules, help, and custom scripts views +- Revamped help section - Complete redesign with better organization and moved legends +- Visual widget indicators - Added pulse animations and visual feedback for toggle widgets +- Improved layouts - Fixed layout issues for devices with merged status bars + +📊 Enhanced Logging System + +- Better log details view - Enhanced display with allow/deny address information +- Improved UID detection - Better handling of special UIDs (including uid -100) and bug fixes +- Seamless log target switching - Dynamic switching between LOG and NFLOG in preferences +- NFLOG improvements - Better NFLOG and LOG handling with updated binaries + +🔒 Security Enhancements + +- Upgraded encryption - Migrated from DES to AES for better security +- Enhanced security utilities - New SecureCrypto and SecurityUtil classes +- Input validation improvements - Better validation and security checks + +🛠 Platform & Compatibility + +📱 Android Support + +- Android 16 preparation - Updated build configuration for future Android support +- Binary updates - Cross-compiled binaries: busybox v1.36.1, iptables v1.8.10 +- Architecture support - Added ARM64 binaries and improved architecture detection +- GitHub Actions CI - Automated binary builds and improved CI/CD pipeline + +🔧 Bug Fixes & Stability + +- Export/Import fixes - Resolved bugs when handling large numbers of files (#1399, #1401) +- Build error fixes - Fixed app:tint and other build-related issues +- Service leak fixes - Improved thread safety and fixed service connection leaks +- USB tethering support - Added auto-detection and support for USB tethering +- DNS forwarding - Improved DNS handling for tethering scenarios + +🔧 Technical Improvements + +⚡ Performance & Threading + +- Thread safety - Improved synchronization and thread-safe operations +- Better exception handling - More robust error handling and recovery +- Optimized rule processing - Faster and more reliable rule application + +🛠 Developer Experience + +- Code cleanup - Extensive refactoring and code organization improvements +- CI/CD enhancements - Updated GitHub Actions and automated workflows +- Binary management - Automated cross-compilation and binary distribution + +📋 Specific Issue Fixes + +- #1386 - Default chain rules only applied when necessary (with smart revert) +- #1410 - Fallback on default commands when needed +- #1423, #1382 - Various stability and functionality fixes +- #1400 - Layout fixes for merged status bar screens +- #1399 - Export only enabled rule types +- #1169 - Export/import rule improvements + +⚠️ Breaking Changes + +- Security upgrade - DES encryption deprecated in favor of AES +- Removed legacy views - Old unsupported view components removed +- Binary updates - Requires newer binaries for optimal performance + +🙏 Contributors + +- @getgo-nobugs - Syntax fixes and improvements +- @NeroProtagonist - CI/CD updates (upload-artifact@v4) +- @Fry-kun - Multiple fixes: typos, layout improvements, export fixes, ARM64 NFLOG binary (initial version) + +Version 3.6.0 + +* Updated libraries and SDK (33) +* Fixes: + - Chinese language not working + - Added Sinhala language + - libsu memory leak + - Optimizing copying binaries during install + - Log freezing on few devices + - Work profile fix for android 12 +* Add support for recent versions of Android. +* Code optimizations. + + +Version 3.5.3 + +* New: Enable delay when applying rules (Required for Android 11+ on some devices) +* Fixes + Applying rules with ipv6 error + #1101 DNS leak when using external dns client. + #1280 work profile apps not shown on Android 11+ + Bluetooth tether + import/export hanging +* Use libsu for root detection. +* Chinese menu issue. +* Dropped xposed support! will be provided as separate module +* Updated libraries and SDK (31) + +Version 3.5.2 + +* Fix: Fail to Import from older versions. +* IPv6 issues on few devices when enabled. +* Chinese lang issue +* Crash fixes. +* Removed run_pie binaries which are no longer used. + + +Version 3.5.1 +* Feature: Cloning of profiles +* Bug: PrivateDNS changes on boot +* Bug: Log target missing on few scenarios +* Bug: Import/Export rules missing on A11 + + +Version 3.5.0 +Features + - Show installed apps without internet permission + - Default setting for installed app - Donate Only + - Private DNS support + - Better support for Multi Profile/Island/Work Profile - Thanks to @n90p + - Firewall logs engine rewritten with notification support + - Support for android 10 and 11 +Bug Fixes: + - Lots of logs related issues + - Export rules with mode + - Language option issue + - device rotation issue +* Update support tools and build libraries(AndroidX) + +Version 3.4.0 +* Feature: Bluetooth,USB tethering as separate rules - Thanks @nxzero +* Feature: Clone columns (Copy rules from one column to other column) +* Feature: Selectable Log target (LOG/NFLOG) +* Fix: Log related issues +* Fix: Notification related issues/Option removed!! +* Main screen UI update - Thanks @vvimjam +* Crash fixes and performance improvements +* Translation updates. + + +Version 3.4.0-BETA2 +* Main UI update only when more than 4 controls +* Notification related fixes +* UI related fixes. +* Crash fixes + +Version 3.4.0-BETA1 +* Feature: Bluetooth,USB tethering as separate rules - Thanks @nxzero +* Feature: Clone columns (Copy rules from one column to other column) +* Feature: Selectable Log target (LOG/NFLOG) +* Main screen UI update - Thanks @vvimjam +* Crash fixes and performance improvements + +Version 3.3.1 + +* Bug: Firewall not blocking on oneplus devices +* Minor UI fixes + +Version 3.3.0 + +* Bug Fixes: + - MAJOR: Rules not applied properly (v4/v6) + - Theme related bug with logs + - Log related bugs on many devices + - Boot rules are not applied on Pie and above + - Disable/Enable of firewall issue + - Crash on applying rules on fewer devices + - Notification bug on fewer devices + +Version 3.2.0 +* Integrate basic themes (Dark/Light or Black (donate version only!) +* Preferences now showing selected values +* Updated libraries +* Bug Fixes: + - AFWall's Logservice stops after sometime + - Crash on LOS 16 due to permission + - Notification not getting cleared upon opening + - Tasker settings not applied bug due to crash + - Root progress showing on main screen (dismiss button added incase) + - Log service process bug + - Applying dialog issue + - kernel in the logs even whitelisted + - Statusbar notification update on firewall status + - Possible memory leaks in async + - mDNS and CLAT on core apps (Android 10) + - Additional startup leak path for supersu + + +Version 3.1.0 +* Performance: IPv4 & IPv6 rules applying time reduced by half +* IPv6 is enabled by default - Disable if not (under preferences -> rules) +* Fix: Tasker plugin issue after profile migration +* Fix: Widget crashing issue +* Fix: New installed app notification issue +* Fix: Device boot rules issue +* Fix: Duplicate app issue on oneplus devices +* Fix: Tor related bug +* Fix: Xposed module unable to download allowed apps + +Version 3.0.4 +* Fix: Domain names are now been correctly resolved +* Fix: Removed notification dot on all notifications +* Fix: Inbound option caused AFWall+ to disable its functionality +* Fix: Hang issue on log detail when ping/resolve +* Removed: SUPER_USER permission which is not relevant anymore + +Version 3.0.3 + +* Fix: Disable firewall issue +* Fix: Traffic stats always zero in app details +* Rewritten: Filter logic for main screen apps +* Xposed: Plugin wasn't able to read preference + + +Version 3.0.2 +* Fix: Issue with Pixel C devices +* Support for Magisk 18.0 and startup leak +* Fix: Notification sound issue on some devices +* Fix: Duplicate name appears on main screen + + +Version 3.0.1 + +* Fix: Status toggle widget 1x1 +* Fix: Ability to hide ongoing notification (Stop firewall and restart to hide after disable it in preferences) +* Fix: Firewall error notification on oreo and above +* Security: Tile toggle checks for password +* User reported crashes +* Updated translations + +Version 3.0.0 + +Features: +* Better support for nougat/oreo and pie +* Firewall toggle tile +* Adaptive Icons +* Notification channels +* Tor support +Bugs: +* Language selection bug +* Filter selection bug +* Compatible with magisk 17.x +* Better handling of background process +* Drops support for 4.x devices +* Update languages +* Updated libraries + +Version 2.9.9 + +* Support for dual apps (experimental) +* DNS Hostname option on log toast(donate feature) +* Multiple memory leaks across screens +* Block log notification now moved under individual app detail screen +* Enabled log cache for faster load +* Widgets now ask for password if enabled (except status widget) + +Bugs: +* Shortcut open rules & preference screen without password +* fingerprint related issues +* selinux deny for startup script +* User reported crash fixes +* Updated string translations +* Added default system language option +* Log notification name issue + +Version 2.9.8 + +* Option to disable notification when applying rules - Recommended to turn on ! +* Added magisk related information in the error report +* Fixed storage permission on export from rules +* Pixel 2 bug on netfilter error on start +* Rare preference crash on some devices +* Upgraded runtime to Java 8 +* Updated support libraries +* Fixed crashes upon loading +* Fixed user reported bugs +* Removed buggy quick apply -- Sincere Apologies! will add it after testing with various usecases + +Version 2.9.7 + +Features: +* Control default chains for IPv4 & IPv6 (preference) +* Quick apply from main UI - using floating apply button - Donate Version Only +* Showing rules count on apply +* Search using UID +* Improved detection for su binaries +* Ability to choose init.d path +* Removed storage permission from start (used only when export/import) +* Xposed plugin updated to Nougat + +Bug Fixes: +* Added mount applet for busybox to fix mount issue for init.d +* Widgets & tasker toggle issue for profiles +* Possible fix for starup rule +* Library updates +* Out of memory crashes when enabled logging +* Runtime crash fixes due to incompatible libraries +* Lots of minor bug fixes and underhood changes to introduce new features + + +Version 2.9.6.1 + +* Removed BIND_ADMIN permission and related device admin feature as per Google - Will be introduced after sometime +* ANR issues due to busybox detection +* Minor UI changes in tasker plugin +* Tasker plugin related bugs related to disable/enable firewall +* User reported crash fixes + + +Version 2.9.6 +* LAN/Data leak issues on 7.x +* Application list refresh issue +* Minor fix in startupscript - Thanks Peake +* Updated support libraries +* Reported crash fixes +* Updated translations + +Version 2.9.5 +* Mobile data support for newer devices +* Custom interval for startup delay +* Custom interval for ping timeout +* Low priority notification +* Fixed issue with import preference crashes Log/Experimental preference +* User reported crashes + +Version 2.9.4 +* Fix log notification stops after sometime +* Fix crash on log preferences. +* mDNS notification can be turned-off +* Bug in notification filterering +* Added back x86/mips support for built-in binaries +* Minor UI change for UID +* Updated support Libraries +* Updated translations + +Version 2.9.3 [24 Feb, 2017] +* Fix crash on log preferences. +* Traffic stats always shown empty. +* Zoom icons. + +Version 2.9.2 [22 Feb, 2017] +Features +* Log- Network options-Donate(TX @vzool) +* Fingerprint support (TX @vzool) +* Preserve Zoom size(+/-) +* Profiles-delete/rename profiles(+) +* Hardware search key + +Bugs +* LAN issue on Nougat +* Boot rules issue +* Logservice start issue +* Notification glitch on profile switch +* Log notification filter button not showing +* Optimizing logTarget detection +* Removed toybox check due to ANR + + etc. +Misc +* Updated: Busybox to latest version +* Removed: x86 & mips support +* Updated libraries +* Translation: Small updates + +Version 2.9.1 [Nov 27, 2016] +* Boot/Connectivity change rules hung on some devices +* Widget always display errors +* Optimized number of su calls +* Log Service does not work on some devices (Reboot or enable/disable logservice after install) +* Update material dialog library +* Updated translations + +Version 2.9.0 [Nov 25, 2016] +* Bug: Sometimes connection gets blocked +* Bug: Sometimes logservice does not work after service terminated +* Bug: Hang issue with phh super user +* Bug: Applying rules dialog issue +* Bug: Old logview clear issue +* Ground work on notification filter +* Code cleanup and removed older API related reference +* Accessibility improvements +* Feature: Block IPv6 - block all default chains of IPv6 to prevent leak +* Feature: Preference enhancements related to rules +* Feature: Application Shortcuts - Nougat 7.1 +* User reported crashes +* Updated Translations + +Version 2.8.0 [Nov 8, 2016] +* Bug: Boot rules are not working on some devices +* Bug: Logs without active rules issue +* Bug: Rules are blocked completely on some devices +* Bug: Removed notification while applying rules +* Feature: New UI menu icons to support themes +* Upgraded support library to API 25 +* User Reported crash fixes +* Updated Translations + +Version 2.7.0 [Nov 1, 2016] +* Bug: Random hang issue with various superuser (phh,cm root) +* Bug: Added option to add delay applying rules on startup +* Bug: Minor fixes on preferences on change +* Bug: Fix for newly installed app on top on few scenarios +* Bug: Startup script related bugs +* Bug: Possible tether issue from last version +* Request: Extra space on notification text! It took lot of time. +* Fix: User reported crash fixes +* Updated: Translations + +Version 2.6.0.1 [Oct 1, 2016] +* Bug: Frequent crash while logservice enabled +* Bug: Persistent notification not shown after reboot +* Translation updates + +Version 2.6.0 [Sep 28, 2016] +* CRITICAL: Connection leak when LAN option is enabled +* CRITICAL: Multiple su process (su leak) when logservice enabled +* Feature: Nougat Support +* Feature: Split screen of Activity (Nougat+) +* Feature: Ability is use device pin/password/+ for app using Android API (Donate) +* Feature: Ability to fetch logs using busybox/toybox/system +* Feature: Improve auto apply rules for specific preference changes +* Using NDK10 for building binary +* UI: Log toast with warning icon +* Bug: Improved log storing logic to avoid cpu/battery/hang/crash issues +* Bug: Connectivity change hang notification issue +* Bug: Xposed related and user reported fixes +* Reduced overall APK size + + +Version 2.5.2 [Aug 14, 2016] +* New Feature: Auto-trim log database +* Enhancement: Logs should load much faster +* Change: Removed lock screen notification from Xposed module, since it was draining battery +* Bug Fix: Fix switch log view bug +* Bug Fix: Fix crash on export and import +* Update: Translation updates + +Version 2.5.1 [Aug 10, 2016] + +* Enhancement: Added Switchback option to Old Log view - Listen to users +* New Feature (Pro): Donate / Donate Key users can view the ipaddress/src/dst in details view (clicking on from new logview) +* Bug fix: Fixed iptables entries for uninstalled apps not resetting + +Version 2.5.0 [Aug 9, 2016] +* Features + - Xposed module - Download manager leak with notification - Application can bypaas AFWall+ by using Download Manager API to download from network. This module helps to block applications from using this API to get around not being allowed to access the internet with AFWall+ + - New Log UI with History - Log service now stores the blocked information in database. Current UI only shows how many times its blocked. Future versions will have more details screens with all ipaddress with DNS Lookup. Also you can start blocking ipaddress directly from Logs in future versions + - Webview filter (applist) - Another possible way apps can use webview to access internet. So now there is a separate system level application for webviews. Please whitelist/blocklist this app accordingly. + - Xposed module - Hide lockscreen notification - This will hide ongoing notification in lockscreen. Due to android restrictions it uses Xposed to hide it + - Log toast position - Now you can customize the position of app block notification + - Toybox support (system level) - CM13/12 and even stock android uses toybox instead of busybox. If AFwall+ does not find busybox, it will look for toybox. If toybox not found, it will use built-in busybox. +* Bugs Fixed + - su leak issues - This issue was related to log service was not able to close properly. + - Random block issue - Now by default AFWall+ sets all default chains to ACCEPT state + - Fix toast related crashes + - All Log related issues/Removed klogripper - klogripper was causing lot of issues on multiple devices. its been replaced with stock dmesg. + - Widget crashes,bug in app lock + - Fix rules export issue + - Kingroot issue - AFWall+ removes the chains used by kingroot now. So, kingroot should not be able to use internet if blocked by AFWall+ + - Improved init.d/su.d related bugs - ipv6 support is improved now, Thanks to F-i-f +* UI + - Rearrange preferences - Language now moved to new group along with Tasker & Xposed module settings. + - Added legends - More detail about the icons used by AFWall+ + - Firewall mode - Since the dropdown was not visible on multiple devices, its moved to ActionBar + - Helper notification on preference change - Some preference changes require repply of firewall rules (DNS for example). AFWall+ notify the user when those changes happen +* Misc + - Updated support libraries + - Updated Translations and cleanup - Huge thanks for Gitoffthelawn +* Thanks to F-i-f and Gitoffthelawn + +Version 2.2.3 [Mar 15, 2016] +* Allow kingroot users to continue with warning message until I find proper solution for kingroot problem. +* AFWall+ will now show in recent apps list +* Removed highly experimental feature added in the last version for now. +* Reported crash issues +* Updated Translations + +Version 2.2.2 [Mar 11, 2016] +* Fix: Issue with auto IPv6 from preference +* Fix: afwall su.d script removal on uncheck preference & Added support for systemless su +* Fix: additional steps to kill klogripper process. +* Disable AFWall+ if KingRoot is detected. AFWall+ will no longer work with KingRoot, see here for more details: https://github.com/ukanth/afwall/issues/501 +* Added highly experimental feature - Keep only AFWall+ chains on connectivity change. +* Updated translations. + +Version 2.2.1 [Mar 3, 2016] +* Fix: Delete su.d script if unchecked from preference +* Fix: Startup hang issue while applying rules +* Fix: Widget size issue +* Added missing translation for Romania - Thanks to @ASebastian/mysterys3by + +Version 2.2.0 [Feb 27, 2016] +* Auto IPv6 support +* Support for Android 6 runtime permission (external storage - import/export) +* Widget alignment issue - Support to manual adjustment +* Device startup rules with 3 sec delay to apply rules properly on some devices +* Fixed random disable issue of firewall +* Fixed random complete blocking issue of firewall +* su.d support(supported by supersu and alternate for init.d to prevent startup leak) +* Mobile data issue for new devices +* Logs should show for more devices +* Application not shown after search completed +* Fixed sort option position on start +* Additional check for netfilter support on startup +* Added additional information for error report for better understanding +* Initial support to store logs to db for history. New UI will be in the next version with History. +* Fixed lot of reported crash issues +* Fix: Language not switching for few languages +* Updated Translations with new language support (pt-BR) +* Library updates: androidlockpattern,material dialogs + +Version 2.1.3 [Nov 24, 2015] +* Fix: Missing data Interfaces for new devices +* UI: Sort option as radio button +* UI: New Languages (Catalan/Bengali) +* Minor UI Improvements and About/FAQ link click issue +* Fixed: Reported crashes fixed + +Version 2.1.2 [Oct 6, 2015] +* UI: Sorting now in main page +* UI: Main screen width issue +* Possible fix for Widget alignment(not tested!) +* Reported Crash fixes +* UI: Status icon fix for Lollipop +* Added proper widget preview +* UI: Profile switch Bug (change of profile) + +Version 2.1.1 [Sep 28, 2015] +* FIX: Rules not saving when profiles are used. + +Version 2.1.0 [Sep 26, 2015] +* Fixed rules not saving on some devices +* Revert Filter from dropdown to radio (UI) +* Kingo superuser issue +* Menu key doesn't work (UI) +* User reported crashes and bug fixes +* lock pattern improvements (UI) +* Additional checks for system busybox +* Log service process leak fix. +* Widget alignment issue on some devices +* Fix F-Droid builds using NDK-r10e + +Version 2.0.0 [Sep 7, 2015] +* Initial Material design +* Support for 5.x Lollipop +* Revamped UI (pull to refresh), Preferences and Icons +* Enhanced security password protection +* Enhanced Import/Export with File Picker +* New Profile Management +* Performance Improvements and optimizations +* Experimental - Added sort apps by uid/install date +* Added/Updated Translations. +* And lots of changes... + +Version 1.3.4 [Aug 3, 2014] +* Feature: Added permanent notification on firewall status (optional) +* Bug: Modified init.d script to support system iptables +* Bug: Fixed FC on multiple devices when enable/disable +* Bug: Fixed issue with widget when password protected +* Minor widget enhancement for old android devices +* Updated Translations + +Version 1.3.3 [Jul 18, 2014] +* Added export & import for preferences/profiles including custom profiles (Donate version only) +* Custom Script for each Profile +* New combined dialog for import and export +* Encryption for application password - Also resets the old passwords. Please set password again! +* Fix for LogService FC issues on 4.4 +* Fix for new apps not showing on top when profiles are enabled +* Fix for Possible SU leak +* Improved notification text +* Improved search filter/profile validation logic +* Updated libs + +Version 1.3.2 [May 31, 2014] +* Added back the old profile switch widget till the new widget gets stable. +* Fix: process leak with log and nflog service. Please do a clean install if it does not work after update. +* Fix: filter application's not working for block notifications. +* Fix: multiple tasker issues (profile 2 applies - profile 3 and rules are not applying when using tasker) +* Fix: profile status not getting reflected on main view when changed using tasker/widget +* Fix: new widget not applying rules properly for profiles. +* Fix: Import rules fails when package not found. +* Fix: User reported NPE & Force close issues. + +Version 1.3.1.1 [Apr 25, 2014] +* Revert Target SDK to 16 to fix issue with boot rules + +Version 1.3.1 [Apr 23, 2014] +* Added experimental filter for block notifications. +* Error report FC +* ip6tables log/toggle issues on most devices +* Widget display issue on some devices #265 +* Better root detection and error display when no root +* Fixed FC on experimental Preferences #270 +* Widget name issues and better icons +* Performance improvement for multiple profiles +* Reuse of rootshell on new logservice +* Widget profile switch issue +* Apply rules on boot fix for some devices + +Version 1.3.0.2 [Apr 3, 2014] +* Bug fixes on 1.3.x + +Version 1.3.0.1 [Apr 1, 2014] +* Old toggle widget is back - Hate you guys :) +* "Allow All Application" option is back - Again hate you guys :) + +Version 1.3.0 [Mar 30, 2014] +* New Widget with support for multiple profiles (single widget) +* Updated lockpattern - stealth mode/max retry count +* DNS Proxy to Preferences (By default UDP 53 will be blocked on <4.3) +* More Log information (PORT/PROTOCOL) +* Fixed application list load performance issues +* Fixed bug in preferences +* Support for Wifi-only tab (auto hide data column) +* Block packet notification (exp!) - Log service +* New Icon,User reported bug fixes including tasker plugin +* Translations updated - Indonesian (thx mirulumam) + +Version 1.2.9 [Feb 8, 2014] +* Feature: Column level select/invert/unselect +* Feature: New Import/Export (with backward compatiblity) +* Feature: Filter by All/Core/System/User applications +* BugFix: Fixed issue with Multiuser iptables rules +* BugFix: Fixed issue with Tasker plugin (enable/disable/Profile switch) +* UI: Revamped About and added FAQ page. +* User reported bug fixes. + +Version 1.2.8 [Jan 19, 2014] +* Traffic stats + App detail View (Long press on App Label) Note: A minimal stats and not a complete statistics of traffic details. +* Add/Remove Additional Profiles +* Multiuser support for Tablets (Experimental) +* Custom rules file support (. /path/to/file) +* Fixed VPN issue with KitKat & Updated libsuperuser library +* Many minor UI enhancements and performance improvements +* Bug Fixes: #154, sdcard mount on startup, user reported crash fixes + +Version 1.2.7 [Nov 23, 2013] +* Improved search functionality & select confirmation. +* Added built-in ip6tables support +* Support for x86/MIPS/ARM devices. +* Built-in iptables is upgraded to the latest version. +* Various user reported crash/bug fixes. +* Build scripts updated for F-Droid and developer friendly builds (ant) +* Added Hungarian/Turkish Translations and updated other translations + +Version 1.2.6.2 [Sep 9, 2013] +* NGLog fixes for various devices including nexus. + +Version 1.2.6.1 +* LOGS should work now on newer devices which has NFLOG chain. +* New RootShell Service to keep AFWall+ active. +* RootShell - Retry on exit 4 while switching from 3G to WiFi or viceversa. +* Removed alternate startup service from experimental- It works without it. +* Improved Caching Logic, Uninstall apps will now remove cache along with rules. +* Reload applications will remove unused cache. +* New Help page - Added another developer (@cernekee) +* Fixed too many prompts when password is enabled. +* Rewritten logic to detect LOG chain. +* Added "error send report" option to help beta testers with more diagnostics information. +* Special UID for DNS Proxy and NTP (4.3 users) +* Added support for custom rules from 1.2.5.2 (it was broken because of afwall iptables chain change) +* Better tether status check +* Keep alive RootShell on some devices. + +Version 1.2.6 [Aug 16, 2013] +* Lots of Refactor to bring stability and performance. Fixed many issues along with it + (HUGE THANKS TO cernekee!) + New option -> Now enable/disable log from preferences +* New option -> Apply rules on Switch Profiles +* New option -> Active Rules is now optional (But required when using LAN/Roaming) +* New option -> Enable inbound connections (Required for sshd, AirDroid etc) +* New busybox binary for ifconfig +* Rules Log has more information +* NFLOG support for newer devices + +Version 1.2.5.2 [May 30, 2013] +* Fixed issue with Wifi blocked on whitelist for couple of devices + +Version 1.2.5.1 +* Improved search functionality. +* Frequent connectivity change rules will now work only on roaming/lan. this should reduce the number of + superuser prompts and reduce lag on some devices. +* Fixed issue with application list not showing. +* Fixed issue with logs where it used to work before. +* Fixed issue with default language (default is set to English, please change it in preference) +* Added translations. + +Version 1.2.5 [May 25, 2013] +* Added Tether support. (Thanks to cernekee) +* Added LAN/WAN support. (Thanks to cernekee) +* Added Import from DroidWall (from Donate Version!) +* Fixed issue with special applications not showing in different color(system apps) (Thanks to cernekee) +* Fixed issue with preferences for defauly system application picker (Thanks to cernekee) +* Fixed issue with Language preferences default (Thanks to cernekee) +* Lots of code refactor/bug fixes (Thanks to cernekee!) +* Fixed issue with multiline in search text. +* Minor UI changes on the application list. +* Added selectable iptables/busybox binary +* Added new translations (Chinese, Greek, more) + +Version 1.2.4.1 [Apr 27, 2013] +* Fixed issue with cleanup afwall rules on disable +* Fixed issue with OUTPUT chain not removed for afwall on disable + +Version 1.2.4 (bump version to match Donate version) [Apr 23, 2013] +* Support IPv6 (Enable it in preference) +* Tasker support enable/disable of AFWall+ +* Improved performance of applying rules and application list. +* Improved application loading progress dialog. +* Show keyboard automatically on password protected dialogs +* Fixed issue with custom script hangs. +* Improved translations strings. +* Fixed issue with multiple password request (in beta testing) +* Improved detection logic for data leak prevention script (Thanks GermainZ) +* Improved multiple profile performance while loading applications. It will no longer apply rules on switching + profiles. You need to manually apply rules after profile switch. +* Added translations for Greek and Produguese languages. + +Version 1.2.1 [Mar 11, 2013] +* Minor issue fixed for "Media Server" not apply properly after reboot +* Fixed iptables rules which breaks wifi/Mobile data limit. +* Updated translations for German/Chineese +* Added Swedish Translation - Many Thanks to CreepyLinguist@Crowdin + +Version 1.2.0 [Mar 3, 2013] +* Added change app language from the preferences (default is system lang) +* Added device admin feature - Extra protection to AFWall+, so that it can't be uninstalled from any other app. +* Added Tasker/Locale plugin (from donate version) with bug fixes. +* Added VPN Support (enable/disable it preferences) - Tested with DroidVPN and works fine! +* Added new widget with quick toggle (enable/disable/profiles) +* Added option to import from DroidWall (only for Donate version for now!) +* Added Active defense (Make sure only AFWall+ able to control the internet) - Not optional! +* Added new super user permission ( koush's superuser permission) +* Added ability to enable/disable roaming feature +* New logic to apply rules - Performance improvement +* Removed deprecated API's for Notification. Going forward this will be improved for ICS/JB +* Improved preferences - Added summary for each preferences and rearranged order +* New menu icons (white icons !) +* Removed all inline style alert messages and alert boxes. Now it just display toast messages. +* Fixed data leak on boot for devices REQUIRES init.d support/S-OFF (enable it in preferences - EXPERIMENTAL!) +(to enable init.d support use this app -> https://play.google.com/store/apps/d...k.initdtoggler) +* New log rule to get the logs from dmesg and enable logs by default +* Enable/Disable logs now from "Firewall Logs" menu. +* Fixed issue with iptable rules are not applying after reboot, mainly CM 10.1 devices (Enable it in preferences - EXPERIMENTAL !) +* Various UI glitches in multi profiles/icons & UID +* Fixed hang/rules issue on startup +* Fixed issue with profiles where the default profile is applied after restart instead of selected one. +* FC issue when using app menu (ActionBarSherlock - NPE) +* Fixed issue with Media Server/VPN not applying properly. +* Simplified Chinese Translation - Many thanks to wufei0513 & tianchaoren@Crowdin +* Czech Translation - Many thanks to syk3s@Crowdin +* Turkish Translation - Many thanks to metah@Crowdin +* Ukrainian Translation - Many thanks to andriykopanytsia,igor@Crowdin + +Version 1.1.9 [Jan 23, 2013] +* Added invert selection for apps (useful when switching whitelist <-> blacklist) +* Fixed issue with special apps (root/shell/media server) not applying +* Fixed issue with new lockpattern not working properly. +* Added MDPI images for icons. +* Code cleanup, mainly strings.xml (removed version from strings.xml etc.) + +Version 1.1.8 +* Fixed FC on new lockpattern + +Version 1.1.7 +* Added lockpattern (you can still use the old style password protection) with SHA1 protection +* Fixed force close issue while adding system apps. +* Fixed issue with select All/none. it wroks properly and doesn't require scroll. Thanks to Pragma! +* Significant improvements while loading applications (hope not a placebo) +* Fixed issue with search case sensitive and expand search will show the keyboard (no more two press!) +* Disable notification when the firewall is disabled. +* Added new language translations + - Spanish translation by spezzino@crowdin + - Dutch translation by DutchWaG@crowdin + - Japanese translation by nnnn@crowdin + - Ukrainian translation by andriykopanytsia@crowdin + +Version 1.1.6 [Jan 3, 2013] +* Back to Chainfire's SU library. More stable but little slower compare to RootTools. Performance will be improved going forward. + I'm planning to rewrite the entire code to make it faster and stable. But for now, it will be continue as it is. +* Fixed issue with rules were not applied after system reboot for couple of devices. +* Fixed issue with custom rules were broken completely. +* Fixed issue with Notification icon size is huge. +* Fixed Force Close of some devices when alert message is displayed. + +Version 1.1.5 [Dec 27, 2012] +* New Busybox binary (at least I feel little faster loading on logs) compiled from latest busybox source. This is packed with handpicked additional and useful busybox commands which will be used in the future versions of AFWall+ to build more advance features! Stay tuned. +* Fixed issue with widget size 1x1 on newer devices +* Fixed issue with firewall rules not applying before shutdown to prevent leak. +* Fixed Force close on many devices while opening application. +* Fixed Force close on some devices when alert message is displayed. + +Version 1.1.4 +* Replace su library with RootTools, much faster and stable! +* Improved detection logic for iptables for ICS/JS devices and removed EXPERIMENTAL option from preferences. +* Now disable icons will free up space on the main view +* Added option to show UID for applications (like DroidWall) +* Fixed Issue with tf201 devices with su permisssions. +* Fixed constant force close on some devices while applying rules. +* Fixes issue with packages reset to root when importing. +* Improved Russian Translations - Many thanks to Kirhe@xda! +* Fixed issue with custom script not applying properly after uid (github issue #89) +* Removed Disable 3G when USB connected preference because of some bugs. I'll put that back after fixing it. + +Version 1.1.3 [Dec 20, 2012] +* Critical bugfix: Rules were not applied after every system reboot! + +Version 1.1.2 +* Minor bug fix for Forceclose on alerts! + +Version 1.1.1 +* Feature : Tasker/Locate Plugin! (Only for donate version for now) +* Feature : Now allow customize names for profiles.(from preferences) +* Feature : Replace alert/toasts with appmsg(displays within the app) - enable it in preference +* Feature : Initial simple improvements for view logs. It will be improved further! +* Preference : Added new preference to enable confirmbox on firewall disable +* FC : Replaced old style deprecated Thread implementation with AsyncTask.(faster and safer) +* FC : NullPointer exception while reading preferences. +* Bug Fix : Shutdown Custom Rule doesn't work. +* Bug Fix : Refresh issue of mode on the multiple profiles switching. +* Bug Fix : Fixed two identical profile names on multiple profiles. +* i18n : Completed french/Germen translations. +* i18n : Added russian language support (Thanks: Google translator toolkit) +* and many small fixes + +Version 1.1.0 [Dec 7, 2012] +* Initial Play Store Version + +Version 1.0.7a +* New icon for AFWall+ (Thanks for hush66!) +* Multiple Profiles (Currently limited to 4) +* Added support for Epic 4G Touch (Thanks to JoshMiers!) +* Unified Preferences (https://github.com/saik0/UnifiedPreference/) +* Translations added: French and German +* Fixed multiple menu on ActionBar with staticbar ( no more two menu items on some devices ) +* Enable/Disable logs now moved to preferences and log menu will be hidden if disabled +* Bug Fix: Update of application packages will not be notified with AFwall. +* Bug Fix: Uninstalling app will reset rule for root application to default. + +Version: 1.0.6.1a [25 Nov 2012] +* Bug Fix : Rules for spcial application were not applied after application restart. + +Version: 1.0.6a [25 Nov 2012] +* Now uses Chainfire's SU library, This will get rid of old shell script approach. I feel it's faster and better approach and helps to enable profiles! + Please Note: If you use afwall.sh outside, starting this version it will not work! +* Improved menubar and confirmation dialogs +* Fixed bug with logging +* Fixed bug on some ICS/JB devices +* Added new EXPERIMENTAL option for ICS/JB devices (uses extra rules) +* Enabled fast scrolling on lists (main list) +* German Translation (Thanks to CHEF-KOCH!) +* Support for 4.2 JB + +Version 1.0.5a [10 Nov 2012] +* Enhanced Rules view with additional actions like copy, flush, export to sdcard and network interfaces. +* Moved flush rules from main menu to enhanced rules view +* Enhanced Log view with additional copy & clear action +* Moved clear log from main menu to enhanced log view +* Fixed FC issues from 1.0.4.x + +Version 1.0.4.1a [9 Nov 2012] +* Fixed force close on viewlog and view rules pages. + +Version 1.0.4a [9 Nov 2012] +* Import/Export Rules (for now it's just a single import & export to external storage) +* Integrated search bar (application search) +* Revamped Log & IPTables rules view (you can now view the logs and rules in a clear view and copy them!) +* Added reenter password confirmation dialog. +* Added additional ifaces to support more devices (working on another solution which will identity interfaces on the particular device) +* Fixed force close when scrolling for some devices +* And more! + +Version 1.0.3a +* Fix for some apps can "bypass" the firewall by just using UDP port 53. Disable port 53 +* Added 3g ifaces to support more devices (should solve issues with firewall for some devices) +* Fixed Widget on/off issue (First enable firewall and then add the widget will do the trick!) +* Fixed Widget size for 4.0+ devices +* Prepared for i18n Support +* Prepared support for XHDPI devices +Note: If you have any issue, please clean the rules via menu and apply again. + +Version 1.0.2a (Please note, if you upgrade from 1.0.1a, rules will be reset!) +* Roaming Option (not tested !) +* Added Shutdown broadcast and applied rule to block all the connections (this should solve the leakage + when phone is rebooted/started before AFwall+ can start!!!) - Not tested! +* Added option to disable application icon (faster loading on slow devices) +* Added option to disable 3g rules when connected via Wired USB (Droidwall issue) +* Added support for more ifaces for 3G (support multiple devices) +* Added clear Rules option in menu (now the iptables will be saved as afwall-3g,afwall-wifi, to solve the issue when both Droidwall & AFWall+ installed ) +* Fixed bug in reload applications +* Fixed bug in applying rules in clear/select all +* Fixed the issue with save/discard rules when press back button. + +Version 1.0.1a +* Improved install notification (only notify when app has internet permission) +* Select All Wifi / 3G or Clean All option! (HUGE FIX) - No Invert select this time. just click on the 3G/WiFi icons will do the trick! +* Fixed dangerous file permissions issues (reported in original Droidwall as an issue) + +Version 1.0.0a +* Initial release based on [DroidWall](http://code.google.com/p/droidwall/) 1.5.7 +* ICS style menubar and theme +* New install notifications +* New preferences options +* Force reload Applications +* Highlight System applications using custom color from preferences diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ef7e7ef --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 03777ba..213f5e2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,385 @@ -# af-wall +# AFWall+ (Android Firewall+) -Android Firewall and IP tables \ No newline at end of file +[![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 + +--- + +

+ AFWall+ Screenshot +

+ +## 🔥 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 + +

+ + Get it on Google Play + + + Get it on F-Droid + + + GitHub Releases + +

+ +📋 **Release Notes**: Check the [changelog](https://github.com/ukanth/afwall/blob/beta/Changelog.md) for what's new in each version. + +--- + +## 🌟 Key Features + +### 🔐 **Granular Control** +- **Per-app network rules** - Allow/block individual apps +- **Network type filtering** - Different rules for WiFi, mobile data, VPN, tethering +- **IPv4 & IPv6 support** - Complete protocol coverage +- **Custom rule scripting** - Advanced users can write custom iptables rules + +### 🎛️ **User Experience** +- **Clean, intuitive interface** - Easy to understand app list with clear allow/block controls +- **Quick search & filtering** - Find apps instantly, sort by name, install date, or permissions +- **Bulk operations** - Enable/disable rules for multiple apps at once +- **Profile management** - Switch between different rule sets (home, work, travel) + +### 📊 **Monitoring & Logging** +- **Real-time network monitoring** - See which apps are trying to connect +- **Detailed connection logs** - Track blocked attempts with timestamps and destinations +- **Notification system** - Get alerts for blocked connection attempts +- **Export/import rules** - Backup your configuration or share with others + +### 🔧 **Advanced Features** +- **Boot protection** - Apply rules before apps start (prevents data leaks during startup) +- **Startup delay management** - Robust boot rule application with network change handling +- **Multi-user support** - Different profiles for different Android users +- **Tasker/Locale integration** - Automate firewall based on conditions +- **Password protection** - Secure your firewall settings +- **Tor and VPN detection** - Special handling for privacy networks + +### 🌐 **Network Types Supported** +- 📶 **Mobile Data** (3G/4G/5G) - including roaming detection +- 📡 **WiFi** - home, work, public hotspots +- 🔗 **VPN** - all VPN types and providers +- 🔄 **Tethering** - WiFi hotspot, USB, Bluetooth +- 🧅 **Tor** - onion routing support +- 🏠 **LAN** - local network access + +--- + +## 📋 System Requirements + +### ✅ **Compatibility** +- **Android versions**: 5.0 (API 21) to 14+ (actively maintained) + - Legacy support: Android 4.x (version 2.9.9), Android 2.x (version 1.3.4.1) +- **Root access**: Required (Magisk, SuperSU, LineageOS su) +- **Architectures**: ARM, ARM64, x86, x86_64 +- **Storage**: ~15MB app + ~5MB for binaries + +### 🔧 **Root Methods Supported** +- ✅ **Magisk** (recommended) +- ✅ **LineageOS built-in su** +- ✅ **SuperSU** (legacy) +- ✅ **KingRoot** (not recommended) + +### 🚫 **Limitations** +- **Requires root access** - No root = no functionality +- **Not an antivirus** - Doesn't scan files for malware +- **Not an ad-blocker** - Blocks network access, not ads within allowed connections +- **VPN conflicts** - Some VPN apps may interfere with firewall rules +- **System-level apps** - Some system processes may bypass rules if they have root access + +--- + +## 🚀 Quick Start Guide + +### 1. **Pre-Installation** +```bash +# Verify root access +su -c "id" +# Should return: uid=0(root) gid=0(root) +``` + +### 2. **Installation** +- Install AFWall+ from your preferred source +- Grant root permission when prompted +- Enable firewall in main screen + +### 3. **Basic Configuration** +1. **Enable the firewall** - Toggle the main switch +2. **Configure apps** - Tap apps to allow WiFi (green) or mobile data (orange) +3. **Apply rules** - Tap the apply button (firewall icon) +4. **Test connectivity** - Verify apps work as expected + +### 4. **Essential Settings** +- **Boot startup delay**: Prevents rule conflicts during boot +- **Notification settings**: Control alert behavior +- **Log settings**: Enable if you want connection monitoring + +--- + +## 🔧 Advanced Configuration + +### 📝 **Custom Rules** +AFWall+ supports custom iptables rules for advanced users: + +```bash +# Example: Allow specific IP range +-A afwall-wifi -d 192.168.1.0/24 -j ACCEPT + +# Example: Block specific port +-A afwall -p tcp --dport 443 -j REJECT +``` + +### 🔄 **Profiles** +Create different rule sets for different scenarios: +- **Home**: Relaxed rules for trusted network +- **Work**: Restrictive rules for corporate network +- **Public**: Maximum security for public WiFi +- **Travel**: Balanced rules for mobile use + +### 📊 **Logging Configuration** +- **Packet logging**: Uses nflog for detailed connection tracking +- **Log rotation**: Automatic cleanup of old logs +- **Export options**: Save logs for external analysis + +--- + +## 🌍 Language Support + +AFWall+ is available in **40+ languages** thanks to our community translators: + +🇺🇸 English • 🇪🇸 Español • 🇫🇷 Français • 🇩🇪 Deutsch • 🇮🇹 Italiano • 🇷🇺 Русский • 🇨🇳 中文 • 🇯🇵 日本語 • 🇰🇷 한국어 • 🇵🇹 Português • 🇳🇱 Nederlands • 🇵🇱 Polski • 🇹🇷 Türkçe • 🇸🇦 العربية • 🇮🇳 हिंदी • And many more! + +**Want to help translate?** Join our [Crowdin translation project](http://crowdin.net/project/afwall). + +--- + +## 🛠️ Development + +### 🏗️ **Building from Source** + +#### **Prerequisites** +- Android SDK (API level 21+) +- Java 17+ +- Git +- Android NDK (for native binaries) + +#### **Quick Build** +```bash +git clone https://github.com/ukanth/afwall.git +cd afwall +./gradlew clean assembleDebug +``` + +#### **Native Binaries** +To compile iptables, busybox, and other native components: +```bash +# Requires Android NDK +export NDK=/opt/android-ndk-r25 +make -C external NDK=$NDK +``` + +### 📁 **Project Structure** +``` +afwall/ +├── app/src/main/java/dev/ukanth/ufirewall/ +│ ├── Api.java # Core iptables interface +│ ├── MainActivity.java # Main UI +│ ├── InterfaceTracker.java # Network state monitoring +│ ├── util/BootRuleManager.java # Boot rule application +│ ├── service/ # Background services +│ ├── broadcast/ # System event receivers +│ └── log/ # Logging subsystem +├── app/src/main/res/raw/ # Native binaries (iptables, busybox) +├── external/ # Native binary sources +└── scripts/ # Build scripts +``` + +### 🧪 **Testing** +```bash +# Run lint checks +./gradlew lint + +# Run unit tests +./gradlew test + +# Install debug build +./gradlew installDebug +``` + +--- + +## 🤝 Contributing + +We welcome contributions! Here's how you can help: + +### 🐛 **Bug Reports** +- Check [existing issues](https://github.com/ukanth/afwall/issues) first +- Follow our [bug report guide](https://github.com/ukanth/afwall/wiki/HOWTO-Report-Bug) +- Include device info, Android version, and logs + +### 💡 **Feature Requests** +- Open an issue with the "enhancement" label +- Describe the use case and expected behavior +- Consider if it fits AFWall+'s scope and philosophy + +### 👨‍💻 **Code Contributions** +```bash +# Standard GitHub workflow +1. Fork the repository +2. Create a feature branch: git checkout -b feature-name +3. Make your changes and test thoroughly +4. Submit a pull request with clear description +``` + +### 🌐 **Translations** +- Join our [Crowdin project](http://crowdin.net/project/afwall) +- No technical knowledge required +- Help make AFWall+ accessible worldwide + +--- + +## 📞 Community & Support + +### 💬 **Discussion Forums** +- **XDA Thread**: [Official community discussion](http://forum.xda-developers.com/showthread.php?t=1957231) +- **GitHub Issues**: Technical problems and feature requests +- **Wiki**: [Comprehensive documentation](https://github.com/ukanth/afwall/wiki) + +### ❓ **Frequently Asked Questions** +Before reporting issues, check our [FAQ](https://github.com/ukanth/afwall/wiki/FAQ) for common solutions. + +### 🆘 **Getting Help** +1. Check the FAQ and wiki +2. Search existing GitHub issues +3. Ask on XDA forums +4. Create a new GitHub issue (last resort) + +--- + +## 📖 Technical Details + +### 🔧 **Architecture** +AFWall+ uses a **layered architecture**: + +1. **UI Layer**: Android activities and fragments for user interaction +2. **Service Layer**: Background services for rule application and monitoring +3. **Core Layer**: iptables rule generation and management +4. **System Layer**: Native binaries and root shell interface + +### 🏗️ **Key Components** +- **BootRuleManager**: Robust boot-time rule application with race condition prevention +- **InterfaceTracker**: Network interface monitoring and change detection +- **Api.java**: Central iptables command generation and execution +- **FirewallService**: Background service for continuous monitoring +- **LogService**: Network packet logging and analysis + +### 📱 **Android Integration** +- **Broadcast Receivers**: Monitor system events (boot, network changes, app installs) +- **Content Providers**: Share configuration data securely +- **Notification System**: User alerts for blocked connections +- **Quick Settings Tile**: Fast firewall toggle (Android 7+) + +--- + +## 🏆 Acknowledgements + +AFWall+ builds upon the work of many open-source projects and contributors: + +### 🌟 **Origins** +- **Original concept**: Derived from [DroidWall](http://code.google.com/p/droidwall) by Rodrigo Rosauro +- **Current maintainer**: [Umakanthan Chandran](https://github.com/ukanth) + +### 📚 **Libraries & Dependencies** +| Component | License | Purpose | +|-----------|---------|---------| +| [iptables](http://netfilter.org/projects/iptables/) | GPL v2 | Linux firewall framework | +| [BusyBox](http://www.busybox.net) | GPL v2 | Unix utilities | +| [libsuperuser](https://github.com/Chainfire/libsuperuser) | Apache 2.0 | Root access management | +| [libsu](https://github.com/topjohnwu/libsu) | Apache 2.0 | Modern root interface | +| [Material Dialogs](https://github.com/afollestad/material-dialogs) | MIT | UI components | +| [DBFlow](https://github.com/Raizlabs/DBFlow) | MIT | Database ORM | +| [PrettyTime](https://github.com/ocpsoft/prettytime) | Apache 2.0 | Human-readable timestamps | + +### 👥 **Contributors** +Thanks to all contributors who have helped improve AFWall+ over the years! + +--- + +## 📄 License + +AFWall+ is released under the **GNU General Public License v3.0**. + +``` +Copyright (C) 2009-2011 Rodrigo Zechin Rosauro +Copyright (C) 2011-2024 Umakanthan Chandran + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +``` + +**Full license text**: [LICENSE](LICENSE) file or [gnu.org/licenses/gpl-3.0](https://www.gnu.org/licenses/gpl-3.0.html) + +--- + +

+ Made with ❤️ for Android privacy and security
+ AFWall+ - Your Network, Your Rules +

\ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..01f6a61 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,84 @@ +apply plugin: 'com.android.application' + +android { + + defaultConfig { + compileSdk 36 + targetSdk 36 + applicationId "dev.ukanth.ufirewall" + //applicationId "dev.ukanth.ufirewall.donate" + minSdkVersion 21 + versionCode 20251001 + versionName "4.0.0" + buildConfigField 'boolean', 'DONATE', 'false' + vectorDrawables.useSupportLibrary = true + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + } + } + + buildFeatures { + buildConfig true + } + + buildTypes { + debug { + minifyEnabled false + proguardFiles 'proguard-rules.pro' + } + release { + minifyEnabled false + proguardFiles 'proguard-rules.pro' + } + } + + + lint { + abortOnError true + disable 'MissingTranslation' + } + + namespace 'dev.ukanth.ufirewall' + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + compileSdk 36 + buildToolsVersion '34.0.0' + + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + } + +} + +dependencies { + + def libsuVersion = '6.0.0' + def dbFlowVersion = '4.2.4' + + implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" + implementation "com.github.topjohnwu.libsu:service:${libsuVersion}" + implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}" + implementation "eu.chainfire:libsuperuser:1.1.0" + implementation "com.github.ukanth:android-lockpattern:8.0.4" + implementation "com.afollestad.material-dialogs:core:0.9.6.0" + implementation "androidx.appcompat:appcompat:1.7.0" + implementation "com.google.android.material:material:1.12.0" + implementation "androidx.cardview:cardview:1.0.0" + implementation "androidx.recyclerview:recyclerview:1.3.2" + implementation "androidx.annotation:annotation:1.9.0" + implementation "androidx.core:core:1.15.0" + implementation "androidx.preference:preference:1.2.1" + implementation "androidx.legacy:legacy-support-v13:1.0.0" + implementation "androidx.activity:activity:1.9.3" + annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:${dbFlowVersion}" + implementation "com.github.Raizlabs.DBFlow:dbflow-core:${dbFlowVersion}" + implementation "com.github.Raizlabs.DBFlow:dbflow:${dbFlowVersion}" + implementation "io.reactivex.rxjava3:rxjava:3.1.9" + implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final" + implementation "dnsjava:dnsjava:3.6.2" + +} diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..ee0eead --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..58ba74c --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +-keepattributes +-keep class org.ocpsoft.prettytime.i18n.** +-keep class * extends com.raizlabs.android.dbflow.config.DatabaseHolder { *; } +-dontpreverify +-dontoptimize +-dontobfuscate +-keep class dev.ukanth.ufirewall.** { *; } +-optimizations !code/allocation/variable + +# Android 16 specific proguard rules +-keep class android.window.** { *; } +-keep class androidx.activity.** { *; } +-dontwarn android.window.** +-dontwarn androidx.window.** + +# Edge-to-edge and window insets support +-keep class androidx.core.view.WindowInsetsCompat** { *; } +-keep class androidx.core.view.ViewCompat** { *; } + +# Notification channel compatibility +-keep class androidx.core.app.NotificationChannelCompat** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f12256d --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/rules.json b/app/src/main/assets/rules.json new file mode 100644 index 0000000..309794c --- /dev/null +++ b/app/src/main/assets/rules.json @@ -0,0 +1,78 @@ +{ + "author": "ukanth", + "version": "1.0", + "rules": [ + { + "name": "Allow Multicast DNS", + "desc": "DNS based service discovery", + "v4": { + "on": [ + "-A afwall-wifi-fork -d 224.0.0.0/24 -j afwall-wifi-lan" + ], + "off": [ + "-D afwall-wifi-fork -d 224.0.0.0/24 -j afwall-wifi-lan" + ] + }, + "v6": { + "on": [ + "-A afwall-wifi-fork -d ffx2::/16 -j afwall-wifi-lan" + ], + "off": [ + "-D afwall-wifi-fork -d ffx2::/16 -j afwall-wifi-lan" + ] + } + }, + { + "name": "Allow ICMP", + "desc": "To allow ICMP packets", + "v4": { + "on": [ + "-A INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT", + "-A INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT", + "-A INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT" + ], + "off": [ + "-D INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT", + "-D INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT", + "-D INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT" + ] + }, + "v6": { + "on": [ + "-A INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT", + "-A INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT", + "-A INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT" + ], + "off": [ + "-D INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT", + "-D INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT", + "-D INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT" + ] + } + }, + { + "name": "Allow Loopback Interface", + "desc": "Allow lo interface routing (mostly on samsung)", + "v4": { + "on": [ + "-A INPUT -i lo -j ACCEPT", + "-A afwall -o lo -j ACCEPT" + ], + "off": [ + "-D INPUT -i lo -j ACCEPT", + "-D afwall -o lo -j ACCEPT" + ] + }, + "v6": { + "on": [ + "-A INPUT -i lo -j ACCEPT", + "-A afwall -o lo -j ACCEPT" + ], + "off": [ + "-D INPUT -i lo -j ACCEPT", + "-D afwall -o lo -j ACCEPT" + ] + } + } + ] +} \ No newline at end of file diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..df4d315bce4a401804379e1fe61f037e91871bb7 GIT binary patch literal 28000 zcmeFZ^+Qz8_dh-xu#|wbgrG>LsJK$QltG7zfP^A~AOg||>;+U5B$QI5q){maK|pp* zP^3#5MOwOJcRzRc`Fj5g-=Dts7u-8DXU?2CbLMg4`q0ee%sv)D768D$b7%D|06@b3 zL;@y8_|IBk_YMFaQs?xv&j$|t%fJNk`t?$`iHmhCc`q(i%)fLWygFKC#i@Hv`~^$S z;ob{Y4055;Se5-;+i*tMo`Qzwb<0~T70Zzm3^_)Oi&oZ78n zP(I=_6=G6m#laZ)(NS4|?f?Jt|B(pFWj*|dsP@kWXTFsWvA2BsRyDU@_1*As{pUOx z=Y7rhYjdKt5B8ngF3pe}`S3&i>FC!zllx8)K`o_CajfpgOGEXhfBDBO9l9&Vq>{2V z{BzMY%<8k#$Jtxw7peE+Vt4t|wb0rL?;WAh;XU>!@yMQE%Q0&WX}3OMSR%xlUQ3`R z?ASS>_s+53s_LwQTsQtiqO>`Aln0s@{R6R2-JiEth5f4W&(&NhpLjE%Lu1BDWTfSX zdgkc5SD^B9hQjK)y58YkuMN2AdPGFbUTf4;B$;^n{rq9~sjcM~kuYBpY=0~BPs?;1 z{f8Q3RG8kfe$hWaqF>3O&3Wx8rx}IVzNX5fR?xDWhJ$P?6IMBd2H!~u<6yS+wV^{| z#^KPH2U-x&qnusW(_2)TmhNaBOVLR@EORsP zl|KXlzdiZ>d5$S~rn^rdh2~~5IFxH+X*POsb2gN5$OXbugHbjoecih`L^+uv#O5GC zJtk+8g0!)Jxw&!dBLv}ZQ`Tq6rm&2ey(sV!6Hw?S+ zSe3yc`b6jpEC?Gu$FtabYr&JucxW3jMwtEDvpeb*u$wzcTeHVekoWC@Jv7#!Bz$!g z!KD0lb*h_ieQ=UKgIWHM$AIJv{dHwIFc1-e3sE##6$gO3fHJ3Pxk!DfITI5l%96x? z$@xdztLHCw-Y$QLLheIiS(S5m83;QeM|U}|A%0Hp&B0S9R8F7yz4rq0S!iWfGqimD z(&%JT$G57X?GFRQGrOA?F>G50bgn;M*^B1=IK+ls(q{>#exLkj2oRG7p-H%nqZdBCXdEqlwm<_mdcRH{xgAwZg%`TEkK z`8KW}MYHZ+2$sFgBEakop_`s&oeTSB*{y>A9G_|ZwE@lPm{Uu9K*0r1%IZ-QyhR#( z(2D}jb0Rk{)$(Oj?)21)_tj%X2 z1l@)Q|1P-j$%+S;w3g;i-#hm6mK+bCSZl-S{=4b#Ru;=pk&iA7Zx*XaKS1%|N0d-U z&ctXz_ZKy{#dnho#K86PX{zb^!4DOPPHIA1?(WSyvZE;tZSDH&{U#m%e)%V?PIdN9 z|GF5`rlVJIq}Tqmm$e!`1+08B0sR=x?s4mvn~sV1xsYZo%7JSH>q}WngJXM#vhvq5 zy0)KH_&1`@EFM+nd=~LjwX~VBi5;J*h{~&ak+8+YvM`(!p{B89x}ng76sTD?IZhji|89uMWAO_g*ep^E^Q?iFF5YSX{SRaA- zqk+L1M4q8W&qg_a35%Y@H3^xAUAk|Hxx+5pus@htm|#*tn^*?L1Y8aF)#1|UBgFeC z7j)dSL#AC0?L@ite3^QgQr5Hlxc8)~sCLSS%Vy5kfik$j05q7}HyK!hcPl*mt81E& zj)zHSJBT^=e?$ed0`Sr&*kP`_+9oOtIAgY43Pm+v^Dr(2L6;F=ltDnaH4)tDuNGr< zya^n2SXp4}0biLH=C@vX@4WZgL10kt)dakr9l)Okw>y}y^%7-hMd#7Op=^MJZy`_% z4XMXo+>;jFiUFX=qOh&AtTNd|bS9KFnfYcN#4*%gd5seK=N%^5 zNCas@>^z*}ZD4`>maes3KLa1>K-)h`2hlG_^uX)yj6IDXrlk#ro&nxnp3 zAUw-?j|7hM1zPi86=v&-=5_rT!sUlM4jj87YER>zasa$7I4=quU!3?b^I+>xZUl5$KFR*v4WP{F z3d}}t9O9Hj>>Woqx`Xq+`+yk4zUq1+Y-Y=RJc?~poTUD@Ltfwk@G|C&g`T37KcNBK zC2d#5*N-BP4QskA#Hf2LLtBhpjI#znDo_?IWB(iWtUx)a)`3WpVjT}j=x@Y~|6{#Z zZr%P9c$M7Z0>CkQKOW$-ySJ-~L|p=3;MEP&t@W*enB!^KRO55!VTF5oVtBFg3uY!v znsf%!A?oP%A9TxB(tvS2BcXw(1!qzLh_YNhWoP)=FTld=|Zp5_dT#YM7p2~7CFQbBtLjYaDLE+P@p*5 zSGx9*8Q;YCpe@Qt6?{N2GC=Sqd_zFZ8DLBnK;ax4O{qd@dtya7IzIE_ksIR|0U{Cw z*pUd3?G~_E==6*O{hp|ImSIx)8VbM%61s76dnPxme&|pWSyEd>WAHR%#YqwqXlH_F zg)kWUtzt178@m>1|`V{b41?PMuy@j#`Zp zqZ%<+mi*$+4hJK)^k!05Oz7HNh_3k}+?a~KnHy12o2vuS+rZezV8YYS4@B)7IImUa z>Rf5HLdv}li!DusT%HC~`pbyAiXvMYKn|7D2>HiRocx1=ba9fWRu8Lo4KBiP*s;`t7wGLFJS>WtSAvR4_u_N$Jx81 zV>af={+BuY_^I!(1%@9n%v75a=HPndYE(AqKDb76CjgdEx0*UkIRzsPp7xe-i1rS& zj$Zj_s+NvJTf13-V0ynivN{w<=9;dU%HpwoUmwMT2m<5M=Y0DQmz znBIsIYf9sUjA%aq4RHNHT#TasfEOqhHIgV|8z*gvd*zA!3$=0xPL4!LRqhv4~f|+9AaT=No&;(R|EQ2tNMj z?$gZl(U?rTEtwDHHKO;F4&X}~1oX@eqbz>D`OUN2!{oNRe%6g@tl_tJ{rHD9j|j6V z!P$IIuA=e2A6d&iMK-S@P3C#4Y*n8OC<lA3?C4V)`GfctzV&<;zO{nJ z$@T20$vvw*yTvq$U9BN3eO-*k0 zT8UcQH@ISdVN*M+L$wqA%rIiswz7CnnBY z*q_#SmN;)_qt|G_GHupzbiijt*`s;%jC=gQ>|{)Jd+MFOj(-`WU0V(f1%HjREJw2n z+V6}`nQijkU`cvZ(93dMqlEM+^zBIQbEywHeKO@=MolRVE1R1vP&hRXlRwjl8m9{Z z`khV?yiWdbpyec=dHw9)Q^6lRX6M$*-ixweMrQ^))+jH91#50G=HIk4vh**_mcaCO za-1RLD@bj0mF9^1X0Hi|C@5uf7aA7*P5-0UmX+_r-_~`{kl%g&Cz%;kMc%(Hd%^KV z&Fdr++42O3kSAeEKMRMY*gGccH7j@8%PM_G)zTV5C7eC&48(ieUL1V&&9boUOc(d+ zoz4==x`%8(*10cW%|2$M-Y_;`#cv*>AEuwb_o^yjUvKZurw)fxCb56ZEQNoz7#txq zR-T-uSTA;UWf62T8qQ^B;!Lvh6j*O;EL^Bm^Hlec!{YB4u1J^Fn>}oA*1sEO^RT_Y zBFjp6_p4!p&%f&c1RT5rbC zyNHI;qK~nhi7{J!eV3n8zcjFNw0kE;d#!uVK4)`G$(aC8drb|=-L$SdHa?+_^4XnPAhmuIe2qdUa-QO z-FT!TJw;>)|J>wXE=r=Y=_bDG*zA1@u; zuN+LcpqPKt+9+nTEZ?uu|6y7PrTDO{WHZ+gQ(p)Aai`TrSaM{??)#{ys_H($Utox8ns*!hd&W{lC4zfehjbg6&-7 z>eP9y_}=0UvU>NQnDDg)4(pu#i_NIHnd(oY`dxPY+g)xf)F<1PTL749hudh4&D?YP z%1~K0ChgHQC?UzaL)J*nX%IG`KHV-A=>J<_8rBOCv)AxvqmQ90_es;0k!@yAB#k2f zYq_nX-#D?hbClPmt$0tdInlLg$NIR>CPyV@>sH63fStWlhWcv&EM8+{;Z^=shW~gH zqiWIltxZ2Ad;3q=8tL_>eq>Q{IH^Gqd)f}q=V(n6Aqny$c(iy{)=u^vmI;k(plGd% zRGgnISyYj^vO2Z4-H>%-d|C5wY}>eR_~o}4u;t*)!nyS8@FtH}?v-!765USgfzfuV zI{y^Lic#*RtZ^4T8Lu&1l%dg9E?7D}c3%2-aGB)*t^ z58UJaaSpNvef~WTAAN@;m*<-X=)*h@jKhmt4I_Sza?pqixQsZws?JA$tH<>%zpsdP zSq$jXC@9Vk(-UX#x*ZCgGdI1|D!c75jI2_4JPL1n$Eacd(PQT5SKDvw7N@_-F(W5GJC4^C=m{#mLTckar^DUx`B}2DZB~5-0{H%$FzGzUw0S8>la7S}qLg6$eQN&YJtg8e3OPWz<8`Q!J*evF;^C)n zS1}b;<^4HfIYy2n654Ng5?*F-#4!L80DjSwV^=-SV1FvFG}?A=NqkFPvLT#$%2<+h z&XE0eE?HtbC#7w7St-XG7S_xgl=Gj6KOaAZ>})sr3s!}d)_9Oya-JK7^oN{@yjK3F z9qt4nfZO{ioPONiGqH8T_}@~4yev7B!4(->+a!)cRsanNo;YvF5LEeRj8CxUy4tW- zN!>#O#LIDCd4{Llbl5}nBd=o4I9MJ#H%q1TFFstYuD97dY_SV_h9;H_7}gbO|9-F4 zVOGwVA34r6twVwCkU`PD&Mn<*IUWR#wr!;n+&du?J^+I7^_fqrInqj{AgN>{ds~(FW7MqfvyZ@yr0HnbR%0iQGz7!*+FrT$DL|l=y<7I;zwhpa}x`-BD30Af4!_meC z34$kgkEw{DmwHI&Pb7FtGyJ9b5RgGFW#S#qBiZ@NYW34j!d;yd`e+#c?w!{Avo zxA9RI`r%k2ml71#v#zw>qZ9lq0hAA+A4F=WcL+N*Flf`b;Si!;?P<0VRCw6}NgsF|N%FK|V+`QI+cVTX zg!1;U|D*G>ojcnZQE7o!1%PiFIL1&WEFK-bJ=1mx05rVgOu)tzGH;BY$bixC4vh?7 z4agSS6%wCwK=6?1vxNiFv?k);wXQX5*kxj%NpvKJf_l9{TN0iT4MI&JL}aX42|o$y zqp!FP2`TLKxluh3fD;zDA`o~lC01B6uHJ!frmY5)`B9=69oPONzyMH-%uEv`=~F+v z{%dZ7SCNfzoTseol>kww>ro?UbUAVzdBn#* z=`Hvsz)BOV#^83AaG(#B*5#TCDA&f@q81n)sQk32y&?HzP@Ryt9h!F|mI$EzcaSR1 zO8Drb8+3(vK%s4%v@!xuFu?YSeE`L^0r^>*^tR0RTd7xLr|G;IF#^UpW~QDufbkdI zCUt*U%?hBniz}Y`Z|y34K)}^0yM{-)IsYBQX_zQf48+bDQ(4oDV=p408>*y?|5m{R z);JU9rc{8bk;FFDLJaOY_IMh18>|#B@n@MZ#+Oys$MxVIz)bx{6+vQ$ z<6Ly5(SPEU#DK^(^xeG1B2Ll=U`=^Ozee2-kQRFz>J$8^x z6H_+80Nf_pYvz0s!XlTf!bO|0`|7f3dg!vL!6N~81i-@{W)c`{%mf{5Sjm9~fu=_K z{hRIILXcT`F~MPSOP#KeKUo3AM(C%>(>ujANp3>Sw4Hp!1XRdM*DobjP2$}1sq~$} zBS;RKh&>Zyi|2^Y4+69qi-J}~3#rd81%*gdE6_4k_;3V(Bk)&F;e(>?=luwHDNTnk zr%@nuE$grg_3c%KsXaP15&}QUJ=VMnm>!-c#YjoR5WP>$hs{yd-L|~6{%=oa2=E~Ah6!xFk%BV3ZI0hN^5Z&5esqYYVqhqX zocf&lMc3`)Bz-sE2!Jgwy@W{F4$Ro+76|SE?amPT_oOlJ}}LigDqJG~da7Y&5amFum?@zJ+BY~XtZj46S3CF<`5Ld9O=z<*Leg1{0fJaegz z%DD8Aqo2HG%xI&>;JY}VItB>%o3I6#m#zn3_j`#mq-O7`K;!)m{C%)IwM?*hSsZDDHrotSR zj{oB$YYi<;Or^d~Ko1{>_~TGCtx4{qPHf=Jscols=o$iFMYGJDp+JMZX%kWJD@@nP z;9ZBSNA5{D?a>yrl>dV_IXP91Dvtq|c#&A9aW1??Qr|ly?c^orQLDyrEa3%>q-B~Y zw0tNNm2+PX*_6AV@c2HcD>0D|z}ro9x4*ENk>mG+@*Xm)E1vOQ+mVGBGr@Y#(0MvVjlvr5BQyuklz7U^_Y9F$cfQLYk~)s2uM^z z>+IvZb2M2&Wl2^c5j#|kTJue$L31+z7iXEryz#c8 z&{X_?GIXGhcXLnD&vaQKd6Qo_KOAS8`#e3JSnzFJ&0^PC9Q8)n@c_+yUvBvUG#(}w z&CySBJu_gRBb=H?!w3%!hZ)0I=*C#ujPGuD->Z-X!T34+a7tl&L2ex{6MXD1y^w+s zVi)F656pKfG-~KqdQlCOA12Zn9-{m`hV%?paIQ3*%xEwO`tRN6VO&)NHvc>G$6QLXrtvNSgw|tz(FKE&(gDnEkSk0`w%eH2@r!uTbwEjZVubQeF4b{ zjFkiFG@U=TuUcgPl1_I2O~piM4-7H9PVZjm9{YlOSU=e)MHA8`#$EjjrWYJn-`Y-F&^Xwpj{g;0plWT z?;q;#lWkHa{u^oq#4c!pBd0+HJ(9QdfEL2|J$GI1F;&h!NHWN}U2aset9S21fgCL0 zA!&@4e~J*|J%^aWH>X&LHhCy2+Z7~L75qI5|AJNe#XiEnJn;CJJZ)~s68}xr0SiYa8H{~ek9dJGGE)QQf0L<2mYLrP%JyHBPMM-9pL=Ap5zPRe@8Nb-!rU#;x4Ma@BNQaC_Wtt=T}XM~pVxcxHEgQC zBA^T^>{ST9zZKWl1!t;A)w1ZMq+}#OW=lcgcm_xso`=*09J9dKb?(RZ5wKGVfZJU#J@$KaQ8{H^B$EoH6LXz%&ri9d+S*-eKcfdddp8dJ`7P24ffMU`%**rg!dK$Ih# zf`FeQ>jC2BM?10hxU$~4P)&&jUd}pSY$(C&mCR`v2;MSaZvV1->Z6YwT~Bw>q^HtV z-|`?JNguuGPJzP@()Qkz^o`Bx)p*NvYTs_J<&_)E1pvtf4tEFm(mIYEg0}@UGlPjM zk-4a#2V5KJ2PjudIyz`Y<0LhK>~bnO}9 zENu_~3wo6Lk(1uz$l6f10AlZ6&<8KrvD6zNmcJHw4p1tn_S44HW^nW7XvylLFbhq_+{Y0L$xca4gl&p(K|Ko-C<^Bi?~$Ju@Sw}1h!sqq0o%X-2*T

Yva97xjp$vbLD`vBmEX-2Aty1{@It` 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 iXPx%LMgK2f1j`>2Z^ok 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

@r}WDp@dtV*&r|F_cT$Ky+lQ9!1#c& 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 + * By disabling this all callbacks will be called from a thread other than + * the main UI thread. + */ + public static boolean handlerEnabled = true; + + + /** + * Setting this will change the default command timeout. + *

+ * The default is 20000ms + */ + public static int defaultCommandTimeout = 20000; + + public enum LogLevel { + VERBOSE, + ERROR, + DEBUG, + WARN + } + // -------------------- + // # Public Methods # + // -------------------- + + /** + * This will close all open shells. + */ + public static void closeAllShells() throws IOException { + Shell.closeAll(); + } + + /** + * This will close the custom shell that you opened. + */ + public static void closeCustomShell() throws IOException { + Shell.closeCustomShell(); + } + + /** + * This will close either the root shell or the standard shell depending on what you specify. + * + * @param root a boolean to specify whether to close the root shell or the standard shell. + */ + public static void closeShell(boolean root) throws IOException { + if (root) { + Shell.closeRootShell(); + } else { + Shell.closeShell(); + } + } + + /** + * Use this to check whether or not a file exists on the filesystem. + * + * @param file String that represent the file, including the full path to the + * file and its name. + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file) { + return exists(file, false); + } + + /** + * Use this to check whether or not a file OR directory exists on the filesystem. + * + * @param file String that represent the file OR the directory, including the full path to the + * file and its name. + * @param isDir boolean that represent whether or not we are looking for a directory + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file, boolean isDir) { + final List result = new ArrayList(); + + String cmdToExecute = "ls " + (isDir ? "-d " : " "); + + Command command = new Command(0, false, cmdToExecute + file) { + @Override + public void commandOutput(int id, String line) { + RootShell.log(line); + result.add(line); + + super.commandOutput(id, line); + } + }; + + try { + //Try without root... + RootShell.getShell(false).add(command); + commandWait(RootShell.getShell(false), command); + + } catch (Exception e) { + RootShell.log("Exception: " + e); + return false; + } + + for (String line : result) { + if (line.trim().equals(file)) { + return true; + } + } + + result.clear(); + + command = new Command(0, false, cmdToExecute + file) { + @Override + public void commandOutput(int id, String line) { + RootShell.log(line); + result.add(line); + + super.commandOutput(id, line); + } + }; + + try { + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + + } catch (Exception e) { + RootShell.log("Exception: " + e); + return false; + } + + //Avoid concurrent modification... + List final_result = new ArrayList(result); + + for (String line : final_result) { + if (line.trim().equals(file)) { + return true; + } + } + + return false; + + } + + /** + * @param binaryName String that represent the binary to find. + * @param singlePath boolean that represents whether to return a single path or multiple. + * + * @return List containing the locations the binary was found at. + */ + public static List findBinary(String binaryName, boolean singlePath) { + return findBinary(binaryName, null, singlePath); + } + + /** + * @param binaryName String that represent the binary to find. + * @param searchPaths List which contains the paths to search for this binary in. + * @param singlePath boolean that represents whether to return a single path or multiple. + * + * @return List containing the locations the binary was found at. + */ + public static List findBinary(final String binaryName, List searchPaths, boolean singlePath) { + + final List foundPaths = new ArrayList(); + + boolean found = false; + + if(searchPaths == null) + { + searchPaths = RootShell.getPath(); + } + + RootShell.log("Checking for " + binaryName); + + //Try to use stat first + try { + for (String path : searchPaths) { + + if(!path.endsWith("/")) + { + path += "/"; + } + + final String currentPath = path; + + Command cc = new Command(0, false, "stat " + path + binaryName) { + @Override + public void commandOutput(int id, String line) { + if (line.contains("File: ") && line.contains(binaryName)) { + foundPaths.add(currentPath); + + RootShell.log(binaryName + " was found here: " + currentPath); + } + + RootShell.log(line); + + super.commandOutput(id, line); + } + }; + + cc = RootShell.getShell(false).add(cc); + commandWait(RootShell.getShell(false), cc); + + if(foundPaths.size() > 0 && singlePath) { + break; + } + } + + found = !foundPaths.isEmpty(); + + } catch (Exception e) { + RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on."); + } + + if (!found) { + RootShell.log("Trying second method"); + + for (String path : searchPaths) { + + if(!path.endsWith("/")) + { + path += "/"; + } + + if (RootShell.exists(path + binaryName)) { + RootShell.log(binaryName + " was found here: " + path); + foundPaths.add(path); + + if(foundPaths.size() > 0 && singlePath) { + break; + } + + } else { + RootShell.log(binaryName + " was NOT found here: " + path); + } + } + } + + Collections.reverse(foundPaths); + + return foundPaths; + } + + /** + * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param shellPath a String to Indicate the path to the shell that you want to open. + * @param timeout an int to Indicate the length of time before giving up on opening a shell. + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException + { + return RootShell.getCustomShell(shellPath, timeout); + } + + /** + * This will return the environment variable PATH + * + * @return List A List of Strings representing the environment variable $PATH + */ + public static List getPath() { + return Arrays.asList(System.getenv("PATH").split(":")); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + * @param retry a int to indicate how many times the ROOT shell should try to open with root priviliges... + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + if (root) { + return Shell.startRootShell(timeout, shellContext, retry); + } else { + return Shell.startShell(timeout); + } + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param shellContext the context to execute the shell with + */ + public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, 0, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + */ + public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, Shell.defaultContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + */ + public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException { + return RootShell.getShell(root, 0); + } + + /** + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven() { + return isAccessGiven(0,3); + } + public static boolean isAccessGiven(int timeout, int retry) { + final Set ID = new HashSet(); + final int IAG = 158; + + try { + RootShell.log("Checking for Root access"); + + Command command = new Command(IAG, false, "id") { + @Override + public void commandOutput(int id, String line) { + if (id == IAG) { + ID.addAll(Arrays.asList(line.split(" "))); + } + super.commandOutput(id, line); + } + }; + + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + //parse the userid + for (String userid : ID) { + RootShell.log(userid); + + if (userid.toLowerCase().contains("uid=0")) { + RootShell.log("Access Given"); + return true; + } + } + + return false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * @return true if BusyBox or Toybox was found. + */ + public static boolean isBusyboxAvailable() + { + return (findBinary("busybox", true)).size() > 0; + } + + /** + * @return true if su was found. + */ + public static boolean isRootAvailable() { + return (findBinary("su", true)).size() > 0; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + */ + public static void log(String msg) { + log(null, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + */ + public static void log(String TAG, String msg) { + log(TAG, msg, LogLevel.DEBUG, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug, 4 for warn + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String msg, LogLevel type, Exception e) { + log(null, msg, type, e); + } + + /** + * This method allows you to check whether logging is enabled. + * Yes, it has a goofy name, but that's to keep it as short as possible. + * After all writing logging calls should be painless. + * This method exists to save Android going through the various Java layers + * that are traversed any time a string is created (i.e. what you are logging) + *

+ * Example usage: + * if(islog) { + * StrinbBuilder sb = new StringBuilder(); + * // ... + * // build string + * // ... + * log(sb.toString()); + * } + * + * @return true if logging is enabled + */ + public static boolean islog() { + return debugMode; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootShell.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String TAG, String msg, LogLevel type, Exception e) { + if (msg != null && !msg.equals("")) { + if (debugMode) { + if (TAG == null) { + TAG = version; + } + + switch (type) { + case VERBOSE: + Log.v(TAG, msg); + break; + case ERROR: + Log.e(TAG, msg, e); + break; + case DEBUG: + Log.d(TAG, msg); + break; + case WARN: + Log.w(TAG, msg); + break; + } + } + } + } + + // -------------------- + // # Public Methods # + // -------------------- + + private static void commandWait(Shell shell, Command cmd) throws Exception { + while (!cmd.isFinished()) { + + RootShell.log(version, shell.getCommandQueuePositionString(cmd)); + RootShell.log(version, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command."); + + synchronized (cmd) { + try { + if (!cmd.isFinished()) { + cmd.wait(2000); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + if (!cmd.isExecuting() && !cmd.isFinished()) { + if (!shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else if (shell.isExecuting && !shell.isReading) { + RootShell.log(version, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else { + RootShell.log(version, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } + } + + } + } +} diff --git a/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java b/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java new file mode 100755 index 0000000..f2a572a --- /dev/null +++ b/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java @@ -0,0 +1,415 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.rootshell; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.StrictMode; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.stericson.rootshell.exceptions.RootDeniedException; +import com.stericson.rootshell.execution.Command; +import com.stericson.rootshell.execution.Shell; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeoutException; + +public class SanityCheckRootShell extends Activity +{ + private ScrollView mScrollView; + private TextView mTextView; + private ProgressDialog mPDialog; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() // or .detectAll() for all detectable problems + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build()); + + RootShell.debugMode = true; + + mTextView = new TextView(this); + mTextView.setText(""); + mScrollView = new ScrollView(this); + mScrollView.addView(mTextView); + setContentView(mScrollView); + + print("SanityCheckRootShell \n\n"); + + if (RootShell.isRootAvailable()) + { + print("Root found.\n"); + } + else + { + print("Root not found"); + } + + try + { + RootShell.getShell(true); + } + catch (IOException e2) + { + // TODO Auto-generated catch block + e2.printStackTrace(); + } + catch (TimeoutException e) + { + print("[ TIMEOUT EXCEPTION! ]\n"); + e.printStackTrace(); + } + catch (RootDeniedException e) + { + print("[ ROOT DENIED EXCEPTION! ]\n"); + e.printStackTrace(); + } + + try + { + if (!RootShell.isAccessGiven()) + { + print("ERROR: No root access to this device.\n"); + return; + } + } + catch (Exception e) + { + print("ERROR: could not determine root access to this device.\n"); + return; + } + + // Display infinite progress bar + mPDialog = new ProgressDialog(this); + mPDialog.setCancelable(false); + mPDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + + new SanityCheckThread(this, new TestHandler()).start(); + } + + protected void print(CharSequence text) + { + mTextView.append(text); + mScrollView.post(new Runnable() + { + public void run() + { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + } + + // Run our long-running tests in their separate thread so as to + // not interfere with proper rendering. + private class SanityCheckThread extends Thread + { + private final Handler mHandler; + + public SanityCheckThread(Context context, Handler handler) + { + mHandler = handler; + } + + public void run() + { + visualUpdate(TestHandler.ACTION_SHOW, null); + + // First test: Install a binary file for future use + // if it wasn't already installed. + /* + visualUpdate(TestHandler.ACTION_PDISPLAY, "Installing binary if needed"); + if(false == RootShell.installBinary(mContext, R.raw.nes, "nes_binary")) { + visualUpdate(TestHandler.ACTION_HIDE, "ERROR: Failed to install binary. Please see log file."); + return; + } + */ + + boolean result; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getPath"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ getPath ]\n"); + + try + { + List paths = RootShell.getPath(); + + for (String path : paths) + { + visualUpdate(TestHandler.ACTION_DISPLAY, path + " k\n\n"); + } + + } + catch (Exception e) + { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing A ton of commands"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Ton of Commands ]\n"); + + for (int i = 0; i < 100; i++) + { + RootShell.exists("/system/xbin/busybox"); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Find Binary"); + result = RootShell.isRootAvailable(); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Root ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + result = RootShell.isBusyboxAvailable(); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Busybox ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing file exists"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Exists() ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootShell.exists("/system/sbin/[") + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Is Access Given"); + result = RootShell.isAccessGiven(); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking for Access to Root ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + + Shell shell; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing output capture"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ busybox ash --help ]\n"); + + try + { + shell = RootShell.getShell(true); + Command cmd = new Command( + 0, + "busybox ash --help") + { + + @Override + public void commandOutput(int id, String line) + { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + //super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - SYSTEM_APP"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - SYSTEM_APP ]\n"); + + try + { + shell = RootShell.getShell(true, Shell.ShellContext.SYSTEM_APP); + Command cmd = new Command( + 0, + "id") + { + + @Override + public void commandOutput(int id, String line) + { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - UNTRUSTED"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - UNTRUSTED ]\n"); + + try + { + shell = RootShell.getShell(true, Shell.ShellContext.UNTRUSTED_APP); + Command cmd = new Command( + 0, + "id") + { + + @Override + public void commandOutput(int id, String line) + { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + try + { + shell = RootShell.getShell(true); + + Command cmd = new Command(42, false, "echo done") + { + + boolean _catch = false; + + @Override + public void commandOutput(int id, String line) + { + if (_catch) + { + RootShell.log("CAUGHT!!!"); + } + + super.commandOutput(id, line); + + } + + @Override + public void commandTerminated(int id, String reason) + { + synchronized (SanityCheckRootShell.this) + { + + _catch = true; + visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete."); + visualUpdate(TestHandler.ACTION_HIDE, null); + + try + { + RootShell.closeAllShells(); + } + catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + + @Override + public void commandCompleted(int id, int exitCode) + { + synchronized (SanityCheckRootShell.this) + { + _catch = true; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete."); + visualUpdate(TestHandler.ACTION_HIDE, null); + + try + { + RootShell.closeAllShells(); + } + catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + }; + + shell.add(cmd); + + } + catch (Exception e) + { + e.printStackTrace(); + } + + } + + private void visualUpdate(int action, String text) + { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(TestHandler.ACTION, action); + bundle.putString(TestHandler.TEXT, text); + msg.setData(bundle); + mHandler.sendMessage(msg); + } + } + + private class TestHandler extends Handler + { + static final public String ACTION = "action"; + static final public int ACTION_SHOW = 0x01; + static final public int ACTION_HIDE = 0x02; + static final public int ACTION_DISPLAY = 0x03; + static final public int ACTION_PDISPLAY = 0x04; + static final public String TEXT = "text"; + + public void handleMessage(Message msg) + { + int action = msg.getData().getInt(ACTION); + String text = msg.getData().getString(TEXT); + + switch (action) + { + case ACTION_SHOW: + mPDialog.show(); + mPDialog.setMessage("Running Root Library Tests..."); + break; + case ACTION_HIDE: + if (null != text) + { print(text); } + mPDialog.hide(); + break; + case ACTION_DISPLAY: + print(text); + break; + case ACTION_PDISPLAY: + mPDialog.setMessage(text); + break; + } + } + } +} diff --git a/app/src/main/java/com/stericson/rootshell/containers/RootClass.java b/app/src/main/java/com/stericson/rootshell/containers/RootClass.java new file mode 100644 index 0000000..239812a --- /dev/null +++ b/app/src/main/java/com/stericson/rootshell/containers/RootClass.java @@ -0,0 +1,328 @@ +package com.stericson.rootshell.containers; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* #ANNOTATIONS @SupportedAnnotationTypes("com.stericson.RootShell.containers.RootClass.Candidate") */ +/* #ANNOTATIONS @SupportedSourceVersion(SourceVersion.RELEASE_6) */ +public class RootClass /* #ANNOTATIONS extends AbstractProcessor */ { + + /* #ANNOTATIONS + @Override + public boolean process(Set typeElements, RoundEnvironment roundEnvironment) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "I was invoked!!!"); + + return false; + } + */ + + static String PATH_TO_DX = "/Users/Chris/Projects/android-sdk-macosx/build-tools/18.0.1/dx"; + + enum READ_STATE { + STARTING, FOUND_ANNOTATION + } + + public RootClass(String[] args) throws ClassNotFoundException, NoSuchMethodException, + IllegalAccessException, InvocationTargetException, InstantiationException { + + // Note: rather than calling System.load("/system/lib/libandroid_runtime.so"); + // which would leave a bunch of unresolved JNI references, + // we are using the 'withFramework' class as a preloader. + // So, yeah, russian dolls: withFramework > RootClass > actual method + + String className = args[0]; + RootArgs actualArgs = new RootArgs(); + actualArgs.args = new String[args.length - 1]; + System.arraycopy(args, 1, actualArgs.args, 0, args.length - 1); + Class classHandler = Class.forName(className); + Constructor classConstructor = classHandler.getConstructor(RootArgs.class); + classConstructor.newInstance(actualArgs); + } + + public @interface Candidate { + + } + + public static class RootArgs { + + public String[] args; + } + + static void displayError(Exception e) { + // Not using system.err to make it easier to capture from + // calling library. + System.out.println("##ERR##" + e.getMessage() + "##"); + e.printStackTrace(); + } + + // I reckon it would be better to investigate classes using getAttribute() + // however this method allows the developer to simply select "Run" on RootClass + // and immediately re-generate the necessary jar file. + static public class AnnotationsFinder { + + private final String AVOIDDIRPATH = "stericson" + File.separator + "RootShell" + File.separator; + + private final List classFiles; + + public AnnotationsFinder() throws IOException { + System.out.println("Discovering root class annotations..."); + classFiles = new ArrayList(); + lookup(new File("src"), classFiles); + System.out.println("Done discovering annotations. Building jar file."); + File builtPath = getBuiltPath(); + if (null != builtPath) { + // Android! Y U no have com.google.common.base.Joiner class? + String rc1 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass.class"; + String rc2 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$RootArgs.class"; + String rc3 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder.class"; + String rc4 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$1.class"; + String rc5 = "com" + File.separator + + "stericson" + File.separator + + "RootShell" + File.separator + + "containers" + File.separator + + "RootClass$AnnotationsFinder$2.class"; + String[] cmd; + boolean onWindows = (-1 != System.getProperty("os.name").toLowerCase().indexOf("win")); + if (onWindows) { + StringBuilder sb = new StringBuilder( + " " + rc1 + " " + rc2 + " " + rc3 + " " + rc4 + " " + rc5 + ); + for (File file : classFiles) { + sb.append(" ").append(file.getPath()); + } + cmd = new String[]{ + "cmd", "/C", + "jar cvf" + + " anbuild.jar" + + sb.toString() + }; + } else { + ArrayList al = new ArrayList(); + al.add("jar"); + al.add("cf"); + al.add("anbuild.jar"); + al.add(rc1); + al.add(rc2); + al.add(rc3); + al.add(rc4); + al.add(rc5); + for (File file : classFiles) { + al.add(file.getPath()); + } + cmd = al.toArray(new String[0]); + } + ProcessBuilder jarBuilder = new ProcessBuilder(cmd); + jarBuilder.directory(builtPath); + try { + jarBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + + File rawFolder = new File("res" + File.separator + "raw"); + if (!rawFolder.exists()) { + rawFolder.mkdirs(); + } + + System.out.println("Done building jar file. Creating dex file."); + if (onWindows) { + cmd = new String[]{ + "cmd", "/C", + "dx --dex --output=res" + File.separator + "raw" + File.separator + "anbuild.dex " + + builtPath + File.separator + "anbuild.jar" + }; + } else { + cmd = new String[]{ + getPathToDx(), + "--dex", + "--output=res" + File.separator + "raw" + File.separator + "anbuild.dex", + builtPath + File.separator + "anbuild.jar" + }; + } + ProcessBuilder dexBuilder = new ProcessBuilder(cmd); + try { + dexBuilder.start().waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + } + System.out.println("All done. ::: anbuild.dex should now be in your project's res" + File.separator + "raw" + File.separator + " folder :::"); + } + + protected void lookup(File path, List fileList) { + String desourcedPath = path.toString().replace("src" + File.separator, ""); + File[] files = path.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + if (-1 == file.getAbsolutePath().indexOf(AVOIDDIRPATH)) { + lookup(file, fileList); + } + } else { + if (file.getName().endsWith(".java")) { + if (hasClassAnnotation(file)) { + final String fileNamePrefix = file.getName().replace(".java", ""); + final File compiledPath = new File(getBuiltPath().toString() + File.separator + desourcedPath); + File[] classAndInnerClassFiles = compiledPath.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(fileNamePrefix); + } + }); + for (final File matchingFile : classAndInnerClassFiles) { + fileList.add(new File(desourcedPath + File.separator + matchingFile.getName())); + } + + } + } + } + } + } + + protected boolean hasClassAnnotation(File file) { + READ_STATE readState = READ_STATE.STARTING; + Pattern p = Pattern.compile(" class ([A-Za-z0-9_]+)"); + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while (null != (line = reader.readLine())) { + switch (readState) { + case STARTING: + if (-1 < line.indexOf("@RootClass.Candidate")) { + readState = READ_STATE.FOUND_ANNOTATION; + } + break; + case FOUND_ANNOTATION: + Matcher m = p.matcher(line); + if (m.find()) { + System.out.println(" Found annotated class: " + m.group(0)); + return true; + } else { + System.err.println("Error: unmatched annotation in " + + file.getAbsolutePath()); + readState = READ_STATE.STARTING; + } + break; + } + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + + protected String getPathToDx() throws IOException { + String androidHome = System.getenv("ANDROID_HOME"); + if (null == androidHome) { + throw new IOException("Error: you need to set $ANDROID_HOME globally"); + } + String dxPath = null; + File[] files = new File(androidHome + File.separator + "build-tools").listFiles(); + int recentSdkVersion = 0; + for (File file : files) { + + String fileName = null; + if (file.getName().contains("-")) { + String[] splitFileName = file.getName().split("-"); + if (splitFileName[1].contains("W")) { + char[] fileNameChars = splitFileName[1].toCharArray(); + fileName = String.valueOf(fileNameChars[0]); + } else { + fileName = splitFileName[1]; + } + } else { + fileName = file.getName(); + } + + int sdkVersion; + + String[] sdkVersionBits = fileName.split("[.]"); + sdkVersion = Integer.parseInt(sdkVersionBits[0]) * 10000; + if (sdkVersionBits.length > 1) { + sdkVersion += Integer.parseInt(sdkVersionBits[1]) * 100; + if (sdkVersionBits.length > 2) { + sdkVersion += Integer.parseInt(sdkVersionBits[2]); + } + } + if (sdkVersion > recentSdkVersion) { + String tentativePath = file.getAbsolutePath() + File.separator + "dx"; + if (new File(tentativePath).exists()) { + recentSdkVersion = sdkVersion; + dxPath = tentativePath; + } + } + } + if (dxPath == null) { + throw new IOException("Error: unable to find dx binary in $ANDROID_HOME"); + } + return dxPath; + } + + protected File getBuiltPath() { + File foundPath = null; + + File ideaPath = new File("out" + File.separator + "production"); // IntelliJ + if (ideaPath.isDirectory()) { + File[] children = ideaPath.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + if (children.length > 0) { + foundPath = new File(ideaPath.getAbsolutePath() + File.separator + children[0].getName()); + } + } + if (null == foundPath) { + File eclipsePath = new File("bin" + File.separator + "classes"); // Eclipse IDE + if (eclipsePath.isDirectory()) { + foundPath = eclipsePath; + } + } + + return foundPath; + } + + + } + + public static void main(String[] args) { + try { + if (args.length == 0) { + new AnnotationsFinder(); + } else { + new RootClass(args); + } + } catch (Exception e) { + displayError(e); + } + } +} diff --git a/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java b/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java new file mode 100644 index 0000000..4ee3849 --- /dev/null +++ b/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java @@ -0,0 +1,32 @@ +/* + * This file is part of the RootShell Project: https://github.com/Stericson/RootShell + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.rootshell.exceptions; + +public class RootDeniedException extends Exception { + + private static final long serialVersionUID = -8713947214162841310L; + + public RootDeniedException(String error) { + super(error); + } +} diff --git a/app/src/main/java/com/stericson/rootshell/execution/Command.java b/app/src/main/java/com/stericson/rootshell/execution/Command.java new file mode 100644 index 0000000..f4b5ec9 --- /dev/null +++ b/app/src/main/java/com/stericson/rootshell/execution/Command.java @@ -0,0 +1,325 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.rootshell.execution; + + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import com.stericson.rootshell.RootShell; + +import java.io.IOException; + +public class Command { + + //directly modified by JavaCommand + protected boolean javaCommand = false; + protected Context context = null; + + public int totalOutput = 0; + + public int totalOutputProcessed = 0; + + ExecutionMonitor executionMonitor = null; + + Handler mHandler = null; + + //Has this command already been used? + protected boolean used = false; + + boolean executing = false; + + String[] command = {}; + + boolean finished = false; + + boolean terminated = false; + + boolean handlerEnabled = true; + + int exitCode = -1; + + int id = 0; + + int timeout = RootShell.defaultCommandTimeout; + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param command the command, or commands, to be executed. + */ + public Command(int id, String... command) { + this.command = command; + this.id = id; + + createHandler(RootShell.handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param handlerEnabled when true the handler will be used to call the + * callback methods if possible. + * @param command the command, or commands, to be executed. + */ + public Command(int id, boolean handlerEnabled, String... command) { + this.command = command; + this.id = id; + + createHandler(handlerEnabled); + } + + /** + * Constructor for executing a normal shell command + * + * @param id the id of the command being executed + * @param timeout the time allowed before the shell will give up executing the command + * and throw a TimeoutException. + * @param command the command, or commands, to be executed. + */ + public Command(int id, int timeout, String... command) { + this.command = command; + this.id = id; + this.timeout = timeout; + + createHandler(RootShell.handlerEnabled); + } + + //If you override this you MUST make a final call + //to the super method. The super call should be the last line of this method. + public void commandOutput(int id, String line) { + RootShell.log("Command", "ID: " + id + ", " + line); + totalOutputProcessed++; + } + + public void commandTerminated(int id, String reason) { + //pass + } + + public void commandCompleted(int id, int exitcode) { + //pass + } + + protected final void commandFinished() { + if (!terminated) { + synchronized (this) { + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_COMPLETED); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandCompleted(id, exitCode); + } + + RootShell.log("Command " + id + " finished."); + finishCommand(); + } + } + } + + private void createHandler(boolean handlerEnabled) { + + this.handlerEnabled = handlerEnabled; + + if (Looper.myLooper() != null && handlerEnabled) { + RootShell.log("CommandHandler created"); + mHandler = new CommandHandler(); + } else { + RootShell.log("CommandHandler not created"); + } + } + + public final void finish() + { + RootShell.log("Command finished at users request!"); + commandFinished(); + } + + protected final void finishCommand() { + this.executing = false; + this.finished = true; + this.notifyAll(); + } + + + public final String getCommand() { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < command.length; i++) { + if (i > 0) { + sb.append('\n'); + } + + sb.append(command[i]); + } + + return sb.toString(); + } + + public final boolean isExecuting() { + return executing; + } + + public final boolean isHandlerEnabled() { + return handlerEnabled; + } + + public final boolean isFinished() { + return finished; + } + + public final int getExitCode() { + return this.exitCode; + } + + protected final void setExitCode(int code) { + synchronized (this) { + exitCode = code; + } + } + + protected final void startExecution() { + this.used = true; + executionMonitor = new ExecutionMonitor(this); + executionMonitor.setPriority(Thread.MIN_PRIORITY); + executionMonitor.start(); + executing = true; + } + + public final void terminate() + { + RootShell.log("Terminating command at users request!"); + terminated("Terminated at users request!"); + } + + protected final void terminate(String reason) { + try { + Shell.closeAll(); + RootShell.log("Terminating all shells."); + terminated(reason); + } catch (IOException e) { + } + } + + protected final void terminated(String reason) { + synchronized (Command.this) { + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_TERMINATED); + bundle.putString(CommandHandler.TEXT, reason); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandTerminated(id, reason); + } + + RootShell.log("Command " + id + " did not finish because it was terminated. Termination reason: " + reason); + setExitCode(-1); + terminated = true; + finishCommand(); + } + } + + protected final void output(int id, String line) { + totalOutput++; + + if (mHandler != null && handlerEnabled) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_OUTPUT); + bundle.putString(CommandHandler.TEXT, line); + msg.setData(bundle); + mHandler.sendMessage(msg); + } else { + commandOutput(id, line); + } + } + + private class ExecutionMonitor extends Thread { + + private final Command command; + + public ExecutionMonitor(Command command) { + this.command = command; + } + + public void run() { + + if(command.timeout > 0) + { + synchronized (command) { + try { + RootShell.log("Command " + command.id + " is waiting for: " + command.timeout); + command.wait(command.timeout); + } catch (InterruptedException e) { + RootShell.log("Exception: " + e); + } + + if (!command.isFinished()) { + RootShell.log("Timeout Exception has occurred for command: " + command.id + "."); + terminate("Timeout Exception"); + } + } + } + } + } + + private class CommandHandler extends Handler { + + static final public String ACTION = "action"; + + static final public String TEXT = "text"; + + static final public int COMMAND_OUTPUT = 0x01; + + static final public int COMMAND_COMPLETED = 0x02; + + static final public int COMMAND_TERMINATED = 0x03; + + public final void handleMessage(Message msg) { + int action = msg.getData().getInt(ACTION); + String text = msg.getData().getString(TEXT); + + switch (action) { + case COMMAND_OUTPUT: + commandOutput(id, text); + break; + case COMMAND_COMPLETED: + commandCompleted(id, exitCode); + break; + case COMMAND_TERMINATED: + commandTerminated(id, text); + break; + } + } + } +} diff --git a/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java b/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java new file mode 100644 index 0000000..4614e21 --- /dev/null +++ b/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java @@ -0,0 +1,58 @@ +package com.stericson.rootshell.execution; + +import android.content.Context; + +public class JavaCommand extends Command +{ + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, Context context, String... command) { + super(id, command); + this.context = context; + this.javaCommand = true; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, boolean handlerEnabled, Context context, String... command) { + super(id, handlerEnabled, command); + this.context = context; + this.javaCommand = true; + } + + /** + * Constructor for executing Java commands rather than binaries + * + * @param context needed to execute java command. + */ + public JavaCommand(int id, int timeout, Context context, String... command) { + super(id, timeout, command); + this.context = context; + this.javaCommand = true; + } + + + @Override + public void commandOutput(int id, String line) + { + super.commandOutput(id, line); + } + + @Override + public void commandTerminated(int id, String reason) + { + // pass + } + + @Override + public void commandCompleted(int id, int exitCode) + { + // pass + } +} diff --git a/app/src/main/java/com/stericson/rootshell/execution/Shell.java b/app/src/main/java/com/stericson/rootshell/execution/Shell.java new file mode 100644 index 0000000..f5a522c --- /dev/null +++ b/app/src/main/java/com/stericson/rootshell/execution/Shell.java @@ -0,0 +1,1029 @@ +/* + * This file is part of the RootShell Project: http://code.google.com/p/RootShell/ + * + * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ +package com.stericson.rootshell.execution; + + +import android.content.Context; + +import com.stericson.rootshell.RootShell; +import com.stericson.rootshell.exceptions.RootDeniedException; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +public class Shell { + + public enum ShellType { + NORMAL, + ROOT, + CUSTOM + } + + //this is only used with root shells + public enum ShellContext { + NORMAL("normal"), //The normal context... + SHELL("u:r:shell:s0"), //unprivileged shell (such as an adb shell) + SYSTEM_SERVER("u:r:system_server:s0"), // system_server, u:r:system:s0 on some firmwares + SYSTEM_APP("u:r:system_app:s0"), // System apps + PLATFORM_APP("u:r:platform_app:s0"), // System apps + UNTRUSTED_APP("u:r:untrusted_app:s0"), // Third-party apps + RECOVERY("u:r:recovery:s0"); //Recovery + + private final String value; + + ShellContext(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + } + + //Statics -- visible to all + private static final String token = "F*D^W@#FGF"; + + private static Shell rootShell = null; + + private static Shell shell = null; + + private static Shell customShell = null; + + private static final String[] suVersion = new String[]{ + null, null + }; + + //the default context for root shells... + public static ShellContext defaultContext = ShellContext.NORMAL; + + //per shell + private int shellTimeout = 25000; + + private ShellType shellType = null; + + private ShellContext shellContext = ShellContext.NORMAL; + + private String error = ""; + + private final Process proc; + + private final BufferedReader inputStream; + + private final BufferedReader errorStream; + + private final OutputStreamWriter outputStream; + + private final List commands = new ArrayList(); + + //indicates whether or not to close the shell + private boolean close = false; + + private Boolean isSELinuxEnforcing = null; + + public boolean isExecuting = false; + + public boolean isReading = false; + + public boolean isClosed = false; + + private final int maxCommands = 5000; + + private int read = 0; + + private int write = 0; + + private int totalExecuted = 0; + + private int totalRead = 0; + + private boolean isCleaning = false; + + private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException { + + RootShell.log("Starting shell: " + cmd); + RootShell.log("Context: " + shellContext.getValue()); + RootShell.log("Timeout: " + shellTimeout); + + this.shellType = shellType; + this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout; + this.shellContext = shellContext; + + if (this.shellContext == ShellContext.NORMAL) { + this.proc = Runtime.getRuntime().exec(cmd); + } else { + String display = getSuVersion(false); + String internal = getSuVersion(true); + + //only done for root shell... + //Right now only SUPERSU supports the --context switch + if (isSELinuxEnforcing() && + (display != null) && + (internal != null) && + (display.endsWith("SUPERSU")) && + (Integer.valueOf(internal) >= 190)) { + cmd += " --context " + this.shellContext.getValue(); + } else { + RootShell.log("Su binary --context switch not supported!"); + RootShell.log("Su binary display version: " + display); + RootShell.log("Su binary internal version: " + internal); + RootShell.log("SELinuxEnforcing: " + isSELinuxEnforcing()); + } + + this.proc = Runtime.getRuntime().exec(cmd); + + } + + this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), StandardCharsets.UTF_8)); + this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), StandardCharsets.UTF_8)); + this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), StandardCharsets.UTF_8); + + /** + * Thread responsible for carrying out the requested operations + */ + Worker worker = new Worker(this); + worker.start(); + + try { + /** + * The flow of execution will wait for the thread to die or wait until the + * given timeout has expired. + * + * The result of the worker, which is determined by the exit code of the worker, + * will tell us if the operation was completed successfully or it the operation + * failed. + */ + worker.join(this.shellTimeout); + + /** + * The operation could not be completed before the timeout occurred. + */ + if (worker.exit == -911) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new TimeoutException(this.error); + } + /** + * Root access denied? + */ + else if (worker.exit == -42) { + + try { + this.proc.destroy(); + } catch (Exception e) { + } + + closeQuietly(this.inputStream); + closeQuietly(this.errorStream); + closeQuietly(this.outputStream); + + throw new RootDeniedException("Root Access Denied"); + } + /** + * Normal exit + */ + else { + /** + * The shell is open. + * + * Start two threads, one to handle the input and one to handle the output. + * + * input, and output are runnables that the threads execute. + */ + Thread si = new Thread(this.input, "Shell Input"); + si.setPriority(Thread.NORM_PRIORITY); + si.start(); + + Thread so = new Thread(this.output, "Shell Output"); + so.setPriority(Thread.NORM_PRIORITY); + so.start(); + } + } catch (InterruptedException ex) { + worker.interrupt(); + Thread.currentThread().interrupt(); + throw new TimeoutException(); + } + } + + + public Command add(Command command) throws IOException { + if (this.close) { + throw new IllegalStateException( + "Unable to add commands to a closed shell"); + } + + if(command.used) { + //The command has been used, don't re-use... + throw new IllegalStateException( + "This command has already been executed. (Don't re-use command instances.)"); + } + + while (this.isCleaning) { + //Don't add commands while cleaning + } + + this.commands.add(command); + + this.notifyThreads(); + + return command; + } + + public final void useCWD(Context context) throws IOException, TimeoutException, RootDeniedException { + add( + new Command( + -1, + false, + "cd " + context.getApplicationInfo().dataDir) + ); + } + + private void cleanCommands() { + this.isCleaning = true; + int toClean = Math.abs(this.maxCommands - (this.maxCommands / 4)); + RootShell.log("Cleaning up: " + toClean); + + this.commands.subList(0, toClean).clear(); + + this.read = this.commands.size() - 1; + this.write = this.commands.size() - 1; + this.isCleaning = false; + } + + private void closeQuietly(final Reader input) { + try { + if (input != null) { + input.close(); + } + } catch (Exception ignore) { + } + } + + private void closeQuietly(final Writer output) { + try { + if (output != null) { + output.close(); + } + } catch (Exception ignore) { + } + } + + public void close() throws IOException { + RootShell.log("Request to close shell!"); + + int count = 0; + while (isExecuting) { + RootShell.log("Waiting on shell to finish executing before closing..."); + count++; + + //fail safe + if (count > 10000) { + break; + } + + } + + synchronized (this.commands) { + /** + * instruct the two threads monitoring input and output + * of the shell to close. + */ + this.close = true; + this.notifyThreads(); + } + + RootShell.log("Shell Closed!"); + + if (this == Shell.rootShell) { + Shell.rootShell = null; + } else if (this == Shell.shell) { + Shell.shell = null; + } else if (this == Shell.customShell) { + Shell.customShell = null; + } + } + + public static void closeCustomShell() throws IOException { + RootShell.log("Request to close custom shell!"); + + if (Shell.customShell == null) { + return; + } + + Shell.customShell.close(); + } + + public static void closeRootShell() throws IOException { + RootShell.log("Request to close root shell!"); + + if (Shell.rootShell == null) { + return; + } + Shell.rootShell.close(); + } + + public static void closeShell() throws IOException { + RootShell.log("Request to close normal shell!"); + + if (Shell.shell == null) { + return; + } + Shell.shell.close(); + } + + public static void closeAll() throws IOException { + RootShell.log("Request to close all shells!"); + + Shell.closeShell(); + Shell.closeRootShell(); + Shell.closeCustomShell(); + } + + public int getCommandQueuePosition(Command cmd) { + return this.commands.indexOf(cmd); + } + + public String getCommandQueuePositionString(Command cmd) { + return "Command is in position " + getCommandQueuePosition(cmd) + " currently executing command at position " + this.write + " and the number of commands is " + commands.size(); + } + + public static Shell getOpenShell() { + if (Shell.customShell != null) { + return Shell.customShell; + } else if (Shell.rootShell != null) { + return Shell.rootShell; + } else { + return Shell.shell; + } + } + + /** + * From libsuperuser. + * + *

+ * Detects the version of the su binary installed (if any), if supported + * by the binary. Most binaries support two different version numbers, + * the public version that is displayed to users, and an internal + * version number that is used for version number comparisons. Returns + * null if su not available or retrieving the version isn't supported. + *

+ *

+ * Note that su binary version and GUI (APK) version can be completely + * different. + *

+ *

+ * This function caches its result to improve performance on multiple + * calls + *

+ * + * @param internal Request human-readable version or application + * internal version + * @return String containing the su version or null + */ + private synchronized String getSuVersion(boolean internal) { + int idx = internal ? 0 : 1; + if (suVersion[idx] == null) { + String version = null; + + // Replace libsuperuser:Shell.run with manual process execution + Process process; + try { + process = Runtime.getRuntime().exec(internal ? "su -V" : "su -v", null); + process.waitFor(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (InterruptedException e) { + e.printStackTrace(); + return null; + } + + // From libsuperuser:StreamGobbler + List stdout = new ArrayList(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + try { + String line = null; + while ((line = reader.readLine()) != null) { + stdout.add(line); + } + } catch (IOException e) { + } + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + } + + process.destroy(); + + if (stdout != null) { + for (String line : stdout) { + if (!internal) { + if (line.contains(".")) { + version = line; + break; + } + } else { + try { + if (Integer.parseInt(line) > 0) { + version = line; + break; + } + } catch (NumberFormatException e) { + } + } + } + } + + suVersion[idx] = version; + } + return suVersion[idx]; + } + + public static boolean isShellOpen() { + return Shell.shell == null; + } + + public static boolean isCustomShellOpen() { + return Shell.customShell == null; + } + + public static boolean isRootShellOpen() { + return Shell.rootShell == null; + } + + public static boolean isAnyShellOpen() { + return Shell.shell != null || Shell.rootShell != null || Shell.customShell != null; + } + + /** + * From libsuperuser. + * + * Detect if SELinux is set to enforcing, caches result + * + * @return true if SELinux set to enforcing, or false in the case of + * permissive or not present + */ + public synchronized boolean isSELinuxEnforcing() { + if (isSELinuxEnforcing == null) { + Boolean enforcing = null; + + // First known firmware with SELinux built-in was a 4.2 (17) + // leak + if (android.os.Build.VERSION.SDK_INT >= 17) { + + // Detect enforcing through sysfs, not always present + File f = new File("/sys/fs/selinux/enforce"); + if (f.exists()) { + try { + InputStream is = new FileInputStream("/sys/fs/selinux/enforce"); + try { + enforcing = (is.read() == '1'); + } finally { + is.close(); + } + } catch (Exception e) { + } + } + + // 4.4+ builds are enforcing by default, take the gamble + if (enforcing == null) { + enforcing = (android.os.Build.VERSION.SDK_INT >= 19); + } + } + + if (enforcing == null) { + enforcing = false; + } + + isSELinuxEnforcing = enforcing; + } + return isSELinuxEnforcing; + } + + /** + * Runnable to write commands to the open shell. + *

+ * When writing commands we stay in a loop and wait for new + * commands to added to "commands" + *

+ * The notification of a new command is handled by the method add in this class + */ + private final Runnable input = new Runnable() { + public void run() { + + try { + while (true) { + + synchronized (commands) { + /** + * While loop is used in the case that notifyAll is called + * and there are still no commands to be written, a rare + * case but one that could happen. + */ + while (!close && write >= commands.size()) { + isExecuting = false; + commands.wait(); + } + } + + if (write >= maxCommands) { + + /** + * wait for the read to catch up. + */ + while (read != write) { + RootShell.log("Waiting for read and write to catch up before cleanup."); + } + /** + * Clean up the commands, stay neat. + */ + cleanCommands(); + } + + /** + * Write the new command + * + * We write the command followed by the token to indicate + * the end of the command execution + */ + if (write < commands.size()) { + isExecuting = true; + Command cmd = commands.get(write); + cmd.startExecution(); + RootShell.log("Executing: " + cmd.getCommand() + " with context: " + shellContext); + + //write the command + outputStream.write(cmd.getCommand()); + outputStream.flush(); + + //write the token... + String line = "\necho " + token + " " + totalExecuted + " $?\n"; + outputStream.write(line); + outputStream.flush(); + + write++; + totalExecuted++; + } else if (close) { + /** + * close the thread, the shell is closing. + */ + isExecuting = false; + outputStream.write("\nexit 0\n"); + outputStream.flush(); + RootShell.log("Closing shell"); + return; + } + } + } catch (IOException | InterruptedException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } + finally { + write = 0; + closeQuietly(outputStream); + } + } + }; + + protected void notifyThreads() { + Thread t = new Thread() { + public void run() { + synchronized (commands) { + commands.notifyAll(); + } + } + }; + + t.start(); + } + + /** + * Runnable to monitor the responses from the open shell. + * + * This include the output and error stream + */ + private final Runnable output = new Runnable() { + public void run() { + try { + Command command = null; + + //as long as there is something to read, we will keep reading. + while (!close || inputStream.ready() || read < commands.size()) { + isReading = false; + String outputLine = inputStream.readLine(); + isReading = true; + + /** + * If we receive EOF then the shell closed? + */ + if (outputLine == null) { + break; + } + + if (command == null) { + if (read >= commands.size()) { + if (close) { + break; + } + + continue; + } + + command = commands.get(read); + } + + /** + * trying to determine if all commands have been completed. + * + * if the token is present then the command has finished execution. + */ + int pos = -1; + + pos = outputLine.indexOf(token); + + if (pos == -1) { + /** + * send the output for the implementer to process + */ + command.output(command.id, outputLine); + } else if (pos > 0) { + /** + * token is suffix of output, send output part to implementer + */ + RootShell.log("Found token, line: " + outputLine); + command.output(command.id, outputLine.substring(0, pos)); + } + + if (pos >= 0) { + outputLine = outputLine.substring(pos); + String[] fields = outputLine.split(" "); + + if (fields.length >= 2 && fields[1] != null) { + int id = 0; + + try { + id = Integer.parseInt(fields[1]); + } catch (NumberFormatException e) { + } + + int exitCode = -1; + + try { + exitCode = Integer.parseInt(fields[2]); + } catch (NumberFormatException e) { + } + + if (id == totalRead) { + processErrors(command); + + + /** + * wait for output to be processed... + * + */ + int iterations = 0; + while (command.totalOutput > command.totalOutputProcessed) { + + if(iterations == 0) + { + iterations++; + RootShell.log("Waiting for output to be processed. " + command.totalOutputProcessed + " Of " + command.totalOutput); + } + + try { + + synchronized (this) + { + this.wait(2000); + } + } catch (Exception e) { + RootShell.log(e.getMessage()); + } + } + + RootShell.log("Read all output"); + + command.setExitCode(exitCode); + command.commandFinished(); + + command = null; + + read++; + totalRead++; + continue; + } + } + } + } + + try { + proc.waitFor(); + proc.destroy(); + } catch (Exception e) { + } + + while (read < commands.size()) { + if (command == null) { + command = commands.get(read); + } + + if(command.totalOutput < command.totalOutputProcessed) + { + command.terminated("All output not processed!"); + command.terminated("Did you forget the super.commandOutput call or are you waiting on the command object?"); + } + else + { + command.terminated("Unexpected Termination."); + } + + command = null; + read++; + } + + read = 0; + + } catch (IOException e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } finally { + closeQuietly(outputStream); + closeQuietly(errorStream); + closeQuietly(inputStream); + + RootShell.log("Shell destroyed"); + isClosed = true; + isReading = false; + } + } + }; + + public void processErrors(Command command) { + try { + while (errorStream.ready() && command != null) { + String line = errorStream.readLine(); + + /** + * If we recieve EOF then the shell closed? + */ + if (line == null) { + break; + } + + /** + * send the output for the implementer to process + */ + command.output(command.id, line); + } + } catch (Exception e) { + RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e); + } + } + + public static Command runRootCommand(Command command) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell().add(command); + } + + public static Command runCommand(Command command) throws IOException, TimeoutException { + return Shell.startShell().add(command); + } + + public static Shell startRootShell() throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(0, 3); + } + + public static Shell startRootShell(int timeout) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, 3); + } + + public static Shell startRootShell(int timeout, int retry) throws IOException, TimeoutException, RootDeniedException { + return Shell.startRootShell(timeout, Shell.defaultContext, retry); + } + + public static Shell startRootShell(int timeout, ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + // keep prompting the user until they accept for x amount of times... + int retries = 0; + + if (Shell.rootShell == null) { + + RootShell.log("Starting Root Shell!"); + String cmd = "su"; + while (Shell.rootShell == null) { + try { + RootShell.log("Trying to open Root Shell, attempt #" + retries); + Shell.rootShell = new Shell(cmd, ShellType.ROOT, shellContext, timeout); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not start shell"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not start shell"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not start shell"); + throw e; + } + } + } + } else if (Shell.rootShell.shellContext != shellContext) { + try { + RootShell.log("Context is different than open shell, switching context... " + Shell.rootShell.shellContext + " VS " + shellContext); + Shell.rootShell.switchRootShellContext(shellContext); + } catch (IOException e) { + if (retries++ >= retry) { + RootShell.log("IOException, could not switch context!"); + throw e; + } + } catch (RootDeniedException e) { + if (retries++ >= retry) { + RootShell.log("RootDeniedException, could not switch context!"); + throw e; + } + } catch (TimeoutException e) { + if (retries++ >= retry) { + RootShell.log("TimeoutException, could not switch context!"); + throw e; + } + } + } else { + RootShell.log("Using Existing Root Shell!"); + } + + return Shell.rootShell; + } + + public static Shell startCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException { + return Shell.startCustomShell(shellPath, 0); + } + + public static Shell startCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException { + + if (Shell.customShell == null) { + RootShell.log("Starting Custom Shell!"); + Shell.customShell = new Shell(shellPath, ShellType.CUSTOM, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Custom Shell!"); + } + + return Shell.customShell; + } + + public static Shell startShell() throws IOException, TimeoutException { + return Shell.startShell(0); + } + + public static Shell startShell(int timeout) throws IOException, TimeoutException { + + try { + if (Shell.shell == null) { + RootShell.log("Starting Shell!"); + Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout); + } else { + RootShell.log("Using Existing Shell!"); + } + return Shell.shell; + } catch (RootDeniedException e) { + //Root Denied should never be thrown. + throw new IOException(); + } + } + + public Shell switchRootShellContext(ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + if (this.shellType == ShellType.ROOT) { + try { + Shell.closeRootShell(); + } catch (Exception e) { + RootShell.log("Problem closing shell while trying to switch context..."); + } + + //create new root shell with new context... + + return Shell.startRootShell(this.shellTimeout, shellContext, 3); + } else { + //can only switch context on a root shell... + RootShell.log("Can only switch context on a root shell!"); + return this; + } + } + + protected static class Worker extends Thread { + + public int exit = -911; + + public Shell shell; + + private Worker(Shell shell) { + this.shell = shell; + } + + public void run() { + + /** + * Trying to open the shell. + * + * We echo "Started" and we look for it in the output. + * + * If we find the output then the shell is open and we return. + * + * If we do not find it then we determine the error and report + * it by setting the value of the variable exit + */ + try { + shell.outputStream.write("echo Started\n"); + shell.outputStream.flush(); + + while (true) { + String line = shell.inputStream.readLine(); + + if (line == null) { + throw new EOFException(); + } else if ("".equals(line)) { + continue; + } else if ("Started".equals(line)) { + this.exit = 1; + setShellOom(); + break; + } + + shell.error = "unknown error occurred."; + } + } catch (IOException e) { + exit = -42; + if (e.getMessage() != null) { + shell.error = e.getMessage(); + } else { + shell.error = "RootAccess denied?."; + } + } + + } + + /* + * setOom for shell processes (sh and su if root shell) and discard outputs + * Negative values make the process LESS likely to be killed in an OOM situation + * Positive values make the process MORE likely to be killed in an OOM situation + */ + private void setShellOom() { + try { + Class processClass = shell.proc.getClass(); + Field field; + try { + field = processClass.getDeclaredField("pid"); + } catch (NoSuchFieldException e) { + field = processClass.getDeclaredField("id"); + } + field.setAccessible(true); + int pid = (Integer) field.get(shell.proc); + shell.outputStream.write("(echo -17 > /proc/" + pid + "/oom_adj) &> /dev/null\n"); + shell.outputStream.write("(echo -17 > /proc/$$/oom_adj) &> /dev/null\n"); + shell.outputStream.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/app/src/main/java/com/stericson/roottools/Constants.java b/app/src/main/java/com/stericson/roottools/Constants.java new file mode 100644 index 0000000..f15ee7e --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/Constants.java @@ -0,0 +1,15 @@ +package com.stericson.roottools; + +public class Constants +{ + public static final String TAG = "RootTools v4.4"; + public static final int FPS = 1; + public static final int BBA = 3; + public static final int BBV = 4; + public static final int GI = 5; + public static final int GS = 6; + public static final int GSYM = 7; + public static final int GET_MOUNTS = 8; + public static final int GET_SYMLINKS = 9; + +} diff --git a/app/src/main/java/com/stericson/roottools/RootTools.java b/app/src/main/java/com/stericson/roottools/RootTools.java new file mode 100644 index 0000000..6b80873 --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/RootTools.java @@ -0,0 +1,848 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.stericson.rootshell.RootShell; +import com.stericson.rootshell.exceptions.RootDeniedException; +import com.stericson.rootshell.execution.Command; +import com.stericson.rootshell.execution.Shell; +import com.stericson.roottools.containers.Mount; +import com.stericson.roottools.containers.Permissions; +import com.stericson.roottools.containers.Symlink; +import com.stericson.roottools.internal.Remounter; +import com.stericson.roottools.internal.RootToolsInternalMethods; +import com.stericson.roottools.internal.Runner; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeoutException; + +public final class RootTools { + + /** + * This class is the gateway to every functionality within the RootTools library.The developer + * should only have access to this class and this class only.This means that this class should + * be the only one to be public.The rest of the classes within this library must not have the + * public modifier. + *

+ * All methods and Variables that the developer may need to have access to should be here. + *

+ * If a method, or a specific functionality, requires a fair amount of code, or work to be done, + * then that functionality should probably be moved to its own class and the call to it done + * here.For examples of this being done, look at the remount functionality. + */ + + private static RootToolsInternalMethods rim = null; + + public static void setRim(RootToolsInternalMethods rim) { + RootTools.rim = rim; + } + + private static final RootToolsInternalMethods getInternals() { + if (rim == null) { + RootToolsInternalMethods.getInstance(); + return rim; + } else { + return rim; + } + } + + // -------------------- + // # Public Variables # + // -------------------- + + public static boolean debugMode = false; + public static String utilPath; + + /** + * Setting this to false will disable the handler that is used + * by default for the 3 callback methods for Command. + *

+ * By disabling this all callbacks will be called from a thread other than + * the main UI thread. + */ + public static boolean handlerEnabled = true; + + + /** + * Setting this will change the default command timeout. + *

+ * The default is 20000ms + */ + public static int default_Command_Timeout = 20000; + + + // --------------------------- + // # Public Variable Getters # + // --------------------------- + + // ------------------ + // # Public Methods # + // ------------------ + + /** + * This will check a given binary, determine if it exists and determine that it has either the + * permissions 755, 775, or 777. + * + * @param util Name of the utility to check. + * @return boolean to indicate whether the binary is installed and has appropriate permissions. + */ + public static boolean checkUtil(String util) { + + return getInternals().checkUtil(util); + } + + /** + * This will close all open shells. + * + * @throws IOException + */ + public static void closeAllShells() throws IOException { + RootShell.closeAllShells(); + } + + /** + * This will close the custom shell that you opened. + * + * @throws IOException + */ + public static void closeCustomShell() throws IOException { + RootShell.closeCustomShell(); + } + + /** + * This will close either the root shell or the standard shell depending on what you specify. + * + * @param root a boolean to specify whether to close the root shell or the standard shell. + * @throws IOException + */ + public static void closeShell(boolean root) throws IOException { + RootShell.closeShell(root); + } + + /** + * Copys a file to a destination. Because cp is not available on all android devices, we have a + * fallback on the cat command + * + * @param source example: /data/data/org.adaway/files/hosts + * @param destination example: /system/etc/hosts + * @param remountAsRw remounts the destination as read/write before writing to it + * @param preserveFileAttributes tries to copy file attributes from source to destination, if only cat is available + * only permissions are preserved + * @return true if it was successfully copied + */ + public static boolean copyFile(String source, String destination, boolean remountAsRw, + boolean preserveFileAttributes) { + return getInternals().copyFile(source, destination, remountAsRw, preserveFileAttributes); + } + + /** + * Deletes a file or directory + * + * @param target example: /data/data/org.adaway/files/hosts + * @param remountAsRw remounts the destination as read/write before writing to it + * @return true if it was successfully deleted + */ + public static boolean deleteFileOrDirectory(String target, boolean remountAsRw) { + return getInternals().deleteFileOrDirectory(target, remountAsRw); + } + + /** + * Use this to check whether or not a file exists on the filesystem. + * + * @param file String that represent the file, including the full path to the + * file and its name. + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file) { + return exists(file, false); + } + + /** + * Use this to check whether or not a file OR directory exists on the filesystem. + * + * @param file String that represent the file OR the directory, including the full path to the + * file and its name. + * @param isDir boolean that represent whether or not we are looking for a directory + * @return a boolean that will indicate whether or not the file exists. + */ + public static boolean exists(final String file, boolean isDir) { + return RootShell.exists(file, isDir); + } + + /** + * This will try and fix a given binary. (This is for Busybox applets or Toolbox applets) By + * "fix", I mean it will try and symlink the binary from either toolbox or Busybox and fix the + * permissions if the permissions are not correct. + * + * @param util Name of the utility to fix. + * @param utilPath path to the toolbox that provides ln, rm, and chmod. This can be a blank string, a + * path to a binary that will provide these, or you can use + * RootTools.getWorkingToolbox() + */ + public static void fixUtil(String util, String utilPath) { + getInternals().fixUtil(util, utilPath); + } + + /** + * This will check an array of binaries, determine if they exist and determine that it has + * either the permissions 755, 775, or 777. If an applet is not setup correctly it will try and + * fix it. (This is for Busybox applets or Toolbox applets) + * + * @param utils Name of the utility to check. + * @return boolean to indicate whether the operation completed. Note that this is not indicative + * of whether the problem was fixed, just that the method did not encounter any + * exceptions. + * @throws Exception if the operation cannot be completed. + */ + public static boolean fixUtils(String[] utils) throws Exception { + return getInternals().fixUtils(utils); + } + + /** + * @param binaryName String that represent the binary to find. + * @param singlePath boolean that represents whether to return a single path or multiple. + * + * @return List containing the paths the binary was found at. + */ + public static List findBinary(String binaryName, boolean singlePath) { + return RootShell.findBinary(binaryName, singlePath); + } + + /** + * @param path String that represents the path to the Busybox binary you want to retrieve the version of. + * @return BusyBox version is found, "" if not found. + */ + public static String getBusyBoxVersion(String path) { + return getInternals().getBusyBoxVersion(path); + } + + /** + * @return BusyBox version is found, "" if not found. + */ + public static String getBusyBoxVersion() { + return RootTools.getBusyBoxVersion(""); + } + + /** + * This will return an List of Strings. Each string represents an applet available from BusyBox. + *

+ * + * @return null If we cannot return the list of applets. + */ + public static List getBusyBoxApplets() throws Exception { + return RootTools.getBusyBoxApplets(""); + } + + /** + * This will return an List of Strings. Each string represents an applet available from BusyBox. + *

+ * + * @param path Path to the busybox binary that you want the list of applets from. + * @return null If we cannot return the list of applets. + */ + public static List getBusyBoxApplets(String path) throws Exception { + return getInternals().getBusyBoxApplets(path); + } + + /** + * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param shellPath a String to Indicate the path to the shell that you want to open. + * @param timeout an int to Indicate the length of time before giving up on opening a shell. + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException { + return RootShell.getCustomShell(shellPath, timeout); + } + + /** + * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param shellPath a String to Indicate the path to the shell that you want to open. + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException { + return RootTools.getCustomShell(shellPath, 10000); + } + + /** + * @param file String that represent the file, including the full path to the file and its name. + * @return An instance of the class permissions from which you can get the permissions of the + * file or if the file could not be found or permissions couldn't be determined then + * permissions will be null. + */ + public static Permissions getFilePermissionsSymlinks(String file) { + return getInternals().getFilePermissionsSymlinks(file); + } + + /** + * This method will return the inode number of a file. This method is dependent on having a version of + * ls that supports the -i parameter. + * + * @param file path to the file that you wish to return the inode number + * @return String The inode number for this file or "" if the inode number could not be found. + */ + public static String getInode(String file) { + return getInternals().getInode(file); + } + + /** + * This will return an ArrayList of the class Mount. The class mount contains the following + * property's: device mountPoint type flags + *

+ * These will provide you with any information you need to work with the mount points. + * + * @return ArrayList an ArrayList of the class Mount. + * @throws Exception if we cannot return the mount points. + */ + public static ArrayList getMounts() throws Exception { + return getInternals().getMounts(); + } + + /** + * This will tell you how the specified mount is mounted. rw, ro, etc... + *

+ * + * @param path The mount you want to check + * @return String What the mount is mounted as. + * @throws Exception if we cannot determine how the mount is mounted. + */ + public static String getMountedAs(String path) throws Exception { + return getInternals().getMountedAs(path); + } + + /** + * This will return the environment variable PATH + * + * @return List A List of Strings representing the environment variable $PATH + */ + public static List getPath() { + return Arrays.asList(System.getenv("PATH").split(":")); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + * @param retry a int to indicate how many times the ROOT shell should try to open with root priviliges... + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { + return RootShell.getShell(root, timeout, shellContext, retry); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @param shellContext the context to execute the shell with + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param shellContext the context to execute the shell with + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, 0, shellContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell. + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException { + return getShell(root, timeout, Shell.defaultContext, 3); + } + + /** + * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output + * and for closing the shell when you are done using it. + * + * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell + * @throws TimeoutException + * @throws com.stericson.RootShell.exceptions.RootDeniedException + * @throws IOException + */ + public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException { + return RootTools.getShell(root, 0); + } + + /** + * Get the space for a desired partition. + * + * @param path The partition to find the space for. + * @return the amount if space found within the desired partition. If the space was not found + * then the value is -1 + * @throws TimeoutException + */ + public static long getSpace(String path) { + return getInternals().getSpace(path); + } + + /** + * This will return a String that represent the symlink for a specified file. + *

+ * + * @param file path to the file to get the Symlink for. (must have absolute path) + * @return String a String that represent the symlink for a specified file or an + * empty string if no symlink exists. + */ + public static String getSymlink(String file) { + return getInternals().getSymlink(file); + } + + /** + * This will return an ArrayList of the class Symlink. The class Symlink contains the following + * property's: path SymplinkPath + *

+ * These will provide you with any Symlinks in the given path. + * + * @param path path to search for Symlinks. + * @return ArrayList an ArrayList of the class Symlink. + * @throws Exception if we cannot return the Symlinks. + */ + public static ArrayList getSymlinks(String path) throws Exception { + return getInternals().getSymlinks(path); + } + + /** + * This will return to you a string to be used in your shell commands which will represent the + * valid working toolbox with correct permissions. For instance, if Busybox is available it will + * return "busybox", if busybox is not available but toolbox is then it will return "toolbox" + * + * @return String that indicates the available toolbox to use for accessing applets. + */ + public static String getWorkingToolbox() { + return getInternals().getWorkingToolbox(); + } + + /** + * Checks if there is enough Space on SDCard + * + * @param updateSize size to Check (long) + * @return true if the Update will fit on SDCard, false if not enough + * space on SDCard. Will also return false, if the SDCard is not mounted as + * read/write + */ + public static boolean hasEnoughSpaceOnSdCard(long updateSize) { + return getInternals().hasEnoughSpaceOnSdCard(updateSize); + } + + /** + * Checks whether the toolbox or busybox binary contains a specific util + * + * @param util + * @param box Should contain "toolbox" or "busybox" + * @return true if it contains this util + */ + public static boolean hasUtil(final String util, final String box) { + //TODO Convert this to use the new shell. + return getInternals().hasUtil(util, box); + } + + /** + * This method can be used to unpack a binary from the raw resources folder and store it in + * /data/data/app.package/files/ This is typically useful if you provide your own C- or + * C++-based binary. This binary can then be executed using sendShell() and its full path. + * + * @param context the current activity's Context + * @param sourceId resource id; typically R.raw.id + * @param destName destination file name; appended to /data/data/app.package/files/ + * @param mode chmod value for this file + * @return a boolean which indicates whether or not we were able to create the new + * file. + */ + public static boolean installBinary(Context context, int sourceId, String destName, String mode) { + return getInternals().installBinary(context, sourceId, destName, mode); + } + + /** + * This method can be used to unpack a binary from the raw resources folder and store it in + * /data/data/app.package/files/ This is typically useful if you provide your own C- or + * C++-based binary. This binary can then be executed using sendShell() and its full path. + * + * @param context the current activity's Context + * @param sourceId resource id; typically R.raw.id + * @param binaryName destination file name; appended to /data/data/app.package/files/ + * @return a boolean which indicates whether or not we were able to create the new + * file. + */ + public static boolean installBinary(Context context, int sourceId, String binaryName) { + return installBinary(context, sourceId, binaryName, "700"); + } + + /** + * This method checks whether a binary is installed. + * + * @param context the current activity's Context + * @param binaryName binary file name; appended to /data/data/app.package/files/ + * @return a boolean which indicates whether or not + * the binary already exists. + */ + public static boolean hasBinary(Context context, String binaryName) { + return getInternals().isBinaryAvailable(context, binaryName); + } + + /** + * This will let you know if an applet is available from BusyBox + *

+ * + * @param applet The applet to check for. + * @param path Path to the busybox binary that you want to check. (do not include binary name) + * @return true if applet is available, false otherwise. + */ + public static boolean isAppletAvailable(String applet, String path) { + return getInternals().isAppletAvailable(applet, path); + } + + /** + * This will let you know if an applet is available from BusyBox + *

+ * + * @param applet The applet to check for. + * @return true if applet is available, false otherwise. + */ + public static boolean isAppletAvailable(String applet) { + return RootTools.isAppletAvailable(applet, ""); + } + /** + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven() { + return RootShell.isAccessGiven(); + } + + /** + * Control how many time of retries should request + * + * @param timeout The timeout + * @param retries The number of retries + * + * @return true if your app has been given root access. + * @throws TimeoutException if this operation times out. (cannot determine if access is given) + */ + public static boolean isAccessGiven(int timeout, int retries) { + return RootShell.isAccessGiven(timeout, retries); + } + + /** + * @return true if BusyBox was found. + */ + public static boolean isBusyboxAvailable() { + return RootShell.isBusyboxAvailable(); + } + + public static boolean isNativeToolsReady(int nativeToolsId, Context context) { + return getInternals().isNativeToolsReady(nativeToolsId, context); + } + + /** + * This method can be used to to check if a process is running + * + * @param processName name of process to check + * @return true if process was found + * @throws TimeoutException (Could not determine if the process is running) + */ + public static boolean isProcessRunning(final String processName) { + //TODO convert to new shell + return getInternals().isProcessRunning(processName); + } + + /** + * @return true if su was found. + */ + public static boolean isRootAvailable() { + return RootShell.isRootAvailable(); + } + + /** + * This method can be used to kill a running process + * + * @param processName name of process to kill + * @return true if process was found and killed successfully + */ + public static boolean killProcess(final String processName) { + //TODO convert to new shell + return getInternals().killProcess(processName); + } + + /** + * This will launch the Android market looking for BusyBox + * + * @param activity pass in your Activity + */ + public static void offerBusyBox(Activity activity) { + getInternals().offerBusyBox(activity); + } + + /** + * This will launch the Android market looking for BusyBox, but will return the intent fired and + * starts the activity with startActivityForResult + * + * @param activity pass in your Activity + * @param requestCode pass in the request code + * @return intent fired + */ + public static Intent offerBusyBox(Activity activity, int requestCode) { + return getInternals().offerBusyBox(activity, requestCode); + } + + /** + * This will launch the Android market looking for SuperUser + * + * @param activity pass in your Activity + */ + public static void offerSuperUser(Activity activity) { + getInternals().offerSuperUser(activity); + } + + /** + * This will launch the Android market looking for SuperUser, but will return the intent fired + * and starts the activity with startActivityForResult + * + * @param activity pass in your Activity + * @param requestCode pass in the request code + * @return intent fired + */ + public static Intent offerSuperUser(Activity activity, int requestCode) { + return getInternals().offerSuperUser(activity, requestCode); + } + + /** + * This will take a path, which can contain the file name as well, and attempt to remount the + * underlying partition. + *

+ * For example, passing in the following string: + * "/system/bin/some/directory/that/really/would/never/exist" will result in /system ultimately + * being remounted. However, keep in mind that the longer the path you supply, the more work + * this has to do, and the slower it will run. + * + * @param file file path + * @param mountType mount type: pass in RO (Read only) or RW (Read Write) + * @return a boolean which indicates whether or not the partition has been + * remounted as specified. + */ + public static boolean remount(String file, String mountType) { + // Recieved a request, get an instance of Remounter + Remounter remounter = new Remounter(); + // send the request. + return (remounter.remount(file, mountType)); + } + + public static boolean remount(String file, String mountType, String customPath) { + // Recieved a request, get an instance of Remounter + Remounter remounter = new Remounter(customPath); + // send the request. + return (remounter.remount(file, mountType)); + } + + /** + * This restarts only Android OS without rebooting the whole device. This does NOT work on all + * devices. This is done by killing the main init process named zygote. Zygote is restarted + * automatically by Android after killing it. + * + * @throws TimeoutException + */ + /* public static void restartAndroid() { + RootTools.log("Restart Android"); + killProcess("zygote"); + }*/ + + /** + * Executes binary in a separated process. Before using this method, the binary has to be + * installed in /data/data/app.package/files/ using the installBinary method. + * + * @param context the current activity's Context + * @param binaryName name of installed binary + * @param parameter parameter to append to binary like "-vxf" + */ + public static void runBinary(Context context, String binaryName, String parameter) { + Runner runner = new Runner(context, binaryName, parameter); + runner.start(); + } + + /** + * Executes a given command with root access or without depending on the value of the boolean passed. + * This will also start a root shell or a standard shell without you having to open it specifically. + *

+ * You will still need to close the shell after you are done using the shell. + * + * @param shell The shell to execute the command on, this can be a root shell or a standard shell. + * @param command The command to execute in the shell + * + * @throws IOException + */ + public static void runShellCommand(Shell shell, Command command) throws IOException { + shell.add(command); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootTools.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + */ + public static void log(String msg) { + log(null, msg, 3, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootTools.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + */ + public static void log(String TAG, String msg) { + log(TAG, msg, 3, null); + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootTools.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String msg, int type, Exception e) { + log(null, msg, type, e); + } + + /** + * This method allows you to check whether logging is enabled. + * Yes, it has a goofy name, but that's to keep it as short as possible. + * After all writing logging calls should be painless. + * This method exists to save Android going through the various Java layers + * that are traversed any time a string is created (i.e. what you are logging) + *

+ * Example usage: + * if(islog) { + * StrinbBuilder sb = new StringBuilder(); + * // ... + * // build string + * // ... + * log(sb.toString()); + * } + * + * @return true if logging is enabled + */ + public static boolean islog() { + return debugMode; + } + + /** + * This method allows you to output debug messages only when debugging is on. This will allow + * you to add a debug option to your app, which by default can be left off for performance. + * However, when you need debugging information, a simple switch can enable it and provide you + * with detailed logging. + *

+ * This method handles whether or not to log the information you pass it depending whether or + * not RootTools.debugMode is on. So you can use this and not have to worry about handling it + * yourself. + * + * @param TAG Optional parameter to define the tag that the Log will use. + * @param msg The message to output. + * @param type The type of log, 1 for verbose, 2 for error, 3 for debug + * @param e The exception that was thrown (Needed for errors) + */ + public static void log(String TAG, String msg, int type, Exception e) { + if (msg != null && !msg.equals("")) { + if (debugMode) { + if (TAG == null) { + TAG = Constants.TAG; + } + + switch (type) { + case 1: + Log.v(TAG, msg); + break; + case 2: + Log.e(TAG, msg, e); + break; + case 3: + Log.d(TAG, msg); + break; + } + } + } + } +} diff --git a/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java b/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java new file mode 100644 index 0000000..9d36748 --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java @@ -0,0 +1,459 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.StrictMode; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.stericson.rootshell.exceptions.RootDeniedException; +import com.stericson.rootshell.execution.Command; +import com.stericson.rootshell.execution.Shell; +import com.stericson.roottools.containers.Permissions; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeoutException; + +public class SanityCheckRootTools extends Activity { + private ScrollView mScrollView; + private TextView mTextView; + private ProgressDialog mPDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectNetwork() // or .detectAll() for all detectable problems + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build()); + + RootTools.debugMode = true; + + mTextView = new TextView(this); + mTextView.setText(""); + mScrollView = new ScrollView(this); + mScrollView.addView(mTextView); + setContentView(mScrollView); + + print("SanityCheckRootTools \n\n"); + + if (RootTools.isRootAvailable()) { + print("Root found.\n"); + } else { + print("Root not found"); + } + + try { + Shell.startRootShell(); + } catch (IOException e2) { + // TODO Auto-generated catch block + e2.printStackTrace(); + } catch (TimeoutException e) { + print("[ TIMEOUT EXCEPTION! ]\n"); + e.printStackTrace(); + } catch (RootDeniedException e) { + print("[ ROOT DENIED EXCEPTION! ]\n"); + e.printStackTrace(); + } + + try { + if (!RootTools.isAccessGiven()) { + print("ERROR: No root access to this device.\n"); + return; + } + } catch (Exception e) { + print("ERROR: could not determine root access to this device.\n"); + return; + } + + // Display infinite progress bar + mPDialog = new ProgressDialog(this); + mPDialog.setCancelable(false); + mPDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + + new SanityCheckThread(this, new TestHandler()).start(); + } + + protected void print(CharSequence text) { + mTextView.append(text); + mScrollView.post(new Runnable() { + public void run() { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + } + + // Run our long-running tests in their separate thread so as to + // not interfere with proper rendering. + private class SanityCheckThread extends Thread { + private final Handler mHandler; + + public SanityCheckThread(Context context, Handler handler) { + mHandler = handler; + } + + public void run() { + visualUpdate(TestHandler.ACTION_SHOW, null); + + // First test: Install a binary file for future use + // if it wasn't already installed. + /* + visualUpdate(TestHandler.ACTION_PDISPLAY, "Installing binary if needed"); + if(false == RootTools.installBinary(mContext, R.raw.nes, "nes_binary")) { + visualUpdate(TestHandler.ACTION_HIDE, "ERROR: Failed to install binary. Please see log file."); + return; + } + */ + + boolean result; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getPath"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ getPath ]\n"); + + try { + List paths = RootTools.getPath(); + + for (String path : paths) { + visualUpdate(TestHandler.ACTION_DISPLAY, path + " k\n\n"); + } + + } catch (Exception e) { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing A ton of commands"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Ton of Commands ]\n"); + + for (int i = 0; i < 100; i++) { + RootTools.exists("/system/xbin/busybox"); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Find Binary"); + result = RootTools.isRootAvailable(); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Root ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing file exists"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Exists() ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.exists("/system/sbin/[") + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Is Access Given"); + result = RootTools.isAccessGiven(); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking for Access to Root ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Remount"); + result = RootTools.remount("/system", "rw"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Remounting System as RW ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing CheckUtil"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox is setup ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.checkUtil("busybox") + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getBusyBoxVersion"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox version ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getBusyBoxVersion("/system/xbin/") + " k\n\n"); + + try { + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing fixUtils"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Utils ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.fixUtils(new String[]{"ls", "rm", "ln", "dd", "chmod", "mount"}) + " k\n\n"); + } catch (Exception e2) { + // TODO Auto-generated catch block + e2.printStackTrace(); + } + + try { + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getSymlink"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking [[ for symlink ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getSymlink("/system/bin/[[") + " k\n\n"); + } catch (Exception e2) { + // TODO Auto-generated catch block + e2.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getInode"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Inodes ]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getInode("/system/bin/busybox") + " k\n\n"); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing GetBusyBoxapplets"); + try { + + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Getting all available Busybox applets ]\n"); + for (String applet : RootTools.getBusyBoxApplets("/data/data/stericson.busybox/files/bb/busybox")) { + visualUpdate(TestHandler.ACTION_DISPLAY, applet + " k\n\n"); + } + + } catch (Exception e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing GetBusyBox version in a special directory!"); + try { + + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Testing GetBusyBox version in a special directory! ]\n"); + String v = RootTools.getBusyBoxVersion("/data/data/stericson.busybox/files/bb/"); + + visualUpdate(TestHandler.ACTION_DISPLAY, v + " k\n\n"); + + } catch (Exception e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getFilePermissionsSymlinks"); + Permissions permissions = RootTools.getFilePermissionsSymlinks("/system/xbin/busybox"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox permissions and symlink ]\n"); + + if (permissions != null) { + visualUpdate(TestHandler.ACTION_DISPLAY, "Symlink: " + permissions.getSymlink() + " k\n\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, "Group Permissions: " + permissions.getGroupPermissions() + " k\n\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, "Owner Permissions: " + permissions.getOtherPermissions() + " k\n\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, "Permissions: " + permissions.getPermissions() + " k\n\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, "Type: " + permissions.getType() + " k\n\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, "User Permissions: " + permissions.getUserPermissions() + " k\n\n"); + } else { + visualUpdate(TestHandler.ACTION_DISPLAY, "Permissions == null k\n\n"); + } + + Shell shell; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing output capture"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ busybox ash --help ]\n"); + + try { + shell = RootTools.getShell(true); + Command cmd = new Command( + 0, + "busybox ash --help") { + + @Override + public void commandOutput(int id, String line) { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "getevent - /dev/input/event0"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ getevent - /dev/input/event0 ]\n"); + + cmd = new Command(0, 0, "getevent /dev/input/event0") { + @Override + public void commandOutput(int id, String line) { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + + }; + shell.add(cmd); + + } catch (Exception e) { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - SYSTEM_APP"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - SYSTEM_APP ]\n"); + + try { + shell = RootTools.getShell(true, Shell.ShellContext.SYSTEM_APP); + Command cmd = new Command( + 0, + "id") { + + @Override + public void commandOutput(int id, String line) { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing PM"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Testing pm list packages -d ]\n"); + + cmd = new Command( + 0, + "sh /system/bin/pm list packages -d") { + + @Override + public void commandOutput(int id, String line) { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } catch (Exception e) { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - UNTRUSTED"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - UNTRUSTED ]\n"); + + try { + shell = RootTools.getShell(true, Shell.ShellContext.UNTRUSTED_APP); + Command cmd = new Command( + 0, + "id") { + + @Override + public void commandOutput(int id, String line) { + visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n"); + super.commandOutput(id, line); + } + }; + shell.add(cmd); + + } catch (Exception e) { + e.printStackTrace(); + } + + visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing df"); + long spaceValue = RootTools.getSpace("/data"); + visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking /data partition size]\n"); + visualUpdate(TestHandler.ACTION_DISPLAY, spaceValue + "k\n\n"); + + try { + shell = RootTools.getShell(true); + + Command cmd = new Command(42, false, "echo done") { + + boolean _catch = false; + + @Override + public void commandOutput(int id, String line) { + if (_catch) { + RootTools.log("CAUGHT!!!"); + } + + super.commandOutput(id, line); + + } + + @Override + public void commandTerminated(int id, String reason) { + synchronized (SanityCheckRootTools.this) { + + _catch = true; + visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete."); + visualUpdate(TestHandler.ACTION_HIDE, null); + + try { + RootTools.closeAllShells(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + + @Override + public void commandCompleted(int id, int exitCode) { + synchronized (SanityCheckRootTools.this) { + _catch = true; + + visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete."); + visualUpdate(TestHandler.ACTION_HIDE, null); + + try { + RootTools.closeAllShells(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + }; + + shell.add(cmd); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private void visualUpdate(int action, String text) { + Message msg = mHandler.obtainMessage(); + Bundle bundle = new Bundle(); + bundle.putInt(TestHandler.ACTION, action); + bundle.putString(TestHandler.TEXT, text); + msg.setData(bundle); + mHandler.sendMessage(msg); + } + } + + private class TestHandler extends Handler { + static final public String ACTION = "action"; + static final public int ACTION_SHOW = 0x01; + static final public int ACTION_HIDE = 0x02; + static final public int ACTION_DISPLAY = 0x03; + static final public int ACTION_PDISPLAY = 0x04; + static final public String TEXT = "text"; + + public void handleMessage(Message msg) { + int action = msg.getData().getInt(ACTION); + String text = msg.getData().getString(TEXT); + + switch (action) { + case ACTION_SHOW: + mPDialog.show(); + mPDialog.setMessage("Running Root Library Tests..."); + break; + case ACTION_HIDE: + if (null != text) { + print(text); + } + mPDialog.hide(); + break; + case ACTION_DISPLAY: + print(text); + break; + case ACTION_PDISPLAY: + mPDialog.setMessage(text); + break; + } + } + } +} diff --git a/app/src/main/java/com/stericson/roottools/containers/Mount.java b/app/src/main/java/com/stericson/roottools/containers/Mount.java new file mode 100644 index 0000000..ce455f2 --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/containers/Mount.java @@ -0,0 +1,70 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.containers; + +import java.io.File; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +public class Mount +{ + final File mDevice; + final File mMountPoint; + final String mType; + final Set mFlags; + + public Mount(File device, File path, String type, String flagsStr) + { + mDevice = device; + mMountPoint = path; + mType = type; + mFlags = new LinkedHashSet(Arrays.asList(flagsStr.split(","))); + } + + public File getDevice() + { + return mDevice; + } + + public File getMountPoint() + { + return mMountPoint; + } + + public String getType() + { + return mType; + } + + public Set getFlags() + { + return mFlags; + } + + @Override + public String toString() + { + return String.format("%s on %s type %s %s", mDevice, mMountPoint, mType, mFlags); + } +} diff --git a/app/src/main/java/com/stericson/roottools/containers/Permissions.java b/app/src/main/java/com/stericson/roottools/containers/Permissions.java new file mode 100644 index 0000000..51aae5f --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/containers/Permissions.java @@ -0,0 +1,125 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.containers; + +public class Permissions +{ + String type; + String user; + String group; + String other; + String symlink; + int permissions; + + public String getSymlink() + { + return this.symlink; + } + + public String getType() + { + return type; + } + + public int getPermissions() + { + return this.permissions; + } + + public String getUserPermissions() + { + return this.user; + } + + public String getGroupPermissions() + { + return this.group; + } + + public String getOtherPermissions() + { + return this.other; + } + + public void setSymlink(String symlink) + { + this.symlink = symlink; + } + + public void setType(String type) + { + this.type = type; + } + + public void setPermissions(int permissions) + { + this.permissions = permissions; + } + + public void setUserPermissions(String user) + { + this.user = user; + } + + public void setGroupPermissions(String group) + { + this.group = group; + } + + public void setOtherPermissions(String other) + { + this.other = other; + } + + public String getUser() + { + return user; + } + + public void setUser(String user) + { + this.user = user; + } + + public String getGroup() + { + return group; + } + + public void setGroup(String group) + { + this.group = group; + } + + public String getOther() + { + return other; + } + + public void setOther(String other) + { + this.other = other; + } + + +} diff --git a/app/src/main/java/com/stericson/roottools/containers/Symlink.java b/app/src/main/java/com/stericson/roottools/containers/Symlink.java new file mode 100644 index 0000000..b811b1a --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/containers/Symlink.java @@ -0,0 +1,47 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.containers; + +import java.io.File; + +public class Symlink +{ + protected final File file; + protected final File symlinkPath; + + public Symlink(File file, File path) + { + this.file = file; + symlinkPath = path; + } + + public File getFile() + { + return this.file; + } + + public File getSymlinkPath() + { + return symlinkPath; + } +} diff --git a/app/src/main/java/com/stericson/roottools/internal/Installer.java b/app/src/main/java/com/stericson/roottools/internal/Installer.java new file mode 100644 index 0000000..54d57ae --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/internal/Installer.java @@ -0,0 +1,300 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.internal; + +import android.content.Context; +import android.util.Log; + +import com.stericson.rootshell.execution.Command; +import com.stericson.rootshell.execution.Shell; +import com.stericson.roottools.RootTools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +class Installer +{ + + //------------- + //# Installer # + //------------- + + static final String LOG_TAG = "RootTools::Installer"; + + static final String BOGUS_FILE_NAME = "bogus"; + + Context context; + String filesPath; + + public Installer(Context context) + throws IOException + { + + this.context = context; + this.filesPath = context.getFilesDir().getCanonicalPath(); + } + + /** + * This method can be used to unpack a binary from the raw resources folder and store it in + * /data/data/app.package/files/ + * This is typically useful if you provide your own C- or C++-based binary. + * This binary can then be executed using sendShell() and its full path. + * + * @param sourceId resource id; typically R.raw.id + * @param destName destination file name; appended to /data/data/app.package/files/ + * @param mode chmod value for this file + * @return a boolean which indicates whether or not we were + * able to create the new file. + */ + protected boolean installBinary(int sourceId, String destName, String mode) + { + File mf = new File(filesPath + File.separator + destName); + if (!mf.exists() || + !getFileSignature(mf).equals( + getStreamSignature( + context.getResources().openRawResource(sourceId)) + )) + { + Log.e(LOG_TAG, "Installing a new version of binary: " + destName); + // First, does our files/ directory even exist? + // We cannot wait for android to lazily create it as we will soon + // need it. + try + { + FileInputStream fis = context.openFileInput(BOGUS_FILE_NAME); + fis.close(); + } + catch (FileNotFoundException e) + { + FileOutputStream fos = null; + try + { + fos = context.openFileOutput("bogus", Context.MODE_PRIVATE); + fos.write("justcreatedfilesdirectory".getBytes()); + } + catch (Exception ex) + { + if (RootTools.debugMode) + { + Log.e(LOG_TAG, ex.toString()); + } + return false; + } + finally + { + if (null != fos) + { + try + { + fos.close(); + context.deleteFile(BOGUS_FILE_NAME); + } + catch (IOException e1) + { + } + } + } + } + catch (IOException ex) + { + if (RootTools.debugMode) + { + Log.e(LOG_TAG, ex.toString()); + } + return false; + } + + // Only now can we start creating our actual file + InputStream iss = context.getResources().openRawResource(sourceId); + ReadableByteChannel rfc = Channels.newChannel(iss); + FileOutputStream oss = null; + try + { + oss = new FileOutputStream(mf); + FileChannel ofc = oss.getChannel(); + long pos = 0; + try + { + long size = iss.available(); + while ((pos += ofc.transferFrom(rfc, pos, size - pos)) < size) + { + } + } + catch (IOException ex) + { + if (RootTools.debugMode) + { + Log.e(LOG_TAG, ex.toString()); + } + return false; + } + } + catch (FileNotFoundException ex) + { + if (RootTools.debugMode) + { + Log.e(LOG_TAG, ex.toString()); + } + return false; + } + finally + { + if (oss != null) + { + try + { + oss.flush(); + oss.getFD().sync(); + oss.close(); + } + catch (Exception e) + { + } + } + } + try + { + iss.close(); + } + catch (IOException ex) + { + if (RootTools.debugMode) + { + Log.e(LOG_TAG, ex.toString()); + } + return false; + } + + try + { + Command command = new Command(0, false, "chmod " + mode + " " + filesPath + File.separator + destName); + Shell.startRootShell().add(command); + commandWait(command); + + } + catch (Exception e) + { + } + } + return true; + } + + protected boolean isBinaryInstalled(String destName) + { + boolean installed = false; + File mf = new File(filesPath + File.separator + destName); + if (mf.exists()) + { + installed = true; + // TODO: pass mode as argument and check it matches + } + return installed; + } + + protected String getFileSignature(File f) + { + String signature = ""; + try + { + signature = getStreamSignature(new FileInputStream(f)); + } + catch (FileNotFoundException ex) + { + Log.e(LOG_TAG, ex.toString()); + } + return signature; + } + + /* + * Note: this method will close any string passed to it + */ + protected String getStreamSignature(InputStream is) + { + String signature = ""; + try + { + MessageDigest md = MessageDigest.getInstance("MD5"); + DigestInputStream dis = new DigestInputStream(is, md); + byte[] buffer = new byte[4096]; + while (-1 != dis.read(buffer)) + { + } + byte[] digest = md.digest(); + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < digest.length; i++) + { + sb.append(Integer.toHexString(digest[i] & 0xFF)); + } + + signature = sb.toString(); + } + catch (IOException ex) + { + Log.e(LOG_TAG, ex.toString()); + } + catch (NoSuchAlgorithmException ex) + { + Log.e(LOG_TAG, ex.toString()); + } + finally + { + try + { + is.close(); + } + catch (IOException e) + { + } + } + return signature; + } + + private void commandWait(Command cmd) + { + synchronized (cmd) + { + try + { + if (!cmd.isFinished()) + { + cmd.wait(2000); + } + } + catch (InterruptedException ex) + { + Log.e(LOG_TAG, ex.toString()); + } + } + } +} diff --git a/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java b/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java new file mode 100644 index 0000000..6b52e20 --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java @@ -0,0 +1,62 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.internal; + +import com.stericson.roottools.containers.Mount; +import com.stericson.roottools.containers.Permissions; +import com.stericson.roottools.containers.Symlink; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +public class InternalVariables +{ + + // ---------------------- + // # Internal Variables # + // ---------------------- + + + protected static boolean nativeToolsReady = false; + protected static boolean found = false; + protected static boolean processRunning = false; + + protected static String[] space; + protected static String getSpaceFor; + protected static String busyboxVersion; + protected static String pid_list = ""; + protected static ArrayList mounts; + protected static ArrayList symlinks; + protected static String inode = ""; + protected static Permissions permissions; + + // regex to get pid out of ps line, example: + // root 2611 0.0 0.0 19408 2104 pts/2 S 13:41 0:00 bash + protected static final String PS_REGEX = "^\\S+\\s+([0-9]+).*$"; + protected static Pattern psPattern; + + static + { + psPattern = Pattern.compile(PS_REGEX); + } +} diff --git a/app/src/main/java/com/stericson/roottools/internal/Remounter.java b/app/src/main/java/com/stericson/roottools/internal/Remounter.java new file mode 100644 index 0000000..f054962 --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/internal/Remounter.java @@ -0,0 +1,238 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.internal; + +import com.stericson.rootshell.execution.Command; +import com.stericson.rootshell.execution.Shell; +import com.stericson.roottools.Constants; +import com.stericson.roottools.RootTools; +import com.stericson.roottools.containers.Mount; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.log.Log; + + +public class Remounter +{ + + private String customPath; + + public Remounter() { + } + + public Remounter(String path) { + this.customPath = path; + } + //------------- + //# Remounter # + //------------- + + /** + * This will take a path, which can contain the file name as well, + * and attempt to remount the underlying partition. + *

+ * For example, passing in the following string: + * "/system/bin/some/directory/that/really/would/never/exist" + * will result in /system ultimately being remounted. + * However, keep in mind that the longer the path you supply, the more work this has to do, + * and the slower it will run. + * + * @param file file path + * @param mountType mount type: pass in RO (Read only) or RW (Read Write) + * @return a boolean which indicates whether or not the partition + * has been remounted as specified. + */ + public boolean remount(String file, String mountType) + { + //if the path has a trailing slash get rid of it. + if (file.endsWith("/") && !file.equals("/")) + { + file = file.substring(0, file.lastIndexOf("/")); + } + //Make sure that what we are trying to remount is in the mount list. + boolean foundMount = false; + while (!foundMount) + { + try + { + for (Mount mount : RootTools.getMounts()) + { + RootTools.log(mount.getMountPoint().toString()); + + if (file.equals(mount.getMountPoint().toString())) + { + foundMount = true; + break; + } + } + } + catch (Exception e) + { + if (RootTools.debugMode) + { + Log.d(Api.TAG, e.getMessage(), e); + } + return false; + } + if (!foundMount) + { + try + { + file = (new File(file).getParent()); + } + catch (Exception e) + { + Log.e(Api.TAG, e.getMessage(), e); + return false; + } + } + } + + Mount mountPoint = findMountPointRecursive(file); + + if (mountPoint != null) + { + + RootTools.log(Constants.TAG, "Remounting " + mountPoint.getMountPoint().getAbsolutePath() + " as " + mountType.toLowerCase()); + final boolean isMountMode = mountPoint.getFlags().contains(mountType.toLowerCase()); + + if (!isMountMode) + { + //grab an instance of the internal class + try + { + Command command = new Command(0, + true, + "busybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(), + "toolbox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(), + "toybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(), + "mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(), + "mount -o remount," + mountType.toLowerCase() + " " + file, + "/system/bin/toolbox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(), + "/system/bin/toybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath() + ); + Shell.startRootShell().add(command); + commandWait(command); + + if(customPath != null) { + command = new Command(0, + true, + customPath + " mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath()); + Shell.startRootShell().add(command); + commandWait(command); + } + } + catch (Exception e) + { + } + + mountPoint = findMountPointRecursive(file); + } + + if (mountPoint != null) + { + RootTools.log(Constants.TAG, mountPoint.getFlags() + " AND " + mountType.toLowerCase()); + if (mountPoint.getFlags().contains(mountType.toLowerCase())) + { + RootTools.log(mountPoint.getFlags().toString()); + return true; + } + else + { + RootTools.log(mountPoint.getFlags().toString()); + return false; + } + } + else + { + RootTools.log("mount is null, file was: " + file + " mountType was: " + mountType); + } + } + else + { + RootTools.log("mount is null, file was: " + file + " mountType was: " + mountType); + } + + return false; + } + + private Mount findMountPointRecursive(String file) + { + try + { + ArrayList mounts = RootTools.getMounts(); + + for (File path = new File(file); path != null; ) + { + for (Mount mount : mounts) + { + if (mount.getMountPoint().equals(path)) + { + return mount; + } + } + } + + return null; + + } + catch (IOException e) + { + if (RootTools.debugMode) + { + e.printStackTrace(); + } + } + catch (Exception e) + { + if (RootTools.debugMode) + { + e.printStackTrace(); + } + } + + return null; + } + + private void commandWait(Command cmd) + { + synchronized (cmd) + { + try + { + if (!cmd.isFinished()) + { + cmd.wait(2000); + } + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + } +} diff --git a/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java b/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java new file mode 100644 index 0000000..f5b8257 --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java @@ -0,0 +1,1342 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.internal; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.util.Log; + +import com.stericson.rootshell.RootShell; +import com.stericson.rootshell.execution.Command; +import com.stericson.rootshell.execution.Shell; +import com.stericson.roottools.Constants; +import com.stericson.roottools.RootTools; +import com.stericson.roottools.containers.Mount; +import com.stericson.roottools.containers.Permissions; +import com.stericson.roottools.containers.Symlink; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; + +public final class RootToolsInternalMethods { + + // -------------------- + // # Internal methods # + // -------------------- + + protected RootToolsInternalMethods() { + } + + public static void getInstance() { + //this will allow RootTools to be the only one to get an instance of this class. + RootTools.setRim(new RootToolsInternalMethods()); + } + + public Permissions getPermissions(String line) { + + String[] lineArray = line.split(" "); + String rawPermissions = lineArray[0]; + + if (rawPermissions.length() == 10 + && (rawPermissions.charAt(0) == '-' + || rawPermissions.charAt(0) == 'd' || rawPermissions + .charAt(0) == 'l') + && (rawPermissions.charAt(1) == '-' || rawPermissions.charAt(1) == 'r') + && (rawPermissions.charAt(2) == '-' || rawPermissions.charAt(2) == 'w')) { + RootTools.log(rawPermissions); + + Permissions permissions = new Permissions(); + + permissions.setType(rawPermissions.substring(0, 1)); + + RootTools.log(permissions.getType()); + + permissions.setUserPermissions(rawPermissions.substring(1, 4)); + + RootTools.log(permissions.getUserPermissions()); + + permissions.setGroupPermissions(rawPermissions.substring(4, 7)); + + RootTools.log(permissions.getGroupPermissions()); + + permissions.setOtherPermissions(rawPermissions.substring(7, 10)); + + RootTools.log(permissions.getOtherPermissions()); + + StringBuilder finalPermissions = new StringBuilder(); + finalPermissions.append(parseSpecialPermissions(rawPermissions)); + finalPermissions.append(parsePermissions(permissions.getUserPermissions())); + finalPermissions.append(parsePermissions(permissions.getGroupPermissions())); + finalPermissions.append(parsePermissions(permissions.getOtherPermissions())); + + permissions.setPermissions(Integer.parseInt(finalPermissions.toString())); + + return permissions; + } + + return null; + } + + public int parsePermissions(String permission) { + permission = permission.toLowerCase(Locale.US); + int tmp; + if (permission.charAt(0) == 'r') { + tmp = 4; + } else { + tmp = 0; + } + + RootTools.log("permission " + tmp); + RootTools.log("character " + permission.charAt(0)); + + if (permission.charAt(1) == 'w') { + tmp += 2; + } else { + tmp += 0; + } + + RootTools.log("permission " + tmp); + RootTools.log("character " + permission.charAt(1)); + + if (permission.charAt(2) == 'x' || permission.charAt(2) == 's' + || permission.charAt(2) == 't') { + tmp += 1; + } else { + tmp += 0; + } + + RootTools.log("permission " + tmp); + RootTools.log("character " + permission.charAt(2)); + + return tmp; + } + + public int parseSpecialPermissions(String permission) { + int tmp = 0; + if (permission.charAt(2) == 's') { + tmp += 4; + } + + if (permission.charAt(5) == 's') { + tmp += 2; + } + + if (permission.charAt(8) == 't') { + tmp += 1; + } + + RootTools.log("special permissions " + tmp); + + return tmp; + } + + /** + * Copys a file to a destination. Because cp is not available on all android devices, we have a + * fallback on the cat command + * + * @param source example: /data/data/org.adaway/files/hosts + * @param destination example: /system/etc/hosts + * @param remountAsRw remounts the destination as read/write before writing to it + * @param preserveFileAttributes tries to copy file attributes from source to destination, if only cat is available + * only permissions are preserved + * @return true if it was successfully copied + */ + public boolean copyFile(String source, String destination, boolean remountAsRw, + boolean preserveFileAttributes) { + + Command command = null; + boolean result = true; + + try { + // mount destination as rw before writing to it + if (remountAsRw) { + RootTools.remount(destination, "RW"); + } + + // if cp is available and has appropriate permissions + if (checkUtil("cp")) { + RootTools.log("cp command is available!"); + + if (preserveFileAttributes) { + command = new Command(0, false, "cp -fp " + source + " " + destination); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + //ensure that the file was copied, an exitcode of zero means success + result = command.getExitCode() == 0; + + } else { + command = new Command(0, false, "cp -f " + source + " " + destination); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + //ensure that the file was copied, an exitcode of zero means success + result = command.getExitCode() == 0; + + } + } else { + if (checkUtil("busybox") && hasUtil("cp", "busybox")) { + RootTools.log("busybox cp command is available!"); + + if (preserveFileAttributes) { + command = new Command(0, false, "busybox cp -fp " + source + " " + destination); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + } else { + command = new Command(0, false, "busybox cp -f " + source + " " + destination); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + } + } else { // if cp is not available use cat + // if cat is available and has appropriate permissions + if (checkUtil("cat")) { + RootTools.log("cp is not available, use cat!"); + + int filePermission = -1; + if (preserveFileAttributes) { + // get permissions of source before overwriting + Permissions permissions = getFilePermissionsSymlinks(source); + filePermission = permissions.getPermissions(); + } + + // copy with cat + command = new Command(0, false, "cat " + source + " > " + destination); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + if (preserveFileAttributes) { + // set premissions of source to destination + command = new Command(0, false, "chmod " + filePermission + " " + destination); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + } + } else { + result = false; + } + } + } + + // mount destination back to ro + if (remountAsRw) { + RootTools.remount(destination, "RO"); + } + } catch (Exception e) { + e.printStackTrace(); + result = false; + } + + if (command != null) { + //ensure that the file was copied, an exitcode of zero means success + result = command.getExitCode() == 0; + } + + return result; + } + + /** + * This will check a given binary, determine if it exists and determine that + * it has either the permissions 755, 775, or 777. + * + * @param util Name of the utility to check. + * @return boolean to indicate whether the binary is installed and has + * appropriate permissions. + */ + public boolean checkUtil(String util) { + List foundPaths = RootShell.findBinary(util, true); + if (foundPaths.size() > 0) { + + for (String path : foundPaths) { + Permissions permissions = RootTools + .getFilePermissionsSymlinks(path + "/" + util); + + if (permissions != null) { + String permission; + + if (Integer.toString(permissions.getPermissions()).length() > 3) { + permission = Integer.toString(permissions.getPermissions()).substring(1); + } else { + permission = Integer.toString(permissions.getPermissions()); + } + + if (permission.equals("755") || permission.equals("777") + || permission.equals("775")) { + RootTools.utilPath = path + "/" + util; + return true; + } + } + } + } + + return false; + + } + + /** + * Deletes a file or directory + * + * @param target example: /data/data/org.adaway/files/hosts + * @param remountAsRw remounts the destination as read/write before writing to it + * @return true if it was successfully deleted + */ + public boolean deleteFileOrDirectory(String target, boolean remountAsRw) { + boolean result = true; + + try { + // mount destination as rw before writing to it + if (remountAsRw) { + RootTools.remount(target, "RW"); + } + + if (hasUtil("rm", "toolbox")) { + RootTools.log("rm command is available!"); + + Command command = new Command(0, false, "rm -r " + target); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + if (command.getExitCode() != 0) { + RootTools.log("target not exist or unable to delete file"); + result = false; + } + } else { + if (checkUtil("busybox") && hasUtil("rm", "busybox")) { + RootTools.log("busybox rm command is available!"); + + Command command = new Command(0, false, "busybox rm -rf " + target); + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + if (command.getExitCode() != 0) { + RootTools.log("target not exist or unable to delete file"); + result = false; + } + } + } + + // mount destination back to ro + if (remountAsRw) { + RootTools.remount(target, "RO"); + } + } catch (Exception e) { + e.printStackTrace(); + result = false; + } + + return result; + } + + /** + * This will try and fix a given binary. (This is for Busybox applets or Toolbox applets) By + * "fix", I mean it will try and symlink the binary from either toolbox or Busybox and fix the + * permissions if the permissions are not correct. + * + * @param util Name of the utility to fix. + * @param utilPath path to the toolbox that provides ln, rm, and chmod. This can be a blank string, a + * path to a binary that will provide these, or you can use + * RootTools.getWorkingToolbox() + */ + public void fixUtil(String util, String utilPath) { + try { + RootTools.remount("/system", "rw"); + + List foundPaths = RootShell.findBinary(util, true); + + if (foundPaths.size() > 0) { + for (String path : foundPaths) { + Command command = new Command(0, false, utilPath + " rm " + path + "/" + util); + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + + } + + Command command = new Command(0, false, utilPath + " ln -s " + utilPath + " /system/bin/" + util, utilPath + " chmod 0755 /system/bin/" + util); + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + + } + + RootTools.remount("/system", "ro"); + } catch (Exception e) { + } + } + + /** + * This will check an array of binaries, determine if they exist and determine that it has + * either the permissions 755, 775, or 777. If an applet is not setup correctly it will try and + * fix it. (This is for Busybox applets or Toolbox applets) + * + * @param utils Name of the utility to check. + * @return boolean to indicate whether the operation completed. Note that this is not indicative + * of whether the problem was fixed, just that the method did not encounter any + * exceptions. + * @throws Exception if the operation cannot be completed. + */ + public boolean fixUtils(String[] utils) throws Exception { + + for (String util : utils) { + if (!checkUtil(util)) { + if (checkUtil("busybox")) { + if (hasUtil(util, "busybox")) { + fixUtil(util, RootTools.utilPath); + } + } else { + if (checkUtil("toolbox")) { + if (hasUtil(util, "toolbox")) { + fixUtil(util, RootTools.utilPath); + } + } else { + return false; + } + } + } + } + + return true; + } + + /** + * This will return an List of Strings. Each string represents an applet available from BusyBox. + *

+ * + * @param path Path to the busybox binary that you want the list of applets from. + * @return null If we cannot return the list of applets. + */ + public List getBusyBoxApplets(String path) throws Exception { + + if (path != null && !path.endsWith("/") && !path.equals("")) { + path += "/"; + } else if (path == null) { + //Don't know what the user wants to do...what am I pshycic? + throw new Exception("Path is null, please specifiy a path"); + } + + final List results = new ArrayList(); + + Command command = new Command(Constants.BBA, false, path + "busybox --list") { + @Override + public void commandOutput(int id, String line) { + if (id == Constants.BBA) { + if (!line.trim().equals("") && !line.trim().contains("not found") && !line.trim().contains("file busy")) { + results.add(line); + } + } + + super.commandOutput(id, line); + } + }; + + //try without root first... + RootShell.getShell(false).add(command); + commandWait(RootShell.getShell(false), command); + + if (results.size() <= 0) { + //try with root... + + command = new Command(Constants.BBA, false, path + "busybox --list") { + @Override + public void commandOutput(int id, String line) { + if (id == Constants.BBA) { + if (!line.trim().equals("") && !line.trim().contains("not found") && !line.trim().contains("file busy")) { + results.add(line); + } + } + + super.commandOutput(id, line); + } + }; + + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + } + + return results; + } + + /** + * @return BusyBox version if found, "" if not found. + */ + public String getBusyBoxVersion(String path) { + + final StringBuilder version = new StringBuilder(); + + if (!path.equals("") && !path.endsWith("/")) { + path += "/"; + } + + try { + Command command = new Command(Constants.BBV, false, path + "busybox") { + @Override + public void commandOutput(int id, String line) { + line = line.trim(); + + boolean foundVersion = false; + + if (id == Constants.BBV) { + RootTools.log("Version Output: " + line); + + String[] temp = line.split(" "); + + if (temp.length > 1 && temp[1].contains("v1.") && !foundVersion) { + foundVersion = true; + version.append(temp[1]); + RootTools.log("Found Version: " + version.toString()); + } + } + + super.commandOutput(id, line); + } + }; + + //try without root first + RootTools.log("Getting BusyBox Version without root"); + Shell shell = RootTools.getShell(false); + shell.add(command); + commandWait(shell, command); + + if (version.length() <= 0) { + + command = new Command(Constants.BBV, false, path + "busybox") { + @Override + public void commandOutput(int id, String line) { + line = line.trim(); + + boolean foundVersion = false; + + if (id == Constants.BBV) { + RootTools.log("Version Output: " + line); + + String[] temp = line.split(" "); + + if (temp.length > 1 && temp[1].contains("v1.") && !foundVersion) { + foundVersion = true; + version.append(temp[1]); + RootTools.log("Found Version: " + version.toString()); + } + } + + super.commandOutput(id, line); + } + }; + + RootTools.log("Getting BusyBox Version with root"); + Shell rootShell = RootTools.getShell(true); + //Now look for it... + rootShell.add(command); + commandWait(rootShell, command); + } + + } catch (Exception e) { + RootTools.log("BusyBox was not found, more information MAY be available with Debugging on."); + return ""; + } + + RootTools.log("Returning found version: " + version.toString()); + return version.toString(); + } + + /** + * @return long Size, converted to kilobytes (from xxx or xxxm or xxxk etc.) + */ + public long getConvertedSpace(String spaceStr) { + try { + double multiplier = 1.0; + char c; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < spaceStr.length(); i++) { + c = spaceStr.charAt(i); + if (!Character.isDigit(c) && c != '.') { + if (c == 'm' || c == 'M') { + multiplier = 1024.0; + } else if (c == 'g' || c == 'G') { + multiplier = 1024.0 * 1024.0; + } + break; + } + sb.append(spaceStr.charAt(i)); + } + return (long) Math.ceil(Double.valueOf(sb.toString()) * multiplier); + } catch (Exception e) { + return -1; + } + } + + /** + * This method will return the inode number of a file. This method is dependent on having a version of + * ls that supports the -i parameter. + * + * @param file path to the file that you wish to return the inode number + * @return String The inode number for this file or "" if the inode number could not be found. + */ + public String getInode(String file) { + try { + Command command = new Command(Constants.GI, false, "/data/local/ls -i " + file) { + + @Override + public void commandOutput(int id, String line) { + if (id == Constants.GI) { + if (!line.trim().equals("") && Character.isDigit(line.trim().substring(0, 1).toCharArray()[0])) { + InternalVariables.inode = line.trim().split(" ")[0]; + } + } + + super.commandOutput(id, line); + } + }; + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + return InternalVariables.inode; + } catch (Exception ignore) { + return ""; + } + } + + public boolean isNativeToolsReady(int nativeToolsId, Context context) { + RootTools.log("Preparing Native Tools"); + InternalVariables.nativeToolsReady = false; + + Installer installer; + try { + installer = new Installer(context); + } catch (IOException ex) { + if (RootTools.debugMode) { + ex.printStackTrace(); + } + return false; + } + + if (installer.isBinaryInstalled("nativetools")) { + InternalVariables.nativeToolsReady = true; + } else { + InternalVariables.nativeToolsReady = installer.installBinary(nativeToolsId, + "nativetools", "700"); + } + return InternalVariables.nativeToolsReady; + } + + /** + * @param file String that represent the file, including the full path to the + * file and its name. + * @return An instance of the class permissions from which you can get the + * permissions of the file or if the file could not be found or + * permissions couldn't be determined then permissions will be null. + */ + public Permissions getFilePermissionsSymlinks(String file) { + RootTools.log("Checking permissions for " + file); + if (RootTools.exists(file)) { + RootTools.log(file + " was found."); + try { + + Command command = new Command( + Constants.FPS, false, "ls -l " + file, + "busybox ls -l " + file, + "/system/bin/failsafe/toolbox ls -l " + file, + "toolbox ls -l " + file) { + @Override + public void commandOutput(int id, String line) { + if (id == Constants.FPS) { + String symlink_final = ""; + + String[] lineArray = line.split(" "); + if (lineArray[0].length() != 10) { + super.commandOutput(id, line); + return; + } + + RootTools.log("Line " + line); + + try { + String[] symlink = line.split(" "); + if (symlink[symlink.length - 2].equals("->")) { + RootTools.log("Symlink found."); + symlink_final = symlink[symlink.length - 1]; + } + } catch (Exception e) { + } + + try { + InternalVariables.permissions = getPermissions(line); + if (InternalVariables.permissions != null) { + InternalVariables.permissions.setSymlink(symlink_final); + } + } catch (Exception e) { + RootTools.log(e.getMessage()); + } + } + + super.commandOutput(id, line); + } + }; + RootShell.getShell(true).add(command); + commandWait(RootShell.getShell(true), command); + + return InternalVariables.permissions; + + } catch (Exception e) { + RootTools.log(e.getMessage()); + return null; + } + } + + return null; + } + + /** + * This will return an ArrayList of the class Mount. The class mount contains the following + * property's: device mountPoint type flags + *

+ * These will provide you with any information you need to work with the mount points. + * + * @return ArrayList an ArrayList of the class Mount. + * @throws Exception if we cannot return the mount points. + */ + public ArrayList getMounts() throws Exception { + + InternalVariables.mounts = new ArrayList<>(); + + if(null == InternalVariables.mounts || InternalVariables.mounts.isEmpty()) { + Shell shell = RootTools.getShell(true); + + Command cmd = new Command(Constants.GET_MOUNTS, + false, + "cat /proc/mounts") { + + @Override + public void commandOutput(int id, String line) { + if (id == Constants.GET_MOUNTS) { + RootTools.log(line); + + String[] fields = line.split(" "); + InternalVariables.mounts.add(new Mount(new File(fields[0]), // device + new File(fields[1]), // mountPoint + fields[2], // fstype + fields[3] // flags + )); + } + + super.commandOutput(id, line); + } + }; + shell.add(cmd); + this.commandWait(shell, cmd); + } + + return InternalVariables.mounts; + } + + /** + * This will tell you how the specified mount is mounted. rw, ro, etc... + *

+ * + * @param path mount you want to check + * @return String What the mount is mounted as. + * @throws Exception if we cannot determine how the mount is mounted. + */ + public String getMountedAs(String path) throws Exception { + InternalVariables.mounts = getMounts(); + String mp; + if (InternalVariables.mounts != null) { + for (Mount mount : InternalVariables.mounts) { + + mp = mount.getMountPoint().getAbsolutePath(); + + if (mp.equals("/")) { + if (path.equals("/")) { + return (String) mount.getFlags().toArray()[0]; + } else { + continue; + } + } + + if (path.equals(mp) || path.startsWith(mp + "/")) { + RootTools.log((String) mount.getFlags().toArray()[0]); + return (String) mount.getFlags().toArray()[0]; + } + } + + throw new Exception(); + } else { + throw new Exception(); + } + } + + /** + * Get the space for a desired partition. + * + * @param path The partition to find the space for. + * @return the amount if space found within the desired partition. If the space was not found + * then the value is -1 + * @throws TimeoutException + */ + public long getSpace(String path) { + InternalVariables.getSpaceFor = path; + boolean found = false; + RootTools.log("Looking for Space"); + try { + final Command command = new Command(Constants.GS, false, "df " + path) { + + @Override + public void commandOutput(int id, String line) { + if (id == Constants.GS) { + if (line.contains(InternalVariables.getSpaceFor.trim())) { + InternalVariables.space = line.split(" "); + } + } + + super.commandOutput(id, line); + } + }; + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + } catch (Exception e) { + } + + if (InternalVariables.space != null) { + RootTools.log("First Method"); + + for (String spaceSearch : InternalVariables.space) { + + RootTools.log(spaceSearch); + + if (found) { + return getConvertedSpace(spaceSearch); + } else if (spaceSearch.equals("used,")) { + found = true; + } + } + + // Try this way + int count = 0, targetCount = 3; + + RootTools.log("Second Method"); + + if (InternalVariables.space[0].length() <= 5) { + targetCount = 2; + } + + for (String spaceSearch : InternalVariables.space) { + + RootTools.log(spaceSearch); + if (spaceSearch.length() > 0) { + RootTools.log(spaceSearch + ("Valid")); + if (count == targetCount) { + return getConvertedSpace(spaceSearch); + } + count++; + } + } + } + RootTools.log("Returning -1, space could not be determined."); + return -1; + } + + /** + * This will return a String that represent the symlink for a specified file. + *

+ * + * @param file file to get the Symlink for. (must have absolute path) + * @return String a String that represent the symlink for a specified file or an + * empty string if no symlink exists. + */ + public String getSymlink(String file) { + RootTools.log("Looking for Symlink for " + file); + + try { + final List results = new ArrayList(); + + Command command = new Command(Constants.GSYM, false, "ls -l " + file) { + + @Override + public void commandOutput(int id, String line) { + if (id == Constants.GSYM) { + if (!line.trim().equals("")) { + results.add(line); + } + } + + super.commandOutput(id, line); + } + }; + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + String[] symlink = results.get(0).split(" "); + if (symlink.length > 2 && symlink[symlink.length - 2].equals("->")) { + RootTools.log("Symlink found."); + + String final_symlink; + + if (!symlink[symlink.length - 1].equals("") && !symlink[symlink.length - 1].contains("/")) { + //We assume that we need to get the path for this symlink as it is probably not absolute. + List paths = RootShell.findBinary(symlink[symlink.length - 1], true); + if (paths.size() > 0) { + //We return the first found location. + final_symlink = paths.get(0) + symlink[symlink.length - 1]; + } else { + //we couldnt find a path, return the symlink by itself. + final_symlink = symlink[symlink.length - 1]; + } + } else { + final_symlink = symlink[symlink.length - 1]; + } + + return final_symlink; + } + } catch (Exception e) { + if (RootTools.debugMode) { + e.printStackTrace(); + } + } + + RootTools.log("Symlink not found"); + return ""; + } + + /** + * This will return an ArrayList of the class Symlink. The class Symlink contains the following + * property's: path SymplinkPath + *

+ * These will provide you with any Symlinks in the given path. + * + * @param path path to search for Symlinks. + * @return ArrayList an ArrayList of the class Symlink. + * @throws Exception if we cannot return the Symlinks. + */ + public ArrayList getSymlinks(String path) throws Exception { + + // this command needs find + if (!checkUtil("find")) { + throw new Exception(); + } + + InternalVariables.symlinks = new ArrayList<>(); + + Command command = new Command(0, false, "find " + path + " -type l -exec ls -l {} \\;") { + @Override + public void commandOutput(int id, String line) { + if (id == Constants.GET_SYMLINKS) { + RootTools.log(line); + + String[] fields = line.split(" "); + InternalVariables.symlinks.add(new Symlink(new File(fields[fields.length - 3]), // file + new File(fields[fields.length - 1]) // SymlinkPath + )); + + } + + super.commandOutput(id, line); + } + }; + Shell.startRootShell().add(command); + commandWait(Shell.startRootShell(), command); + + if (InternalVariables.symlinks != null) { + return InternalVariables.symlinks; + } else { + throw new Exception(); + } + } + + /** + * This will return to you a string to be used in your shell commands which will represent the + * valid working toolbox with correct permissions. For instance, if Busybox is available it will + * return "busybox", if busybox is not available but toolbox is then it will return "toolbox" + * + * @return String that indicates the available toolbox to use for accessing applets. + */ + public String getWorkingToolbox() { + if (RootTools.checkUtil("busybox")) { + return "busybox"; + } else if (RootTools.checkUtil("toolbox")) { + return "toolbox"; + } else { + return ""; + } + } + + /** + * Checks if there is enough Space on SDCard + * + * @param updateSize size to Check (long) + * @return true if the Update will fit on SDCard, false if not enough + * space on SDCard. Will also return false, if the SDCard is not mounted as + * read/write + */ + @SuppressWarnings("deprecation") + public boolean hasEnoughSpaceOnSdCard(long updateSize) { + RootTools.log("Checking SDcard size and that it is mounted as RW"); + String status = Environment.getExternalStorageState(); + if (!status.equals(Environment.MEDIA_MOUNTED)) { + return false; + } + File path = Environment.getExternalStorageDirectory(); + StatFs stat = new StatFs(path.getPath()); + long blockSize = 0; + long availableBlocks = 0; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + blockSize = stat.getBlockSize(); + availableBlocks = stat.getAvailableBlocks(); + } else { + blockSize = stat.getBlockSizeLong(); + availableBlocks = stat.getAvailableBlocksLong(); + } + return (updateSize < availableBlocks * blockSize); + } + + /** + * Checks whether the toolbox or busybox binary contains a specific util + * + * @param util + * @param box Should contain "toolbox" or "busybox" + * @return true if it contains this util + */ + public boolean hasUtil(final String util, final String box) { + + InternalVariables.found = false; + + // only for busybox and toolbox + if (!(box.endsWith("toolbox") || box.endsWith("busybox"))) { + return false; + } + + try { + + Command command = new Command(0, false, box.endsWith("toolbox") ? box + " " + util : box + " --list") { + + @Override + public void commandOutput(int id, String line) { + if (box.endsWith("toolbox")) { + if (!line.contains("no such tool")) { + InternalVariables.found = true; + } + } else if (box.endsWith("busybox")) { + // go through all lines of busybox --list + if (line.contains(util)) { + RootTools.log("Found util!"); + InternalVariables.found = true; + } + } + + super.commandOutput(id, line); + } + }; + RootTools.getShell(true).add(command); + commandWait(RootTools.getShell(true), command); + + if (InternalVariables.found) { + RootTools.log("Box contains " + util + " util!"); + return true; + } else { + RootTools.log("Box does not contain " + util + " util!"); + return false; + } + } catch (Exception e) { + RootTools.log(e.getMessage()); + return false; + } + } + + /** + * This method can be used to unpack a binary from the raw resources folder and store it in + * /data/data/app.package/files/ This is typically useful if you provide your own C- or + * C++-based binary. This binary can then be executed using sendShell() and its full path. + * + * @param context the current activity's Context + * @param sourceId resource id; typically R.raw.id + * @param destName destination file name; appended to /data/data/app.package/files/ + * @param mode chmod value for this file + * @return a boolean which indicates whether or not we were able to create the new + * file. + */ + public boolean installBinary(Context context, int sourceId, String destName, String mode) { + Installer installer; + + try { + installer = new Installer(context); + } catch (IOException ex) { + if (RootTools.debugMode) { + ex.printStackTrace(); + } + return false; + } + + return (installer.installBinary(sourceId, destName, mode)); + } + + /** + * This method checks whether a binary is installed. + * + * @param context the current activity's Context + * @param binaryName binary file name; appended to /data/data/app.package/files/ + * @return a boolean which indicates whether or not + * the binary already exists. + */ + public boolean isBinaryAvailable(Context context, String binaryName) { + Installer installer; + + try { + installer = new Installer(context); + } catch (IOException ex) { + if (RootTools.debugMode) { + ex.printStackTrace(); + } + return false; + } + + return (installer.isBinaryInstalled(binaryName)); + } + + /** + * This will let you know if an applet is available from BusyBox + *

+ * + * @param applet The applet to check for. + * @return true if applet is available, false otherwise. + */ + public boolean isAppletAvailable(String applet, String binaryPath) { + try { + for (String aplet : getBusyBoxApplets(binaryPath)) { + if (aplet.equals(applet)) { + return true; + } + } + return false; + } catch (Exception e) { + RootTools.log(e.toString()); + return false; + } + } + + /** + * This method can be used to to check if a process is running + * + * @param processName name of process to check + * @return true if process was found + * @throws TimeoutException (Could not determine if the process is running) + */ + public boolean isProcessRunning(final String processName) { + + RootTools.log("Checks if process is running: " + processName); + + InternalVariables.processRunning = false; + + try { + Command command = new Command(0, false, "ps") { + @Override + public void commandOutput(int id, String line) { + if (line.contains(processName)) { + InternalVariables.processRunning = true; + } + + super.commandOutput(id, line); + } + }; + RootTools.getShell(true).add(command); + commandWait(RootTools.getShell(true), command); + + } catch (Exception e) { + RootTools.log(e.getMessage()); + } + + return InternalVariables.processRunning; + } + + /** + * This method can be used to kill a running process + * + * @param processName name of process to kill + * @return true if process was found and killed successfully + */ + public boolean killProcess(final String processName) { + RootTools.log("Killing process " + processName); + + InternalVariables.pid_list = ""; + + //Assume that the process is running + InternalVariables.processRunning = true; + + try { + + Command command = new Command(0, false, "ps") { + @Override + public void commandOutput(int id, String line) { + if (line.contains(processName)) { + Matcher psMatcher = InternalVariables.psPattern.matcher(line); + + try { + if (psMatcher.find()) { + String pid = psMatcher.group(1); + + InternalVariables.pid_list += " " + pid; + InternalVariables.pid_list = InternalVariables.pid_list.trim(); + + RootTools.log("Found pid: " + pid); + } else { + RootTools.log("Matching in ps command failed!"); + } + } catch (Exception e) { + RootTools.log("Error with regex!"); + e.printStackTrace(); + } + } + + super.commandOutput(id, line); + } + }; + RootTools.getShell(true).add(command); + commandWait(RootTools.getShell(true), command); + + // get all pids in one string, created in process method + String pids = InternalVariables.pid_list; + + // kill processes + if (!pids.equals("")) { + try { + // example: kill -9 1234 1222 5343 + command = new Command(0, false, "kill -9 " + pids); + RootTools.getShell(true).add(command); + commandWait(RootTools.getShell(true), command); + + return true; + } catch (Exception e) { + RootTools.log(e.getMessage()); + } + } else { + //no pids match, must be dead + return true; + } + } catch (Exception e) { + RootTools.log(e.getMessage()); + } + + return false; + } + + /** + * This will launch the Android market looking for BusyBox + * + * @param activity pass in your Activity + */ + public void offerBusyBox(Activity activity) { + RootTools.log("Launching Market for BusyBox"); + Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse("market://details?id=stericson.busybox")); + activity.startActivity(i); + } + + /** + * This will launch the Android market looking for BusyBox, but will return the intent fired and + * starts the activity with startActivityForResult + * + * @param activity pass in your Activity + * @param requestCode pass in the request code + * @return intent fired + */ + public Intent offerBusyBox(Activity activity, int requestCode) { + RootTools.log("Launching Market for BusyBox"); + Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse("market://details?id=stericson.busybox")); + activity.startActivityForResult(i, requestCode); + return i; + } + + /** + * This will launch the Play Store looking for SuperUser + * + * @param activity pass in your Activity + */ + public void offerSuperUser(Activity activity) { + RootTools.log("Launching Play Store for SuperSU"); + Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse("market://details?id=eu.chainfire.supersu")); + activity.startActivity(i); + } + + /** + * This will launch the Play Store looking for SuperSU, but will return the intent fired + * and starts the activity with startActivityForResult + * + * @param activity pass in your Activity + * @param requestCode pass in the request code + * @return intent fired + */ + public Intent offerSuperUser(Activity activity, int requestCode) { + RootTools.log("Launching Play Store for SuperSU"); + Intent i = new Intent(Intent.ACTION_VIEW, + Uri.parse("market://details?id=eu.chainfire.supersu")); + activity.startActivityForResult(i, requestCode); + return i; + } + + private void commandWait(Shell shell, Command cmd) throws Exception { + + while (!cmd.isFinished()) { + + RootTools.log(Constants.TAG, shell.getCommandQueuePositionString(cmd)); + RootTools.log(Constants.TAG, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command."); + + synchronized (cmd) { + try { + if (!cmd.isFinished()) { + cmd.wait(2000); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + if (!cmd.isExecuting() && !cmd.isFinished()) { + if (!shell.isExecuting && !shell.isReading) { + Log.e(Constants.TAG, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else if (shell.isExecuting && !shell.isReading) { + Log.e(Constants.TAG, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } else { + Log.e(Constants.TAG, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand()); + Exception e = new Exception(); + e.setStackTrace(Thread.currentThread().getStackTrace()); + e.printStackTrace(); + } + } + + } + } +} diff --git a/app/src/main/java/com/stericson/roottools/internal/Runner.java b/app/src/main/java/com/stericson/roottools/internal/Runner.java new file mode 100644 index 0000000..4221fca --- /dev/null +++ b/app/src/main/java/com/stericson/roottools/internal/Runner.java @@ -0,0 +1,98 @@ +/* + * This file is part of the RootTools Project: http://code.google.com/p/RootTools/ + * + * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks + * + * This code is dual-licensed under the terms of the Apache License Version 2.0 and + * the terms of the General Public License (GPL) Version 2. + * You may use this code according to either of these licenses as is most appropriate + * for your project on a case-by-case basis. + * + * The terms of each license can be found in the root directory of this project's repository as well as at: + * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * http://www.gnu.org/licenses/gpl-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under these Licenses is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See each License for the specific language governing permissions and + * limitations under that License. + */ + +package com.stericson.roottools.internal; + +import android.content.Context; +import android.util.Log; + +import com.stericson.rootshell.execution.Command; +import com.stericson.rootshell.execution.Shell; +import com.stericson.roottools.RootTools; + +import java.io.IOException; + +public class Runner extends Thread +{ + + private static final String LOG_TAG = "RootTools::Runner"; + + Context context; + String binaryName; + String parameter; + + public Runner(Context context, String binaryName, String parameter) + { + this.context = context; + this.binaryName = binaryName; + this.parameter = parameter; + } + + public void run() + { + String privateFilesPath = null; + try + { + privateFilesPath = context.getFilesDir().getCanonicalPath(); + } + catch (IOException e) + { + if (RootTools.debugMode) + { + Log.e(LOG_TAG, "Problem occured while trying to locate private files directory!"); + } + e.printStackTrace(); + } + if (privateFilesPath != null) + { + try + { + Command command = new Command(0, false, privateFilesPath + "/" + binaryName + " " + parameter); + Shell.startRootShell().add(command); + commandWait(command); + + } + catch (Exception e) + { + } + } + } + + private void commandWait(Command cmd) + { + synchronized (cmd) + { + try + { + if (!cmd.isFinished()) + { + cmd.wait(2000); + } + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + } + +} diff --git a/app/src/main/java/com/twofortyfouram/locale/BreadCrumber.java b/app/src/main/java/com/twofortyfouram/locale/BreadCrumber.java new file mode 100644 index 0000000..539c4e5 --- /dev/null +++ b/app/src/main/java/com/twofortyfouram/locale/BreadCrumber.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012 two forty four a.m. LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package com.twofortyfouram.locale; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import dev.ukanth.ufirewall.R; + +/** + * Utility class to generate a breadcrumb title string for {@code Activity} instances in Locale. + *

+ * This class cannot be instantiated. + */ +public final class BreadCrumber +{ + /** + * Private constructor prevents instantiation + * + * @throws UnsupportedOperationException because this class cannot be instantiated. + */ + private BreadCrumber() + { + throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ + } + + /** + * Static helper method to generate bread crumbs. Bread crumb strings will be properly formatted for the + * current language, including right-to-left languages, as long as the proper + * {@link com.twofortyfouram.locale.platform.R.string#twofortyfouram_locale_breadcrumb_format} string + * resources have been created. + * + * @param context {@code Context} for loading platform resources. Cannot be null. + * @param intent {@code Intent} to extract the bread crumb from. + * @param currentCrumb The last element of the bread crumb path. + * @return {@code String} presentation of the bread crumb. If the intent parameter is null, then this + * method returns currentCrumb. If currentCrumb is null, then this method returns the empty string + * "". If intent contains a private Serializable instances as an extra, then this method returns + * the empty string "". + * @throws IllegalArgumentException if {@code context} is null. + */ + public static CharSequence generateBreadcrumb(final Context context, final Intent intent, + final String currentCrumb) + { + if (null == context) + { + throw new IllegalArgumentException("context cannot be null"); //$NON-NLS-1$ + } + + try + { + if (null == currentCrumb) + { + Log.w(Constants.LOG_TAG, "currentCrumb cannot be null"); //$NON-NLS-1$ + return ""; //$NON-NLS-1$ + } + if (null == intent) + { + Log.w(Constants.LOG_TAG, "intent cannot be null"); //$NON-NLS-1$ + return currentCrumb; + } + + /* + * Note: this is vulnerable to a private serializable attack, but the try-catch will solve that. + */ + final String breadcrumbString = intent.getStringExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BREADCRUMB); + if (null != breadcrumbString) + { + return context.getString(R.string.twofortyfouram_locale_breadcrumb_format, breadcrumbString, context.getString(R.string.twofortyfouram_locale_breadcrumb_separator), currentCrumb); + } + return currentCrumb; + } + catch (final Exception e) + { + Log.e(Constants.LOG_TAG, "Encountered error generating breadcrumb", e); //$NON-NLS-1$ + return ""; //$NON-NLS-1$ + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/twofortyfouram/locale/Constants.java b/app/src/main/java/com/twofortyfouram/locale/Constants.java new file mode 100644 index 0000000..39da5bd --- /dev/null +++ b/app/src/main/java/com/twofortyfouram/locale/Constants.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012 two forty four a.m. LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package com.twofortyfouram.locale; + +/** + * Utility class containing constants for the Locale Developer Platform. + */ +/* + * This class is NOT part of the public API. + */ +/* package */final class Constants +{ + /** + * Private constructor prevents instantiation + * + * @throws UnsupportedOperationException because this class cannot be instantiated. + */ + private Constants() + { + throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ + } + + /** + * Log tag for logcat messages generated by the Locale Developer Platform + */ + /* + * This is NOT a public API. Third party apps should NOT use this log tag for their own log messages. + */ + /* package */static final String LOG_TAG = "LocaleApiLibrary"; //$NON-NLS-1$ + + /** + * String package name for Locale. + */ + /* + * This is NOT a public API. Third parties should NOT rely on this being the only package name for Locale. + */ + /* package */static final String LOCALE_PACKAGE = "com.twofortyfouram.locale"; //$NON-NLS-1$ +} \ No newline at end of file diff --git a/app/src/main/java/com/twofortyfouram/locale/Intent.java b/app/src/main/java/com/twofortyfouram/locale/Intent.java new file mode 100644 index 0000000..40df3a7 --- /dev/null +++ b/app/src/main/java/com/twofortyfouram/locale/Intent.java @@ -0,0 +1,195 @@ +/* + * Copyright 2012 two forty four a.m. LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package com.twofortyfouram.locale; + +import android.os.Parcelable; + +/** + * Contains Intent constants necessary for interacting with the Locale Developer Platform. + */ +public final class Intent +{ + /** + * Private constructor prevents instantiation. + * + * @throws UnsupportedOperationException because this class cannot be instantiated. + */ + private Intent() + { + throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$ + } + + /** + * Ordered broadcast result code indicating that a plug-in condition's state is satisfied (true). + * + * @see Intent#ACTION_QUERY_CONDITION + */ + public static final int RESULT_CONDITION_SATISFIED = 16; + + /** + * Ordered broadcast result code indicating that a plug-in condition's state is not satisfied (false). + * + * @see Intent#ACTION_QUERY_CONDITION + */ + public static final int RESULT_CONDITION_UNSATISFIED = 17; + + /** + * Ordered broadcast result code indicating that a plug-in condition's state is unknown (neither true nor + * false). + *

+ * If a condition returns UNKNOWN, then Locale will use the last known return value on a best-effort + * basis. Best-effort means that Locale may not persist known values forever (e.g. last known values could + * hypothetically be cleared after a device reboot, a restart of the Locale process, or other events). If + * there is no last known return value, then unknown is treated as not satisfied (false). + *

+ * The purpose of an UNKNOWN result is to allow a plug-in condition more than 10 seconds to process a + * requery. A {@code BroadcastReceiver} must return within 10 seconds, otherwise it will be killed by + * Android. A plug-in that needs more than 10 seconds might initially return + * {@link #RESULT_CONDITION_UNKNOWN}, subsequently request a requery, and then return either + * {@link #RESULT_CONDITION_SATISFIED} or {@link #RESULT_CONDITION_UNSATISFIED}. + * + * @see Intent#ACTION_QUERY_CONDITION + */ + public static final int RESULT_CONDITION_UNKNOWN = 18; + + /** + * {@code Intent} action {@code String} broadcast by Locale to create or edit a plug-in setting. When + * Locale broadcasts this {@code Intent}, it will be sent directly to the package and class of the + * plug-in's {@code Activity}. The {@code Intent} may contain a {@link #EXTRA_BUNDLE} that was previously + * set by the {@code Activity} result of {@link #ACTION_EDIT_SETTING}. + *

+ * There SHOULD be only one {@code Activity} per APK that implements this {@code Intent}. If a single APK + * wishes to export multiple plug-ins, it MAY implement multiple Activity instances that implement this + * {@code Intent}, however there must only be a single {@link #ACTION_FIRE_SETTING} receiver. In this + * scenario, it is the responsibility of the Activities to store enough data in {@link #EXTRA_BUNDLE} to + * allow this receiver to disambiguate which "plug-in" is being fired. To avoid user confusion, it is + * recommended that only a single plug-in be implemented per APK. + * + * @see Intent#EXTRA_BUNDLE + * @see Intent#EXTRA_STRING_BREADCRUMB + */ + public static final String ACTION_EDIT_SETTING = "com.twofortyfouram.locale.intent.action.EDIT_SETTING"; //$NON-NLS-1$ + + /** + * {@code Intent} action {@code String} broadcast by Locale to fire a plug-in setting. When Locale + * broadcasts this {@code Intent}, it will be sent directly to the package and class of the plug-in's + * {@code BroadcastReceiver}. The {@code Intent} will contain a {@link #EXTRA_BUNDLE} that was previously + * set by the {@code Activity} result of {@link #ACTION_EDIT_SETTING}. + *

+ * There MUST be only one {@code BroadcastReceiver} per APK that implements this {@code Intent}. + * + * @see Intent#EXTRA_BUNDLE + */ + public static final String ACTION_FIRE_SETTING = "com.twofortyfouram.locale.intent.action.FIRE_SETTING"; //$NON-NLS-1$ + + /** + * {@code Intent} action {@code String} broadcast by Locale to create or edit a plug-in condition. When + * Locale broadcasts this {@code Intent}, it will be sent directly to the package and class of the + * plug-in's {@code Activity}. The {@code Intent} may contain a store-and-forward {@link #EXTRA_BUNDLE} + * that was previously set by the {@code Activity} result of {@link #ACTION_EDIT_CONDITION}. + *

+ * There SHOULD be only one {@code Activity} per APK that implements this {@code Intent}. If a single APK + * wishes to export multiple plug-ins, it MAY implement multiple Activity instances that implement this + * {@code Intent}, however there must only be a single {@link #ACTION_QUERY_CONDITION} receiver. In this + * scenario, it is the responsibility of the Activities to store enough data in {@link #EXTRA_BUNDLE} to + * allow this receiver to disambiguate which "plug-in" is being queried. To avoid user confusion, it is + * recommended that only a single plug-in be implemented per APK. + * + * @see Intent#EXTRA_BUNDLE + * @see Intent#EXTRA_STRING_BREADCRUMB + */ + public static final String ACTION_EDIT_CONDITION = "com.twofortyfouram.locale.intent.action.EDIT_CONDITION"; //$NON-NLS-1$ + + /** + * Ordered {@code Intent} action {@code String} broadcast by Locale to query a plug-in condition. When + * Locale broadcasts this {@code Intent}, it will be sent directly to the package and class of the + * plug-in's {@code BroadcastReceiver}. The {@code Intent} will contain a {@link #EXTRA_BUNDLE} that was + * previously set by the {@code Activity} result of {@link #ACTION_EDIT_CONDITION}. + *

+ * Since this is an ordered broadcast, the receiver is expected to set an appropriate result code from + * {@link #RESULT_CONDITION_SATISFIED}, {@link #RESULT_CONDITION_UNSATISFIED}, and + * {@link #RESULT_CONDITION_UNKNOWN}. + *

+ * There MUST be only one {@code BroadcastReceiver} per APK that implements this {@code Intent}. + * + * @see Intent#EXTRA_BUNDLE + * @see Intent#RESULT_CONDITION_SATISFIED + * @see Intent#RESULT_CONDITION_UNSATISFIED + * @see Intent#RESULT_CONDITION_UNKNOWN + */ + public static final String ACTION_QUERY_CONDITION = "com.twofortyfouram.locale.intent.action.QUERY_CONDITION"; //$NON-NLS-1$ + + /** + * {@code Intent} action {@code String} to notify Locale that a plug-in condition is requesting that + * Locale query it via {@link #ACTION_QUERY_CONDITION}. This merely serves as a hint to Locale that a + * condition wants to be queried. There is no guarantee as to when or if the plug-in will be queried after + * this {@code Intent} is broadcast. If Locale does not respond to the plug-in condition after a + * {@link #ACTION_REQUEST_QUERY} Intent is sent, the plug-in SHOULD shut itself down and stop requesting + * requeries. A lack of response from Locale indicates that Locale is not currently interested in this + * plug-in. When Locale becomes interested in the plug-in again, Locale will send + * {@link #ACTION_QUERY_CONDITION}. + *

+ * The extra {@link #EXTRA_ACTIVITY} MUST be included, otherwise Locale will ignore this {@code Intent}. + *

+ * Plug-in conditions SHOULD NOT use this unless there is some sort of asynchronous event that has + * occurred, such as a broadcast {@code Intent} being received by the plug-in. Plug-ins SHOULD NOT + * periodically request a requery as a way of implementing polling behavior. + * + * @see Intent#EXTRA_ACTIVITY + */ + public static final String ACTION_REQUEST_QUERY = "com.twofortyfouram.locale.intent.action.REQUEST_QUERY"; //$NON-NLS-1$ + + /** + * Type: {@code String} + *

+ * Maps to a {@code String} that represents the {@code Activity} bread crumb path. + * + * @see BreadCrumber + */ + public static final String EXTRA_STRING_BREADCRUMB = "com.twofortyfouram.locale.intent.extra.BREADCRUMB"; //$NON-NLS-1$ + + /** + * Type: {@code String} + *

+ * Maps to a {@code String} that represents a blurb. This is returned as an {@code Activity} result extra + * from {@link #ACTION_EDIT_CONDITION} or {@link #ACTION_EDIT_SETTING}. + *

+ * The blurb is a concise description displayed to the user of what the plug-in is configured to do. + */ + public static final String EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"; //$NON-NLS-1$ + + /** + * Type: {@code Bundle} + *

+ * Maps to a {@code Bundle} that contains all of a plug-in's extras. + *

+ * Plug-ins MUST NOT store {@link Parcelable} objects in this {@code Bundle}, because {@code Parcelable} + * is not a long-term storage format. Also, plug-ins MUST NOT store any serializable object that is not + * exposed by the Android SDK. + *

+ * The maximum size of a Bundle that can be sent across process boundaries is on the order of 500 + * kilobytes (base-10), while Locale further limits plug-in Bundles to about 100 kilobytes (base-10). + * Although the maximum size is about 100 kilobytes, plug-ins SHOULD keep Bundles much smaller for + * performance and memory usage reasons. + */ + public static final String EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"; //$NON-NLS-1$ + + /** + * Type: {@code String} + *

+ * Maps to a {@code String} that represents the name of a plug-in's {@code Activity}. + * + * @see Intent#ACTION_REQUEST_QUERY + */ + public static final String EXTRA_ACTIVITY = "com.twofortyfouram.locale.intent.extra.ACTIVITY"; //$NON-NLS-1$ +} \ No newline at end of file diff --git a/app/src/main/java/com/twofortyfouram/locale/PackageUtilities.java b/app/src/main/java/com/twofortyfouram/locale/PackageUtilities.java new file mode 100644 index 0000000..a1ef768 --- /dev/null +++ b/app/src/main/java/com/twofortyfouram/locale/PackageUtilities.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012 two forty four a.m. LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package com.twofortyfouram.locale; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A simple utility class to find a package that is compatible with hosting the Locale Developer Platform. + */ +/* + * This class is NOT part of the public Locale Developer Platform API + */ +public final class PackageUtilities +{ + /** + * A hard-coded set of Android packages that support the Locale Developer Platform. + */ + /* + * This is NOT a public field and is subject to change in future releases of the Developer Platform. A + * conscious design decision was made to use hard-coded package names, rather than dynamic discovery of + * packages that might be compatible with hosting the Locale Developer Platform API. This is for two + * reasons: to ensure the host is implemented correctly (hosts must pass the extensive Locale Platform + * Host compatibility test suite) and to prevent malicious applications from crashing plug-ins by + * providing bad values. As additional apps implement the Locale Developer Platform, their package names + * will also be added to this list. + */ + /* + * Note: this is implemented as a Set rather than a String[], in order to enforce immutability. + */ + private static final Set COMPATIBLE_PACKAGES = constructPackageSet(); + + /** + * @return a list wrapped in {@link Collections#unmodifiableList(List)} that represents the set of + * Locale-compatible packages. + */ + private static Set constructPackageSet() + { + final HashSet packages = new HashSet(); + + packages.add(Constants.LOCALE_PACKAGE); + + /* + * Note: Tasker is not 100% compatible with Locale's plug-in API, but it is close enough that these + * packages are enabled. Tasker's known incompatibilities are documented on the Tasker website. + */ + packages.add("net.dinglisch.android.taskerm"); //$NON-NLS-1$ + packages.add("net.dinglisch.android.tasker"); //$NON-NLS-1$ + packages.add("net.dinglisch.android.taskercupcake"); //$NON-NLS-1$ + + return Collections.unmodifiableSet(packages); + } + + /** + * Obtains the {@code String} package name of a currently-installed package which implements the host + * component of the Locale Developer Platform. + *

+ * Note: A TOCTOU error exists, due to the fact that the package could be uninstalled at any time. + *

+ * Note: If there are multiple hosts, this method will return one of them. The interface of this method + * makes no guarantee which host will returned, nor whether that host will be consistently returned. + * + * @param manager an instance of {@code PackageManager}. Cannot be null. + * @param packageHint hint as to which package should take precedence. This parameter may be null. + * @return {@code String} package name of a host for the Locale Developer Platform, such as + * "com.twofortyfouram.locale". If no such package is found, returns null. + */ + public static String getCompatiblePackage(final PackageManager manager, final String packageHint) + { + /* + * The interface for this method makes no guarantees as to which host will be returned. However the + * implementation is more predictable. + */ + + final List installedPackages = manager.getInstalledPackages(0); + + if (COMPATIBLE_PACKAGES.contains(packageHint)) + { + for (final PackageInfo packageInfo : installedPackages) + { + final String temp = packageInfo.packageName; + if (packageHint.equals(temp)) + { + return temp; + } + } + } + + for (final String compatiblePackageName : COMPATIBLE_PACKAGES) + { + if (compatiblePackageName.equals(packageHint)) + { + continue; + } + + for (final PackageInfo packageInfo : installedPackages) + { + final String temp = packageInfo.packageName; + if (compatiblePackageName.equals(temp)) + { + return temp; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/Api.java b/app/src/main/java/dev/ukanth/ufirewall/Api.java new file mode 100644 index 0000000..1f52351 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/Api.java @@ -0,0 +1,4419 @@ +/** + * All iptables "communication" is handled by this class. + *

+ * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro + * Copyright (C) 2011-2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Rodrigo Zechin Rosauro, Umakanthan Chandran + * @version 1.2 + */ + +package dev.ukanth.ufirewall; + +import static dev.ukanth.ufirewall.util.G.ctx; +import static dev.ukanth.ufirewall.util.G.ipv4Fwd; +import static dev.ukanth.ufirewall.util.G.ipv4Input; +import static dev.ukanth.ufirewall.util.G.ipv6Fwd; +import static dev.ukanth.ufirewall.util.G.ipv6Input; +import static dev.ukanth.ufirewall.util.G.showAllApps; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Base64; +import android.util.SparseArray; +import android.widget.Toast; +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.TaskStackBuilder; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.raizlabs.android.dbflow.sql.language.Delete; +import com.raizlabs.android.dbflow.sql.language.SQLite; +import com.raizlabs.android.dbflow.sql.language.Select; +import com.stericson.roottools.RootTools; +import com.topjohnwu.superuser.Shell; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + +import dev.ukanth.ufirewall.MainActivity.GetAppList; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.log.LogData; +import dev.ukanth.ufirewall.log.LogData_Table; +import dev.ukanth.ufirewall.preferences.DefaultConnectionPref; +import dev.ukanth.ufirewall.preferences.DefaultConnectionPref_Table; +import dev.ukanth.ufirewall.profiles.ProfileData; +import dev.ukanth.ufirewall.profiles.ProfileHelper; +import dev.ukanth.ufirewall.service.FirewallService; +import dev.ukanth.ufirewall.service.RootCommand; +import dev.ukanth.ufirewall.util.G; +import dev.ukanth.ufirewall.util.JsonHelper; +import dev.ukanth.ufirewall.util.UidResolver; +import dev.ukanth.ufirewall.widget.StatusWidget; + +/** + * Contains shared programming interfaces. + * All iptables "communication" is handled by this class. + */ +public final class Api { + /** + * application logcat tag + */ + public static final String TAG = "AFWall"; + + /** + * special application UID used to indicate "any application" + */ + public static final int SPECIAL_UID_ANY = -10; + /** + * special application UID used to indicate the Linux Kernel + */ + public static final int SPECIAL_UID_KERNEL = -11; + /** + * special application UID used for dnsmasq DHCP/DNS + */ + public static final int SPECIAL_UID_TETHER = -12; + + /** + * special application UID used for NTP + */ + public static final int SPECIAL_UID_NTP = -14; + + public static final int NOTIFICATION_ID = 1; + public static final String PREF_FIREWALL_STATUS = "AFWallStatus"; + public static final String DEFAULT_PREFS_NAME = "AFWallPrefs"; + //for import/export rules + //revertback to old approach for performance + public static final String PREF_3G_PKG_UIDS = "AllowedPKG3G_UIDS"; + public static final String PREF_WIFI_PKG_UIDS = "AllowedPKGWifi_UIDS"; + public static final String PREF_ROAMING_PKG_UIDS = "AllowedPKGRoaming_UIDS"; + public static final String PREF_VPN_PKG_UIDS = "AllowedPKGVPN_UIDS"; + public static final String PREF_TETHER_PKG_UIDS = "AllowedPKGTether_UIDS"; + public static final String PREF_LAN_PKG_UIDS = "AllowedPKGLAN_UIDS"; + public static final String PREF_TOR_PKG_UIDS = "AllowedPKGTOR_UIDS"; + public static final String PREF_CUSTOMSCRIPT = "CustomScript"; + public static final String PREF_CUSTOMSCRIPT2 = "CustomScript2"; // Executed on shutdown + public static final String PREF_MODE = "BlockMode"; + public static final String PREF_ENABLED = "Enabled"; + // Modes + public static final String MODE_WHITELIST = "whitelist"; + public static final String MODE_BLACKLIST = "blacklist"; + public static final String STATUS_CHANGED_MSG = "dev.ukanth.ufirewall.intent.action.STATUS_CHANGED"; + public static final String TOGGLE_REQUEST_MSG = "dev.ukanth.ufirewall.intent.action.TOGGLE_REQUEST"; + public static final String CUSTOM_SCRIPT_MSG = "dev.ukanth.ufirewall.intent.action.CUSTOM_SCRIPT"; + // Message extras (parameters) + public static final String STATUS_EXTRA = "dev.ukanth.ufirewall.intent.extra.STATUS"; + public static final String SCRIPT_EXTRA = "dev.ukanth.ufirewall.intent.extra.SCRIPT"; + public static final String SCRIPT2_EXTRA = "dev.ukanth.ufirewall.intent.extra.SCRIPT2"; + public static final int ERROR_NOTIFICATION_ID = 9; + private static final int WIFI_EXPORT = 0; + private static final int DATA_EXPORT = 1; + private static final int ROAM_EXPORT = 2; + // Messages + private static final int VPN_EXPORT = 3; + private static final int TETHER_EXPORT = 6; + private static final int LAN_EXPORT = 4; + private static final int TOR_EXPORT = 5; + private static final String[] ITFS_WIFI = InterfaceTracker.ITFS_WIFI; + private static final String[] ITFS_3G = InterfaceTracker.ITFS_3G; + private static final String[] ITFS_VPN = InterfaceTracker.ITFS_VPN; + private static final String[] ITFS_TETHER = InterfaceTracker.ITFS_TETHER; + // iptables can exit with status 4 if two processes tried to update the same table + private static final int IPTABLES_TRY_AGAIN = 4; + private static final String[] dynChains = {"-3g-postcustom", "-3g-fork", "-wifi-postcustom", "-wifi-fork"}; + private static final String[] natChains = {"", "-tor-check", "-tor-filter"}; + private static final String[] staticChains = {"", "-input", "-3g", "-wifi", "-reject", "-vpn", "-3g-tether", "-3g-home", "-3g-roam", "-wifi-tether", "-wifi-wan", "-wifi-lan", "-usb-tether", "-tor", "-tor-reject", "-tether", "-3g-home-reject", "-3g-roam-reject", "-wifi-wan-reject", "-wifi-lan-reject", "-vpn-reject", "-tether-reject"}; + private static volatile boolean globalStatus = false; + + private static final Object GLOBAL_STATUS_LOCK = new Object(); + + public static List getListOfUids() { + return listOfUids; + } + + private static List listOfUids = new ArrayList<>(); + + + private static Map uidToApplicationInfoMap = null; + + + private static final Pattern dual_pattern = Pattern.compile("package:(.*) uid:(.*)", Pattern.MULTILINE); + + /** + * @brief Special user/group IDs that aren't associated with + * any particular app. + *

+ * See: + * include/private/android_filesystem_config.h + * in platform/system/core.git. + *

+ * The accounts listed below are the only ones from + * android_filesystem_config.h that are known to be used as + * the UID of a process that uses the network. The other + * accounts in that .h file are either: + * * used as supplemental group IDs for granting extra + * privileges to apps, + * * used as UIDs of processes that don't need the network, + * or + * * have not yet been reported by users as needing the + * network. + *

+ * The list is sorted in ascending UID order. + */ + private static final String[] specialAndroidAccounts = { + "root", + "adb", + "media", + "vpn", + "drm", + "gps", + "shell" + }; + private static final Pattern p = Pattern.compile("UserHandle\\{(.*)\\}"); + // Preferences + public static String PREFS_NAME = "AFWallPrefs"; + // Cached applications + public static List applications = null; + public static Set recentlyInstalled = new HashSet<>(); + //for custom scripts + //public static String ipPath = null; + public static String bbPath = null; + private static final String charsetName = "UTF8"; + private static final String algorithm = "DES"; + private static final int base64Mode = Base64.DEFAULT; + //private static volatile String AFWALL_CHAIN_NAME = "afwall"; + private static final Object CHAIN_NAME_LOCK = new Object(); + private static Map specialApps = null; + private static volatile boolean rulesUpToDate = false; + private static final Object RULES_LOCK = new Object(); + public static void setRulesUpToDate(boolean rulesUpToDate) { + synchronized (RULES_LOCK) { + Api.rulesUpToDate = rulesUpToDate; + } + } + public static boolean getRulesUpToDate() { + synchronized (RULES_LOCK) { + return Api.rulesUpToDate; + } + } + + + // returns c.getString(R.string._item) + public static String getSpecialDescription(Context ctx, String acct) { + try { + int rid = ctx.getResources().getIdentifier(acct + "_item", "string", ctx.getPackageName()); + return ctx.getString(rid); + } catch (Resources.NotFoundException exception) { + return null; + } + } + + public static String getSpecialDescriptionSystem(Context ctx, String packageName) { + switch (packageName) { + case "any": + return ctx.getString(R.string.all_item); + case "kernel": + return ctx.getString(R.string.kernel_item); + case "tether": + return ctx.getString(R.string.tethering_item); + case "ntp": + return ctx.getString(R.string.ntp_item); + } + return ""; + } + + /** + * Display a simple alert box + * + * @param ctx context + * @param msgText message + */ + public static void toast(final Context ctx, final CharSequence msgText) { + if (ctx != null) { + Handler mHandler = new Handler(Looper.getMainLooper()); + mHandler.post(() -> Toast.makeText(G.getContext(), msgText, Toast.LENGTH_SHORT).show()); + } + } + + public static void toast(final Context ctx, final CharSequence msgText, final int toastlen) { + if (ctx != null) { + Handler mHandler = new Handler(Looper.getMainLooper()); + mHandler.post(() -> Toast.makeText(G.getContext(), msgText, toastlen).show()); + } + } + + public static String getBinaryPath(Context ctx, boolean setv6) { + String ip_path = G.ip_path(); + String binaryName = setv6 ? "ip6tables" : "iptables"; + + // If built-in binaries have previously failed with exit 126, prefer system binaries + if (G.isBuiltinIptablesFailed() && !ip_path.equals("builtin")) { + Log.i(TAG, "Built-in iptables previously failed, preferring system binary for " + binaryName); + String systemBinaryPath = findSystemBinary(binaryName); + if (systemBinaryPath != null) { + if (Api.bbPath == null) { + Api.bbPath = getBusyBoxPath(ctx, true); + } + return systemBinaryPath; + } + Log.w(TAG, "System binary " + binaryName + " not found despite previous built-in failure"); + } + + // First priority: check system binary if preference is "system" or "auto" + if (ip_path.equals("system") || ip_path.equals("auto")) { + String systemBinaryPath = findSystemBinary(binaryName); + if (systemBinaryPath != null) { + if (Api.bbPath == null) { + Api.bbPath = getBusyBoxPath(ctx, true); + } + return systemBinaryPath; + } + + // If system binary not found and preference is "system", log warning + if (ip_path.equals("system")) { + Log.w(TAG, "System binary " + binaryName + " not found, falling back to built-in"); + } + } + + // Second priority: use built-in binary + // Check if built-in binary exists for current architecture + String builtinDir = ctx.getDir("bin", 0).getAbsolutePath() + "/"; + String builtinPath = builtinDir + binaryName; + + File builtinFile = new File(builtinPath); + if (builtinFile.exists() && builtinFile.canExecute()) { + if (Api.bbPath == null) { + Api.bbPath = getBusyBoxPath(ctx, true); + } + return builtinPath; + } + + // Fallback: try to install built-in binaries if they don't exist + Log.w(TAG, "Built-in binary " + binaryName + " not found, attempting to install binaries"); + if (assertBinaries(ctx, false)) { + if (Api.bbPath == null) { + Api.bbPath = getBusyBoxPath(ctx, true); + } + return builtinPath; + } + + // Last resort: return the path even if binary doesn't exist (will likely fail at runtime) + Log.e(TAG, "No working " + binaryName + " binary found, returning built-in path anyway"); + if (Api.bbPath == null) { + Api.bbPath = getBusyBoxPath(ctx, true); + } + return builtinPath; + } + + /** + * Find system binary by checking common system paths + * + * @param binaryName the name of the binary to find + * @return full path to the binary if found, null otherwise + */ + public static String findSystemBinary(String binaryName) { + // Common paths where system iptables/ip6tables binaries are located + String[] systemPaths = { + "/system/bin/" + binaryName, + "/system/xbin/" + binaryName, + "/vendor/bin/" + binaryName, + "/sbin/" + binaryName, + "/usr/bin/" + binaryName, + "/bin/" + binaryName + }; + + for (String path : systemPaths) { + File binaryFile = new File(path); + if (binaryFile.exists() && binaryFile.canExecute()) { + Log.i(TAG, "Found system binary: " + path); + return path; + } + } + + // Also try using 'which' command if available + try { + Shell.Result result = Shell.cmd("which " + binaryName).exec(); + if (result.isSuccess() && !result.getOut().isEmpty()) { + String whichPath = result.getOut().get(0).trim(); + File whichFile = new File(whichPath); + if (whichFile.exists() && whichFile.canExecute()) { + Log.i(TAG, "Found system binary via 'which': " + whichPath); + return whichPath; + } + } + } catch (Exception e) { + Log.d(TAG, "Unable to use 'which' command to find " + binaryName + ": " + e.getMessage()); + } + + Log.d(TAG, "System binary " + binaryName + " not found in any standard location"); + return null; + } + + /** + * Determine toybox/busybox or built in + * + * @param ctx + * @param considerSystem + * @return + */ + public static String getBusyBoxPath(Context ctx, boolean considerSystem) { + String bb_path = G.bb_path(); + + // First priority: check system busybox if preference is "system" or "auto" and considerSystem is true + if (considerSystem && (bb_path.equals("system") || bb_path.equals("auto"))) { + String systemBusybox = findSystemBinary("busybox"); + if (systemBusybox != null) { + return systemBusybox + " "; + } + + // If system busybox not found and preference is "system", log warning and fall back + if (bb_path.equals("system")) { + Log.w(TAG, "System busybox not found, falling back to built-in"); + } + } + + // Second priority: use built-in busybox + String dir = ctx.getDir("bin", 0).getAbsolutePath(); + String builtinPath = dir + "/busybox"; + + File builtinFile = new File(builtinPath); + if (builtinFile.exists() && builtinFile.canExecute()) { + return builtinPath + " "; + } + + // Fallback: return built-in path even if it doesn't exist yet (may be installed later) + if (!builtinFile.exists()) { + Log.w(TAG, "Built-in busybox not found at " + builtinPath + ", returning path anyway"); + } else { + Log.w(TAG, "Built-in busybox exists but not executable at " + builtinPath + ", permissions: " + + (builtinFile.canRead() ? "R" : "-") + + (builtinFile.canWrite() ? "W" : "-") + + (builtinFile.canExecute() ? "X" : "-")); + } + return builtinPath + " "; + } + + /** + * Get NFLog Path - Enhanced version with fallback support + * + * @param ctx Context + * @return path to best available nflog binary + */ + public static String getNflogPath(Context ctx) { + String dir = ctx.getDir("bin", 0).getAbsolutePath(); + String originalPath = dir + "/nflog"; + File originalFile = new File(originalPath); + + if (!originalFile.exists()) { + Log.w(TAG, "No NFLOG binary found at: " + originalPath); + return null; + } + + if (!originalFile.canExecute()) { + Log.w(TAG, "NFLOG binary not executable at: " + originalPath); + // Try to make it executable + try { + originalFile.setExecutable(true); + if (!originalFile.canExecute()) { + Log.e(TAG, "Failed to make nflog executable"); + return null; + } + } catch (Exception e) { + Log.e(TAG, "Failed to make nflog executable: " + e.getMessage()); + return null; + } + } + + Log.i(TAG, "Using original NFLOG binary"); + return originalPath; + } + + /** + * Get enhanced NFLOG command with optimized parameters + * + * @param ctx Context + * @param queueNum NFLOG queue number + * @return complete command string with optimizations + */ + public static String getEnhancedNflogCommand(Context ctx, int queueNum) { + String nflogPath = getNflogPath(ctx); + if (nflogPath == null) { + return null; + } + + // Use standard nflog command with queue number + return nflogPath + " " + queueNum; + } + + + /** + * Copies a raw resource file, given its ID to the given location + * + * @param ctx context + * @param resid resource id + * @param file destination file + * @param mode file permissions (E.g.: "755") + * @throws IOException on error + * @throws InterruptedException when interrupted + */ + private static void copyRawFile(Context ctx, int resid, File file, String mode) throws IOException, InterruptedException { + final String abspath = file.getAbsolutePath(); + // Write the iptables binary + final FileOutputStream out = new FileOutputStream(file); + final InputStream is = ctx.getResources().openRawResource(resid); + byte[] buf = new byte[1024]; + int len; + while ((len = is.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + is.close(); + // Change the permissions + + executeSecureCommand(new String[]{"chmod", mode, abspath}); + } + + /** + * Execute system commands securely using ProcessBuilder to prevent command injection + * + * @param command Array of command and arguments (prevents shell interpretation) + * @throws IOException if command execution fails + * @throws InterruptedException if command is interrupted + */ + private static void executeSecureCommand(String[] command) throws IOException, InterruptedException { + if (command == null || command.length == 0) { + throw new IllegalArgumentException("Command cannot be null or empty"); + } + + // Validate command and arguments don't contain dangerous characters + for (String arg : command) { + if (arg == null || arg.contains("\n") || arg.contains("\r") || + arg.contains(";") || arg.contains("&") || arg.contains("|") || + arg.contains("`") || arg.contains("$")) { + Log.w(TAG, "Rejecting command with potentially dangerous characters: " + java.util.Arrays.toString(command)); + throw new SecurityException("Command contains illegal characters"); + } + } + + ProcessBuilder pb = new ProcessBuilder(command); + pb.environment().clear(); // Clear environment to prevent injection via env vars + Process process = pb.start(); + int exitCode = process.waitFor(); + + if (exitCode != 0) { + // For chmod commands, permission denied is expected on Android - don't fail the installation + if (command.length > 0 && "chmod".equals(command[0])) { + Log.w(TAG, "chmod command failed (expected on Android without root): exit code " + exitCode + " for " + java.util.Arrays.toString(command)); + return; // Don't throw exception for chmod failures + } + Log.w(TAG, "Command failed with exit code " + exitCode + ": " + java.util.Arrays.toString(command)); + throw new IOException("Command execution failed with exit code: " + exitCode); + } + } + + /** + * Look up uid for each user by name, and if he exists, append an iptables rule. + * + * @param listCommands current list of iptables commands to execute + * @param users list of users to whom the rule applies + * @param prefix "iptables" command and the portion of the rule preceding "-m owner --uid-owner X" + * @param suffix the remainder of the iptables rule, following "-m owner --uid-owner X" + */ + private static void addRuleForUsers(List listCommands, String[] users, String prefix, String suffix) { + for (String user : users) { + int uid = android.os.Process.getUidForName(user); + if (uid != -1) + listCommands.add(prefix + " -m owner --uid-owner " + uid + " " + suffix); + } + } + + private static void addRulesForUidlist(List cmds, List uids, String chain, boolean whitelist) { + String action = whitelist ? " -j RETURN" : " -j " + chain + "-reject"; + + if (uids.contains(SPECIAL_UID_ANY)) { + if (!whitelist) { + cmds.add("-A " + chain + action); + } else { + cmds.add("-A " + chain + " -j RETURN"); + } + } else { + for (Integer uid : uids) { + if (uid != null && uid >= 0) { + cmds.add("-A " + chain + " -m owner --uid-owner " + uid + action); + } + } + + /*// netd runs as root, and on Android 4.3+ it handles all DNS queries + if (uids.indexOf(SPECIAL_UID_DNSPROXY) >= 0) { + addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p udp --dport 53", action); + }*/ + + if (whitelist) { + addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p udp --dport 53", " -j RETURN"); + addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p tcp --dport 53", " -j RETURN"); + } else { + addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p udp --dport 53", " -j RETURN"); + addRuleForUsers(cmds, new String[]{"root"}, "-A " + chain + " -p tcp --dport 53", " -j RETURN"); + } + + + // NTP service runs as "system" user + if (uids.contains(SPECIAL_UID_NTP)) { + addRuleForUsers(cmds, new String[]{"system"}, "-A " + chain + " -p udp --dport 123", action); + } + + + if (G.getPrivateDnsStatus()) { + cmds.add("-A " + chain + " -p tcp --dport 853" + " -j ACCEPT"); + // disabling HTTPS over DNS + //cmds.add("-A " + chain + " -p tcp --dport 443" + " -j ACCEPT"); + } + + boolean kernel_checked = uids.contains(SPECIAL_UID_KERNEL); + + if (whitelist) { + if (kernel_checked) { + // reject any other UIDs, but allow the kernel through + // Use fallback rule if owner module is not available + if (G.hasOwnerModule()) { + Log.d(TAG, "Adding whitelist kernel rule with owner module for chain " + chain); + cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j " + chain + "-reject"); + } else { + Log.w(TAG, "Owner module not available, using fallback rule for chain " + chain); + cmds.add("-A " + chain + " -j " + chain + "-reject"); + } + } else { + // kernel is blocked so reject everything + String rejectRule = "-A " + chain + " -j " + chain + "-reject"; + cmds.add(rejectRule); + } + } else { + if (kernel_checked) { + // allow any other UIDs, but block the kernel + if (G.hasOwnerModule()) { + cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j RETURN"); + cmds.add("-A " + chain + " -j " + chain + "-reject"); + } else { + Log.w(TAG, "Owner module not available, using fallback rule for chain " + chain); + cmds.add("-A " + chain + " -j " + chain + "-reject"); + } + } + } + + //add 1052 for LAN + if(G.enableLAN() && G.hasOwnerModule()) { + cmds.add("-A " + "afwall-wifi-lan" + " -m owner --uid-owner 1052 -j RETURN"); + } + + if (G.hasOwnerModule()) { + cmds.add("-A " + "afwall-wifi-wan" + " -m owner --uid-owner 1052 -j RETURN"); + } + } + } + + private static void addRejectRules(List cmds, String chainName) { + // set up reject chain to log or not log + // this can be changed dynamically through the Firewall Logs activity + + if (G.enableLogService()) { + if (G.logTarget().trim().equals("LOG")) { + //cmds.add("-A " + chainName + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL-ALLOW}\" --log-level 4 --log-uid"); + String logRule = "-A " + chainName + "-reject" + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL}\" --log-level 4 --log-uid --log-tcp-options --log-ip-options"; + Log.d(TAG, "Adding LOG rule to reject chain: " + logRule); + cmds.add(logRule); + } else if (G.logTarget().trim().equals("NFLOG")) { + //cmds.add("-A " + chainName + " -j NFLOG --nflog-prefix \"{AFL-ALLOW}\" --nflog-group 40"); + String nflogRule = "-A " + chainName + "-reject" + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40"; + Log.d(TAG, "Adding NFLOG rule to reject chain: " + nflogRule); + cmds.add(nflogRule); + } + } + String rejectRule = "-A " + chainName + "-reject" + " -j REJECT"; + Log.d(TAG, "Adding final REJECT rule: " + rejectRule); + cmds.add(rejectRule); + + // Also populate individual reject chains that are used by whitelist mode + String[] rejectChainSuffixes = {"-3g-home-reject", "-3g-roam-reject", "-wifi-wan-reject", + "-wifi-lan-reject", "-vpn-reject", "-tether-reject"}; + for (String suffix : rejectChainSuffixes) { + String individualRejectChain = chainName + suffix; + Log.d(TAG, "Populating individual reject chain: " + individualRejectChain); + if (G.enableLogService() && G.logTarget().trim().equals("NFLOG")) { + String nflogRule = "-A " + individualRejectChain + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40"; + Log.d(TAG, "Adding NFLOG to individual reject chain: " + nflogRule); + cmds.add(nflogRule); + } + String individualRejectRule = "-A " + individualRejectChain + " -j REJECT"; + Log.d(TAG, "Adding REJECT to individual reject chain: " + individualRejectRule); + cmds.add(individualRejectRule); + } + } + + private static void addTorRules(List cmds, List uids, Boolean whitelist, Boolean ipv6, String chainName) { + for (Integer uid : uids) { + if (uid != null && uid >= 0) { + if (G.enableInbound() || ipv6) { + cmds.add("-A " + chainName + "-tor-reject -m owner --uid-owner " + uid + " -j " + chainName + "-reject"); + } + if (!ipv6) { + cmds.add("-t nat -A " + chainName + "-tor-check -m owner --uid-owner " + uid + " -j " + chainName + "-tor-filter"); + } + } + } + if (ipv6) { + cmds.add("-A " + chainName + " -j " + chainName + "-tor-reject"); + } else { + Integer socks_port = 9050; + Integer http_port = 8118; + Integer dns_port = 5400; + Integer tcp_port = 9040; + cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + socks_port + " -j RETURN"); + cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + http_port + " -j RETURN"); + cmds.add("-t nat -A " + chainName + "-tor-filter -p udp --dport 53 -j REDIRECT --to-ports " + dns_port); + cmds.add("-t nat -A " + chainName + "-tor-filter -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j REDIRECT --to-ports " + tcp_port); + cmds.add("-t nat -A " + chainName + "-tor-filter -j MARK --set-mark 0x500"); + cmds.add("-t nat -A " + chainName + " -j " + chainName + "-tor-check"); + cmds.add("-A " + chainName + "-tor -m mark --mark 0x500 -j " + chainName + "-reject"); + cmds.add("-A " + chainName + " -j " + chainName + "-tor"); + } + if (G.enableInbound()) { + cmds.add("-A " + chainName + "-input -j " + chainName + "-tor-reject"); + } + } + + private static String sanitizeRule(String rule) { + // Remove potentially dangerous characters and commands + if (rule.contains("&&") || rule.contains("||") || rule.contains(";") || + rule.contains("|") || rule.contains("`") || rule.contains("$") || + rule.contains("rm ") || rule.contains("dd ") || rule.contains("chmod ") || + rule.contains("chown ") || rule.contains("su ") || rule.contains("sudo ")) { + Log.w(TAG, "Rejecting potentially dangerous custom rule: " + rule); + return null; + } + + // Only allow basic iptables/ip6tables commands + if (!rule.startsWith("iptables ") && !rule.startsWith("ip6tables ") && + !rule.startsWith("-A ") && !rule.startsWith("-I ") && + !rule.startsWith("-D ") && !rule.startsWith("-F ") && + !rule.startsWith("-P ") && !rule.startsWith("-N ")) { + Log.w(TAG, "Rejecting non-iptables rule: " + rule); + return null; + } + + return rule; + } + + private static void addCustomRules(String prefName, List cmds) { + String customRulesStr = G.pPrefs.getString(prefName, ""); + if (customRulesStr.isEmpty()) return; + + String[] customRules = customRulesStr.split("[\\r\\n]+"); + for (String rule : customRules) { + if (rule.matches(".*\\S.*")) { + // Sanitize the rule to prevent command injection + String sanitizedRule = sanitizeRule(rule.trim()); + if (sanitizedRule != null && !sanitizedRule.isEmpty()) { + cmds.add("#LITERAL# " + sanitizedRule); + } + } + } + } + + /** + * Reconfigure the firewall rules based on interface changes seen at runtime: tethering + * enabled/disabled, IP address changes, etc. This should only affect a small number of + * rules; we want to avoid calling applyIptablesRulesImpl() too often since applying + * 100+ rules is expensive. + * + * @param ctx application context + * @param cmds command list + */ + private static void addInterfaceRouting(Context ctx, List cmds, boolean ipv6, String chainName) { + try { + //force only for v4 + final InterfaceDetails cfg = InterfaceTracker.getCurrentCfg(ctx, !ipv6); + final boolean whitelist = G.pPrefs.getString(PREF_MODE, MODE_WHITELIST).equals(MODE_WHITELIST); + for (String s : dynChains) { + cmds.add("-F " + chainName + s); + } + + if (whitelist) { + // always allow the DHCP client full wifi access + addRuleForUsers(cmds, new String[]{"dhcp", "wifi"}, "-A " + chainName + "-wifi-postcustom", "-j RETURN"); + } + + if (cfg.isWifiTethered || cfg.isUsbTethered) { + if (cfg.isWifiTethered) { + cmds.add("-A " + chainName + "-wifi-postcustom -j " + chainName + "-wifi-tether"); + } else { + cmds.add("-A " + chainName + "-wifi-postcustom -j " + chainName + "-wifi-fork"); + } + + if (cfg.isUsbTethered) { + cmds.add("-A " + chainName + "-3g-postcustom -j " + chainName + "-usb-tether"); + } else { + cmds.add("-A " + chainName + "-3g-postcustom -j " + (cfg.isWifiTethered ? chainName + "-3g-tether" : chainName + "-3g-fork")); + } + } else { + cmds.add("-A " + chainName + "-wifi-postcustom -j " + chainName + "-wifi-fork"); + cmds.add("-A " + chainName + "-3g-postcustom -j " + chainName + "-3g-fork"); + } + + if (G.enableLAN() && !cfg.isWifiTethered) { + if (ipv6) { + if (!cfg.lanMaskV6.equals("")) { + cmds.add("-A " + chainName + "-wifi-fork -d " + cfg.lanMaskV6 + " -j " + chainName + "-wifi-lan"); + cmds.add("-A " + chainName + "-wifi-fork '!' -d " + cfg.lanMaskV6 + " -j " + chainName + "-wifi-wan"); + } else { + Log.i(TAG, "no ipv6 found: " + G.enableIPv6() + "," + cfg.lanMaskV6); + } + } else { + if (!cfg.lanMaskV4.equals("")) { + cmds.add("-A " + chainName + "-wifi-fork -d " + cfg.lanMaskV4 + " -j " + chainName + "-wifi-lan"); + cmds.add("-A " + chainName + "-wifi-fork '!' -d " + cfg.lanMaskV4 + " -j " + chainName + "-wifi-wan"); + } else { + Log.i(TAG, "no ipv4 found:" + G.enableIPv6() + "," + cfg.lanMaskV4); + } + } + if (cfg.lanMaskV4.equals("") && cfg.lanMaskV6.equals("")) { + Log.i(TAG, "No ipaddress found for LAN"); + // lets find one more time + //atleast allow internet - don't block completely + cmds.add("-A " + chainName + "-wifi-fork -j " + chainName + "-wifi-wan"); + } + } else { + cmds.add("-A " + chainName + "-wifi-fork -j " + chainName + "-wifi-wan"); + } + + if (G.enableRoam() && cfg.isRoaming) { + cmds.add("-A " + chainName + "-3g-fork -j " + chainName + "-3g-roam"); + } else { + cmds.add("-A " + chainName + "-3g-fork -j " + chainName + "-3g-home"); + } + + + } catch (Exception e) { + Log.i(TAG, "Exception while applying shortRules " + e.getMessage()); + } + + } + + public static String getSpecialAppName(int uid) { + // First, try special apps (AFWall+ specific entries) + List packageInfoData = getSpecialData(); + for (PackageInfoData infoData : packageInfoData) { + if (infoData.uid == uid) { + return infoData.names.get(0); + } + } + + // If not found in special apps, use comprehensive UID resolver + return UidResolver.resolveUid(ctx, uid); + } + + + private static void applyShortRules(Context ctx, List cmds, boolean ipv6) { + Log.i(TAG, "Setting OUTPUT chain to DROP"); + cmds.add("-P OUTPUT DROP"); + /*FIXME: Adding custom rules might increase the time */ + Log.i(TAG, "Applying custom rules"); + addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds); + String chainName = getThreadSafeChainName(); + addInterfaceRouting(ctx, cmds, ipv6, chainName); + Log.i(TAG, "Setting OUTPUT chain to ACCEPT"); + cmds.add("-P OUTPUT ACCEPT"); + } + + + /** + * Purge and re-add all rules (internal implementation). + * + * @param ctx application context (mandatory) + * @param showErrors indicates if errors should be alerted + */ + private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet ruleDataSet, final boolean showErrors, + List out, boolean ipv6) { + return applyIptablesRulesImpl(ctx, ruleDataSet, showErrors, out, ipv6, null); + } + + private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet ruleDataSet, final boolean showErrors, + List out, boolean ipv6, String threadSafeChainName) { + if (ctx == null) { + return false; + } + + assertBinaries(ctx, showErrors); + + final InterfaceDetails cfg = InterfaceTracker.getCurrentCfg(ctx, !ipv6); + + // Use thread-safe chain name if provided, otherwise determine it safely + final String chainName; + if (threadSafeChainName != null) { + chainName = threadSafeChainName; + } else { + chainName = getThreadSafeChainName(); + } + final boolean whitelist = G.pPrefs.getString(PREF_MODE, MODE_WHITELIST).equals(MODE_WHITELIST); + + List cmds = new ArrayList(); + + Log.i(TAG, "Constructing rules for " + (ipv6 ? "v6": "v4")); + + //check before make them ACCEPT state + if (ipv4Input() || (ipv6 && ipv6Input())) { + cmds.add("-P INPUT ACCEPT"); + } + + if (ipv4Fwd() || (ipv6 && ipv6Fwd())) { + cmds.add("-P FORWARD ACCEPT"); + } + + try { + // prevent data leaks due to incomplete rules + cmds.add("-P OUTPUT DROP"); + + // Create and flush all chains first to ensure they exist + // Use NOCHK to avoid errors if chain already exists, then flush to ensure clean state + for (String s : staticChains) { + cmds.add("#NOCHK# -N " + chainName + s); + cmds.add("#NOCHK# -F " + chainName + s); + } + for (String s : dynChains) { + cmds.add("#NOCHK# -N " + chainName + s); + cmds.add("#NOCHK# -F " + chainName + s); + } + + + cmds.add("#NOCHK# -D OUTPUT -j " + chainName); + cmds.add("-I OUTPUT 1 -j " + chainName); + + + if (G.enableInbound()) { + cmds.add("#NOCHK# -D INPUT -j " + chainName + "-input"); + cmds.add("-I INPUT 1 -j " + chainName + "-input"); + } + + if (G.enableTor() && !ipv6) { + for (String s : natChains) { + cmds.add("#NOCHK# -t nat -N " + chainName + s); + cmds.add("-t nat -F " + chainName + s); + } + cmds.add("#NOCHK# -t nat -D OUTPUT -j " + chainName); + cmds.add("-t nat -I OUTPUT 1 -j " + chainName); + } + + // custom rules in afwall-{3g,wifi,reject} supersede everything else + addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds); + + cmds.add("-A " + chainName + "-3g -j " + chainName + "-3g-postcustom"); + cmds.add("-A " + chainName + "-wifi -j " + chainName + "-wifi-postcustom"); + addRejectRules(cmds, chainName); + + if (G.enableInbound()) { + // we don't have any rules in the INPUT chain prohibiting inbound traffic, but + // local processes can't reply to half-open connections without this rule + cmds.add("-A " + chainName + " -m state --state ESTABLISHED -j RETURN"); + cmds.add("-A " + chainName + "-input -m state --state ESTABLISHED -j RETURN"); + } + + addInterfaceRouting(ctx, cmds, ipv6, chainName); + + // send wifi, 3G, VPN packets to the appropriate dynamic chain based on interface + if (G.enableVPN()) { + // if !enableVPN then we ignore those interfaces (pass all traffic) + for (final String itf : ITFS_VPN) { + cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-vpn"); + } + // KitKat policy based routing - see: + // http://forum.xda-developers.com/showthread.php?p=48703545 + // This covers mark range 0x3c - 0x47. The official range is believed to be + // 0x3c - 0x45 but this is close enough. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + cmds.add("-A " + chainName + " -m mark --mark 0x3c/0xfffc -g " + chainName + "-vpn"); + cmds.add("-A " + chainName + " -m mark --mark 0x40/0xfff8 -g " + chainName + "-vpn"); + } + } + + if (G.enableTether()) { + for (final String itf : ITFS_TETHER) { + cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-tether"); + } + } + + for (final String itf : ITFS_WIFI) { + cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-wifi"); + } + + for (final String itf : ITFS_3G) { + cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-3g"); + } + + // special rules to allow tethering + // note that this can only blacklist DNS/DHCP services, not all tethered traffic + String[] users_dhcp = {"root", "nobody", "network_stack"}; + String[] users_dns = {"root", "nobody", "dns_tether"}; + String action = " -j " + (whitelist ? "RETURN" : chainName + "-reject"); + + if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER)) { + // DHCP replies to client + addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-wifi-tether", "-p udp --sport=67 --dport=68" + action); + // DNS replies to client + addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p udp --sport=53" + action); + addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p tcp --sport=53" + action); + + } + + // USB tethering rules + if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER) || containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) { + // DHCP replies to USB tethered client + addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-usb-tether", "-p udp --sport=67 --dport=68" + action); + // DNS replies to USB tethered client + addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p udp --sport=53" + action); + addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p tcp --sport=53" + action); + } + if (containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) { + // DHCP replies to client + addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-tether", "-p udp --sport=67 --dport=68" + action); + // DNS replies to client + addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p udp --sport=53" + action); + addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p tcp --sport=53" + action); + } + + // DNS requests to upstream servers - support all connection types + if (containsUidOrAny(ruleDataSet.dataList, SPECIAL_UID_TETHER)) { + // Define all tethering chains that need DNS upstream access + String[] tetherChains = {"-3g-tether", "-wifi-tether", "-usb-tether", "-tether"}; + + for (String chain : tetherChains) { + addRuleForUsers(cmds, users_dns, "-A " + chainName + chain, "-p udp --dport=53" + action); + addRuleForUsers(cmds, users_dns, "-A " + chainName + chain, "-p tcp --dport=53" + action); + } + } + + // if tethered, try to match the above rules (if enabled). no match -> fall through to the + // normal 3G/wifi rules + cmds.add("-A " + chainName + "-wifi-tether -j " + chainName + "-wifi-fork"); + cmds.add("-A " + chainName + "-3g-tether -j " + chainName + "-3g-fork"); + + // NOTE: we still need to open a hole to let WAN-only UIDs talk to a DNS server + // on the LAN - use specific DNS servers instead of opening to all LAN hosts + if (whitelist) { + // Add rules for specific DNS servers instead of all LAN hosts + addDnsServerRules(cmds, cfg, chainName + "-wifi-lan", ipv6); + + // Fallback: if no specific DNS servers found, use the old broad rule + if (cfg.dnsServersV4.isEmpty() && cfg.dnsServersV6.isEmpty()) { + cmds.add("-A " + chainName + "-wifi-lan -p udp --dport 53 -j RETURN"); + cmds.add("-A " + chainName + "-wifi-lan -p tcp --dport 53 -j RETURN"); + } + + //bug fix allow dns to be open on Pie for all connection type + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + cmds.add("-A " + chainName + "-wifi-wan" + " -p udp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-3g-home" + " -p udp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-3g-roam" + " -p udp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-vpn" + " -p udp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-tether" + " -p udp --dport 53" + " -j RETURN"); + + cmds.add("-A " + chainName + "-wifi-wan" + " -p tcp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-3g-home" + " -p tcp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-3g-roam" + " -p tcp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-vpn" + " -p tcp --dport 53" + " -j RETURN"); + cmds.add("-A " + chainName + "-tether" + " -p tcp --dport 53" + " -j RETURN"); + } + } + // now add the per-uid rules for 3G home, 3G roam, wifi WAN, wifi LAN, VPN + // in whitelist mode the last rule in the list routes everything else to afwall-reject + addRulesForUidlist(cmds, ruleDataSet.dataList, chainName + "-3g-home", whitelist); + addRulesForUidlist(cmds, ruleDataSet.roamList, chainName + "-3g-roam", whitelist); + addRulesForUidlist(cmds, ruleDataSet.wifiList, chainName + "-wifi-wan", whitelist); + addRulesForUidlist(cmds, ruleDataSet.lanList, chainName + "-wifi-lan", whitelist); + addRulesForUidlist(cmds, ruleDataSet.vpnList, chainName + "-vpn", whitelist); + addRulesForUidlist(cmds, ruleDataSet.tetherList, chainName + "-tether", whitelist); + if (G.enableTor()) { + addTorRules(cmds, ruleDataSet.torList, whitelist, ipv6, chainName); + } + cmds.add("-P OUTPUT ACCEPT"); + } catch (Exception e) { + Log.e(e.getClass().getName(), e.getMessage(), e); + } + + iptablesCommands(cmds, out, ipv6); + return true; + } + + /** + * Checks if a collection contains specified uid or {@code SPECIAL_UID_ANY} + * + * @param uidList collection of uids + * @param uidToCheck uid to check + * @return true if {@code uidList} contains {@code SPECIAL_UID_ANY} or {@code uidToCheck} + */ + private static boolean containsUidOrAny(Collection uidList, int uidToCheck) { + return uidList.contains(SPECIAL_UID_ANY) || uidList.contains(uidToCheck); + } + + /** + * Add the repetitive parts (ipPath and such) to an iptables command list + * + * @param in Commands in the format: "-A foo ...", "#NOCHK# -A foo ...", or "#LITERAL# " + * @param out A list of UNIX commands to execute + */ + private static void iptablesCommands(List in, List out, boolean ipv6) { + String ipPath = getBinaryPath(G.ctx, ipv6); + + String waitTime = ""; + if(G.ip_path().equals("system")) { + // Always use wait flag with system iptables to prevent lock contention + waitTime = " -w 5"; + } + boolean firstLit = true; + for (String s : in) { + s = s + waitTime; + if (s.matches("#LITERAL# .*")) { + if (firstLit) { + // export vars for the benefit of custom scripts + // "true" is a dummy command which needs to return success + firstLit = false; + out.add("export IPTABLES=\"" + ipPath + "\"; " + + "export BUSYBOX=\"" + bbPath + "\"; " + + "export IPV6=" + (ipv6 ? "1" : "0") + "; " + + "true"); + } + out.add(s.replaceFirst("^#LITERAL# ", "")); + } else if (s.matches("#NOCHK# .*")) { + out.add(s.replaceFirst("^#NOCHK# ", "#NOCHK# " + ipPath + " ")); + } else { + out.add(ipPath + " " + s); + } + } + } + + private static void fixupLegacyCmds(List cmds) { + for (int i = 0; i < cmds.size(); i++) { + String s = cmds.get(i); + if (s.matches("#NOCHK# .*")) { + s = s.replaceFirst("^#NOCHK# ", ""); + } else { + s += " || exit"; + } + cmds.set(i, s); + } + } + + /** + * Get thread-safe chain name, handling multi-user scenarios safely + */ + private static String getThreadSafeChainName() { + synchronized (CHAIN_NAME_LOCK) { + if (G.isMultiUser() && G.getMultiUserId() > 0) { + return "afwall" + G.getMultiUserId(); + } + return "afwall"; + } + } + + public static void waitAndTerminate(ExecutorService executorService) { + executorService.shutdown(); + try { + if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } catch (InterruptedException ex) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + public static void applySavedIptablesRules(Context ctx, boolean showErrors, RootCommand callback) { + synchronized (GLOBAL_STATUS_LOCK) { + if(!globalStatus) { + globalStatus = true; + + try { + RuleDataSet dataSet = getDataSet(); + List ipv4cmds = new ArrayList<>(); + List ipv6cmds = new ArrayList<>(); + + // Create thread-safe chain name for this execution + final String chainName = getThreadSafeChainName(); + + // Apply IPv4 rules first (sequentially) + try { + Log.i(TAG, "Applying IPv4 rules"); + applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv4cmds, false, chainName); + applySavedIp4tablesRules(ctx, ipv4cmds, callback); + Log.i(TAG, "Successfully applied IPv4 rules"); + } catch (Exception e) { + Log.e(TAG, "Error applying IPv4 rules", e); + throw new RuntimeException(e); + } + + // Apply IPv6 rules second (sequentially after IPv4) + if (G.enableIPv6()) { + try { + Log.i(TAG, "Applying IPv6 rules"); + applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv6cmds, true, chainName); + applySavedIp6tablesRules(ctx, ipv6cmds, new RootCommand()); + Log.i(TAG, "Successfully applied IPv6 rules"); + } catch (Exception e) { + Log.e(TAG, "Error applying IPv6 rules", e); + throw new RuntimeException(e); + } + } + + Log.i(TAG, "Successfully applied all firewall rules"); + + } catch (Exception e) { + Log.e(TAG, "Error applying rules", e); + } finally { + globalStatus = false; + setRulesUpToDate(true); + } + } else { + Log.i(TAG, "ignore applySavedIptablesRules as existing thread running"); + } + } + } + + + private static RuleDataSet getDataSet() { + initSpecial(); + + final String savedPkg_wifi_uid = G.pPrefs.getString(PREF_WIFI_PKG_UIDS, ""); + final String savedPkg_3g_uid = G.pPrefs.getString(PREF_3G_PKG_UIDS, ""); + final String savedPkg_roam_uid = G.pPrefs.getString(PREF_ROAMING_PKG_UIDS, ""); + final String savedPkg_vpn_uid = G.pPrefs.getString(PREF_VPN_PKG_UIDS, ""); + final String savedPkg_tether_uid = G.pPrefs.getString(PREF_TETHER_PKG_UIDS, ""); + final String savedPkg_lan_uid = G.pPrefs.getString(PREF_LAN_PKG_UIDS, ""); + final String savedPkg_tor_uid = G.pPrefs.getString(PREF_TOR_PKG_UIDS, ""); + + + List wifiList = getListFromPref(savedPkg_wifi_uid); + List dataList = getListFromPref(savedPkg_3g_uid); + + + // Warn if no applications are configured - this means no blocking will occur + if (wifiList.isEmpty() && dataList.isEmpty()) { + Log.w(TAG, "WARNING: No applications configured for firewall rules - firewall will not block any traffic!"); + Log.w(TAG, "Please configure applications in AFWall+ main screen and apply rules."); + } + + return new RuleDataSet(wifiList, + dataList, + getListFromPref(savedPkg_roam_uid), + getListFromPref(savedPkg_vpn_uid), + getListFromPref(savedPkg_tether_uid), + getListFromPref(savedPkg_lan_uid), + getListFromPref(savedPkg_tor_uid)); + + } + + /** + * Purge and re-add all saved rules (not in-memory ones). + * This is much faster than just calling "applyIptablesRules", since it don't need to read installed applications. + * + * @param ctx application context (mandatory) + * @param callback If non-null, use a callback instead of blocking the current thread + */ + public static boolean applySavedIp4tablesRules(Context ctx, List cmds, RootCommand callback) { + if (ctx == null) { + return false; + } + try { + callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, cmds); + return true; + } catch (Exception e) { + Log.e(TAG, "Exception while applying IPv4 rules: " + e.getMessage(), e); + // Only apply default chains if it's a critical failure + // Avoid overriding user chain preferences unnecessarily + if (e.getMessage() != null && !e.getMessage().contains("Chain") && !e.getMessage().contains("policy")) { + Log.w(TAG, "Applying default chains due to rule application failure"); + applyDefaultChains(ctx, callback); + } else { + Log.w(TAG, "Skipping default chains application to preserve user chain preferences"); + } + return false; + } + } + + + public static boolean applySavedIp6tablesRules(Context ctx, List cmds, RootCommand callback) { + if (ctx == null) { + return false; + } + try { + callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, cmds,true); + return true; + } catch (Exception e) { + Log.e(TAG, "Exception while applying IPv6 rules: " + e.getMessage(), e); + // Only apply default chains if it's a critical failure + // Avoid overriding user chain preferences unnecessarily + if (e.getMessage() != null && !e.getMessage().contains("Chain") && !e.getMessage().contains("policy")) { + Log.w(TAG, "Applying default chains due to rule application failure"); + applyDefaultChains(ctx, callback); + } else { + Log.w(TAG, "Skipping default chains application to preserve user chain preferences"); + } + return false; + } + } + + + public static boolean fastApply(Context ctx, RootCommand callback) { + try { + if (!getRulesUpToDate()) { + Log.i(TAG, "Using full Apply"); + applySavedIptablesRules(ctx, true, callback); + } else { + Log.i(TAG, "Using fastApply"); + List out = new ArrayList(); + List cmds; + cmds = new ArrayList(); + applyShortRules(ctx, cmds, false); + iptablesCommands(cmds, out, false); + if (G.enableIPv6()) { + cmds = new ArrayList(); + applyShortRules(ctx, cmds, true); + iptablesCommands(cmds, out, true); + } + callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out); + } + } catch (Exception e) { + Log.e(TAG, "Exception in fastApply: " + e.getMessage(), e); + // Only apply default chains if it's a critical failure + // Avoid overriding user chain preferences unnecessarily + if (e.getMessage() != null && !e.getMessage().contains("Chain") && !e.getMessage().contains("policy")) { + Log.w(TAG, "Applying default chains due to fastApply failure"); + applyDefaultChains(ctx, callback); + } else { + Log.w(TAG, "Skipping default chains application in fastApply to preserve user chain preferences"); + } + } + setRulesUpToDate(true); + return true; + } + + /** + * Save current rules using the preferences storage. + * + * @param ctx application context (mandatory) + */ + public static RuleDataSet generateRules(Context ctx, List apps, boolean store) { + + setRulesUpToDate(false); + RuleDataSet dataSet = null; + + if (apps != null) { + // Builds a pipe-separated list of names + HashSet newpkg_wifi = new HashSet(); + HashSet newpkg_3g = new HashSet(); + HashSet newpkg_roam = new HashSet(); + HashSet newpkg_vpn = new HashSet(); + HashSet newpkg_tether = new HashSet(); + HashSet newpkg_lan = new HashSet(); + HashSet newpkg_tor = new HashSet(); + + for (int i = 0; i < apps.size(); i++) { + if (apps.get(i) != null) { + if (apps.get(i).selected_wifi) { + newpkg_wifi.add(apps.get(i).uid); + } else { + if (!store) newpkg_wifi.add(-apps.get(i).uid); + } + if (apps.get(i).selected_3g) { + newpkg_3g.add(apps.get(i).uid); + } else { + if (!store) newpkg_3g.add(-apps.get(i).uid); + } + if (G.enableRoam()) { + if (apps.get(i).selected_roam) { + newpkg_roam.add(apps.get(i).uid); + } else { + if (!store) newpkg_roam.add(-apps.get(i).uid); + } + } + if (G.enableVPN()) { + if (apps.get(i).selected_vpn) { + newpkg_vpn.add(apps.get(i).uid); + } else { + if (!store) newpkg_vpn.add(-apps.get(i).uid); + } + } + if (G.enableTether()) { + if (apps.get(i).selected_tether) { + newpkg_tether.add(apps.get(i).uid); + } else { + if (!store) newpkg_tether.add(-apps.get(i).uid); + } + } + if (G.enableLAN()) { + if (apps.get(i).selected_lan) { + newpkg_lan.add(apps.get(i).uid); + } else { + if (!store) newpkg_lan.add(-apps.get(i).uid); + } + } + if (G.enableTor()) { + if (apps.get(i).selected_tor) { + newpkg_tor.add(apps.get(i).uid); + } else { + if (!store) newpkg_tor.add(-apps.get(i).uid); + } + } + } + } + + String wifi = android.text.TextUtils.join("|", newpkg_wifi); + String data = android.text.TextUtils.join("|", newpkg_3g); + String roam = android.text.TextUtils.join("|", newpkg_roam); + String vpn = android.text.TextUtils.join("|", newpkg_vpn); + String tether = android.text.TextUtils.join("|", newpkg_tether); + String lan = android.text.TextUtils.join("|", newpkg_lan); + String tor = android.text.TextUtils.join("|", newpkg_tor); + // save the new list of UIDs + if (store) { + SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + Editor edit = prefs.edit(); + edit.putString(PREF_WIFI_PKG_UIDS, wifi); + edit.putString(PREF_3G_PKG_UIDS, data); + edit.putString(PREF_ROAMING_PKG_UIDS, roam); + edit.putString(PREF_VPN_PKG_UIDS, vpn); + edit.putString(PREF_TETHER_PKG_UIDS, tether); + edit.putString(PREF_LAN_PKG_UIDS, lan); + edit.putString(PREF_TOR_PKG_UIDS, tor); + edit.apply(); + } else { + dataSet = new RuleDataSet(new ArrayList<>(newpkg_wifi), + new ArrayList<>(newpkg_3g), + new ArrayList<>(newpkg_roam), + new ArrayList<>(newpkg_vpn), + new ArrayList<>(newpkg_tether), + new ArrayList<>(newpkg_lan), + new ArrayList<>(newpkg_tor)); + } + } + return dataSet; + + } + + /** + * Purge all iptables rules. + * + * @param ctx mandatory context + * @param showErrors indicates if errors should be alerted + * @param callback If non-null, use a callback instead of blocking the current thread + * @return true if the rules were purged + */ + public static void purgeIptables(Context ctx, boolean showErrors, RootCommand callback) { + String chainName = getThreadSafeChainName(); + + List cmds = new ArrayList<>(); + List cmdsv4 = new ArrayList<>(); + List out = new ArrayList<>(); + + for (String s : staticChains) { + cmds.add("-F " + chainName + s); + } + for (String s : dynChains) { + cmds.add("-F " + chainName + s); + } + if (G.enableTor()) { + for (String s : natChains) { + cmdsv4.add("-t nat -F " + chainName + s); + } + cmdsv4.add("#NOCHK# -t nat -D OUTPUT -j " + chainName); + } else { + cmdsv4.add("#NOCHK# -D OUTPUT -j " + chainName); + } + + //make sure reset the OUTPUT chain to accept state. + cmds.add("-P OUTPUT ACCEPT"); + + //Delete only when the afwall chain exist ! + //cmds.add("-D OUTPUT -j " + chainName); + + if (G.enableInbound()) { + cmds.add("-D INPUT -j " + chainName + "-input"); + } + + addCustomRules(Api.PREF_CUSTOMSCRIPT2, cmds); + + // Execute the purge commands and call the callback + Log.i(TAG, "Executing purge commands for IPv4"); + cmds.addAll(cmdsv4); + iptablesCommands(cmds, out, false); + + if (G.enableIPv6()) { + Log.i(TAG, "Executing purge commands for IPv6"); + List cmdsv6 = new ArrayList<>(); + for (String s : staticChains) { + cmdsv6.add("-F " + chainName + s); + } + for (String s : dynChains) { + cmdsv6.add("-F " + chainName + s); + } + cmdsv6.add("#NOCHK# -D OUTPUT -j " + chainName); + cmdsv6.add("-P OUTPUT ACCEPT"); + if (G.enableInbound()) { + cmdsv6.add("-D INPUT -j " + chainName + "-input"); + } + iptablesCommands(cmdsv6, out, true); + } + + Log.i(TAG, "Purge completed, calling callback"); + callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out); + } + + /** + * Add DNS-specific iptables rules for identified DNS servers instead of broad LAN access + */ + private static void addDnsServerRules(List cmds, InterfaceDetails cfg, String chain, boolean ipv6) { + String protocol = ipv6 ? "ip6tables" : "iptables"; + java.util.List dnsServers = ipv6 ? cfg.dnsServersV6 : cfg.dnsServersV4; + + for (String dnsServer : dnsServers) { + if (dnsServer != null && !dnsServer.isEmpty()) { + // Add rules for both UDP and TCP DNS traffic to specific servers + cmds.add("-A " + chain + " -d " + dnsServer + " -p udp --dport 53 -j RETURN"); + cmds.add("-A " + chain + " -d " + dnsServer + " -p tcp --dport 53 -j RETURN"); + } + } + } + + + /** + * Retrieve the current set of IPv4 or IPv6 rules and pass it to a callback + * + * @param ctx application context + * @param callback callback to receive rule list + * @param useIPV6 true to list IPv6 rules, false to list IPv4 rules + */ + public static void fetchIptablesRules(Context ctx, boolean useIPV6, RootCommand callback) { + List cmds = new ArrayList<>(); + List out = new ArrayList<>(); + cmds.add("-n -v -L"); + iptablesCommands(cmds, out, false); + if (useIPV6) { + iptablesCommands(cmds, out, true); + } + callback.run(ctx, out); + } + + /** + * Run a list of commands with both iptables and ip6tables + * + * @param ctx application context + * @param cmds list of commands to run + * @param callback callback for completion + */ + public static void apply46(Context ctx, List cmds, RootCommand callback) { + List out = new ArrayList(); + iptablesCommands(cmds, out, false); + + if (G.enableIPv6()) { + iptablesCommands(cmds, out, true); + } + callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out); + } + + public static void applyIPv6Quick(Context ctx, List cmds, RootCommand callback) { + List out = new ArrayList(); + ////setBinaryPath(ctx, true); + iptablesCommands(cmds, out, true); + callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out); + } + + public static void applyQuick(Context ctx, List cmds, RootCommand callback) { + List out = new ArrayList(); + + //setBinaryPath(ctx, false); + iptablesCommands(cmds, out, false); + + //related to #511, disable ipv6 but use startup leak. + if (G.enableIPv6() || G.fixLeak()) { + //setBinaryPath(ctx, true); + iptablesCommands(cmds, out, true); + } + callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out); + } + + /** + * Delete all kingroot firewall rules. For diagnostic purposes only. + * + * @param ctx application context + * @param callback callback for completion + */ + public static void flushAllRules(Context ctx, RootCommand callback) { + List cmds = new ArrayList(); + cmds.add("-F"); + cmds.add("-X"); + apply46(ctx, cmds, callback); + } + + /** + * Enable or disable logging by rewriting the afwall-reject chain. Logging + * will be enabled or disabled based on the preference setting. + * + * @param ctx application context + * @param callback callback for completion + */ + public static void updateLogRules(Context ctx, RootCommand callback) { + if (!isEnabled(ctx)) { + return; + } + String chainName = getThreadSafeChainName(); + List cmds = new ArrayList(); + cmds.add("#NOCHK# -N " + chainName + "-reject"); + cmds.add("-F " + chainName + "-reject"); + addRejectRules(cmds, chainName); + apply46(ctx, cmds, callback); + } + + + //purge 2 hour data + public static void purgeOldLog() { + long purgeInterval = System.currentTimeMillis() - 7200000; + long count = new Select(com.raizlabs.android.dbflow.sql.language.Method.count()).from(LogData.class).count(); + //records are more + if(count > 5000) { + new Delete().from(LogData.class).where(LogData_Table.timestamp.lessThan(purgeInterval)).async().execute(); + } + } + + /** + * Fetch kernel logs via busybox dmesg. This will include {AFL} lines from + * logging rejected packets. + * + * @return true if logging is enabled, false otherwise + */ + public static List fetchLogs() { + //load hour data due to performance issue with old view + long loadInterval = System.currentTimeMillis() - 3600000; + List log = SQLite.select() + .from(LogData.class) + .where(LogData_Table.timestamp.greaterThan(loadInterval)) + .orderBy(LogData_Table.timestamp, true) + .queryList(); + purgeOldLog(); + //fetch last 100 records + if (log.size() > 100) { + return log.subList((log.size() - 100), log.size()); + } else { + return log; + } + } + + /** + * List all interfaces via "ifconfig -a" + * + * @param ctx application context + * @param callback Callback for completion status + */ + public static void runIfconfig(Context ctx, RootCommand callback) { + // Try system ifconfig first, then busybox for all versions + callback.run(ctx, "ifconfig -a || " + getBusyBoxPath(ctx, true) + " ifconfig -a"); + } + + public static void runNetworkInterface(Context ctx, RootCommand callback) { + // Try Android API method first for all versions + try { + StringBuilder result = new StringBuilder(); + java.util.Enumeration interfaces = java.net.NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + java.net.NetworkInterface networkInterface = interfaces.nextElement(); + result.append(networkInterface.getName()).append("\n"); + } + if (result.length() > 0) { + // Create a mock RootCommand with API results + RootCommand apiResult = new RootCommand(); + apiResult.res = result; + apiResult.exitCode = 0; + apiResult.done = true; + if (callback.cb != null) { + callback.cb.cbFunc(apiResult); + } + return; + } + } catch (Exception e) { + Log.d(TAG, "Android API network interface detection failed: " + e.getMessage()); + } + + // Fallback to shell commands with multiple options + String cmd = "ls /sys/class/net 2>/dev/null || " + + getBusyBoxPath(ctx, true) + " ls /sys/class/net 2>/dev/null || " + + "ip link show 2>/dev/null"; + callback.run(ctx, cmd); + } + + + public static void fixFolderPermissionsAsync(Context mContext) { + AsyncTask.execute(() -> { + try { + mContext.getFilesDir().setExecutable(true, false); + mContext.getFilesDir().setReadable(true, false); + File sharedPrefsFolder = new File(mContext.getFilesDir().getAbsolutePath() + + "/../shared_prefs"); + sharedPrefsFolder.setExecutable(true, false); + sharedPrefsFolder.setReadable(true, false); + } catch (Exception e) { + Log.e(Api.TAG, e.getMessage(), e); + } + }); + } + + /** + * @param ctx application context (mandatory) + * @return a list of applications + */ + public static List getApps(Context ctx, GetAppList appList) { + + initSpecial(); + if (applications != null && applications.size() > 0) { + // return cached instance + return applications; + } + + SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + + String savedPkg_wifi_uid = prefs.getString(PREF_WIFI_PKG_UIDS, ""); + String savedPkg_3g_uid = prefs.getString(PREF_3G_PKG_UIDS, ""); + String savedPkg_roam_uid = prefs.getString(PREF_ROAMING_PKG_UIDS, ""); + String savedPkg_vpn_uid = prefs.getString(PREF_VPN_PKG_UIDS, ""); + String savedPkg_tether_uid = prefs.getString(PREF_TETHER_PKG_UIDS, ""); + String savedPkg_lan_uid = prefs.getString(PREF_LAN_PKG_UIDS, ""); + String savedPkg_tor_uid = prefs.getString(PREF_TOR_PKG_UIDS, ""); + + List selected_wifi; + List selected_3g; + List selected_roam = new ArrayList<>(); + List selected_vpn = new ArrayList<>(); + List selected_tether = new ArrayList<>(); + List selected_lan = new ArrayList<>(); + List selected_tor = new ArrayList<>(); + + + selected_wifi = getListFromPref(savedPkg_wifi_uid); + selected_3g = getListFromPref(savedPkg_3g_uid); + + if (G.enableRoam()) { + selected_roam = getListFromPref(savedPkg_roam_uid); + } + if (G.enableVPN()) { + selected_vpn = getListFromPref(savedPkg_vpn_uid); + } + if (G.enableTether()) { + selected_tether = getListFromPref(savedPkg_tether_uid); + } + if (G.enableLAN()) { + selected_lan = getListFromPref(savedPkg_lan_uid); + } + if (G.enableTor()) { + selected_tor = getListFromPref(savedPkg_tor_uid); + } + //revert back to old approach + + //always use the defaul preferences to store cache value - reduces the application usage size + SharedPreferences cachePrefs = ctx.getSharedPreferences(DEFAULT_PREFS_NAME, Context.MODE_PRIVATE); + + int count = 0; + try { + listOfUids = new ArrayList<>(); + //this code will be executed on devices running ICS or later + final UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE); + List list = um.getUserProfiles(); + + for (UserHandle user : list) { + Matcher m = p.matcher(user.toString()); + if (m.find() && m.groupCount() > 0) { + int id = Integer.parseInt(m.group(1)); + if (id > 0) { + listOfUids.add(id); + } + } + } + //use pm list packages -f -U --user 10 + int pkgManagerFlags = PackageManager.GET_META_DATA; + // it's useless to iterate over uninstalled packages if we don't support multi-profile apps + if (G.supportDual()) { + pkgManagerFlags |= PackageManager.GET_UNINSTALLED_PACKAGES; + } + PackageManager pkgmanager = ctx.getPackageManager(); + List installed = pkgmanager.getInstalledApplications(pkgManagerFlags); + SparseArray syncMap = new SparseArray<>(); + Editor edit = cachePrefs.edit(); + boolean changed = false; + String name; + String cachekey; + String cacheLabel = "cache.label."; + PackageInfoData app; + ApplicationInfo apinfo; + + Date install = new Date(); + install.setTime(System.currentTimeMillis() - (180000)); + + SparseArray multiUserAppsMap = new SparseArray<>(); + HashMap packagesForUser = new HashMap<>(); + if(G.supportDual()) { + packagesForUser = getPackagesForUser(listOfUids); + } + + for (int i = 0; i < installed.size(); i++) { + //for (ApplicationInfo apinfo : installed) { + count = count + 1; + apinfo = installed.get(i); + + if (appList != null) { + appList.doProgress(count); + } + + boolean firstseen = false; + app = syncMap.get(apinfo.uid); + // filter applications which are not allowed to access the Internet + if (app == null && PackageManager.PERMISSION_GRANTED != pkgmanager.checkPermission(Manifest.permission.INTERNET, apinfo.packageName) && !showAllApps()) { + continue; + } + // try to get the application label from our cache - getApplicationLabel() is horribly slow!!!! + cachekey = cacheLabel + apinfo.packageName; + name = prefs.getString(cachekey, ""); + if (name.length() == 0 || isRecentlyInstalled(apinfo.packageName)) { + // get label and put on cache + name = pkgmanager.getApplicationLabel(apinfo).toString(); + edit.putString(cachekey, name); + changed = true; + firstseen = true; + } + if (app == null) { + app = new PackageInfoData(); + app.uid = apinfo.uid; + app.installTime = new File(apinfo.sourceDir).lastModified(); + app.names = new ArrayList(); + app.names.add(name); + app.appinfo = apinfo; + if (app.appinfo != null && (app.appinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + //user app + app.appType = 1; + } else { + //system app + app.appType = 0; + } + app.pkgName = apinfo.packageName; + if ((apinfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0) + syncMap.put(apinfo.uid, app); + } else { + app.names.add(name); + } + + app.firstseen = firstseen; + // check if this application is selected + if (!app.selected_wifi && Collections.binarySearch(selected_wifi, app.uid) >= 0) { + app.selected_wifi = true; + } + if (!app.selected_3g && Collections.binarySearch(selected_3g, app.uid) >= 0) { + app.selected_3g = true; + } + if (G.enableRoam() && !app.selected_roam && Collections.binarySearch(selected_roam, app.uid) >= 0) { + app.selected_roam = true; + } + if (G.enableVPN() && !app.selected_vpn && Collections.binarySearch(selected_vpn, app.uid) >= 0) { + app.selected_vpn = true; + } + if (G.enableTether() && !app.selected_tether && Collections.binarySearch(selected_tether, app.uid) >= 0) { + app.selected_tether = true; + } + if (G.enableLAN() && !app.selected_lan && Collections.binarySearch(selected_lan, app.uid) >= 0) { + app.selected_lan = true; + } + if (G.enableTor() && !app.selected_tor && Collections.binarySearch(selected_tor, app.uid) >= 0) { + app.selected_tor = true; + } + if (G.supportDual()) { + checkPartOfMultiUser(apinfo, name, listOfUids, packagesForUser, multiUserAppsMap); + } + } + + if (G.supportDual()) { + //run through multi user map + for (int i = 0; i < multiUserAppsMap.size(); i++) { + app = multiUserAppsMap.valueAt(i); + if (!app.selected_wifi && Collections.binarySearch(selected_wifi, app.uid) >= 0) { + app.selected_wifi = true; + } + if (!app.selected_3g && Collections.binarySearch(selected_3g, app.uid) >= 0) { + app.selected_3g = true; + } + if (G.enableRoam() && !app.selected_roam && Collections.binarySearch(selected_roam, app.uid) >= 0) { + app.selected_roam = true; + } + if (G.enableVPN() && !app.selected_vpn && Collections.binarySearch(selected_vpn, app.uid) >= 0) { + app.selected_vpn = true; + } + if (G.enableTether() && !app.selected_tether && Collections.binarySearch(selected_tether, app.uid) >= 0) { + app.selected_tether = true; + } + if (G.enableLAN() && !app.selected_lan && Collections.binarySearch(selected_lan, app.uid) >= 0) { + app.selected_lan = true; + } + if (G.enableTor() && !app.selected_tor && Collections.binarySearch(selected_tor, app.uid) >= 0) { + app.selected_tor = true; + } + syncMap.put(app.uid, app); + } + } + + List specialData = getSpecialData(); + + if (specialApps == null) { + specialApps = new HashMap(); + } + for (int i = 0; i < specialData.size(); i++) { + app = specialData.get(i); + //core apps + app.appType = 2; + specialApps.put(app.pkgName, app.uid); + //default DNS/NTP + if (app.uid != -1 && syncMap.get(app.uid) == null) { + // check if this application is allowed + if (!app.selected_wifi && Collections.binarySearch(selected_wifi, app.uid) >= 0) { + app.selected_wifi = true; + } + if (!app.selected_3g && Collections.binarySearch(selected_3g, app.uid) >= 0) { + app.selected_3g = true; + } + if (G.enableRoam() && !app.selected_roam && Collections.binarySearch(selected_roam, app.uid) >= 0) { + app.selected_roam = true; + } + if (G.enableVPN() && !app.selected_vpn && Collections.binarySearch(selected_vpn, app.uid) >= 0) { + app.selected_vpn = true; + } + if (G.enableTether() && !app.selected_tether && Collections.binarySearch(selected_tether, app.uid) >= 0) { + app.selected_tether = true; + } + if (G.enableLAN() && !app.selected_lan && Collections.binarySearch(selected_lan, app.uid) >= 0) { + app.selected_lan = true; + } + if (G.enableTor() && !app.selected_tor && Collections.binarySearch(selected_tor, app.uid) >= 0) { + app.selected_tor = true; + } + syncMap.put(app.uid, app); + } + } + + if (changed) { + edit.apply(); + } + /* convert the map into an array */ + applications = Collections.synchronizedList(new ArrayList()); + for (int i = 0; i < syncMap.size(); i++) { + applications.add(syncMap.valueAt(i)); + } + return applications; + } catch (Exception e) { + Log.i(TAG, "Exception in getting app list", e); + } + return new ArrayList<>(); + } + + /* public boolean isSuPackage(PackageManager pm, String suPackage) { + boolean found = false; + try { + PackageInfo info = pm.getPackageInfo(suPackage, 0); + if (info.applicationInfo != null) { + found = true; + } + //found = s + " v" + info.versionName; + } catch (NameNotFoundException e) { + } + return found; + }*/ + + public static List getSpecialData() { + List specialData = new ArrayList<>(); + specialData.add(new PackageInfoData(SPECIAL_UID_ANY, ctx.getString(R.string.all_item), "dev.afwall.special.any")); + specialData.add(new PackageInfoData(SPECIAL_UID_KERNEL, ctx.getString(R.string.kernel_item), "dev.afwall.special.kernel")); + specialData.add(new PackageInfoData(SPECIAL_UID_TETHER, ctx.getString(R.string.tethering_item), "dev.afwall.special.tether")); + specialData.add(new PackageInfoData(SPECIAL_UID_NTP, ctx.getString(R.string.ntp_item), "dev.afwall.special.ntp")); + + + specialData.add(new PackageInfoData(1020, ctx.getString(R.string.mdnslabel), "dev.afwall.special.mdnsr")); + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + specialData.add(new PackageInfoData(1029, ctx.getString(R.string.clat), "dev.afwall.special.clat")); + } + + /*if (additional) { + specialData.add(new PackageInfoData(1020, "mDNS", "dev.afwall.special.mDNS")); + }*/ + for (String acct : specialAndroidAccounts) { + String dsc = getSpecialDescription(ctx, acct); + if (dsc != null) { + String pkg = "dev.afwall.special." + acct; + specialData.add(new PackageInfoData(acct, dsc, pkg)); + } + } + return specialData; + } + + private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, List uid1, HashMap pkgs, SparseArray syncMap) { + try { + for (Integer integer : uid1) { + int appUid = Integer.parseInt(integer + "" + apinfo.uid + ""); + try{ + //String[] pkgs = pkgmanager.getPackagesForUid(appUid); + if (packagesExistForUserUid(pkgs, appUid)) { + PackageInfoData app = new PackageInfoData(); + app.uid = appUid; + app.installTime = new File(apinfo.sourceDir).lastModified(); + app.names = new ArrayList(); + app.names.add(name + "(M)"); + app.appinfo = apinfo; + if (app.appinfo != null && (app.appinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + //user app + app.appType = 1; + } else { + //system app + app.appType = 0; + } + app.pkgName = apinfo.packageName; + syncMap.put(appUid, app); + } + }catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + } + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + } + + private static boolean packagesExistForUserUid(HashMap pkgs, int appUid) { + if(pkgs.containsKey(appUid)){ + return true; + } + return false; + } + + public static HashMap getPackagesForUser(List userProfile) { + HashMap listApps = new HashMap<>(); + for(Integer integer: userProfile) { + try { + Shell.Result result = Shell.cmd("pm list packages -U --user " + integer).exec(); + List out = result.getOut(); + Matcher matcher; + for (String item : out) { + matcher = dual_pattern.matcher(item); + if (matcher.find() && matcher.groupCount() > 0) { + String packageName = matcher.group(1); + String packageId = matcher.group(2); + Log.i(TAG, packageId + " " + packageName); + listApps.put(Integer.parseInt(packageId), packageName); + } + } + } catch (java.util.concurrent.RejectedExecutionException e) { + Log.w(TAG, "Package listing rejected for user " + integer + ": " + e.getMessage()); + break; // Stop processing other users if execution rejected + } catch (Exception e) { + Log.e(TAG, "Failed to list packages for user " + integer + ": " + e.getMessage()); + // Continue with next user on other errors + } + } + return listApps.size() > 0 ? listApps : null; + } + + private static boolean isRecentlyInstalled(String packageName) { + boolean isRecent = false; + if (recentlyInstalled != null && recentlyInstalled.contains(packageName)) { + isRecent = true; + recentlyInstalled.remove(packageName); + } + return isRecent; + } + + private static List getListFromPref(String savedPkg_uid) { + StringTokenizer tok = new StringTokenizer(savedPkg_uid, "|"); + List listUids = new ArrayList<>(); + while (tok.hasMoreTokens()) { + String uid = tok.nextToken(); + if (!uid.equals("")) { + listUids.add(Integer.parseInt(uid)); + } + } + // Sort the array to allow using "Arrays.binarySearch" later + Collections.sort(listUids); + return listUids; + } + + /*public static boolean isAppAllowed(Context context, ApplicationInfo applicationInfo, SharedPreferences sharedPreferences, SharedPreferences pPrefs) { + InterfaceDetails details = InterfaceTracker.getCurrentCfg(context, true); + //allow webview to download since webview requires INTERNET permission + if (applicationInfo.packageName.equals("com.android.webview") || applicationInfo.packageName.equals("com.google.android.webview")) { + return true; + } + if (details != null && details.netEnabled) { + String mode = pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST); + Log.i(TAG, "Calling isAppAllowed method from DM with Mode: " + mode); + switch ((details.netType)) { + case ConnectivityManager.TYPE_WIFI: + String savedPkg_wifi_uid = pPrefs.getString(PREF_WIFI_PKG_UIDS, ""); + if (savedPkg_wifi_uid.isEmpty()) { + savedPkg_wifi_uid = sharedPreferences.getString(PREF_WIFI_PKG_UIDS, ""); + } + Log.i(TAG, "DM check for UID: " + applicationInfo.uid); + Log.i(TAG, "DM allowed UIDs: " + savedPkg_wifi_uid); + if (mode.equals(Api.MODE_WHITELIST) && savedPkg_wifi_uid.contains(applicationInfo.uid + "")) { + return true; + } else return mode.equals(Api.MODE_BLACKLIST) && !savedPkg_wifi_uid.contains(applicationInfo.uid + ""); + + case ConnectivityManager.TYPE_MOBILE: + String savedPkg_3g_uid = pPrefs.getString(PREF_3G_PKG_UIDS, ""); + if (details.isRoaming) { + savedPkg_3g_uid = pPrefs.getString(PREF_ROAMING_PKG_UIDS, ""); + } + Log.i(TAG, "DM check for UID: " + applicationInfo.uid); + Log.i(TAG, "DM allowed UIDs: " + savedPkg_3g_uid); + if (mode.equals(Api.MODE_WHITELIST) && savedPkg_3g_uid.contains(applicationInfo.uid + "")) { + return true; + } else return mode.equals(Api.MODE_BLACKLIST) && !savedPkg_3g_uid.contains(applicationInfo.uid + ""); + } + } + + return true; + }*/ + + /** + * Get Default Chain status + * + * @param ctx + * @param callback + */ + public static void getChainStatus(Context ctx, RootCommand callback) { + List cmds = new ArrayList(); + cmds.add("-S INPUT"); + cmds.add("-S OUTPUT"); + cmds.add("-S FORWARD"); + List out = new ArrayList<>(); + + iptablesCommands(cmds, out, false); + + ArrayList base = new ArrayList(); + base.add("-S INPUT"); + base.add("-S OUTPUT"); + cmds.add("-S FORWARD"); + iptablesCommands(base, out, true); + + callback.run(ctx, out); + } + + /** + * Apply single rule + * + * @param ctx + * @param rule + * @param isIpv6 + * @param callback + */ + public static void applyRule(Context ctx, String rule, boolean isIpv6, RootCommand callback) { + List cmds = new ArrayList(); + cmds.add(rule); + //setBinaryPath(ctx, isIpv6); + List out = new ArrayList<>(); + iptablesCommands(cmds, out, isIpv6); + callback.run(ctx, out); + } + + /** + * Runs a script as root (multiple commands separated by "\n") + * + * @param ctx mandatory context + * @param script the script to be executed + * @param res the script output response (stdout + stderr) + * @return the script exit code + * @throws IOException on any error executing the script, or writing it to disk + */ + public static int runScriptAsRoot(Context ctx, List script, StringBuilder res) throws IOException { + int returnCode = -1; + + if ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper())) { + Log.e(TAG, "runScriptAsRoot should not be called from the main thread\nCall Trace:\n"); + for (StackTraceElement e : new Throwable().getStackTrace()) { + Log.e(TAG, e.toString()); + } + } + + try { + RunCommand runCommand = new RunCommand(); + returnCode = runCommand.execute(script, res, ctx).get(); + } catch (RejectedExecutionException r) { + Log.w(TAG, "Shell execution rejected, likely due to app shutdown: " + r.getLocalizedMessage()); + returnCode = -1; + } catch (InterruptedException e) { + Log.w(TAG, "Shell execution was interrupted: " + e.getLocalizedMessage()); + Thread.currentThread().interrupt(); // Restore interrupted status + returnCode = -1; + } catch (java.util.concurrent.ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof java.io.InterruptedIOException) { + Log.w(TAG, "Shell execution interrupted (IO): " + cause.getMessage()); + } else if (cause instanceof java.util.concurrent.RejectedExecutionException) { + Log.w(TAG, "Shell execution rejected in wrapped exception: " + cause.getMessage()); + } else { + Log.e(TAG, "Shell execution failed with ExecutionException: " + e.getLocalizedMessage()); + } + returnCode = -1; + } catch (Exception e) { + Log.e(TAG, "Unexpected error during shell execution: " + e.getLocalizedMessage()); + returnCode = -1; + } + + return returnCode; + } + + private static boolean installBinary(Context ctx, int resId, String filename) { + try { + File binDir = ctx.getDir("bin", 0); + File f = new File(binDir, filename); + + Log.d(TAG, "Installing binary: " + filename + " to " + f.getAbsolutePath()); + + if (f.exists()) { + Log.d(TAG, "Removing existing binary: " + filename); + if (!f.delete()) { + Log.w(TAG, "Failed to delete existing binary: " + filename); + } + } + + copyRawFile(ctx, resId, f, "0755"); + + // Verify the binary was installed correctly + if (!f.exists()) { + Log.e(TAG, "Binary installation failed - file does not exist: " + filename); + return false; + } + + if (!f.canExecute()) { + Log.w(TAG, "Binary installed but not executable: " + filename); + // Try to fix permissions manually + try { + f.setExecutable(true, false); + Log.d(TAG, "Fixed permissions for: " + filename); + } catch (Exception e) { + Log.e(TAG, "Failed to fix permissions for: " + filename + " - " + e.getMessage()); + } + } + + Log.d(TAG, "Successfully installed binary: " + filename + + " (size: " + f.length() + " bytes, executable: " + f.canExecute() + ")"); + return true; + + } catch (Exception e) { + Log.e(TAG, "installBinary failed for " + filename + ": " + e.getClass().getSimpleName() + + " - " + e.getLocalizedMessage(), e); + return false; + } + } + + /** + * Install binary if the resource exists, using reflection to check for resource availability + * @param ctx Context + * @param resourceName Name of the resource (e.g., "busybox_arm64") + * @param filename Target filename + * @return true if installed successfully or resource doesn't exist, false on installation error + */ + private static boolean installBinaryIfExists(Context ctx, String resourceName, String filename) { + try { + // Use reflection to check if the resource exists + Class rawClass = R.raw.class; + java.lang.reflect.Field field = rawClass.getDeclaredField(resourceName); + int resId = field.getInt(null); + + // Resource exists, try to install it + return installBinary(ctx, resId, filename); + } catch (NoSuchFieldException e) { + // Resource doesn't exist - this is expected when binaries are not yet added + Log.d(TAG, "Resource " + resourceName + " not found - this is expected if binary is not yet available"); + return false; + } catch (Exception e) { + Log.e(TAG, "Error checking/installing binary " + resourceName + ": " + e.getMessage()); + return false; + } + } + + private static boolean installBinariesX86(Context ctx) { + if (!installBinary(ctx, R.raw.busybox_x86, "busybox")) return false; + if (!installBinary(ctx, R.raw.iptables_x86, "iptables")) return false; + if (!installBinary(ctx, R.raw.ip6tables_x86, "ip6tables")) return false; + if (!installBinary(ctx, R.raw.nflog_x86, "nflog")) return false; + + + return true; + } + + + private static boolean installBinariesArm64(Context ctx) { + if (!installBinary(ctx, R.raw.busybox_arm64, "busybox")) return false; + if (!installBinary(ctx, R.raw.iptables_arm64, "iptables")) return false; + if (!installBinary(ctx, R.raw.ip6tables_arm64, "ip6tables")) return false; + if (!installBinary(ctx, R.raw.nflog_arm64, "nflog")) return false; + + + return true; + } + + private static boolean installBinariesArm(Context ctx) { + if (!installBinary(ctx, R.raw.busybox_arm, "busybox")) return false; + if (!installBinary(ctx, R.raw.iptables_arm, "iptables")) return false; + if (!installBinary(ctx, R.raw.ip6tables_arm, "ip6tables")) return false; + if (!installBinary(ctx, R.raw.nflog_arm, "nflog")) return false; + + + return true; + } + + private static boolean installBinariesForAbi(Context ctx, String abi) { + if (abi.startsWith("x86")) { + return installBinariesX86(ctx); + } else if (abi.startsWith("arm64")) { + return installBinariesArm64(ctx); + } else { + return installBinariesArm(ctx); + } + } + + private static int getPackageVersion(Context ctx) { + try { + return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionCode; + } catch (NameNotFoundException e) { + Log.e(TAG, "Can't determine the package version!"); + return -1; + } + } + + private static String getAbi() { + if (Build.VERSION.SDK_INT > 21) { + return Build.SUPPORTED_ABIS[0]; + } else { + return Build.CPU_ABI; + } + } + + // Static lock object for synchronizing binary installation + private static final Object BINARY_INSTALL_LOCK = new Object(); + + /** + * Asserts that the binary files are installed in the cache directory. + * + * @param ctx context + * @param showErrors indicates if errors should be alerted + * @return false if the binary files could not be installed + */ + public static boolean assertBinaries(Context ctx, boolean showErrors) { + synchronized (BINARY_INSTALL_LOCK) { + Log.d(TAG, "assertBinaries() called - Entry point"); + + int currentVer = getPackageVersion(ctx); + boolean wasAlreadyInstalled = (G.appVersion() == currentVer); + Log.d(TAG, "assertBinaries() - currentVer=" + currentVer + ", storedVer=" + G.appVersion() + ", wasAlreadyInstalled=" + wasAlreadyInstalled); + + if (wasAlreadyInstalled) { + // The version hasn't changed: Check if binaries are still functional + Log.d(TAG, "assertBinaries() - Verifying existing binaries..."); + if (verifyBinaries(ctx)) { + Log.d(TAG, "assertBinaries() - Verification passed, returning true (no reinstall needed)"); + return true; + } else { + Log.w(TAG, "Binaries verification failed, forcing reinstallation"); + } + } + + String abi = getAbi(); + + Log.d(TAG, "Installing binaries for " + abi + " (currentVer=" + currentVer + + ", storedVer=" + G.appVersion() + ", wasAlreadyInstalled=" + wasAlreadyInstalled + ")..."); + + if (!installBinariesForAbi(ctx, abi)) + { + Log.e(TAG, "Installation of the binaries for " + abi + " failed!"); + toast(ctx, ctx.getString(R.string.error_binary), Toast.LENGTH_LONG); + return false; + } + + // Arch-independent scripts: + if (!installBinary(ctx, R.raw.afwallstart, "afwallstart")) + { + Log.e(TAG, "Installation of the arch-independent binaries failed!"); + toast(ctx, ctx.getString(R.string.error_binary)); + return false; + } + + Log.d(TAG, "Installed binaries for " + abi + "."); + + // Only show toast for actual new installations (not verification failures) + if (!wasAlreadyInstalled) { + Log.d(TAG, "New installation completed - showing toast"); + toast(ctx, ctx.getString(R.string.toast_bin_installed), Toast.LENGTH_SHORT); + } else { + Log.d(TAG, "Binaries reinstalled (wasAlreadyInstalled=true) - no toast shown"); + } + + G.appVersion(currentVer); // This indicates that the installation of the binaries for this version was successful. + + return true; + } // End synchronized block + } + + /** + * Force reinstallation of binaries regardless of version + * + * @param ctx Context + * @param showErrors indicates if errors should be alerted + * @return true if installation successful + */ + public static boolean forceReinstallBinaries(Context ctx, boolean showErrors) { + Log.i(TAG, "Forcing binary reinstallation..."); + + // Clear the version to force reinstallation + G.appVersion(-1); + + return assertBinaries(ctx, showErrors); + } + + /** + * Verify that installed binaries are functional + * + * @param ctx Context + * @return true if binaries are functional, false if they need reinstallation + */ + private static boolean verifyBinaries(Context ctx) { + Log.d(TAG, "verifyBinaries() called - Starting verification"); + String dir = ctx.getDir("bin", 0).getAbsolutePath(); + Log.d(TAG, "verifyBinaries() - Binary directory: " + dir); + + // Check if busybox exists and is executable + File busybox = new File(dir, "busybox"); + boolean exists = busybox.exists(); + boolean canExecute = busybox.canExecute(); + boolean canRead = busybox.canRead(); + long size = busybox.length(); + Log.d(TAG, "verifyBinaries() - Checking busybox: exists=" + exists + ", canExecute=" + canExecute + ", canRead=" + canRead + ", size=" + size + " bytes"); + if (!exists || !canExecute) { + Log.w(TAG, "Busybox binary missing or not executable"); + return false; + } + + // Test busybox functionality by running a simple command + // Note: On modern Android, binaries in app private directories may not be executable + // from the app context, but they will work when executed with root privileges + try { + Log.d(TAG, "verifyBinaries() - Testing busybox functionality with 'echo test'"); + ProcessBuilder pb = new ProcessBuilder(busybox.getAbsolutePath(), "echo", "test"); + pb.environment().clear(); + Process process = pb.start(); + int exitCode = process.waitFor(); + Log.d(TAG, "verifyBinaries() - Busybox test exitCode: " + exitCode); + + if (exitCode != 0) { + Log.w(TAG, "Busybox test command failed with exit code: " + exitCode); + return false; + } + + // Read and verify output + java.util.Scanner scanner = new java.util.Scanner(process.getInputStream()); + if (scanner.hasNextLine()) { + String output = scanner.nextLine().trim(); + Log.d(TAG, "verifyBinaries() - Busybox test output: '" + output + "'"); + scanner.close(); + if (!"test".equals(output)) { + Log.w(TAG, "Busybox test output unexpected: " + output); + return false; + } + } else { + scanner.close(); + Log.w(TAG, "Busybox test produced no output"); + return false; + } + + } catch (Exception e) { + String errorMsg = e.getMessage(); + if (errorMsg != null && (errorMsg.contains("Permission denied") || errorMsg.contains("error=13"))) { + Log.w(TAG, "Busybox execution test failed due to Android security restrictions (expected behavior)"); + Log.w(TAG, "Binary will be available for root execution. Skipping direct execution test."); + // Don't fail verification for permission denied - the binary will work with root + // Just log the issue and continue with other checks + } else { + Log.w(TAG, "Busybox verification failed: " + errorMsg); + return false; + } + } + + // Check other critical binaries exist + Log.d(TAG, "verifyBinaries() - Checking other required binaries"); + String[] requiredBinaries = {"iptables", "ip6tables"}; + for (String binary : requiredBinaries) { + File binaryFile = new File(dir, binary); + Log.d(TAG, "verifyBinaries() - Checking " + binary + ": exists=" + binaryFile.exists() + ", canExecute=" + binaryFile.canExecute()); + if (!binaryFile.exists() || !binaryFile.canExecute()) { + Log.w(TAG, "Required binary missing or not executable: " + binary); + return false; + } + } + + Log.d(TAG, "Binary verification successful - All checks passed"); + return true; + } + + /** + * Check if the firewall is enabled + * + * @param ctx mandatory context + * @return boolean + */ + public static boolean isEnabled(Context ctx) { + if (ctx == null) return false; + return ctx.getSharedPreferences(PREF_FIREWALL_STATUS, Context.MODE_PRIVATE).getBoolean(PREF_ENABLED, false); + } + + /** + * Defines if the firewall is enabled and broadcasts the new status + * + * @param ctx mandatory context + * @param enabled enabled flag + */ + public static void setEnabled(Context ctx, boolean enabled, boolean showErrors) { + if (ctx == null) return; + SharedPreferences prefs = ctx.getSharedPreferences(PREF_FIREWALL_STATUS, Context.MODE_PRIVATE); + if (prefs.getBoolean(PREF_ENABLED, false) == enabled) { + return; + } + setRulesUpToDate(false); + + Editor edit = prefs.edit(); + edit.putBoolean(PREF_ENABLED, enabled); + if (!edit.commit()) { + if (showErrors) toast(ctx, ctx.getString(R.string.error_write_pref)); + return; + } + + Intent myService = new Intent(ctx, FirewallService.class); + ctx.stopService(myService); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + ctx.startForegroundService(myService); + } else { + ctx.startService(myService); + } + + /* notify */ + Intent message = new Intent(ctx, StatusWidget.class); + message.setAction(STATUS_CHANGED_MSG); + message.putExtra(Api.STATUS_EXTRA, enabled); + ctx.sendBroadcast(message); + } + + + public static void errorNotification(Context ctx) { + + String NOTIFICATION_CHANNEL_ID = "firewall.error"; + String channelName = ctx.getString(R.string.firewall_error_notify); + + NotificationManager manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); + manager.cancel(ERROR_NOTIFICATION_ID); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT); + notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + if (G.getNotificationPriority() == 0) { + notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT); + } + notificationChannel.setSound(null, null); + notificationChannel.setShowBadge(false); + notificationChannel.enableLights(false); + notificationChannel.enableVibration(false); + + // Android 16+ specific notification channel configurations + if (Build.VERSION.SDK_INT >= 36) { + notificationChannel.setAllowBubbles(false); + } + + manager.createNotificationChannel(notificationChannel); + } + + + Intent appIntent = new Intent(ctx, MainActivity.class); + appIntent.setAction(Intent.ACTION_MAIN); + appIntent.addCategory(Intent.CATEGORY_LAUNCHER); + appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + // Artificial stack so that navigating backward leads back to the Home screen + TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx) + .addParentStack(MainActivity.class) + .addNextIntent(new Intent(ctx, MainActivity.class)); + + PendingIntent notifyPendingIntent = PendingIntent.getActivity(ctx, 0, appIntent, PendingIntent.FLAG_IMMUTABLE); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID); + notificationBuilder.setContentIntent(notifyPendingIntent); + + Notification notification = notificationBuilder.setOngoing(false) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setContentTitle(ctx.getString(R.string.error_notification_title)) + .setContentText(ctx.getString(R.string.error_notification_text)) + .setTicker(ctx.getString(R.string.error_notification_ticker)) + .setSmallIcon(R.drawable.notification_warn) + .setAutoCancel(true) + .setContentIntent(notifyPendingIntent) + .build(); + + manager.notify(ERROR_NOTIFICATION_ID, notification); + } + + public static void updateNotification(boolean status, Context ctx) { + + String NOTIFICATION_CHANNEL_ID = "firewall.service"; + String channelName = ctx.getString(R.string.firewall_service); + + NotificationManager manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); + manager.cancel(NOTIFICATION_ID); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW); + notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + if (G.getNotificationPriority() == 0) { + notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT); + } + notificationChannel.setSound(null, null); + notificationChannel.setShowBadge(false); + notificationChannel.enableLights(false); + notificationChannel.enableVibration(false); + + // Android 16+ specific notification channel configurations + if (Build.VERSION.SDK_INT >= 36) { + notificationChannel.setAllowBubbles(false); + } + + manager.createNotificationChannel(notificationChannel); + } + + Intent appIntent = new Intent(ctx, MainActivity.class); + appIntent.setAction(Intent.ACTION_MAIN); + appIntent.addCategory(Intent.CATEGORY_LAUNCHER); + appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + int icon = status ? R.drawable.notification : R.drawable.notification_error; + String notificationText = status ? getNotificationText(ctx) : ctx.getString(R.string.inactive); + + PendingIntent notifyPendingIntent = PendingIntent.getActivity(ctx, 0, appIntent, PendingIntent.FLAG_IMMUTABLE); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID); + notificationBuilder.setContentIntent(notifyPendingIntent); + + Notification notification = notificationBuilder.setOngoing(true) + .setContentTitle(ctx.getString(R.string.app_name)) + .setTicker(ctx.getString(R.string.app_name)) + .setSound(null) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setContentText(notificationText) + .setSmallIcon(icon) + .build(); + + notification.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_NO_CLEAR; + manager.notify(NOTIFICATION_ID, notification); + } + + private static String getNotificationText(Context ctx) { + if (G.enableMultiProfile()) { + String storedProfile = G.storedProfile(); + switch (storedProfile) { + case "AFWallPrefs": + return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("default", ctx.getString(R.string.defaultProfile)) + ")"; + case "AFWallProfile1": + return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile1", ctx.getString(R.string.profile1)) + ")"; + case "AFWallProfile2": + return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile2", ctx.getString(R.string.profile2)) + ")"; + case "AFWallProfile3": + return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile3", ctx.getString(R.string.profile3)) + ")"; + default: + return ctx.getString(R.string.active) + " (" + storedProfile + ")"; + } + } else { + return ctx.getString(R.string.active); + } + } + + + private static boolean removePackageRef(Context ctx, String pkg, int pkgRemoved, SharedPreferences.Editor editor, String store) { + StringBuilder newUids = new StringBuilder(); + StringTokenizer tokenizer = new StringTokenizer(pkg, "|"); + boolean changed = false; + String uidStr = String.valueOf(pkgRemoved); + + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + if (!uidStr.equals(token)) { + if (newUids.length() > 0) { + newUids.append('|'); + } + newUids.append(token); + } else { + changed = true; + } + } + + if (changed) { + editor.putString(store, newUids.toString()); + editor.apply(); + } + return changed; + } + + + /** + * Remove the cache.label key from preferences, so that next time the app appears on the top + * + * @param pkgName + * @param ctx + */ + public static void removeCacheLabel(String pkgName, Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences("AFWallPrefs", Context.MODE_PRIVATE); + try { + prefs.edit().remove("cache.label." + pkgName).commit(); + } catch (Exception e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + /** + * Cleansup the uninstalled packages from the cache - will have slight performance + * + * @param ctx + */ + public static void removeAllUnusedCacheLabel(Context ctx) { + try { + SharedPreferences prefs = ctx.getSharedPreferences("AFWallPrefs", Context.MODE_PRIVATE); + final String cacheLabel = "cache.label."; + String pkgName; + String cacheKey; + PackageManager pm = ctx.getPackageManager(); + Map allPrefs = prefs.getAll(); + + for (Map.Entry prefEntry : allPrefs.entrySet()) { + String key = prefEntry.getKey(); + if (key.startsWith(cacheLabel)) { + cacheKey = key; + pkgName = key.replace(cacheLabel, ""); + if (prefs.getString(cacheKey, "").length() > 0 && !isPackageExists(pm, pkgName)) { + prefs.edit().remove(cacheKey).apply(); + } + } + } + } catch (Exception e) { + // Handle the exception appropriately (e.g., log or print the stack trace) + } + } + + + /** + * Cleanup the cache from profiles - Improve performance. + * + * @param pm + * @param targetPackage + */ + + public static boolean isPackageExists(PackageManager pm, String targetPackage) { + try { + pm.getPackageInfo(targetPackage, PackageManager.GET_META_DATA); + } catch (NameNotFoundException e) { + return false; + } + return true; + } + + public static PackageInfo getPackageDetails(Context ctx, HashMap listMaps, int uid) { + try { + final PackageManager pm = ctx.getPackageManager(); + if (listMaps != null && listMaps.containsKey(uid)) { + return pm.getPackageInfo(listMaps.get(uid), PackageManager.GET_META_DATA); + } else { + return null; + } + } catch (NameNotFoundException e) { + return null; + } + } + + + public static Drawable getApplicationIcon(Context context, int appUid) { + if (uidToApplicationInfoMap == null) { + PackageManager packageManager = context.getPackageManager(); + List installedApplications = packageManager.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES); + uidToApplicationInfoMap = new HashMap<>(); + for (ApplicationInfo applicationInfo : installedApplications) { + if (!uidToApplicationInfoMap.containsKey(applicationInfo.uid)) { + uidToApplicationInfoMap.put(applicationInfo.uid, applicationInfo); + } + } + } + + ApplicationInfo applicationInfo = uidToApplicationInfoMap.get(appUid); + if (applicationInfo != null) { + PackageManager packageManager = context.getPackageManager(); + return applicationInfo.loadIcon(packageManager); // The application icon. + } else { + return context.getDrawable(R.drawable.ic_unknown); // The default icon. + } + } + + /** + * Called when an application in removed (un-installed) from the system. + * This will look for that application in the selected list and update the persisted values if necessary + * + * @param ctx mandatory app context + */ + public static void applicationRemoved(Context ctx, int pkgRemoved, RootCommand callback) { + SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + boolean isRuleChanged = false; + + String[] prefKeys = { + PREF_WIFI_PKG_UIDS, + PREF_3G_PKG_UIDS, + PREF_ROAMING_PKG_UIDS, + PREF_VPN_PKG_UIDS, + PREF_TETHER_PKG_UIDS, + PREF_LAN_PKG_UIDS, + PREF_TOR_PKG_UIDS + }; + + String[] savedPackages = { + prefs.getString(PREF_WIFI_PKG_UIDS, ""), + prefs.getString(PREF_3G_PKG_UIDS, ""), + prefs.getString(PREF_ROAMING_PKG_UIDS, ""), + prefs.getString(PREF_VPN_PKG_UIDS, ""), + prefs.getString(PREF_TETHER_PKG_UIDS, ""), + prefs.getString(PREF_LAN_PKG_UIDS, ""), + prefs.getString(PREF_TOR_PKG_UIDS, "") + }; + + boolean[] ruleChanged = new boolean[savedPackages.length]; + + for (int i = 0; i < savedPackages.length; i++) { + ruleChanged[i] = removePackageRef(ctx, savedPackages[i], pkgRemoved, editor, prefKeys[i]); + if (ruleChanged[i]) { + isRuleChanged = true; + } + } + + if (isRuleChanged) { + editor.apply(); + if (isEnabled(ctx)) { + applySavedIptablesRules(ctx, false, new RootCommand()); + } + } + } + + + public static void donateDialog(final Context ctx, boolean showToast) { + if (showToast) { + Toast.makeText(ctx, ctx.getText(R.string.donate_only), Toast.LENGTH_LONG).show(); + } else { + try { + new MaterialDialog.Builder(ctx).cancelable(false) + .title(R.string.buy_donate) + .content(R.string.donate_only) + .positiveText(R.string.buy_donate) + .negativeText(R.string.close) + .icon(ctx.getResources().getDrawable(R.drawable.ic_launcher)) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("market://search?q=pub:ukpriya")); + ctx.startActivity(intent); + } + }) + + .onNegative(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.cancel(); + G.isDo(false); + } + }) + .show(); + } catch (Exception e) { + Toast.makeText(ctx, ctx.getText(R.string.donate_only), Toast.LENGTH_LONG).show(); + } + } + } + + public static void exportRulesToFileConfirm(final Context ctx) { + String fileName = "afwall-backup-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json"; + if (exportRules(ctx, fileName)) { + Api.toast(ctx, ctx.getString(R.string.export_rules_success) + " " + fileName); + } else { + Api.toast(ctx, ctx.getString(R.string.export_rules_fail)); + } + } + + public static void exportAllPreferencesToFileConfirm(final Context ctx) { + String fileName = "afwall-backup-all-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json"; + if (exportAll(ctx, fileName)) { + Api.toast(ctx, ctx.getString(R.string.export_rules_success) + " " + fileName); + } else { + Api.toast(ctx, ctx.getString(R.string.export_rules_fail)); + } + } + + public static void exportRulesToFileWithPicker(final Context ctx) { + showExportFileDialog(ctx, false); + } + + public static void exportAllPreferencesToFileWithPicker(final Context ctx) { + showExportFileDialog(ctx, true); + } + + private static void showExportFileDialog(final Context ctx, final boolean exportAll) { + try { + File defaultPath; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + defaultPath = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "/"); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + defaultPath = new File(ctx.getExternalFilesDir(null), "/"); + } else { + defaultPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/afwall/"); + defaultPath.mkdirs(); + } + + dev.ukanth.ufirewall.util.FileDialog fileDialog = new dev.ukanth.ufirewall.util.FileDialog((Activity) ctx, defaultPath, true); + fileDialog.setSelectDirectoryOption(true); + fileDialog.addDirectoryListener(directory -> { + String fileName = "afwall-backup" + (exportAll ? "-all" : "") + "-" + + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json"; + File fullPath = new File(directory, fileName); + + boolean success; + if (exportAll) { + success = exportAllToFile(ctx, fullPath); + } else { + success = exportRulesToFile(ctx, fullPath); + } + + if (success) { + Api.toast(ctx, ctx.getString(R.string.export_rules_success) + " " + fullPath.getAbsolutePath()); + } else { + Api.toast(ctx, ctx.getString(R.string.export_rules_fail)); + } + }); + fileDialog.showDialog(); + } catch (Exception e) { + // Fallback to original method if file dialog fails + if (exportAll) { + exportAllPreferencesToFileConfirm(ctx); + } else { + exportRulesToFileConfirm(ctx); + } + } + } + + private static boolean exportRulesToFile(Context ctx, File file) { + boolean res = false; + try (FileOutputStream fOut = new FileOutputStream(file); + OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) { + + JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx)); + JSONArray jArray = new JSONArray("[" + obj.toString() + "]"); + JSONObject exportObject = new JSONObject(); + exportObject.put("rules", jArray); + String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST); + exportObject.put("mode", mode); + + myOutWriter.write(exportObject.toString()); + myOutWriter.flush(); // Ensure data is written + res = true; + Log.i(TAG, "Successfully exported rules to: " + file.getAbsolutePath()); + } catch (Exception e) { + Log.e(TAG, "Error exporting rules to file: " + file.getAbsolutePath(), e); + } + return res; + } + + private static boolean exportAllToFile(Context ctx, File file) { + boolean res = false; + try (FileOutputStream fOut = new FileOutputStream(file); + OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) { + + JSONObject exportObject = new JSONObject(); + if (G.enableMultiProfile()) { + if (!G.isProfileMigrated()) { + JSONObject profileObject = new JSONObject(); + for (String profile : G.profiles) { + profileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile))); + } + exportObject.put("profiles", profileObject); + + JSONObject addProfileObject = new JSONObject(); + for (String profile : G.getAdditionalProfiles()) { + addProfileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile))); + } + exportObject.put("additional_profiles", addProfileObject); + } else { + JSONObject profileObject = new JSONObject(); + String profileName = "AFWallPrefs"; + profileObject.put(profileName, new JSONObject(getRulesForProfile(ctx, profileName))); + + List profileDataList = ProfileHelper.getProfiles(); + for (ProfileData profile : profileDataList) { + profileName = profile.getName(); + if (profile.getIdentifier().startsWith("AFWallProfile")) { + profileName = profile.getIdentifier(); + } + profileObject.put(profile.getName(), new JSONObject(getRulesForProfile(ctx, profileName))); + } + exportObject.put("_profiles", profileObject); + } + } else { + JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx)); + exportObject.put("default", obj); + } + + exportObject.put("prefs", getAllAppPreferences(ctx, G.gPrefs)); + // Export profile-specific preferences (mode, custom rules, etc.) + if (G.pPrefs != null) { + exportObject.put("profilePrefs", getAllAppPreferences(ctx, G.pPrefs)); + } + + myOutWriter.write(exportObject.toString()); + myOutWriter.flush(); // Ensure data is written + res = true; + Log.i(TAG, "Successfully exported all preferences to: " + file.getAbsolutePath()); + } catch (Exception e) { + Log.e(TAG, "Error exporting all preferences to file: " + file.getAbsolutePath(), e); + } + return res; + } + + private static void updateExportPackage(Map exportMap, String packageName, boolean isChecked, int identifier) throws JSONException { + if (!isChecked) { + return; + } + JSONObject obj; + if (packageName != null) { + if (exportMap.containsKey(packageName)) { + obj = exportMap.get(packageName); + obj.put(identifier + "", true); + } else { + obj = new JSONObject(); + obj.put(identifier + "", true); + exportMap.put(packageName, obj); + } + } + } + + private static void updatePackage(Context ctx, String savedPkg_uid, Map exportMap, int identifier) throws JSONException { + StringTokenizer tok = new StringTokenizer(savedPkg_uid, "|"); + while (tok.hasMoreTokens()) { + String uid = tok.nextToken(); + if (!uid.isEmpty()) { + String packageName = ctx.getPackageManager().getNameForUid(Integer.parseInt(uid)); + updateExportPackage(exportMap, packageName, /*is_checked=*/ true, identifier); + } + } + } + + private static Map getCurrentRulesAsMap(Context ctx) { + List apps = getApps(ctx, null); + Map exportMap = new HashMap<>(); + + try { + for (PackageInfoData app : apps) { + updateExportPackage(exportMap, app.pkgName, app.selected_wifi, WIFI_EXPORT); + updateExportPackage(exportMap, app.pkgName, app.selected_3g, DATA_EXPORT); + updateExportPackage(exportMap, app.pkgName, app.selected_roam, ROAM_EXPORT); + updateExportPackage(exportMap, app.pkgName, app.selected_vpn, VPN_EXPORT); + updateExportPackage(exportMap, app.pkgName, app.selected_tether, TETHER_EXPORT); + updateExportPackage(exportMap, app.pkgName, app.selected_lan, LAN_EXPORT); + updateExportPackage(exportMap, app.pkgName, app.selected_tor, TOR_EXPORT); + } + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return exportMap; + } + + + public static boolean exportAll(Context ctx, final String fileName) { + boolean res = false; + try { + File file; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Android 11+ (API 30+): Use scoped storage + file = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Android 10 (API 29): Use app-specific directory + file = new File(ctx.getExternalFilesDir(null), fileName); + } else { + // Android 9 and below: Use legacy external storage + File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "afwall"); + dir.mkdirs(); + file = new File(dir, fileName); + } + + try (FileOutputStream fOut = new FileOutputStream(file); + OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) { + + JSONObject exportObject = new JSONObject(); + if (G.enableMultiProfile()) { + if (!G.isProfileMigrated()) { + JSONObject profileObject = new JSONObject(); + for (String profile : G.profiles) { + profileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile))); + } + exportObject.put("profiles", profileObject); + + JSONObject addProfileObject = new JSONObject(); + for (String profile : G.getAdditionalProfiles()) { + addProfileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile))); + } + exportObject.put("additional_profiles", addProfileObject); + } else { + JSONObject profileObject = new JSONObject(); + String profileName = "AFWallPrefs"; + profileObject.put(profileName, new JSONObject(getRulesForProfile(ctx, profileName))); + + List profileDataList = ProfileHelper.getProfiles(); + for (ProfileData profile : profileDataList) { + profileName = profile.getName(); + if (profile.getIdentifier().startsWith("AFWallProfile")) { + profileName = profile.getIdentifier(); + } + profileObject.put(profile.getName(), new JSONObject(getRulesForProfile(ctx, profileName))); + } + exportObject.put("_profiles", profileObject); + } + } else { + JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx)); + exportObject.put("default", obj); + } + + exportObject.put("prefs", getAllAppPreferences(ctx, G.gPrefs)); + // Export profile-specific preferences (mode, custom rules, etc.) + if (G.pPrefs != null) { + exportObject.put("profilePrefs", getAllAppPreferences(ctx, G.pPrefs)); + } + + String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST); + exportObject.put("mode", mode); + + myOutWriter.append(exportObject.toString()); + res = true; + } + + } catch (Exception e) { + Log.d(TAG, e.getLocalizedMessage(), e); + } + + return res; + } + + + private static Map getRulesForProfile(Context ctx, String profile) throws JSONException { + Map exportMap = new HashMap<>(); + SharedPreferences prefs = ctx.getSharedPreferences(profile, Context.MODE_PRIVATE); + updatePackage(ctx, prefs.getString(PREF_WIFI_PKG_UIDS, ""), exportMap, WIFI_EXPORT); + updatePackage(ctx, prefs.getString(PREF_3G_PKG_UIDS, ""), exportMap, DATA_EXPORT); + updatePackage(ctx, prefs.getString(PREF_ROAMING_PKG_UIDS, ""), exportMap, ROAM_EXPORT); + updatePackage(ctx, prefs.getString(PREF_VPN_PKG_UIDS, ""), exportMap, VPN_EXPORT); + updatePackage(ctx, prefs.getString(PREF_TETHER_PKG_UIDS, ""), exportMap, TETHER_EXPORT); + updatePackage(ctx, prefs.getString(PREF_LAN_PKG_UIDS, ""), exportMap, LAN_EXPORT); + updatePackage(ctx, prefs.getString(PREF_TOR_PKG_UIDS, ""), exportMap, TOR_EXPORT); + return exportMap; + } + + private static JSONArray getAllAppPreferences(Context ctx, SharedPreferences gPrefs) throws JSONException { + Map keys = gPrefs.getAll(); + JSONArray arr = new JSONArray(); + for (Map.Entry entry : keys.entrySet()) { + JSONObject obj = new JSONObject(); + obj.put(entry.getKey(), entry.getValue().toString()); + arr.put(obj); + } + return arr; + } + + public static boolean exportRules(Context ctx, final String fileName) { + boolean res = false; + + File file; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Android 11+ (API 30+): Use scoped storage + file = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Android 10 (API 29): Use app-specific directory + file = new File(ctx.getExternalFilesDir(null), fileName); + } else { + // Android 9 and below: Use legacy external storage + File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/afwall/"); + dir.mkdirs(); + file = new File(dir, fileName); + } + + try { + + FileOutputStream fOut = new FileOutputStream(file); + OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut); + + //default Profile - current one + JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx)); + JSONArray jArray = new JSONArray("[" + obj.toString() + "]"); + + JSONObject exportObject = new JSONObject(); + exportObject.put("rules", jArray); + + String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST); + exportObject.put("mode", mode); + + myOutWriter.append(exportObject.toString()); + res = true; + myOutWriter.close(); + fOut.close(); + + + } catch (FileNotFoundException e) { + Log.e(TAG, e.getLocalizedMessage()); + } catch (JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } catch (IOException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + return res; + } + + + private static boolean importRulesRoot(Context ctx, File file, StringBuilder msg) { + boolean returnVal = false; + BufferedReader br = null; + try { + com.topjohnwu.superuser.Shell.Result result = com.topjohnwu.superuser.Shell.cmd("cat " + file.getAbsolutePath()).exec(); + List out = result.getOut(); + String data = TextUtils.join("", out); + + try { + //old export format + JSONArray array = new JSONArray(data); + updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME); + } catch (JSONException e) { + //new exported format + JSONObject jsonObject = new JSONObject(data); + //save mode + if(jsonObject.get("mode") != null) { + G.pPrefs.edit().putString(PREF_MODE, jsonObject.getString("mode")).apply(); + } + JSONArray array = (JSONArray) jsonObject.get("rules"); + updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME); + } + returnVal = true; + } catch (java.util.concurrent.RejectedExecutionException e) { + Log.w(TAG, "Import rules file read rejected: " + e.getMessage()); + } catch (JSONException e) { + Log.e(TAG, "JSON parsing error during import: " + e.getLocalizedMessage()); + } catch (Exception e) { + Log.e(TAG, "Failed to import rules from file: " + e.getLocalizedMessage()); + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + } + return returnVal; + } + private static boolean importRules(Context ctx, File file, StringBuilder msg) { + boolean returnVal = false; + + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + StringBuilder text = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + text.append(line); + } + String data = text.toString(); + if (data.trim().isEmpty()) { + msg.append("Import file contains no data"); + return false; + } + + JSONObject jsonObject = new JSONObject(data); + if (jsonObject.has("mode")) { + G.pPrefs.edit().putString(PREF_MODE, jsonObject.getString("mode")).apply(); + } + JSONArray array = jsonObject.optJSONArray("rules"); + if (array != null) { + updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME); + } else { + updateRulesFromJson(ctx, jsonObject, PREFS_NAME); + } + + returnVal = true; + } catch (FileNotFoundException e) { + if (e.getMessage().contains("EACCES")) { + return importRulesRoot(ctx, file, msg); + } else { + msg.append(ctx.getString(R.string.import_rules_missing)); + } + } catch (IOException | JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + return returnVal; + } + + + private static void updateRulesFromJson(Context ctx, JSONObject object, String preferenceName) throws JSONException { + final StringBuilder[] uidBuilders = new StringBuilder[7]; + uidBuilders[WIFI_EXPORT] = new StringBuilder(); + uidBuilders[DATA_EXPORT] = new StringBuilder(); + uidBuilders[ROAM_EXPORT] = new StringBuilder(); + uidBuilders[VPN_EXPORT] = new StringBuilder(); + uidBuilders[TETHER_EXPORT] = new StringBuilder(); + uidBuilders[LAN_EXPORT] = new StringBuilder(); + uidBuilders[TOR_EXPORT] = new StringBuilder(); + + Map json = JsonHelper.toMap(object); + final PackageManager pm = ctx.getPackageManager(); + + for (Map.Entry entry : json.entrySet()) { + String pkgName = entry.getKey(); + if (pkgName.contains(":")) { + pkgName = pkgName.split(":")[0]; + } + + JSONObject jsonObj = (JSONObject) JsonHelper.toJSON(entry.getValue()); + Iterator keys = jsonObj.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + int exportType = Integer.parseInt(key); + StringBuilder uidBuilder = uidBuilders[exportType]; + + if (uidBuilder.length() != 0) { + uidBuilder.append('|'); + } + + if (pkgName.startsWith("dev.afwall.special")) { + uidBuilder.append(specialApps.get(pkgName)); + } else { + try { + uidBuilder.append(pm.getApplicationInfo(pkgName, 0).uid); + } catch (NameNotFoundException e) { + // Handle exception if needed + } + } + } + } + + final SharedPreferences prefs = ctx.getSharedPreferences(preferenceName, Context.MODE_PRIVATE); + final Editor edit = prefs.edit(); + edit.putString(PREF_WIFI_PKG_UIDS, uidBuilders[WIFI_EXPORT].toString()); + edit.putString(PREF_3G_PKG_UIDS, uidBuilders[DATA_EXPORT].toString()); + edit.putString(PREF_ROAMING_PKG_UIDS, uidBuilders[ROAM_EXPORT].toString()); + edit.putString(PREF_VPN_PKG_UIDS, uidBuilders[VPN_EXPORT].toString()); + edit.putString(PREF_TETHER_PKG_UIDS, uidBuilders[TETHER_EXPORT].toString()); + edit.putString(PREF_LAN_PKG_UIDS, uidBuilders[LAN_EXPORT].toString()); + edit.putString(PREF_TOR_PKG_UIDS, uidBuilders[TOR_EXPORT].toString()); + + edit.apply(); + } + + private static boolean shouldIgnoreKey(String key) { + String[] ignore = {"appVersion", "fixLeak", "enableLogService", "sort", "storedProfile", "hasRoot", "logChains", "kingDetect", "fingerprintEnabled"}; + return Arrays.asList(ignore).contains(key); + } + + private static boolean isIntType(String key) { + String[] intType = {"logPingTime", "customDelay", "patternMax", "widgetX", "widgetY", "notification_priority"}; + return Arrays.asList(intType).contains(key); + } + + private static void importProfiles(Context ctx, JSONObject profileObject) throws JSONException { + Iterator keys = profileObject.keys(); + while (keys.hasNext()) { + String key = keys.next(); + try { + JSONObject obj = profileObject.getJSONObject(key); + updateRulesFromJson(ctx, obj, key); + } catch (JSONException e) { + if (e.getMessage().contains("No value")) { + // continue; + } + } + } + } + private static boolean importAll(Context ctx, File file, StringBuilder msg) { + boolean returnVal = false; + + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + StringBuilder text = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + text.append(line); + } + String data = text.toString(); + if (data.trim().isEmpty()) { + msg.append("Import file contains no data"); + return false; + } + + JSONObject object = new JSONObject(data); + // Basic validation of expected JSON structure + if (!object.has("prefs") && !object.has("profiles") && !object.has("_profiles") && !object.has("default")) { + msg.append("Import file does not contain valid AFWall+ data"); + Log.w(TAG, "Invalid import file structure - missing expected keys"); + return false; + } + + // Allow/deny rule + if (object.has("mode")) { + G.pPrefs.edit().putString(PREF_MODE, object.getString("mode")).apply(); + } + + JSONArray prefArray = object.getJSONArray("prefs"); + for (int i = 0; i < prefArray.length(); i++) { + JSONObject prefObj = prefArray.getJSONObject(i); + Iterator keys = prefObj.keys(); + + while (keys.hasNext()) { + String key = keys.next(); + String value = prefObj.getString(key); + if (shouldIgnoreKey(key)) { + continue; + } + if (value.equals("true") || value.equals("false")) { + G.gPrefs.edit().putBoolean(key, Boolean.parseBoolean(value)).apply(); + } else { + try { + if (key.equals("multiUserId")) { + G.gPrefs.edit().putLong(key, Long.parseLong(value)).apply(); + } else if (isIntType(key)) { + G.gPrefs.edit().putString(key, value).apply(); + } else { + int intValue = Integer.parseInt(value); + G.gPrefs.edit().putInt(key, intValue).apply(); + } + } catch (NumberFormatException e) { + G.gPrefs.edit().putString(key, value).apply(); + } + } + } + } + + // Import profile-specific preferences if available + if (object.has("profilePrefs")) { + JSONArray profilePrefArray = object.getJSONArray("profilePrefs"); + for (int i = 0; i < profilePrefArray.length(); i++) { + JSONObject prefObj = profilePrefArray.getJSONObject(i); + Iterator keys = prefObj.keys(); + + while (keys.hasNext()) { + String key = keys.next(); + String value = prefObj.getString(key); + if (shouldIgnoreKey(key)) { + continue; + } + if (value.equals("true") || value.equals("false")) { + G.pPrefs.edit().putBoolean(key, Boolean.parseBoolean(value)).apply(); + } else { + try { + if (key.equals("multiUserId")) { + G.pPrefs.edit().putLong(key, Long.parseLong(value)).apply(); + } else if (isIntType(key)) { + G.pPrefs.edit().putString(key, value).apply(); + } else { + int intValue = Integer.parseInt(value); + G.pPrefs.edit().putInt(key, intValue).apply(); + } + } catch (NumberFormatException e) { + G.pPrefs.edit().putString(key, value).apply(); + } + } + } + } + } + + if (G.enableMultiProfile()) { + if (G.isProfileMigrated()) { + JSONObject profileObject = object.getJSONObject("_profiles"); + importProfiles(ctx, profileObject); + } else { + JSONObject profileObject = object.getJSONObject("profiles"); + importProfiles(ctx, profileObject); + JSONObject customProfileObject = object.getJSONObject("additional_profiles"); + importProfiles(ctx, customProfileObject); + } + } else { + JSONObject defaultRules = object.getJSONObject("default"); + updateRulesFromJson(ctx, defaultRules, PREFS_NAME); + } + returnVal = true; + } catch (FileNotFoundException e) { + msg.append(ctx.getString(R.string.import_rules_missing)); + } catch (IOException | JSONException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + + return returnVal; + } + + public static boolean loadSharedPreferencesFromFile(Context ctx, StringBuilder builder, String fileName, boolean loadAll) { + boolean res = false; + File file = new File(fileName); + if (file.exists()) { + // Basic file validation + if (file.length() == 0) { + builder.append("Import file is empty"); + Log.w(TAG, "Import file is empty: " + fileName); + return false; + } + if (file.length() > 50 * 1024 * 1024) { // 50MB limit + builder.append("Import file is too large (>50MB)"); + Log.w(TAG, "Import file is too large: " + fileName + " (" + file.length() + " bytes)"); + return false; + } + + Log.i(TAG, "Importing from file: " + fileName + " (loadAll: " + loadAll + ")"); + if (loadAll) { + res = importAll(ctx, file, builder); + } else { + res = importRules(ctx, file, builder); + } + } else { + builder.append("Import file does not exist: " + fileName); + Log.w(TAG, "Import file does not exist: " + fileName); + } + return res; + } + + /** + * Probe log target + * @param ctx + */ + public static void probeLogTarget(final Context ctx) { + + } + + @SuppressLint("InlinedApi") + public static void showInstalledAppDetails(Context context, String packageName) { + final String SCHEME = "package"; + Intent intent = new Intent(); + final int apiLevel = Build.VERSION.SDK_INT; + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri = Uri.fromParts(SCHEME, packageName, null); + intent.setData(uri); + context.startActivity(intent); + } + + public static boolean isNetfilterSupported() { + boolean netfiler_exists = new File("/proc/net/netfilter").exists(); + try { + Shell.Result result = Shell.cmd("cat /proc/net/ip_tables_targets").exec(); + return netfiler_exists && result.isSuccess(); + } catch (java.util.concurrent.RejectedExecutionException e) { + Log.w(TAG, "Netfilter check rejected: " + e.getMessage()); + return false; + } catch (Exception e) { + Log.e(TAG, "Failed to check netfilter support: " + e.getMessage()); + return false; + } + } + + private static void initSpecial() { + if (specialApps == null || specialApps.size() == 0) { + specialApps = new HashMap(); + specialApps.put("dev.afwall.special.any", SPECIAL_UID_ANY); + specialApps.put("dev.afwall.special.kernel", SPECIAL_UID_KERNEL); + specialApps.put("dev.afwall.special.tether", SPECIAL_UID_TETHER); + //specialApps.put("dev.afwall.special.dnsproxy",SPECIAL_UID_DNSPROXY); + specialApps.put("dev.afwall.special.ntp", SPECIAL_UID_NTP); + for (String acct : specialAndroidAccounts) { + String pkg = "dev.afwall.special." + acct; + int uid = android.os.Process.getUidForName(acct); + specialApps.put(pkg, uid); + } + } + } + + public static void updateLanguage(Context context, String lang) { + if (lang.equals("sys")) { + Locale defaultLocale = Resources.getSystem().getConfiguration().locale; + Locale.setDefault(defaultLocale); + Resources res = context.getResources(); + Configuration conf = res.getConfiguration(); + conf.locale = defaultLocale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + context.createConfigurationContext(conf); + } else { + context.getResources().updateConfiguration(conf, context.getResources().getDisplayMetrics()); + } + } else if (!"".equals(lang)) { + Locale locale = new Locale(lang); + if (lang.contains("_")) { + locale = new Locale(lang.split("_")[0], lang.split("_")[1]); + } + Locale.setDefault(locale); + Resources res = context.getResources(); + Configuration conf = res.getConfiguration(); + conf.locale = locale; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + context.createConfigurationContext(conf); + } else { + context.getResources().updateConfiguration(conf, context.getResources().getDisplayMetrics()); + } + } + } + + public static void setUserOwner(Context context) { + if (supportsMultipleUsers(context)) { + try { + Method getUserHandle = UserManager.class.getMethod("getUserHandle"); + int userHandle = (Integer) getUserHandle.invoke(context.getSystemService(Context.USER_SERVICE)); + G.setMultiUserId(userHandle); + } catch (Exception ex) { + Log.e(TAG, "Exception on setUserOwner " + ex.getMessage()); + } + } + } + + @SuppressLint("NewApi") + public static boolean supportsMultipleUsers(Context context) { + final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + try { + Method supportsMultipleUsers = UserManager.class.getMethod("supportsMultipleUsers"); + return (Boolean) supportsMultipleUsers.invoke(um); + } catch (Exception ex) { + return false; + } + } + + public static String loadData(final Context context, + final String resourceName) throws IOException { + int resourceIdentifier = context + .getApplicationContext() + .getResources() + .getIdentifier(resourceName, "raw", + context.getApplicationContext().getPackageName()); + if (resourceIdentifier != 0) { + InputStream inputStream = context.getApplicationContext() + .getResources().openRawResource(resourceIdentifier); + BufferedReader reader = new BufferedReader(new InputStreamReader( + inputStream, StandardCharsets.UTF_8)); + String line; + StringBuffer data = new StringBuffer(); + while ((line = reader.readLine()) != null) { + data.append(line); + } + reader.close(); + return data.toString(); + } + return null; + } + + /** + * Encrypt the password - DEPRECATED: Use SecureCrypto.encryptSecure() for new code + * This method is kept for backward compatibility only + * + * @param key + * @param data + * @return + * @deprecated Use SecureCrypto.encryptSecure() instead for better security + */ + @Deprecated + public static String hideCrypt(String key, String data) { + if (key == null || data == null) + return null; + String encodeStr = null; + try { + DESKeySpec desKeySpec = new DESKeySpec(key.getBytes(charsetName)); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm); + SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec); + byte[] dataBytes = data.getBytes(charsetName); + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + encodeStr = Base64.encodeToString(cipher.doFinal(dataBytes), base64Mode); + + } catch (Exception e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return encodeStr; + } + + /** + * Decrypt the password - DEPRECATED: Use SecureCrypto.decryptSecure() for new code + * This method is kept for backward compatibility only + * + * @param key + * @param data + * @return + * @deprecated Use SecureCrypto.decryptSecure() instead for better security + */ + @Deprecated + public static String unhideCrypt(String key, String data) { + if (key == null || data == null) + return null; + + String decryptStr = null; + try { + byte[] dataBytes = Base64.decode(data, base64Mode); + DESKeySpec desKeySpec = new DESKeySpec(key.getBytes(charsetName)); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm); + SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec); + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + byte[] dataBytesDecrypted = (cipher.doFinal(dataBytes)); + decryptStr = new String(dataBytesDecrypted); + } catch (Exception e) { + Log.e(TAG, e.getLocalizedMessage()); + } + return decryptStr; + } + + public static boolean isMobileNetworkSupported(final Context ctx) { + boolean hasMobileData = true; + try { + ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm != null) { + if (cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) == null) { + hasMobileData = false; + } + } + } catch (SecurityException e) { + Log.e(TAG, e.getMessage(), e); + } + return hasMobileData; + } + + public static String getCurrentPackage(Context ctx) { + PackageInfo pInfo = null; + try { + pInfo = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0); + } catch (NameNotFoundException e) { + Log.e(Api.TAG, "Package not found", e); + } + return pInfo.packageName; + } + + public static int getConnectivityStatus(Context context) { + + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + + assert cm != null; + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + + if (null != activeNetwork) { + + if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) + return 1; + + if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) + return 2; + + if (activeNetwork.getType() == ConnectivityManager.TYPE_BLUETOOTH) + return 3; + } + return 0; + } + + /** + * Apply default chains based on preference + * + * @param ctx + */ + public static void applyDefaultChains(Context ctx, RootCommand callback) { + List cmds = new ArrayList<>(); + cmds.add(G.ipv4Input() ? "-P INPUT ACCEPT" : "-P INPUT DROP"); + cmds.add(G.ipv4Fwd() ? "-P FORWARD ACCEPT" : "-P FORWARD DROP"); + cmds.add(G.ipv4Output() ? "-P OUTPUT ACCEPT" : "-P OUTPUT DROP"); + applyQuick(ctx, cmds, callback); + applyDefaultChainsv6(ctx, callback); + } + + public static void applyDefaultChainsv6(Context ctx, RootCommand callback) { + if (G.controlIPv6()) { + List cmds = new ArrayList<>(); + cmds.add(G.ipv6Input() ? "-P INPUT ACCEPT" : "-P INPUT DROP"); + cmds.add(G.ipv6Fwd() ? "-P FORWARD ACCEPT" : "-P FORWARD DROP"); + cmds.add(G.ipv6Output() ? "-P OUTPUT ACCEPT" : "-P OUTPUT DROP"); + applyIPv6Quick(ctx, cmds, callback); + } + } + + /** + * Delete all firewall rules. For diagnostic purposes only. + * + * @param ctx application context + * @param callback callback for completion + */ + public static void flushOtherRules(Context ctx, RootCommand callback) { + List cmds = new ArrayList(); + cmds.add("-F firewall"); + cmds.add("-X firewall"); + apply46(ctx, cmds, callback); + } + + // Clipboard + public static void copyToClipboard(Context context, String val) { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("label", val); + clipboard.setPrimaryClip(clip); + } + + public static void sendToastBroadcast(Context ctx, String message) { + Intent broadcastIntent = new Intent(); + broadcastIntent.setAction("TOAST"); + broadcastIntent.putExtra("MSG", message); + ctx.sendBroadcast(broadcastIntent); + } + + public static String getFixLeakPath(String fileName) { + if (G.initPath() != null) { + return G.initPath() + "/" + fileName; + } + return null; + } + + public static boolean isFixPathFileExist(String fileName) { + String path = getFixLeakPath(fileName); + if (path != null) { + File file = new File(path); + return file.exists(); + } + return false; + } + + public static boolean mountDir(Context context, String path, String mountType) { + if (path != null) { + String busyboxPath = Api.getBusyBoxPath(context, true); + if (!busyboxPath.trim().isEmpty()) { + return RootTools.remount(path, mountType, busyboxPath); + } else { + return false; + } + } + return false; + } + + public static void checkAndCopyFixLeak(final Context context, final String fileName) { + if (G.initPath() != null && G.fixLeak() && !isFixPathFileExist(fileName)) { + final String srcPath = new File(ctx.getDir("bin", 0), fileName) + .getAbsolutePath(); + + new Thread(() -> { + String path = G.initPath(); + if (path != null) { + File f = new File(path); + if (mountDir(context, getFixLeakPath(fileName), "RW")) { + //make sure it's executable + new RootCommand() + .setReopenShell(true) + .setLogging(true) + .run(ctx, "chmod 755 " + f.getAbsolutePath()); + RootTools.copyFile(srcPath, (f.getAbsolutePath() + "/" + fileName), + true, false); + mountDir(context, getFixLeakPath(fileName), "RO"); + } + } + }).start(); + } + } + + public static Context updateBaseContextLocale(Context context) { + String language = G.locale(); // Helper method to get saved language from SharedPreferences + Locale locale = new Locale(language); + + if (language.equals("zh") || language.equals("zh_CN")) { + locale = Locale.SIMPLIFIED_CHINESE; + } else if (language.equals("zh_TW")) { + locale = Locale.TRADITIONAL_CHINESE; + } + + Locale.setDefault(locale); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return updateResourcesLocale(context, locale); + } + return updateResourcesLocaleLegacy(context, locale); + } + + @TargetApi(Build.VERSION_CODES.N) + private static Context updateResourcesLocale(Context context, Locale locale) { + Configuration configuration = context.getResources().getConfiguration(); + configuration.setLocale(locale); + return context.createConfigurationContext(configuration); + } + + private static Context updateResourcesLocaleLegacy(Context context, Locale locale) { + Resources resources = context.getResources(); + Configuration configuration = resources.getConfiguration(); + configuration.locale = locale; + resources.updateConfiguration(configuration, resources.getDisplayMetrics()); + return context; + } + + public static void setDefaultPermission(ApplicationInfo applicationInfo) { + + boolean isModified = false; + SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + Editor edit = prefs.edit(); + + // Get the mode type + int modeType = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST).equals(Api.MODE_WHITELIST) ? 0 : 1; + + // Get the preference list + List list = SQLite.select().from(DefaultConnectionPref.class) + .where(DefaultConnectionPref_Table.modeType.eq(modeType)) + .queryList(); + + for (DefaultConnectionPref pref : list) { + if (pref.isState()) { + int uid = applicationInfo.uid; + switch (pref.getUid()) { + case 0: + edit.putString(PREF_LAN_PKG_UIDS, prefs.getString(PREF_LAN_PKG_UIDS, "") + "|" + uid); + isModified = true; + break; + case 1: + edit.putString(PREF_WIFI_PKG_UIDS, prefs.getString(PREF_WIFI_PKG_UIDS, "") + "|" + uid); + isModified = true; + break; + case 2: + edit.putString(PREF_3G_PKG_UIDS, prefs.getString(PREF_3G_PKG_UIDS, "") + "|" + uid); + isModified = true; + break; + case 3: + edit.putString(PREF_ROAMING_PKG_UIDS, prefs.getString(PREF_ROAMING_PKG_UIDS, "") + "|" + uid); + isModified = true; + break; + case 4: + edit.putString(PREF_TOR_PKG_UIDS, prefs.getString(PREF_TOR_PKG_UIDS, "") + "|" + uid); + isModified = true; + break; + case 5: + edit.putString(PREF_VPN_PKG_UIDS, prefs.getString(PREF_VPN_PKG_UIDS, "") + "|" + uid); + isModified = true; + break; + case 6: + edit.putString(PREF_TETHER_PKG_UIDS, prefs.getString(PREF_TETHER_PKG_UIDS, "") + "|" + uid); + isModified = true; + break; + } + } + } + if (isModified) { + edit.apply(); + // Make sure rules are modified flag is set + Api.setRulesUpToDate(false); + fastApply(ctx, new RootCommand()); + } + } + + static class RuleDataSet { + + List wifiList; + List dataList; + List lanList; + List roamList; + List vpnList; + List tetherList; + List torList; + + RuleDataSet(List uidsWifi, List uids3g, + List uidsRoam, List uidsVPN, List uidsTether, + List uidsLAN, List uidsTor) { + this.wifiList = uidsWifi; + this.dataList = uids3g; + this.roamList = uidsRoam; + this.vpnList = uidsVPN; + this.tetherList = uidsTether; + this.lanList = uidsLAN; + this.torList = uidsTor; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((wifiList == null) ? 0 : dataList.hashCode()); + return result; + } + + @Override + public String toString() { + String builder = (wifiList != null ? android.text.TextUtils.join(",", wifiList) : "") + + (dataList != null ? android.text.TextUtils.join(",", dataList) : "") + + (lanList != null ? android.text.TextUtils.join(",", lanList) : "") + + (roamList != null ? android.text.TextUtils.join(",", roamList) : "") + + (vpnList != null ? android.text.TextUtils.join(",", vpnList) : "") + + (tetherList != null ? android.text.TextUtils.join(",", tetherList) : "") + + (torList != null ? android.text.TextUtils.join(",", torList) : ""); + return builder.trim(); + } + } + + /** + * Safe shell command execution that handles library-level crashes + */ + private static List executeSafeShellCommand(String command) { + // First try the primary libsu approach + try { + // Check if we can get a valid shell + if (Shell.getShell() == null || !Shell.getShell().isAlive()) { + Log.w(TAG, "Shell is not available or not alive, trying fallback"); + return executeFallbackShellCommand(command); + } + + // Execute with timeout and proper error handling + Shell.Result result = Shell.cmd(command).exec(); + return result != null ? result.getOut() : null; + + } catch (java.util.concurrent.RejectedExecutionException e) { + Log.w(TAG, "Shell execution rejected - trying fallback: " + e.getMessage()); + return executeFallbackShellCommand(command); + } catch (RuntimeException e) { + // Check for wrapped ExecutionException with InterruptedIOException + Throwable cause = e.getCause(); + if (cause instanceof java.util.concurrent.ExecutionException) { + java.util.concurrent.ExecutionException execEx = (java.util.concurrent.ExecutionException) cause; + if (execEx.getCause() instanceof java.io.InterruptedIOException) { + Log.w(TAG, "Shell execution interrupted at library level - trying fallback: " + execEx.getCause().getMessage()); + return executeFallbackShellCommand(command); + } + } + // Re-throw if it's not a known interruption issue + throw e; + } catch (Exception e) { + Log.w(TAG, "Unexpected error in safe shell execution, trying fallback: " + e.getMessage()); + return executeFallbackShellCommand(command); + } + } + + /** + * Fallback shell execution using the legacy RootShell library + * This provides an alternative when libsu fails due to interruptions + */ + private static List executeFallbackShellCommand(String command) { + try { + Log.d(TAG, "Using fallback shell execution for command: " + command); + + // Use the legacy RootShell library as fallback + final java.util.List output = new java.util.ArrayList<>(); + final boolean[] completed = {false}; + + com.stericson.rootshell.execution.Command cmd = new com.stericson.rootshell.execution.Command(0, command) { + @Override + public void commandCompleted(int id, int exitcode) { + super.commandCompleted(id, exitcode); + completed[0] = true; + } + + @Override + public void commandOutput(int id, String line) { + super.commandOutput(id, line); + if (line != null) { + output.add(line); + } + } + }; + + // Execute with timeout + com.stericson.roottools.RootTools.getShell(true, 0).add(cmd); + + // Wait for completion with timeout + long startTime = System.currentTimeMillis(); + while (!completed[0] && (System.currentTimeMillis() - startTime) < 30000) { + Thread.sleep(100); + } + + if (completed[0]) { + Log.d(TAG, "Fallback shell execution completed successfully"); + return output; + } else { + Log.w(TAG, "Fallback shell execution timed out"); + return null; + } + + } catch (Exception e) { + Log.e(TAG, "Fallback shell execution also failed: " + e.getMessage()); + return null; + } + } + + private static class RunCommand extends AsyncTask, Integer> { + + private int exitCode = -1; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected Integer doInBackground(Object... params) { + @SuppressWarnings("unchecked") + + List commands = (List) params[0]; + StringBuilder res = (StringBuilder) params[1]; + Log.i(TAG, "Executing root commands of" + commands.size()); + try { + // Check if task is cancelled before proceeding + if (isCancelled()) { + Log.d(TAG, "RunCommand task was cancelled, aborting execution"); + return -1; + } + + if (Shell.getShell().isRoot() && !Shell.isAppGrantedRoot()) + return -1; + if (commands != null && commands.size() > 0) { + // Check again before executing shell command + if (isCancelled()) { + Log.d(TAG, "RunCommand task was cancelled before shell execution"); + return -1; + } + + // Use a safe shell execution wrapper + List output = executeSafeShellCommand(String.valueOf(commands)); + if (output != null) { + exitCode = 0; + if (output.size() > 0) { + for (String str : output) { + res.append(str); + res.append("\n"); + } + } + } else { + exitCode = 1; + } + } + } catch (java.util.concurrent.RejectedExecutionException e) { + Log.w(TAG, "Shell execution rejected, likely due to app shutdown: " + e.getMessage()); + exitCode = -1; + } catch (RuntimeException ex) { + // Check if this is a wrapped ExecutionException with InterruptedIOException + Throwable cause = ex.getCause(); + if (cause instanceof java.util.concurrent.ExecutionException) { + java.util.concurrent.ExecutionException execEx = (java.util.concurrent.ExecutionException) cause; + if (execEx.getCause() instanceof java.io.InterruptedIOException) { + Log.w(TAG, "Shell command execution was interrupted: " + execEx.getCause().getMessage()); + exitCode = -1; + return exitCode; + } + } else if (ex.getCause() instanceof java.io.InterruptedIOException) { + Log.w(TAG, "Shell command execution was interrupted: " + ex.getCause().getMessage()); + exitCode = -1; + return exitCode; + } + Log.e(TAG, "Shell command execution failed: " + ex.getMessage()); + if (res != null) + res.append("\n").append(ex); + exitCode = -1; + } catch (Exception ex) { + Log.e(TAG, "Shell command execution failed with unexpected exception: " + ex.getMessage()); + if (res != null) + res.append("\n").append(ex); + exitCode = -1; + } + return exitCode; + } + + @Override + protected void onCancelled() { + Log.d(TAG, "RunCommand task was cancelled"); + super.onCancelled(); + } + + @Override + protected void onCancelled(Integer result) { + Log.d(TAG, "RunCommand task was cancelled with result: " + result); + super.onCancelled(result); + } + + + } + + /** + * Small structure to hold an application info + */ + public static final class PackageInfoData { + + /** + * linux user id + */ + public int uid; + /** + * application names belonging to this user id + */ + public List names; + /** + * rules saving & load + **/ + public String pkgName; + + /** + * Application Type + */ + public int appType; + + /** + * indicates if this application is selected for wifi + */ + public boolean selected_wifi; + /** + * indicates if this application is selected for 3g + */ + public boolean selected_3g; + /** + * indicates if this application is selected for roam + */ + public boolean selected_roam; + /** + * indicates if this application is selected for vpn + */ + public boolean selected_vpn; + /** + * indicates if this application is selected for tether + */ + public boolean selected_tether; + /** + * indicates if this application is selected for lan + */ + public boolean selected_lan; + /** + * indicates if this application is selected for tor mode + */ + public boolean selected_tor; + /** + * toString cache + */ + public String tostr; + /** + * application info + */ + public ApplicationInfo appinfo; + /** + * cached application icon + */ + public Drawable cached_icon; + /** + * indicates if the icon has been loaded already + */ + public boolean icon_loaded; + + /* install time */ + public long installTime; + + /** + * first time seen? + */ + public boolean firstseen; + + public PackageInfoData() { + } + + public PackageInfoData(int uid, String name, String pkgNameStr) { + this.uid = uid; + this.names = new ArrayList(); + this.names.add(name); + this.pkgName = pkgNameStr; + } + + public PackageInfoData(String user, String name, String pkgNameStr) { + this(android.os.Process.getUidForName(user), name, pkgNameStr); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof PackageInfoData)) { + return false; + } + + PackageInfoData pkg = (PackageInfoData) o; + + return pkg.uid == uid && + pkg.pkgName.equals(pkgName); + } + + @Override + public int hashCode() { + int result = 17; + if (appinfo != null) { + result = 31 * result + appinfo.hashCode(); + } + result = 31 * result + uid; + result = 31 * result + pkgName.hashCode(); + return result; + } + + /** + * Screen representation of this application + */ + @Override + public String toString() { + if (tostr == null) { + StringBuilder s = new StringBuilder(); + //if (uid > 0) s.append(uid + ": "); + for (int i = 0; i < names.size(); i++) { + if (i != 0) s.append(", "); + s.append(names.get(i)); + } + s.append("\n"); + tostr = s.toString(); + } + return tostr; + } + + public String toStringWithUID() { + if (tostr == null) { + StringBuilder s = new StringBuilder(); + s.append("[ "); + s.append(uid); + s.append(" ] "); + for (int i = 0; i < names.size(); i++) { + if (i != 0) s.append(", "); + s.append(names.get(i)); + } + s.append("\n"); + tostr = s.toString(); + } + return tostr; + } + + } + + public static void copySharedPreferences(SharedPreferences fromPreferences, SharedPreferences.Editor toEditor) { + for (Map.Entry entry : fromPreferences.getAll().entrySet()) { + Object value = entry.getValue(); + String key = entry.getKey(); + if (value instanceof String) { + toEditor.putString(key, ((String) value)); + } else if (value instanceof Set) { + toEditor.putStringSet(key, (Set) value); // EditorImpl.putStringSet already creates a copy of the set + } else if (value instanceof Integer) { + toEditor.putInt(key, (Integer) value); + } else if (value instanceof Long) { + toEditor.putLong(key, (Long) value); + } else if (value instanceof Float) { + toEditor.putFloat(key, (Float) value); + } else if (value instanceof Boolean) { + toEditor.putBoolean(key, (Boolean) value); + } + } + toEditor.commit(); + } + + @NonNull + public static Bitmap getBitmapFromDrawable(@NonNull Drawable drawable) { + final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bmp); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bmp; + } + + + + @TargetApi(Build.VERSION_CODES.M) + public static boolean batteryOptimized(Context context) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return !pm.isIgnoringBatteryOptimizations(context.getPackageName()); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/InterfaceDetails.java b/app/src/main/java/dev/ukanth/ufirewall/InterfaceDetails.java new file mode 100644 index 0000000..28f6197 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/InterfaceDetails.java @@ -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 . + * + * @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 dnsServersV4 = new java.util.ArrayList<>(); + public java.util.List 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java b/app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java new file mode 100644 index 0000000..02b42a0 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/InterfaceTracker.java @@ -0,0 +1,523 @@ +/** + * Keep track of 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 . + * + * @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 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 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 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)); + } + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/MainActivity.java b/app/src/main/java/dev/ukanth/ufirewall/MainActivity.java new file mode 100644 index 0000000..166fff0 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/MainActivity.java @@ -0,0 +1,2723 @@ +/** + * Main application activity. + * This is the screen displayed when you open the application + *

+ * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro + * Copyright (C) 2011-2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Rodrigo Zechin Rosauro, Umakanthan Chandran + * @version 1.1 + */ + +package dev.ukanth.ufirewall; + +import static dev.ukanth.ufirewall.util.G.TAG; +import static dev.ukanth.ufirewall.util.G.ctx; +import static dev.ukanth.ufirewall.util.G.isDonate; +import static dev.ukanth.ufirewall.util.SecurityUtil.LOCK_VERIFICATION; +import static dev.ukanth.ufirewall.util.SecurityUtil.REQ_ENTER_PATTERN; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.KeyguardManager; +import android.app.NotificationManager; +import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences.Editor; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextUtils.TruncateAt; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RadioGroup; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.view.ViewCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; +import com.topjohnwu.superuser.Shell; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import dev.ukanth.ufirewall.Api.PackageInfoData; +import dev.ukanth.ufirewall.activity.CustomScriptActivity; +import dev.ukanth.ufirewall.activity.HelpActivity; +import dev.ukanth.ufirewall.activity.LogActivity; +import dev.ukanth.ufirewall.activity.OldLogActivity; +import dev.ukanth.ufirewall.activity.RulesActivity; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.preferences.PreferencesActivity; +import dev.ukanth.ufirewall.profiles.ProfileData; +import dev.ukanth.ufirewall.profiles.ProfileHelper; +import dev.ukanth.ufirewall.service.FirewallService; +import dev.ukanth.ufirewall.service.LogService; +import dev.ukanth.ufirewall.service.RootCommand; +import dev.ukanth.ufirewall.util.AppListArrayAdapter; +import dev.ukanth.ufirewall.util.FileDialog; +import dev.ukanth.ufirewall.util.G; +import dev.ukanth.ufirewall.util.PackageComparator; +import dev.ukanth.ufirewall.util.SecurityUtil; +import haibison.android.lockpattern.utils.AlpSettings; +import kotlin.Suppress; + + +public class MainActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener, OnClickListener, SwipeRefreshLayout.OnRefreshListener, + RadioGroup.OnCheckedChangeListener { + + private static final int SHOW_ABOUT_RESULT = 1200; + private static final int PREFERENCE_RESULT = 1205; + private static final int SHOW_CUSTOM_SCRIPT = 1201; + private static final int SHOW_RULES_ACTIVITY = 1202; + private static final int SHOW_LOGS_ACTIVITY = 1203; + private static final int VERIFY_CHECK = 10000; + private static final int MY_PERMISSIONS_REQUEST_WRITE_STORAGE = 1; + private static final int MY_PERMISSIONS_REQUEST_READ_STORAGE = 2; + private static final int MY_PERMISSIONS_REQUEST_WRITE_STORAGE_ASSET = 3; + private static final int PERMISSION_BLUETOOTH = 4; + + private static final int PERMISSION_NOTIFICATION = 5; + + public static boolean dirty = false; + + + private Menu mainMenu; + private ListView listview = null; + private MaterialDialog plsWait; + private ArrayAdapter spinnerAdapter = null; + private SwipeRefreshLayout mSwipeLayout; + private int index; + private int top; + private List mlocalList = new ArrayList<>(new LinkedHashSet()); + private int initDone = 0; + private Spinner mSpinner; + private TextWatcher filterTextWatcher; + private MaterialDialog runProgress; + + private BroadcastReceiver uiProgressReceiver4, uiProgressReceiver6, toastReceiver, themeRefreshReceiver, uiRefreshReceiver; + private IntentFilter uiFilter4, uiFilter6; + + //all async reference with context + private GetAppList getAppList; + private RunApply runApply; + private PurgeTask purgeTask; + private int currentUI = 0; + private static final int DEFAULT_COLUMN = 2; + private int selectedColumns = DEFAULT_COLUMN; + private static final int DEFAULT_VIEW_LIMIT = 4; + private View view; + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean dirty) { + MainActivity.dirty = dirty; + } + + /** + * Called when the activity is first created + * . + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initTheme(); + G.registerPrivateLink(); + checkPermissions(); + + try { + final int FLAG_HARDWARE_ACCELERATED = WindowManager.LayoutParams.class + .getDeclaredField("FLAG_HARDWARE_ACCELERATED").getInt(null); + getWindow().setFlags(FLAG_HARDWARE_ACCELERATED, + FLAG_HARDWARE_ACCELERATED); + } catch (Exception e) { + } + + updateSelectedColumns(); + + if (selectedColumns <= DEFAULT_VIEW_LIMIT) { + currentUI = 0; + setContentView(R.layout.main_old); + } else { + currentUI = 1; + setContentView(R.layout.main); + } + + Toolbar toolbar = findViewById(R.id.main_toolbar); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + setSupportActionBar(toolbar); + + + this.findViewById(R.id.img_wifi).setOnClickListener(this); + /* + this.findViewById(R.id.img_reset).setOnClickListener(this); + this.findViewById(R.id.img_invert).setOnClickListener(this); + this.findViewById(R.id.img_clone).setOnClickListener(this); + */ + this.findViewById(R.id.img_action).setOnClickListener(this); + + AlpSettings.Display.setStealthMode(getApplicationContext(), G.enableStealthPattern()); + AlpSettings.Display.setMaxRetries(getApplicationContext(), G.getMaxPatternTry()); + + Api.assertBinaries(this, true); + + initDone = 0; + mSwipeLayout = findViewById(R.id.swipe_container); + mSwipeLayout.setOnRefreshListener(this); + + + if (!G.hasRoot()) { + (new RootCheck()).setContext(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + //might not have rootshell + startRootShell(); + new SecurityUtil(MainActivity.this).passCheck(); + registerNetworkObserver(); + // Ensure FirewallService is started if firewall is enabled + if (Api.isEnabled(this)) { + Api.setEnabled(this, true, false); + } + } + registerUIbroadcast4(); + registerUIbroadcast6(); + registerToastbroadcast(); + initTextWatcher(); + registerThemeIntent(); + registerUIRefresh(); + } + + private void checkPermissions() { + if (G.enableTether()) { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) + != PackageManager.PERMISSION_GRANTED) { + // permissions have not been granted. + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.BLUETOOTH_CONNECT}, + PERMISSION_BLUETOOTH); + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + // permissions have not been granted. + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, + PERMISSION_NOTIFICATION); + } + } + } + + private void updateSelectedColumns() { + selectedColumns = DEFAULT_COLUMN; + selectedColumns = G.enableLAN() ? selectedColumns + 1 : selectedColumns; + selectedColumns = G.enableRoam() ? selectedColumns + 1 : selectedColumns; + selectedColumns = G.enableVPN() ? selectedColumns + 1 : selectedColumns; + selectedColumns = G.enableTether() ? selectedColumns + 1 : selectedColumns; + selectedColumns = G.enableTor() ? selectedColumns + 1 : selectedColumns; + } + + private void registerLogService() { + if (G.enableLogService()) { + Log.i(G.TAG, "Starting Log Service"); + final Intent logIntent = new Intent(getBaseContext(), LogService.class); + startService(logIntent); + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private void registerUIRefresh() { + IntentFilter filter = new IntentFilter("dev.ukanth.ufirewall.ui.CHECKREFRESH"); + uiRefreshReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateSelectedColumns(); + if (selectedColumns <= DEFAULT_VIEW_LIMIT && currentUI == 1) { + recreate(); + } else if (selectedColumns > DEFAULT_VIEW_LIMIT && currentUI == 0) { + recreate(); + } else { + recreate(); + } + } + }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(uiRefreshReceiver, filter, RECEIVER_EXPORTED); + } else { + registerReceiver(uiRefreshReceiver, filter); + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private void registerThemeIntent() { + + IntentFilter filter = new IntentFilter("dev.ukanth.ufirewall.theme.REFRESH"); + themeRefreshReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initTheme(); + recreate(); + } + }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(themeRefreshReceiver, filter, RECEIVER_EXPORTED); + } else { + registerReceiver(themeRefreshReceiver, filter); + } + } + + private void probeLogTarget() { + List availableLogTargets = new ArrayList<>(); + if (G.logTargets() == null) { + try { + new RootCommand() + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + if (state.exitCode != 0) { + return; + } + for (String str : state.res.toString().split("\n")) { + if (str.equals("LOG") || str.equals("NFLOG")) { + availableLogTargets.add(str); + } + } + if (availableLogTargets.size() > 0) { + String joined = TextUtils.join(",", availableLogTargets); + G.logTargets(joined); + } + } + }).setLogging(true) + .run(ctx, "cat /proc/net/ip_tables_targets"); + } catch (Exception e) { + Log.e(Api.TAG, "Exception in getting iptables log targets", e); + } + } + } + + 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 initTextWatcher() { + filterTextWatcher = new TextWatcher() { + + public void afterTextChanged(Editable s) { + showApplications(s.toString()); + } + + + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, + int count) { + showApplications(s.toString()); + } + }; + } + + + private void registerNetworkObserver() { + //start log service + if (G.enableLogService()) { + Intent logIntent = new Intent(getBaseContext(), LogService.class); + startService(logIntent); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + String action = intent.getAction(); + if (action == null) { + return; + } + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + } + + + /*private void migrateNotification() { + try { + if (!G.isNotificationMigrated()) { + List idList = G.getBlockedNotifyList(); + for (Integer uid : idList) { + LogPreference preference = new LogPreference(); + preference.setUid(uid); + preference.setTimestamp(System.currentTimeMillis()); + preference.setDisable(true); + FlowManager.getDatabase(LogPreferenceDB.class).beginTransactionAsync(databaseWrapper -> preference.save(databaseWrapper)).build().execute(); + } + G.isNotificationMigrated(true); + } + } catch (Exception e) { + Log.e(G.TAG, "Unable to migrate notification", e); + } + }*/ + + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private void registerToastbroadcast() { + IntentFilter filter = new IntentFilter("TOAST"); + toastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Api.toast(getApplicationContext(), intent.getExtras().get("MSG") != null ? intent.getExtras().get("MSG").toString() : "", Toast.LENGTH_SHORT); + } + }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(toastReceiver, filter, RECEIVER_EXPORTED); + } else { + registerReceiver(toastReceiver, filter); + } + } + + private void registerUIbroadcast4() { + uiFilter4 = new IntentFilter("UPDATEUI4"); + + uiProgressReceiver4 = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Bundle b = intent.getExtras(); + if (runProgress != null) { + TextView view = (TextView) runProgress.findViewById(R.id.apply4); + view.setText(b.get("INDEX") + "/" + b.get("SIZE")); + view.invalidate(); + } + } + }; + } + + private void registerUIbroadcast6() { + uiFilter6 = new IntentFilter("UPDATEUI6"); + + uiProgressReceiver6 = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Bundle b = intent.getExtras(); + if (runProgress != null) { + TextView view = (TextView) runProgress.findViewById(R.id.apply6); + view.setText(b.get("INDEX") + "/" + b.get("SIZE")); + view.invalidate(); + } + } + }; + } + + @Override + public void onRefresh() { + index = 0; + top = 0; + Api.applications = null; + showOrLoadApplications(); + mSwipeLayout.setRefreshing(false); + } + + private void updateRadioFilter() { + RadioGroup radioGroup = findViewById(R.id.appFilterGroup); + if (G.showFilter()) { + switch (G.selectedFilter()) { + case 0: + radioGroup.check(R.id.rpkg_core); + break; + case 1: + radioGroup.check(R.id.rpkg_sys); + break; + case 2: + radioGroup.check(R.id.rpkg_user); + break; + default: + radioGroup.check(R.id.rpkg_all); + break; + } + } else { + radioGroup.check(R.id.rpkg_all); + } + radioGroup.setOnCheckedChangeListener(this); + } + + private void selectFilterGroup() { + if (G.showFilter()) { + RadioGroup radioGroup = findViewById(R.id.appFilterGroup); + int selectedId = radioGroup.getCheckedRadioButtonId(); + if (selectedId == R.id.rpkg_core) { + filterApps(2); + } else if (selectedId == R.id.rpkg_sys) { + filterApps(0); + } else if (selectedId == R.id.rpkg_user) { + filterApps(1); + } else { + filterApps(-1); + } + } else { + filterApps(-1); + } + + } + + /** + * Filter application based on app tpe + * + * @param i + */ + private void filterApps(int i) { + Set returnList = new HashSet<>(); + List inputList; + List allApps = Api.getApps(getApplicationContext(), null); + if (i >= 0) { + for (PackageInfoData infoData : allApps) { + if (infoData != null) { + if (infoData.appType == i) { + returnList.add(infoData); + } + } + } + inputList = new ArrayList<>(returnList); + } else { + if (allApps != null && allApps.size() > 0) { + inputList = allApps; + } else { + inputList = new ArrayList<>(returnList); + } + } + if (inputList != null && inputList.size() > 0) { + try { + Collections.sort(inputList, new PackageComparator()); + } catch (Exception e) { + } + ArrayAdapter appAdapter; + if (selectedColumns <= DEFAULT_VIEW_LIMIT) { + appAdapter = new AppListArrayAdapter(this, getApplicationContext(), inputList, true); + } else { + appAdapter = new AppListArrayAdapter(this, getApplicationContext(), inputList); + } + this.listview.setAdapter(appAdapter); + appAdapter.notifyDataSetChanged(); + // restore + this.listview.setSelectionFromTop(index, top); + } else { + } + } + + @Override + protected void onStop() { + super.onStop(); + } + + @Override + protected void onRestart() { + super.onRestart(); + } + + + private void updateIconStatus() { + if (Api.isEnabled(getApplicationContext())) { + getSupportActionBar().setIcon(R.drawable.notification); + } else { + getSupportActionBar().setIcon(R.drawable.notification_error); + } + } + + private void startRootShell() { + List cmds = new ArrayList(); + cmds.add("true"); + new RootCommand().setFailureToast(R.string.error_su) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + //failed to acquire root + if (state.exitCode != 0) { + runOnUiThread(() -> { + disableFirewall(); + showRootNotFoundMessage(); + }); + } + } + }).run(getApplicationContext(), cmds); + //check if log targets support + probeLogTarget(); + } + + private void showRootNotFoundMessage() { + if (G.isActivityVisible()) { + try { + new MaterialDialog.Builder(this).cancelable(false) + .title(R.string.error_common) + .content(R.string.error_su) + .onPositive((dialog, which) -> dialog.dismiss()) + .onNegative((dialog, which) -> { + MainActivity.this.finish(); + android.os.Process.killProcess(android.os.Process.myPid()); + dialog.dismiss(); + }) + .positiveText(R.string.Continue) + .negativeText(R.string.exit) + .show(); + } catch (Exception e) { + Api.toast(this, getString(R.string.error_su_toast), Toast.LENGTH_SHORT); + } + } + } + + @Override + public void onResume() { + super.onResume(); + /*if (showQuickButton()) { + fab.setVisibility(View.VISIBLE); + } else { + fab.setVisibility(View.GONE); + }*/ + LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(uiProgressReceiver4, uiFilter4); + LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(uiProgressReceiver6, uiFilter6); + + G.activityResumed(); + + //getSupportActionBar().setBackgroundDrawable(new ColorDrawable(G.primaryColor())); + /* Window window = getWindow(); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(G.primaryDarkColor());*/ + + } + + private void reloadPreferences() { + + updateSelectedColumns(); + + getSupportActionBar().setDisplayShowHomeEnabled(true); + G.reloadPrefs(); + checkPreferences(); + //language + Api.updateLanguage(getApplicationContext(), G.locale()); + + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + + //verifyMultiProfile(); + refreshHeader(); + updateIconStatus(); + + clearNotification(); + + if (G.disableIcons()) { + this.findViewById(R.id.imageHolder).setVisibility(View.GONE); + } else { + this.findViewById(R.id.imageHolder).setVisibility(View.VISIBLE); + } + + if (G.showFilter()) { + this.findViewById(R.id.filerOption).setVisibility(View.VISIBLE); + } else { + this.findViewById(R.id.filerOption).setVisibility(View.GONE); + } + + if (G.enableMultiProfile()) { + this.findViewById(R.id.profileOption).setVisibility(View.VISIBLE); + } else { + this.findViewById(R.id.profileOption).setVisibility(View.GONE); + } + if (G.enableRoam()) { + addColumns(R.id.img_roam); + } else { + hideColumns(R.id.img_roam); + } + if (G.enableVPN()) { + addColumns(R.id.img_vpn); + } else { + hideColumns(R.id.img_vpn); + } + if (G.enableTether()) { + addColumns(R.id.img_tether); + } else { + hideColumns(R.id.img_tether); + } + + if (!Api.isMobileNetworkSupported(getApplicationContext())) { + ImageView view = this.findViewById(R.id.img_3g); + view.setVisibility(View.GONE); + + } else { + this.findViewById(R.id.img_3g).setOnClickListener(this); + } + + if (G.enableLAN()) { + addColumns(R.id.img_lan); + } else { + hideColumns(R.id.img_lan); + } + if (G.enableTor()) { + addColumns(R.id.img_tor); + } else { + hideColumns(R.id.img_tor); + } + + + updateRadioFilter(); + + if (G.enableMultiProfile()) { + setupMultiProfile(); + } + + selectFilterGroup(); + } + + private void clearNotification() { + //make sure we cancel notification posted by app notification. + NotificationManager mNotificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); + mNotificationManager.cancel(100); + } + + /** + * This will be used to migrate the profiles to a better one ( get ridoff of default profile ) + */ + + + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + if (checkedId == R.id.rpkg_all) { + filterApps(-1); + G.saveSelectedFilter(99); + } else if (checkedId == R.id.rpkg_core) { + filterApps(2); + G.saveSelectedFilter(0); + } else if (checkedId == R.id.rpkg_sys) { + filterApps(0); + G.saveSelectedFilter(1); + } else if (checkedId == R.id.rpkg_user) { + filterApps(1); + G.saveSelectedFilter(2); + } + } + + @Override + public void onStart() { + super.onStart(); + initDone = 0; + reloadPreferences(); + } + + private void addColumns(int id) { + ImageView view = this.findViewById(id); + view.setVisibility(View.VISIBLE); + view.setOnClickListener(this); + } + + private void hideColumns(int id) { + ImageView view = this.findViewById(id); + view.setVisibility(View.GONE); + view.setOnClickListener(this); + } + + private void setupMultiProfile() { + reloadProfileList(true); + mSpinner = findViewById(R.id.profileGroup); + spinnerAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item, + mlocalList); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSpinner.setAdapter(spinnerAdapter); + mSpinner.setOnItemSelectedListener(this); + String currentProfile = G.storedProfile(); + if (currentProfile != null) { + if (!G.isProfileMigrated()) { + switch (currentProfile) { + case Api.DEFAULT_PREFS_NAME: + mSpinner.setSelection(0); + break; + case "AFWallProfile1": + mSpinner.setSelection(1); + break; + case "AFWallProfile2": + mSpinner.setSelection(2); + break; + case "AFWallProfile3": + mSpinner.setSelection(3); + break; + default: + mSpinner.setSelection(spinnerAdapter.getPosition(currentProfile), false); + } + } else { + if (!currentProfile.equals(Api.DEFAULT_PREFS_NAME)) { + ProfileData data = ProfileHelper.getProfileByIdentifier(currentProfile); + if (data != null) { + mSpinner.setSelection(spinnerAdapter.getPosition(data.getName()), false); + } + } else { + mSpinner.setSelection(spinnerAdapter.getPosition(currentProfile), false); + } + } + } + } + + private void reloadProfileList(boolean reset) { + if (reset) { + mlocalList = new ArrayList<>(new LinkedHashSet()); + } + + mlocalList.add(G.gPrefs.getString("default", getString(R.string.defaultProfile))); + + if (!G.isProfileMigrated()) { + mlocalList.add(G.gPrefs.getString("profile1", getString(R.string.profile1))); + mlocalList.add(G.gPrefs.getString("profile2", getString(R.string.profile2))); + mlocalList.add(G.gPrefs.getString("profile3", getString(R.string.profile3))); + List profilesList = G.getAdditionalProfiles(); + for (String profiles : profilesList) { + if (profiles != null && profiles.length() > 0) { + mlocalList.add(profiles); + } + } + } else { + List profilesList = ProfileHelper.getProfiles(); + for (ProfileData data : profilesList) { + mlocalList.add(data.getName()); + } + } + } + + public void deviceCheck() { + if (Build.VERSION.SDK_INT >= 21) { + if ((G.isDoKey(getApplicationContext()) || isDonate())) { + KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + if (keyguardManager.isKeyguardSecure()) { + Intent createConfirmDeviceCredentialIntent = keyguardManager.createConfirmDeviceCredentialIntent(null, null); + if (createConfirmDeviceCredentialIntent != null) { + try { + startActivityForResult(createConfirmDeviceCredentialIntent, LOCK_VERIFICATION); + } catch (ActivityNotFoundException e) { + } + } + } else { + Toast.makeText(this, getText(R.string.android_version), Toast.LENGTH_SHORT).show(); + } + } else { + Api.donateDialog(MainActivity.this, true); + } + } + } + + @Override + protected void onPause() { + super.onPause(); + try { + if ((plsWait != null) && plsWait.isShowing()) { + plsWait.dismiss(); + } + + } catch (final IllegalArgumentException e) { + // Handle or log or ignore + } catch (final Exception e) { + // Handle or log or ignore + } finally { + plsWait = null; + } + //this.listview.setAdapter(null); + //mLastPause = Syst em.currentTimeMillis(); + //isOnPause = true; + //checkForProfile = true; + index = this.listview.getFirstVisiblePosition(); + View v = this.listview.getChildAt(0); + top = (v == null) ? 0 : v.getTop(); + G.activityPaused(); + LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(uiProgressReceiver4); + LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(uiProgressReceiver6); + } + + /** + * Check if the stored preferences are OK + */ + private void checkPreferences() { + final Editor editor = G.pPrefs.edit(); + boolean changed = false; + if (G.pPrefs.getString(Api.PREF_MODE, "").length() == 0) { + editor.putString(Api.PREF_MODE, Api.MODE_WHITELIST); + changed = true; + } + if (changed) + editor.commit(); + } + + /** + * Refresh informative header + */ + private void refreshHeader() { + final String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST); + //final TextView labelmode = (TextView) this.findViewById(R.id.label_mode); + final Resources res = getResources(); + + if (mode.equals(Api.MODE_WHITELIST)) { + if (mainMenu != null) { + mainMenu.findItem(R.id.allowmode).setChecked(true); + mainMenu.findItem(R.id.menu_mode).setIcon(R.drawable.ic_allow); + } + } else { + if (mainMenu != null) { + mainMenu.findItem(R.id.blockmode).setChecked(true); + mainMenu.findItem(R.id.menu_mode).setIcon(R.drawable.ic_deny); + } + } + //int resid = (mode.equals(Api.MODE_WHITELIST) ? R.string.mode_whitelist: R.string.mode_blacklist); + //labelmode.setText(res.getString(R.string.mode_header, res.getString(resid))); + } + + /** + * If the applications are cached, just show them, otherwise load and show + */ + private void showOrLoadApplications() { + //nocache!! + getAppList = new GetAppList(MainActivity.this); + if (plsWait == null && (getAppList.getStatus() == AsyncTask.Status.PENDING || getAppList.getStatus() == AsyncTask.Status.FINISHED)) { + getAppList.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + initDone = initDone + 1; + if (initDone > 1) { + Spinner spinner = findViewById(R.id.profileGroup); + String profileName = spinner.getSelectedItem().toString(); + if (!G.isProfileMigrated()) { + switch (position) { + case 0: + G.setProfile(true, "AFWallPrefs"); + break; + case 1: + G.setProfile(true, "AFWallProfile1"); + break; + case 2: + G.setProfile(true, "AFWallProfile2"); + break; + case 3: + G.setProfile(true, "AFWallProfile3"); + break; + default: + if (profileName != null) { + G.setProfile(true, profileName); + } + + } + setDirty(true); + } else { + switch (position) { + case 0: + G.setProfile(true, "AFWallPrefs"); + break; + default: + if (profileName != null) { + ProfileData data = ProfileHelper.getProfileByName(profileName); + G.setProfile(true, data.getIdentifier()); + } + } + setDirty(true); + } + G.reloadProfile(); + refreshHeader(); + showOrLoadApplications(); + if (G.applyOnSwitchProfiles()) { + applyOrSaveRules(); + } + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + + /** + * Show the list of applications + */ + private void showApplications(final String searchStr) { + + setDirty(false); + + List searchApp = new ArrayList<>(); + HashSet unique = new HashSet<>(); + final List apps = Api.getApps(this, null); + boolean isResultsFound = false; + + if (searchStr != null && searchStr.length() > 1) { + for (PackageInfoData app : apps) { + for (String str : app.names) { + if (str != null && searchStr != null) { + if (str.contains(searchStr.toLowerCase()) || str.toLowerCase().contains(searchStr.toLowerCase()) + && !searchApp.contains(app) || (G.showUid() && (str + " " + app.uid).contains(searchStr) && !unique.contains(app.uid))) { + searchApp.add(app); + unique.add(app.uid); + isResultsFound = true; + } + } + } + } + } + + List apps2 = null; + if (searchStr != null && searchStr.equals("")) { + apps2 = apps; + } else if (isResultsFound || searchApp.size() > 0) { + apps2 = searchApp; + } + // Sort applications - selected first, then alphabetically + try { + if (apps2 != null) { + Collections.sort(apps2, new PackageComparator()); + ArrayAdapter appAdapter; + if (selectedColumns <= DEFAULT_VIEW_LIMIT) { + appAdapter = new AppListArrayAdapter(this, getApplicationContext(), apps2, true); + } else { + appAdapter = new AppListArrayAdapter(this, getApplicationContext(), apps2); + } + this.listview.setAdapter(appAdapter); + // restore + this.listview.setSelectionFromTop(index, top); + } + } catch (Exception e) { + } + } + + @Override + public boolean onSearchRequested() { + if (mainMenu != null) { + MenuItem menuItem = mainMenu.findItem(R.id.menu_search); // R.string.search is the id of the searchview + if (menuItem != null) { + if (menuItem.isActionViewExpanded()) { + menuItem.collapseActionView(); + } else { + menuItem.expandActionView(); + search(menuItem); + } + } + } + return super.onSearchRequested(); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + //language + Api.updateLanguage(getApplicationContext(), G.locale()); + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.menu_bar, menu); + + // Get widget's instance + mainMenu = menu; + //make sure we update sort entry + runOnUiThread(new Runnable() { + @Override + public void run() { + switch (G.sortBy()) { + case "s0": + mainMenu.findItem(R.id.sort_default).setChecked(true); + break; + case "s1": + mainMenu.findItem(R.id.sort_lastupdate).setChecked(true); + break; + case "s2": + mainMenu.findItem(R.id.sort_uid).setChecked(true); + break; + } + + refreshHeader(); + } + }); + return true; + } + + public void menuSetApplyOrSave(final Menu menu, final boolean isEnabled) { + runOnUiThread(() -> { + if (menu != null) { + if (isEnabled) { + menu.findItem(R.id.menu_toggle).setTitle(R.string.fw_disabled).setIcon(R.drawable.notification_error); + menu.findItem(R.id.menu_apply).setTitle(R.string.applyrules); + getSupportActionBar().setIcon(R.drawable.notification); + } else { + menu.findItem(R.id.menu_toggle).setTitle(R.string.fw_enabled).setIcon(R.drawable.notification); + menu.findItem(R.id.menu_apply).setTitle(R.string.saverules); + getSupportActionBar().setIcon(R.drawable.notification_error); + } + } + }); + } + + @Override + public boolean onPrepareOptionsMenu(final Menu menu) { + //language + Api.updateLanguage(getApplicationContext(), G.locale()); + if (menu != null) { + menuSetApplyOrSave(mainMenu, Api.isEnabled(MainActivity.this)); + } + return true; + } + + private void disableFirewall() { + Api.setEnabled(this, false, true); + menuSetApplyOrSave(mainMenu, false); + } + + private void disableOrEnable() { + final boolean enabled = !Api.isEnabled(this); + Api.setEnabled(this, enabled, true); + if (enabled) { + applyOrSaveRules(); + } else { + if (G.enableConfirm()) { + confirmDisable(); + } else { + purgeRules(); + } + } + //refreshHeader(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + int selectedItem = item.getItemId(); + if (selectedItem == R.id.menu_toggle) { + disableOrEnable(); + return true; + } else if (selectedItem == R.id.allowmode) { + item.setChecked(true); + Editor editor = getSharedPreferences(Api.PREFS_NAME, 0).edit(); + editor.putString(Api.PREF_MODE, Api.MODE_WHITELIST); + editor.commit(); + refreshHeader(); + return true; + } else if (selectedItem == R.id.blockmode) { + item.setChecked(true); + Editor editor2 = getSharedPreferences(Api.PREFS_NAME, 0).edit(); + editor2.putString(Api.PREF_MODE, Api.MODE_BLACKLIST); + editor2.commit(); + refreshHeader(); + return true; + } else if (selectedItem == R.id.sort_default) { + G.sortBy("s0"); + item.setChecked(true); + Api.applications = null; + showOrLoadApplications(); + return true; + } else if (selectedItem == R.id.sort_lastupdate) { + G.sortBy("s1"); + item.setChecked(true); + Api.applications = null; + showOrLoadApplications(); + return true; + } else if (selectedItem == R.id.sort_uid) { + G.sortBy("s2"); + item.setChecked(true); + Api.applications = null; + showOrLoadApplications(); + return true; + } else if (selectedItem == R.id.menu_apply) { + applyOrSaveRules(); + return true; + } else if (selectedItem == R.id.menu_exit) { + finish(); + return true; + } else if (selectedItem == R.id.menu_help) { + showAbout(); + return true; + } else if (selectedItem == R.id.menu_log) { + showLog(); + return true; + } else if (selectedItem == R.id.menu_rules) { + showRules(); + return true; + } else if (selectedItem == R.id.menu_setcustom) { + setCustomScript(); + return true; + } else if (selectedItem == R.id.menu_preference) { + showPreferences(); + return true; + } else if (selectedItem == R.id.menu_search) { + search(item); + return true; + } else if (selectedItem == R.id.menu_export) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Do some stuff + showExportDialog(); + } else { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + // permissions have not been granted. + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + MY_PERMISSIONS_REQUEST_WRITE_STORAGE); + } else { + showExportDialog(); + } + } + return true; + } else if (selectedItem == R.id.menu_import) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Copy old data and show import dialog when complete + copyOldExportedData(); + } else { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + // permissions have not been granted. + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + MY_PERMISSIONS_REQUEST_READ_STORAGE); + + } else { + showImportDialog(); + } + } + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + private void copyOldExportedData() { + if (!G.hasCopyOld()) { + copyOldExportedDataAsync(() -> { + // On completion, show import dialog + runOnUiThread(() -> { + showImportDialog(); + }); + }); + } else { + // Already copied, show dialog immediately + showImportDialog(); + } + } + + private void copyOldExportedDataAsync(Runnable onComplete) { + // Show progress dialog + MaterialDialog progressDialog = null; + try { + progressDialog = new MaterialDialog.Builder(this) + .title("Migrating Files") + .content("Copying backup files to new location...") + .progress(true, 0) + .cancelable(false) + .show(); + } catch (Exception e) { + Log.w(TAG, "Could not show progress dialog due to MaterialDialog compatibility issue", e); + // Fallback: Show toast notification + Api.toast(this, "Migrating backup files to new location..."); + } + + final MaterialDialog finalProgressDialog = progressDialog; + + // Run file copy operation in background thread + new Thread(() -> { + try { + //using root to copy existing data to current directory on A11+ + String existingDir = Environment.getExternalStorageDirectory() + "//afwall//"; + String targetDir = ctx.getExternalFilesDir(null) + "/"; + String command = "cp -R " + existingDir + " " + targetDir; + Log.i(TAG, "Invoking migration script " + command); + + com.topjohnwu.superuser.Shell.Result result = com.topjohnwu.superuser.Shell.cmd(command).exec(); + + if (result.getCode() == 0) { + Log.i(TAG, "Migration script completed successfully"); + G.hasCopyOldExports(true); + } else { + Log.w(TAG, "Migration script failed with code: " + result.getCode()); + Log.w(TAG, "Migration output: " + result.getOut()); + } + + } catch (java.util.concurrent.RejectedExecutionException e) { + Log.w(TAG, "File migration rejected: " + e.getMessage()); + } catch (Exception e) { + // Check if the cause is an InterruptedIOException + if (e.getCause() instanceof java.io.InterruptedIOException) { + Log.w(TAG, "File migration interrupted: " + e.getCause().getMessage()); + } else { + Log.e(TAG, "Error during file migration", e); + } + } finally { + // Dismiss progress dialog and run completion callback on UI thread + runOnUiThread(() -> { + if (finalProgressDialog != null && finalProgressDialog.isShowing()) { + finalProgressDialog.dismiss(); + } + if (onComplete != null) { + onComplete.run(); + } + }); + } + }).start(); + } + + private void search(MenuItem item) { + item.setActionView(R.layout.searchbar); + final EditText filterText = item.getActionView().findViewById( + R.id.searchApps); + filterText.addTextChangedListener(filterTextWatcher); + filterText.setEllipsize(TruncateAt.END); + filterText.setSingleLine(); + + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + // Do something when collapsed + selectFilterGroup(); + return true; // Return true to collapse action view + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + filterText.post(() -> { + filterText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(filterText, InputMethodManager.SHOW_IMPLICIT); + }); + return true; // Return true to expand action view + } + }); + } + + private void showImportDialog() { + try { + new MaterialDialog.Builder(this) + .title(R.string.imports) + .cancelable(false) + .items(new String[]{ + getString(R.string.import_rules), + getString(R.string.import_all) + (G.isDoKey(getApplicationContext()) || isDonate() ? "" : " (" + getString(R.string.donate_only_short) + ")")}) + .itemsCallbackSingleChoice(-1, (dialog, view, which, text) -> { + switch (which) { + case 0: + //Intent intent = new Intent(MainActivity.this, FileChooserActivity.class); + //startActivityForResult(intent, FILE_CHOOSER_LOCAL); + File mPath = null; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + mPath = new File(Environment.getExternalStorageDirectory() + "//afwall//"); + } else { + mPath = new File(ctx.getExternalFilesDir(null) + "/"); + } + FileDialog fileDialog = new FileDialog(MainActivity.this, mPath, true); + + //fileDialog.setFlag(true); + //fileDialog.setFileEndsWith(new String[] {"backup", "afwall-backup"}, "all"); + fileDialog.addFileListener(file -> { + + String fileSelected = file.toString(); + StringBuilder builder = new StringBuilder(); + if (Api.loadSharedPreferencesFromFile(MainActivity.this, builder, fileSelected, false)) { + Api.applications = null; + showOrLoadApplications(); + Api.toast(MainActivity.this, getString(R.string.import_rules_success) + fileSelected); + } else { + if (builder.toString().equals("")) { + Api.toast(MainActivity.this, getString(R.string.import_rules_fail)); + } else { + Api.toast(MainActivity.this, builder.toString()); + } + } + }); + fileDialog.showDialog(); + break; + case 1: + + if (G.isDoKey(getApplicationContext()) || isDonate()) { + + File mPath2 = null; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + mPath2 = new File(Environment.getExternalStorageDirectory() + "//afwall//"); + } else { + mPath2 = new File(ctx.getExternalFilesDir(null), "/"); + } + FileDialog fileDialog2 = new FileDialog(MainActivity.this, mPath2, false); + fileDialog2.addFileListener(file -> { + String fileSelected = file.toString(); + StringBuilder builder = new StringBuilder(); + if (Api.loadSharedPreferencesFromFile(MainActivity.this, builder, fileSelected, true)) { + Api.applications = null; + showOrLoadApplications(); + Api.toast(MainActivity.this, getString(R.string.import_rules_success) + fileSelected); + Intent intent = getIntent(); + finish(); + startActivity(intent); + } else { + if (builder.toString().equals("")) { + Api.toast(MainActivity.this, getString(R.string.import_rules_fail)); + } else { + Api.toast(MainActivity.this, builder.toString()); + } + } + }); + fileDialog2.showDialog(); + } else { + Api.donateDialog(MainActivity.this, false); + } + break; + } + return true; + }) + .positiveText(R.string.imports) + .negativeText(R.string.Cancel) + .show(); + } catch (Exception e) { + Log.e(TAG, "MaterialDialog failed, likely due to cursor tinting issue on newer Android versions", e); + // Fallback: Show a simple toast message and try alternative approach + Api.toast(this, "Import dialog unavailable due to Android compatibility issue. Please use file manager to manually copy backup files to AFWall directory."); + } + } + + private void showExportDialog() { + try { + new MaterialDialog.Builder(this) + .title(R.string.exports) + .cancelable(false) + .items(new String[]{ + getString(R.string.export_rules), + getString(R.string.export_all) + (G.isDoKey(getApplicationContext()) || isDonate() ? "" : " (" + getString(R.string.donate_only_short) + ")")}) + .itemsCallbackSingleChoice(-1, (dialog, view, which, text) -> { + switch (which) { + case 0: + Api.exportRulesToFileWithPicker(MainActivity.this); + break; + case 1: + if (G.isDoKey(getApplicationContext()) || isDonate()) { + Api.exportAllPreferencesToFileWithPicker(MainActivity.this); + } else { + showExportAllWarningDialog(); + } + break; + } + return true; + }).positiveText(R.string.exports) + .negativeText(R.string.Cancel) + .show(); + } catch (Exception e) { + Log.e(TAG, "MaterialDialog failed, likely due to cursor tinting issue on newer Android versions", e); + Api.toast(this, "Export dialog unavailable due to Android compatibility issue. Please use Settings > Export to access export functionality."); + } + } + + private void showExportAllWarningDialog() { + try { + new MaterialDialog.Builder(this) + .title(R.string.export_all) + .content(R.string.export_all_warning) + .positiveText(R.string.exports) + .negativeText(R.string.Cancel) + .onPositive((dialog, which) -> { + Api.exportAllPreferencesToFileWithPicker(MainActivity.this); + }) + .show(); + } catch (Exception e) { + Log.e(TAG, "MaterialDialog failed, likely due to cursor tinting issue on newer Android versions", e); + // Fallback: Just show the export directly with a toast warning + Api.toast(this, getString(R.string.export_all_warning)); + Api.exportAllPreferencesToFileWithPicker(MainActivity.this); + } + } + + private void showPreferences() { + Intent i = new Intent(this, PreferencesActivity.class); + //startActivity(i); + startActivityForResult(i, PREFERENCE_RESULT); + } + + private void showAbout() { + Intent i = new Intent(this, HelpActivity.class); + startActivityForResult(i, SHOW_ABOUT_RESULT); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + switch (requestCode) { + case MY_PERMISSIONS_REQUEST_WRITE_STORAGE: { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showExportDialog(); + } else { + Toast.makeText(this, R.string.permissiondenied_importexport, Toast.LENGTH_SHORT).show(); + } + return; + } + + case MY_PERMISSIONS_REQUEST_WRITE_STORAGE_ASSET: { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Api.assertBinaries(this, true); + } else { + Toast.makeText(this, R.string.permissiondenied_asset, Toast.LENGTH_SHORT).show(); + } + return; + } + + case MY_PERMISSIONS_REQUEST_READ_STORAGE: { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showImportDialog(); + } else { + Toast.makeText(this, R.string.permissiondenied_importexport, Toast.LENGTH_SHORT).show(); + } + return; + } + } + } + + public void confirmDisable() { + + new MaterialDialog.Builder(this) + .title(R.string.confirmMsg) + //.content(R.string.confirmMsg) + .cancelable(false) + .onPositive((dialog, which) -> { + purgeRules(); + Api.updateNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + dialog.dismiss(); + }) + .onNegative((dialog, which) -> { + Api.setEnabled(getApplicationContext(), true, true); + dialog.dismiss(); + }) + .positiveText(R.string.Yes) + .negativeText(R.string.No) + .show(); + } + + /** + * Set a new init script + */ + private void setCustomScript() { + Intent intent = new Intent(); + intent.setClass(this, CustomScriptActivity.class); + startActivityForResult(intent, SHOW_CUSTOM_SCRIPT); + } + + /*private void startCustomRules() { + if ((G.isDoKey(getApplicationContext()) || isDonate())) { + Intent intent = new Intent(); + intent.setClass(this, CustomRulesActivity.class); + startActivity(intent); + } else { + Api.donateDialog(MainActivity.this, false); + } + }*/ + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case LOCK_VERIFICATION: + case REQ_ENTER_PATTERN: { + switch (resultCode) { + case RESULT_OK: + showOrLoadApplications(); + break; + default: + MainActivity.this.finish(); + android.os.Process.killProcess(android.os.Process.myPid()); + break; + } + } + break; + + case VERIFY_CHECK: { + Log.i(Api.TAG, "In VERIFY_CHECK"); + switch (resultCode) { + case RESULT_OK: + G.isDo(true); + break; + case RESULT_CANCELED: + G.isDo(false); + } + } + break; + //isPassVerify = true; + case PREFERENCE_RESULT: { + invalidateOptionsMenu(); + //recreate(); + } + break; + } + if (resultCode == RESULT_OK + && data != null && Api.CUSTOM_SCRIPT_MSG.equals(data.getAction())) { + final String script = data.getStringExtra(Api.SCRIPT_EXTRA); + final String script2 = data.getStringExtra(Api.SCRIPT2_EXTRA); + setCustomScript(script, script2); + } + } + + /** + * Set a new init script + * + * @param script new script (empty to remove) + * @param script2 new "shutdown" script (empty to remove) + */ + private void setCustomScript(String script, String script2) { + final Editor editor = getSharedPreferences(Api.PREFS_NAME, 0).edit(); + // Remove unnecessary white-spaces, also replace '\r\n' if necessary + script = script.trim().replace("\r\n", "\n"); + script2 = script2.trim().replace("\r\n", "\n"); + editor.putString(Api.PREF_CUSTOMSCRIPT, script); + editor.putString(Api.PREF_CUSTOMSCRIPT2, script2); + int msgid; + if (editor.commit()) { + if (script.length() > 0 || script2.length() > 0) { + msgid = R.string.custom_script_defined; + } else { + msgid = R.string.custom_script_removed; + } + } else { + msgid = R.string.custom_script_error; + } + Api.toast(MainActivity.this, MainActivity.this.getString(msgid)); + if (Api.isEnabled(this)) { + // If the firewall is enabled, re-apply the rules + applyOrSaveRules(); + } + } + + /** + * Show iptables rules on a dialog + */ + private void showRules() { + Intent i = new Intent(this, RulesActivity.class); + startActivityForResult(i, SHOW_RULES_ACTIVITY); + } + + /** + * Show logs on a dialog + */ + private void showLog() { + if (G.oldLogView()) { + Intent i = new Intent(this, OldLogActivity.class); + startActivityForResult(i, SHOW_LOGS_ACTIVITY); + } else { + Intent i = new Intent(this, LogActivity.class); + startActivityForResult(i, SHOW_LOGS_ACTIVITY); + } + } + + /** + * Apply or save iptables rules, showing a visual indication + */ + private void applyOrSaveRules() { + final boolean enabled = Api.isEnabled(this); + final Context ctx = getApplicationContext(); + + Api.generateRules(ctx, Api.getApps(ctx, null), true); + + if (!enabled) { + Api.setEnabled(ctx, false, true); + Api.toast(ctx, ctx.getString(R.string.rules_saved)); + setDirty(false); + return; + } + //Api.showNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + Api.updateNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + runApply = new RunApply(MainActivity.this); + runApply.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + /** + * Purge iptables rules, showing a visual indication + */ + private void purgeRules() { + purgeTask = new PurgeTask(MainActivity.this); + purgeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.img_wifi || id == R.id.img_3g + || id == R.id.img_roam + || id == R.id.img_vpn + || id == R.id.img_tether + || id == R.id.img_lan + || id == R.id.img_tor) { + selectActionConfirmation(v.getId()); + } else if (id == R.id.img_action) { + selectAction(); + } + } + + private void selectAction() { + new MaterialDialog.Builder(this) + .title(R.string.confirmation) + .cancelable(true) + .negativeText(R.string.Cancel) + .items(new String[]{ + getString(R.string.invert_all), + getString(R.string.clone), + getString(R.string.clear_all)}) + .itemsCallback((dialog, view, which, text) -> { + switch (which) { + case 0: + selectActionConfirmation(getString(R.string.reverse_all), 0); + break; + case 1: + cloneColumn(getString(R.string.legend_clone)); + break; + case 2: + selectActionConfirmation(getString(R.string.unselect_all), 1); + break; + } + }) + .onNegative((dialog, which) -> dialog.dismiss()) + .show(); + + } + + private void selectAllLAN(boolean flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_lan = flag; + //addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void selectAllTor(boolean flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_tor = flag; + //addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + /** + * Cache any batch event by user + * + * @param + */ + /*public static void addToQueue(@NonNull PackageInfoData data) { + *//*if (queue == null) { + queue = new HashSet<>(); + } + //add or update based on new data + queue.add(data); + getFab().setBackgroundTintList(ColorStateList.valueOf(Color.RED));*//* + }*/ + private void selectAllVPN(boolean flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_vpn = flag; + //addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void selectAlltether(boolean flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_tether = flag; + //addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void selectRevert(int flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + if (flag == R.id.img_wifi) { + data.selected_wifi = !data.selected_wifi; + } else if (flag == R.id.img_3g) { + data.selected_3g = !data.selected_3g; + } else if (flag == R.id.img_roam) { + data.selected_roam = !data.selected_roam; + } else if (flag == R.id.img_vpn) { + data.selected_vpn = !data.selected_vpn; + } else if (flag == R.id.img_tether) { + data.selected_tether = !data.selected_tether; + } else if (flag == R.id.img_lan) { + data.selected_lan = !data.selected_lan; + } else if (flag == R.id.img_tor) { + data.selected_tor = !data.selected_tor; + } + //addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void selectRevert() { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_wifi = !data.selected_wifi; + data.selected_3g = !data.selected_3g; + data.selected_roam = !data.selected_roam; + data.selected_vpn = !data.selected_vpn; + data.selected_tether = !data.selected_tether; + data.selected_lan = !data.selected_lan; + data.selected_tor = !data.selected_tor; + //addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void selectAllRoam(boolean flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_roam = flag; + //addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void clearAll() { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + data.selected_wifi = false; + data.selected_3g = false; + data.selected_roam = false; + data.selected_vpn = false; + data.selected_tether = false; + data.selected_lan = false; + data.selected_tor = false; + //addToQueue(data); + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void selectAll3G(boolean flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_3g = flag; + //addToQueue(data); + } + // addToQueue(data); + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + + } + + private void selectAllWifi(boolean flag) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + int count = adapter.getCount(), item; + if (adapter != null) { + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + data.selected_wifi = flag; + // addToQueue(data); + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + @Override + public boolean onKeyUp(final int keyCode, final KeyEvent event) { + + if (event.getAction() == KeyEvent.ACTION_UP) { + switch (keyCode) { + case KeyEvent.KEYCODE_MENU: + if (mainMenu != null) { + mainMenu.performIdentifierAction(R.id.menu_list_item, 0); + return true; + } + break; + case KeyEvent.KEYCODE_BACK: + if (isDirty()) { + new MaterialDialog.Builder(this) + .title(R.string.confirmation) + .cancelable(false) + .content(R.string.unsaved_changes_message) + .positiveText(R.string.apply) + .negativeText(R.string.discard) + .onPositive((dialog, which) -> { + applyOrSaveRules(); + dialog.dismiss(); + }) + .onNegative((dialog, which) -> { + setDirty(false); + Api.applications = null; + finish(); + //System.exit(0); + //force reload rules. + MainActivity.super.onKeyDown(keyCode, event); + dialog.dismiss(); + }) + .show(); + return true; + + } else { + setDirty(false); + //finish(); + //System.exit(0); + } + + + } + } + return super.onKeyUp(keyCode, event); + } + + /** + * @param i + */ + + private void selectActionConfirmation(String displayMessage, final int i) { + + new MaterialDialog.Builder(this) + .title(R.string.confirmation).content(displayMessage) + .cancelable(true) + .positiveText(R.string.OK) + .negativeText(R.string.Cancel) + .onPositive((dialog, which) -> { + switch (i) { + case 0: + selectRevert(); + break; + case 1: + clearAll(); + } + dialog.dismiss(); + }) + + .onNegative((dialog, which) -> dialog.dismiss()) + .show(); + } + + private void cloneColumn(String displayMessage) { + String[] items = new String[]{ + getString(R.string.lan), + getString(R.string.wifi), + getString(R.string.data), + getString(R.string.vpn), + getString(R.string.roam), + getString(R.string.tether), + getString(R.string.tor)}; + + new MaterialDialog.Builder(this) + .title(R.string.confirmation).content(displayMessage) + .cancelable(true) + .positiveText(R.string.OK) + .negativeText(R.string.Cancel) + .items(items) + .itemsCallbackSingleChoice(-1, (dialog, view, which, text) -> { + switch (which) { + default: + new MaterialDialog.Builder(this) + .title(R.string.confirmation).content(displayMessage) + .cancelable(true) + .positiveText(R.string.OK) + .negativeText(R.string.Cancel) + .items(items) + .itemsCallbackSingleChoice(-1, (dialog2, view2, which2, text2) -> { + switch (which) { + default: + copyColumns(which, which2); + + } + return true; + }) + .positiveText(R.string.clone) + + .onNegative((dialog2, which2) -> dialog.dismiss()) + .show(); + } + return true; + }) + .positiveText(R.string.from) + .onNegative((dialog, which) -> dialog.dismiss()) + .show(); + } + + private void copyColumns(int which, int which2) { + if (this.listview == null) { + this.listview = this.findViewById(R.id.listview); + } + ListAdapter adapter = listview.getAdapter(); + if (adapter != null) { + int count = adapter.getCount(), item; + for (item = 0; item < count; item++) { + PackageInfoData data = (PackageInfoData) adapter.getItem(item); + if (data.uid != Api.SPECIAL_UID_ANY) { + switch (which) { + case 0: + switch (which2) { + case 0: + break; + case 1: + data.selected_wifi = data.selected_lan; + break; + case 2: + data.selected_3g = data.selected_lan; + break; + case 3: + data.selected_vpn = data.selected_lan; + break; + case 4: + data.selected_roam = data.selected_lan; + break; + case 5: + data.selected_tether = data.selected_lan; + break; + case 6: + data.selected_tor = data.selected_lan; + break; + } + break; + case 1: + switch (which2) { + case 0: + data.selected_lan = data.selected_wifi; + break; + case 1: + break; + case 2: + data.selected_3g = data.selected_wifi; + break; + case 3: + data.selected_vpn = data.selected_wifi; + break; + case 4: + data.selected_roam = data.selected_wifi; + break; + case 5: + data.selected_tether = data.selected_wifi; + break; + case 6: + data.selected_tor = data.selected_wifi; + break; + } + break; + case 2: + switch (which2) { + case 0: + data.selected_lan = data.selected_3g; + break; + case 1: + data.selected_wifi = data.selected_3g; + break; + case 2: + break; + case 3: + data.selected_vpn = data.selected_3g; + break; + case 4: + data.selected_roam = data.selected_3g; + break; + case 5: + data.selected_tether = data.selected_3g; + break; + case 6: + data.selected_tor = data.selected_3g; + break; + } + break; + case 3: + switch (which2) { + case 0: + data.selected_lan = data.selected_vpn; + break; + case 1: + data.selected_wifi = data.selected_vpn; + break; + case 2: + data.selected_3g = data.selected_vpn; + break; + case 3: + break; + case 4: + data.selected_roam = data.selected_vpn; + break; + case 5: + data.selected_tether = data.selected_vpn; + break; + case 6: + data.selected_tor = data.selected_vpn; + break; + } + break; + case 4: + switch (which2) { + case 0: + data.selected_lan = data.selected_roam; + break; + case 1: + data.selected_wifi = data.selected_roam; + break; + case 2: + data.selected_3g = data.selected_roam; + break; + case 3: + data.selected_vpn = data.selected_roam; + break; + case 4: + break; + case 5: + data.selected_tether = data.selected_roam; + break; + case 6: + data.selected_tor = data.selected_roam; + break; + } + break; + case 5: + switch (which2) { + case 0: + data.selected_lan = data.selected_tether; + break; + case 1: + data.selected_wifi = data.selected_tether; + break; + case 2: + data.selected_3g = data.selected_tether; + break; + case 3: + data.selected_vpn = data.selected_tether; + break; + case 4: + data.selected_roam = data.selected_tether; + break; + case 5: + break; + case 6: + data.selected_tor = data.selected_tether; + break; + } + break; + case 6: + switch (which2) { + case 0: + data.selected_lan = data.selected_tor; + break; + case 1: + data.selected_wifi = data.selected_tor; + break; + case 2: + data.selected_3g = data.selected_tor; + break; + case 3: + data.selected_vpn = data.selected_tor; + break; + case 4: + data.selected_roam = data.selected_tor; + break; + case 5: + data.selected_tether = data.selected_tor; + break; + case 6: + break; + } + break; + } + } + setDirty(true); + } + ((BaseAdapter) adapter).notifyDataSetChanged(); + } + } + + private void selectActionConfirmation(final int i) { + + new MaterialDialog.Builder(this) + .title(R.string.select_action) + .cancelable(true) + .items(new String[]{ + getString(R.string.check_all), + getString(R.string.invert_all), + getString(R.string.uncheck_all)}) + .itemsCallback((dialog, view, which, text) -> { + if (which == 0) { + if (i == R.id.img_wifi) { + dialog.setTitle(text + getString(R.string.wifi)); + selectAllWifi(true); + } else if (i == R.id.img_3g) { + dialog.setTitle(text + getString(R.string.data)); + selectAll3G(true); + } else if (i == R.id.img_roam) { + dialog.setTitle(text + getString(R.string.roam)); + selectAllRoam(true); + } else if (i == R.id.img_vpn) { + dialog.setTitle(text + getString(R.string.vpn)); + selectAllVPN(true); + } else if (i == R.id.img_tether) { + dialog.setTitle(text + getString(R.string.tether)); + selectAlltether(true); + } else if (i == R.id.img_lan) { + dialog.setTitle(text + getString(R.string.lan)); + selectAllLAN(true); + } else if (i == R.id.img_tor) { + dialog.setTitle(text + getString(R.string.tor)); + selectAllTor(true); + } + } else if (which == 1) { + if (i == R.id.img_wifi) { + dialog.setTitle(text + getString(R.string.wifi)); + } else if (i == R.id.img_3g) { + dialog.setTitle(text + getString(R.string.data)); + } else if (i == R.id.img_roam) { + dialog.setTitle(text + getString(R.string.roam)); + } else if (i == R.id.img_vpn) { + dialog.setTitle(text + getString(R.string.vpn)); + } else if (i == R.id.img_tether) { + dialog.setTitle(text + getString(R.string.tether)); + } else if (i == R.id.img_lan) { + dialog.setTitle(text + getString(R.string.lan)); + } else if (i == R.id.img_tor) { + dialog.setTitle(text + getString(R.string.tor)); + } + selectRevert(i); + dirty = true; + } else if (which == 2) { + + if (i == R.id.img_wifi) { + dialog.setTitle(text + getString(R.string.wifi)); + selectAllWifi(false); + } else if (i == R.id.img_3g) { + dialog.setTitle(text + getString(R.string.data)); + selectAll3G(false); + } else if (i == R.id.img_roam) { + dialog.setTitle(text + getString(R.string.roam)); + selectAllRoam(false); + } else if (i == R.id.img_vpn) { + dialog.setTitle(text + getString(R.string.vpn)); + selectAllVPN(false); + } else if (i == R.id.img_tether) { + dialog.setTitle(text + getString(R.string.tether)); + selectAlltether(false); + } else if (i == R.id.img_lan) { + dialog.setTitle(text + getString(R.string.lan)); + selectAllLAN(false); + } else if (i == R.id.img_tor) { + dialog.setTitle(text + getString(R.string.tor)); + selectAllTor(false); + } + } + }).show(); + } + + protected boolean isSuPackage(PackageManager pm, String suPackage) { + try { + PackageInfo info = pm.getPackageInfo(suPackage, 0); + return info.applicationInfo != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (getAppList != null) { + getAppList.cancel(true); + } + if (runApply != null) { + runApply.cancel(true); + } + + if (purgeTask != null) { + purgeTask.cancel(true); + } + + if (toastReceiver != null) { + unregisterReceiver(toastReceiver); + } + if (themeRefreshReceiver != null) { + unregisterReceiver(themeRefreshReceiver); + } + if (uiRefreshReceiver != null) { + unregisterReceiver(uiRefreshReceiver); + } + + // Clean up shell instances to prevent interruption crashes + try { + // Force close any existing shell instances + com.topjohnwu.superuser.Shell shell = com.topjohnwu.superuser.Shell.getCachedShell(); + if (shell != null && !shell.isAlive()) { + shell.close(); + } + } catch (Exception e) { + } + } + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(Api.updateBaseContextLocale(base)); + } + + private class PurgeTask extends AsyncTask { + + private MaterialDialog progress; + private final WeakReference activityReference; + + PurgeTask(MainActivity context) { + activityReference = new WeakReference<>(context); + } + + @Override + protected void onPreExecute() { + progress = new MaterialDialog.Builder(activityReference.get()) + .title(R.string.working) + .cancelable(true) + .content(R.string.purging_rules) + .progress(true, 0) + .show(); + } + + @Override + protected Boolean doInBackground(Void... voids) { + if (G.hasRoot()) { + //store the root value + G.hasRoot(true); + Api.purgeIptables(ctx, true, new RootCommand() + .setSuccessToast(R.string.rules_deleted) + .setFailureToast(R.string.error_purge) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + // error exit -> assume the rules are still enabled + // we shouldn't wind up in this situation, but if we do, the user's + // best bet is to click Apply then toggle Enabled again + try { + progress.dismiss(); + } catch (Exception ex) { + } + boolean nowEnabled = state.exitCode != 0; + runOnUiThread(() -> { + Api.setEnabled(ctx, nowEnabled, true); + menuSetApplyOrSave(mainMenu, nowEnabled); + refreshHeader(); + }); + + } + })); + return true; + } else { + return false; + } + } + + @Override + protected void onPostExecute(Boolean aVoid) { + super.onPostExecute(aVoid); + if (!aVoid) { + Toast.makeText(ctx, ctx.getString(R.string.error_su_toast), Toast.LENGTH_SHORT).show(); + try { + progress.dismiss(); + progress = null; + } catch (Exception ex) { + } finally { + assert progress != null; + progress.dismiss(); + progress = null; + } + } + } + } + + public class GetAppList extends AsyncTask { + + private final WeakReference activityReference; + + GetAppList(MainActivity context) { + activityReference = new WeakReference<>(context); + } + + @Override + protected void onPreExecute() { + plsWait = new MaterialDialog.Builder(activityReference.get()).cancelable(false). + title(getString(R.string.reading_apps)).progress(false, getPackageManager().getInstalledApplications(0) + .size(), true).show(); + doProgress(0); + } + + public void doProgress(int value) { + publishProgress(value); + } + + @Override + protected Void doInBackground(Void... params) { + Api.getApps(activityReference.get(), this); + if (isCancelled()) + return null; + //publishProgress(-1); + return null; + } + + @Override + protected void onPostExecute(Void result) { + selectFilterGroup(); + doProgress(-1); + try { + try { + if (plsWait != null && plsWait.isShowing()) { + plsWait.dismiss(); + } + } catch (final IllegalArgumentException e) { + // Handle or log or ignore + } catch (final Exception e) { + // Handle or log or ignore + } finally { + plsWait.dismiss(); + plsWait = null; + } + mSwipeLayout.setRefreshing(false); + } catch (Exception e) { + // nothing + if (plsWait != null) { + plsWait.dismiss(); + plsWait = null; + } + } + } + + @Override + protected void onProgressUpdate(Integer... progress) { + + if (progress[0] == 0 || progress[0] == -1) { + //do nothing + } else { + if (plsWait != null) { + plsWait.incrementProgress(progress[0]); + } + } + } + } + + private class RunApply extends AsyncTask { + boolean enabled = Api.isEnabled(getApplicationContext()); + + private final WeakReference activityReference; + + RunApply(MainActivity context) { + activityReference = new WeakReference<>(context); + } + + @Override + protected void onPreExecute() { + runProgress = new MaterialDialog.Builder(activityReference.get()) + .title(R.string.su_check_title) + .cancelable(true) + .customView(R.layout.apply_view, false) + //.content(enabled ? R.string.applying_rules + // : R.string.saving_rules) + .negativeText("Dismiss") + .show(); + if (G.enableIPv6()) { + runProgress.findViewById(R.id.apply6layout).setVisibility(View.VISIBLE); + } + } + + @Override + protected Boolean doInBackground(Void... params) { + //set the progress + if (G.hasRoot()) { + Api.setRulesUpToDate(false); + RootCommand rootCommand = new RootCommand() + .setSuccessToast(R.string.rules_applied) + .setFailureToast(R.string.error_apply) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + try { + if (runProgress != null) { + runProgress.dismiss(); + } + } catch (Exception ex) { + } + if (state.exitCode == 0) { + setDirty(false); + } + //queue.clear(); + runOnUiThread(() -> { + setDirty(false); + if (state.exitCode != 0) { + Api.errorNotification(activityReference.get()); + menuSetApplyOrSave(activityReference.get().mainMenu, false); + Api.setEnabled(activityReference.get(), false, true); + } else { + menuSetApplyOrSave(activityReference.get().mainMenu, enabled); + Api.setEnabled(activityReference.get(), enabled, true); + } + refreshHeader(); + }); + } + }); + Api.applySavedIptablesRules(activityReference.get(), true, rootCommand); + return true; + } else { + runOnUiThread(() -> { + setDirty(false); + try { + runProgress.dismiss(); + } catch (Exception ex) { + } + }); + return false; + } + + } + + @Override + protected void onPostExecute(Boolean aVoid) { + super.onPostExecute(aVoid); + if (!aVoid) { + Toast.makeText(activityReference.get(), getString(R.string.error_su_toast), Toast.LENGTH_SHORT).show(); + disableFirewall(); + refreshHeader(); + try { + runProgress.dismiss(); + } catch (Exception ex) { + } + } + } + } + + private class RootCheck extends AsyncTask { + MaterialDialog suDialog = null; + boolean unsupportedSU = false; + boolean[] suGranted = {false}; + private Context context = null; + //private boolean suAvailable = false; + + public RootCheck setContext(Context context) { + this.context = context; + return this; + } + + @Override + protected void onPreExecute() { + suDialog = new MaterialDialog.Builder(context). + cancelable(false).title(getString(R.string.su_check_title)).progress(true, 0).content(context.getString(R.string.su_check_message)) + .show(); + } + + @Override + protected Void doInBackground(Void... params) { + //open shell if required + Shell.getShell().isRoot(); + suGranted[0] = Shell.isAppGrantedRoot(); + unsupportedSU = isSuPackage(getPackageManager(), "com.kingroot.kinguser"); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + try { + if (suDialog != null) { + suDialog.dismiss(); + } + } catch (final Exception e) { + } finally { + suDialog = null; + } + if (!Api.isNetfilterSupported() && !isFinishing()) { + new MaterialDialog.Builder(MainActivity.this).cancelable(false) + .title(R.string.error_common) + .content(R.string.error_netfilter) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + dialog.dismiss(); + } + }) + .onNegative((dialog, which) -> { + MainActivity.this.finish(); + android.os.Process.killProcess(android.os.Process.myPid()); + dialog.dismiss(); + }) + .positiveText(R.string.Continue) + .negativeText(R.string.exit) + .show(); + } + /*// more details on https://github.com/ukanth/afwall/issues/501 + if (isSuPackage(getPackageManager(), "com.kingroot.kinguser")) { + G.kingDetected(true); + }*/ + if (!suGranted[0] && !unsupportedSU && !isFinishing()) { + disableFirewall(); + showRootNotFoundMessage(); + } else { + G.hasRoot(true); + startRootShell(); + new SecurityUtil(MainActivity.this).passCheck(); + registerNetworkObserver(); + // Ensure FirewallService is started if firewall is enabled + if (Api.isEnabled(MainActivity.this)) { + Api.setEnabled(MainActivity.this, true, false); + } + } + } + } + + @RequiresApi(28) + private static class OnUnhandledKeyEventListenerWrapper implements View.OnUnhandledKeyEventListener { + private final ViewCompat.OnUnhandledKeyEventListenerCompat mCompatListener; + + OnUnhandledKeyEventListenerWrapper(ViewCompat.OnUnhandledKeyEventListenerCompat listener) { + this.mCompatListener = listener; + } + + public boolean onUnhandledKeyEvent(View v, KeyEvent event) { + return this.mCompatListener.onUnhandledKeyEvent(v, event); + } + } + +} + diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/AppDetailActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/AppDetailActivity.java new file mode 100644 index 0000000..40877bc --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/AppDetailActivity.java @@ -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 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(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/BaseActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/BaseActivity.java new file mode 100644 index 0000000..60aa1e4 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/BaseActivity.java @@ -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); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/CustomRulesActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/CustomRulesActivity.java new file mode 100644 index 0000000..683b5ff --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/CustomRulesActivity.java @@ -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 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)); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/CustomScriptActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/CustomScriptActivity.java new file mode 100644 index 0000000..93bea3e --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/CustomScriptActivity.java @@ -0,0 +1,159 @@ +/** + * Custom scripts activity. + * This screen is displayed to change the custom scripts. + *

+ * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro + * Copyright (C) 2011-2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Rodrigo Zechin Rosauro, Umakanthan Chandran + * @version 1.1 + */ +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(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/DataDumpActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/DataDumpActivity.java new file mode 100644 index 0000000..d122eac --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/DataDumpActivity.java @@ -0,0 +1,480 @@ +/** + * Common framework for LogActivity and RulesActivity + *

+ * Copyright (C) 2011-2012 Umakanthan Chandran + * Copyright (C) 2011-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 . + * + * @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 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)); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/HelpActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/HelpActivity.java new file mode 100644 index 0000000..55b5173 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/HelpActivity.java @@ -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); + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/LogActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/LogActivity.java new file mode 100644 index 0000000..bbd0770 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/LogActivity.java @@ -0,0 +1,389 @@ +/** + * Display/purge logs and toggle logging + *

+ * Copyright (C) 2011-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 . + * + * @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 getLogData() { + //load 3 day data + long loadInterval = System.currentTimeMillis() - 259200000; + + List 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 = 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 List> split(List list, final int L) { + List> parts = new ArrayList>(); + final int N = list.size(); + for (int i = 0; i < N; i += L) { + parts.add(new ArrayList( + list.subList(i, Math.min(N, i + L))) + ); + } + return parts; + } + + + private List updateMap(final List logDataList, CollectLog collectLog) { + final HashMap logMap = new HashMap<>(); + final HashMap count = new HashMap<>(); + final HashMap lastBlocked = new HashMap<>(); + List analyticsList = new ArrayList(); + + //int counter = 0; + final int size = logDataList.size(); + //List> parts = split(logDataList, 10); + /*for(List listLog: parts) { + Thread t = new Thread() {*/ + LogData tmpData,data; + // public void run() { + for (int i=0; i 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 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); + }*/ + + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/LogDetailActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/LogDetailActivity.java new file mode 100644 index 0000000..138d82e --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/LogDetailActivity.java @@ -0,0 +1,752 @@ +/** + * Display/purge logs and toggle logging + *

+ * Copyright (C) 2011-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 . + * + * @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 logDataList; + private static List 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 getLogData(final int uid) { + return SQLite.select() + .from(LogData.class) + .where(LogData_Table.uid.eq(uid)) + .orderBy(LogData_Table.timestamp, false) + .queryList(); + } + + private List 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 { + 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 destinationCounts = new java.util.HashMap<>(); + java.util.Set 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 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 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 { + public String filename = ""; + private final Context ctx; + + private final WeakReference 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); + }*/ + + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/OldLogActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/OldLogActivity.java new file mode 100644 index 0000000..4307a8d --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/OldLogActivity.java @@ -0,0 +1,119 @@ +/** + * Display/purge logs and toggle logging + *

+ * Copyright (C) 2011-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 . + * + * @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 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(); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/ProfileActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/ProfileActivity.java new file mode 100644 index 0000000..17247e2 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/ProfileActivity.java @@ -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 profilesList = new ArrayList(); + 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 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 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; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/RulesActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/RulesActivity.java new file mode 100644 index 0000000..dc624f7 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/RulesActivity.java @@ -0,0 +1,369 @@ +/** + * Display firewall rules and interface info + *

+ * Copyright (C) 2011-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 . + * + * @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 prefs = G.gPrefs.getAll(); + for (String s : new TreeSet(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() { + @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(); + } + + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/activity/StartActivity.java b/app/src/main/java/dev/ukanth/ufirewall/activity/StartActivity.java new file mode 100644 index 0000000..a57f686 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/activity/StartActivity.java @@ -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(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/admin/AdminDeviceReceiver.java b/app/src/main/java/dev/ukanth/ufirewall/admin/AdminDeviceReceiver.java new file mode 100644 index 0000000..5c1a4a8 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/admin/AdminDeviceReceiver.java @@ -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"); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/broadcast/ConnectivityChangeReceiver.java b/app/src/main/java/dev/ukanth/ufirewall/broadcast/ConnectivityChangeReceiver.java new file mode 100644 index 0000000..39090bd --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/broadcast/ConnectivityChangeReceiver.java @@ -0,0 +1,83 @@ +/** + * Detect the connectivity changes (for roaming and LAN subnet changes) + *

+ * Copyright (C) 2011-2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author 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); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/broadcast/OnBootReceiver.java b/app/src/main/java/dev/ukanth/ufirewall/broadcast/OnBootReceiver.java new file mode 100644 index 0000000..ec745f8 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/broadcast/OnBootReceiver.java @@ -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){ + + } + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/broadcast/PackageBroadcast.java b/app/src/main/java/dev/ukanth/ufirewall/broadcast/PackageBroadcast.java new file mode 100644 index 0000000..c9c994d --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/broadcast/PackageBroadcast.java @@ -0,0 +1,192 @@ +/** + * Broadcast receiver responsible for removing rules that affect uninstalled apps. + *

+ * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro + * Copyright (C) 2011-2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Rodrigo Zechin Rosauro, Umakanthan Chandran + * @version 1.1 + */ +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); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRule.java b/app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRule.java new file mode 100644 index 0000000..80436e8 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRule.java @@ -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; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRuleDatabase.java b/app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRuleDatabase.java new file mode 100644 index 0000000..e17f9a3 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/customrules/CustomRuleDatabase.java @@ -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; +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/events/LogChangeEvent.java b/app/src/main/java/dev/ukanth/ufirewall/events/LogChangeEvent.java new file mode 100644 index 0000000..b90ca32 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/events/LogChangeEvent.java @@ -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; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/events/LogEvent.java b/app/src/main/java/dev/ukanth/ufirewall/events/LogEvent.java new file mode 100644 index 0000000..ff98545 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/events/LogEvent.java @@ -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; + } + + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/events/RulesEvent.java b/app/src/main/java/dev/ukanth/ufirewall/events/RulesEvent.java new file mode 100644 index 0000000..04c2540 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/events/RulesEvent.java @@ -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; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/events/RxCommandEvent.java b/app/src/main/java/dev/ukanth/ufirewall/events/RxCommandEvent.java new file mode 100644 index 0000000..58b7046 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/events/RxCommandEvent.java @@ -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 sSubject = PublishSubject.create(); + + private RxCommandEvent() { + // hidden constructor + } + + public static Disposable subscribe(@NonNull Consumer action) { + return sSubject.subscribe(action); + } + + public static void publish(@NonNull Object message) { + sSubject.onNext(message); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/events/RxEvent.java b/app/src/main/java/dev/ukanth/ufirewall/events/RxEvent.java new file mode 100644 index 0000000..3fd21a3 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/events/RxEvent.java @@ -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 sSubject = PublishSubject.create(); + + public RxEvent() { + // hidden constructor + } + + public Disposable subscribe(@NonNull Consumer action) { + return sSubject.subscribe(action, throwable -> { + Log.i("AFWall", throwable.getLocalizedMessage()); + }); + } + + public void publish(@NonNull Object message) { + sSubject.onNext(message); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/Log.java b/app/src/main/java/dev/ukanth/ufirewall/log/Log.java new file mode 100644 index 0000000..ce56cdc --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/Log.java @@ -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 . + * + * @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 circ = new LinkedList(); + + 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); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogData.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogData.java new file mode 100644 index 0000000..21554a5 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogData.java @@ -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; + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogDatabase.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogDatabase.java new file mode 100644 index 0000000..79acb70 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogDatabase.java @@ -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 { + + + public Migration2(Class table) { + super(table); + } + + @Override + public void onPreMigrate() { + addColumn(SQLiteType.TEXT, "hostname"); + addColumn(SQLiteType.INTEGER, "type"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogDetailRecyclerViewAdapter.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogDetailRecyclerViewAdapter.java new file mode 100644 index 0000000..c3ae8ee --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogDetailRecyclerViewAdapter.java @@ -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 { + + + private final List logData; + private final Context context; + private LogData data; + private final RecyclerItemClickListener recyclerItemClickListener; + + // Track repeated connection attempts for better UX + private final Map connectionAttempts = new ConcurrentHashMap<>(); + + // Cache for expensive operations + private final Map serviceCache = new HashMap<>(); + private final Map interfaceNameCache = new HashMap<>(); + + + public LogDetailRecyclerViewAdapter(final Context context, RecyclerItemClickListener recyclerItemClickListener) { + this.context = context; + logData = new ArrayList<>(); + this.recyclerItemClickListener = recyclerItemClickListener; + } + + public void updateData(List 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 getLogData() { + return logData; + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java new file mode 100644 index 0000000..3689654 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogInfo.java @@ -0,0 +1,345 @@ +/** + * Capture Logs from dmesg and return the formatted string + *

+ *

+ * Copyright (C) 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 . + * + * @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 dstBlocked; // Number of packets blocked per destination IP address + + public LogInfo() { + this.dstBlocked = new HashMap(); + } + + + public static String parseLog(Context ctx, List listLogData) { + + //final BufferedReader r = new BufferedReader(new StringReader(dmesg.toString())); + StringBuilder res = new StringBuilder(); + Integer appid; + final SparseArray map = new SparseArray(); + 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 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 appNameMap = new HashMap(); + final List 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; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogPreference.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogPreference.java new file mode 100644 index 0000000..3085268 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogPreference.java @@ -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; + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogPreferenceDB.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogPreferenceDB.java new file mode 100644 index 0000000..f8ab9b2 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogPreferenceDB.java @@ -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; +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/LogRecyclerViewAdapter.java b/app/src/main/java/dev/ukanth/ufirewall/log/LogRecyclerViewAdapter.java new file mode 100644 index 0000000..d9e62ce --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/LogRecyclerViewAdapter.java @@ -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 { + + + private final List 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 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)); + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/RecyclerItemClickListener.java b/app/src/main/java/dev/ukanth/ufirewall/log/RecyclerItemClickListener.java new file mode 100644 index 0000000..5432d38 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/RecyclerItemClickListener.java @@ -0,0 +1,9 @@ +package dev.ukanth.ufirewall.log; + +/** + * Created by ukanth on 10/8/16. + */ +public interface RecyclerItemClickListener { + void onItemClick(LogData logData); +} + diff --git a/app/src/main/java/dev/ukanth/ufirewall/log/ShellCommand.java b/app/src/main/java/dev/ukanth/ufirewall/log/ShellCommand.java new file mode 100644 index 0000000..c18bf3c --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/log/ShellCommand.java @@ -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 . + * + * @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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/plugin/BundleScrubber.java b/app/src/main/java/dev/ukanth/ufirewall/plugin/BundleScrubber.java new file mode 100644 index 0000000..d8bad50 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/plugin/BundleScrubber.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012 two forty four a.m. LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package 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: + * . + */ +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; + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/plugin/FireReceiver.java b/app/src/main/java/dev/ukanth/ufirewall/plugin/FireReceiver.java new file mode 100644 index 0000000..cee9bfe --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/plugin/FireReceiver.java @@ -0,0 +1,301 @@ +/* + * Copyright 2012 two forty four a.m. LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/plugin/LocaleEdit.java b/app/src/main/java/dev/ukanth/ufirewall/plugin/LocaleEdit.java new file mode 100644 index 0000000..6562d3b --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/plugin/LocaleEdit.java @@ -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 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(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java b/app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java new file mode 100644 index 0000000..79c4be1 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/plugin/PluginBundleManager.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012 two forty four a.m. LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + */ + +package 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}. + *

+ * 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} + *

+ * 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. + *

+ * 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$ + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/CustomBinaryPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/CustomBinaryPreferenceFragment.java new file mode 100644 index 0000000..d86de86 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/CustomBinaryPreferenceFragment.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPref.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPref.java new file mode 100644 index 0000000..212f7eb --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPref.java @@ -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; + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPrefDB.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPrefDB.java new file mode 100644 index 0000000..fbed3e0 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/DefaultConnectionPrefDB.java @@ -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; +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/ExpPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/ExpPreferenceFragment.java new file mode 100644 index 0000000..f7e7e17 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/ExpPreferenceFragment.java @@ -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 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)); + } + + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/LanguagePreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/LanguagePreferenceFragment.java new file mode 100644 index 0000000..c388585 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/LanguagePreferenceFragment.java @@ -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) { + + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/LogPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/LogPreferenceFragment.java new file mode 100644 index 0000000..cb0838f --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/LogPreferenceFragment.java @@ -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 ar = new ArrayList(); + ArrayList val = new ArrayList(); + 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 integers) { + int[] ret = new int[integers.size()]; + Iterator 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 entriesList = new ArrayList(); + final ArrayList entryValuesList = new ArrayList(); + + List apps = new ArrayList<>(); + //List apps = Api.getSpecialData(true); + + Api.PackageInfoData info = new Api.PackageInfoData(); + info.uid = 1020; + info.pkgName = "dev.afwall.special.mDNS"; + info.names = new ArrayList(); + 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 blockedList = new ArrayList(); + 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 entryValuesList) { + List 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(); + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/MultiProfilePreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/MultiProfilePreferenceFragment.java new file mode 100644 index 0000000..1c7e953 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/MultiProfilePreferenceFragment.java @@ -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(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java new file mode 100644 index 0000000..1ffd62f --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/PreferencesActivity.java @@ -0,0 +1,397 @@ +/** + * Preference Interface. + * All iptables "communication" is handled by this class. + *

+ * Copyright (C) 2011-2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author 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

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)); + } + + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/RulesPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/RulesPreferenceFragment.java new file mode 100644 index 0000000..caf8618 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/RulesPreferenceFragment.java @@ -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); + } + + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java new file mode 100644 index 0000000..5049aba --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/SecPreferenceFragment.java @@ -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); + } + } + } + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareContract.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareContract.java new file mode 100644 index 0000000..9d90f68 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareContract.java @@ -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; +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreference.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreference.java new file mode 100644 index 0000000..0e23304 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreference.java @@ -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 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 getAll() { + return queryAll(); + } + + @Override + public String getString(String key, String defValue) { + return (String)querySingle(key, defValue, ShareContract.TYPE_STRING); + } + + @Override + @TargetApi(11) + public Set getStringSet(String key, Set 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 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 map = new HashMap(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 mToAdd = new ArrayList(); + private final Set mToRemove = new HashSet(); + + 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 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 mListener; + + private PreferenceContentObserver(OnSharedPreferenceChangeListener listener) { + super(mHandler); + mListener = new WeakReference(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); + } + } + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreferenceProvider.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreferenceProvider.java new file mode 100644 index 0000000..65983d1 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/SharePreferenceProvider.java @@ -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 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(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 preferenceMap = preferences.getAll(); + MatrixCursor cursor = new MatrixCursor(projection); + if (nameKeyPair.key.length() == 0) { + for (Map.Entry 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 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 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; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareProfilePreference.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareProfilePreference.java new file mode 100644 index 0000000..a2102b6 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareProfilePreference.java @@ -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; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareUtils.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareUtils.java new file mode 100644 index 0000000..bbdc879 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/ShareUtils.java @@ -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 toStringSet(Object value) { + return (Set)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 stringSet) { + StringBuilder sb = new StringBuilder(); + for (String s : stringSet) { + sb.append(s.replace("\\", "\\\\").replace(";", "\\;")); + sb.append(';'); + } + return sb.toString(); + } + + public static Set deserializeStringSet(String serializedString) { + HashSet stringSet = new HashSet(); + 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/ThemePreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/ThemePreferenceFragment.java new file mode 100644 index 0000000..5d64a13 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/ThemePreferenceFragment.java @@ -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; + } + } + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/UIPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/UIPreferenceFragment.java new file mode 100644 index 0000000..b976c70 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/UIPreferenceFragment.java @@ -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 entriesList = new ArrayList(); + final ArrayList entryValuesList = new ArrayList(); + + 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 listPerf = new ArrayList(); + List selectedItems = new ArrayList(); + List ignoredItems = new ArrayList(); + 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 integers) { + int[] ret = new int[integers.size()]; + Iterator iterator = integers.iterator(); + for (int i = 0; i < ret.length; i++) { + ret[i] = iterator.next().intValue(); + } + return ret; + } + + + private Integer[] selectItems(ArrayList entryValuesList, int modeType) { + List items = new ArrayList<>(); + for (Integer in : G.readDefaultConnection(modeType)) { + if (entryValuesList.contains(in)) { + items.add(entryValuesList.indexOf(in)); + } + } + return items.toArray(new Integer[0]); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/preferences/WidgetPreferenceFragment.java b/app/src/main/java/dev/ukanth/ufirewall/preferences/WidgetPreferenceFragment.java new file mode 100644 index 0000000..447ed57 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/preferences/WidgetPreferenceFragment.java @@ -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) { + + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileAdapter.java b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileAdapter.java new file mode 100644 index 0000000..19f1d2a --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileAdapter.java @@ -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 { + + private final List profileList; + private final Context context; + + + public ProfileAdapter(List 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; + + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileData.java b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileData.java new file mode 100644 index 0000000..92e3a93 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileData.java @@ -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(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileHelper.java b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileHelper.java new file mode 100644 index 0000000..f731dae --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfileHelper.java @@ -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 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 listProfile = new ArrayList<>(); + List addProfiles = G.getAdditionalProfiles(); + List 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); + } + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfilesDatabase.java b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfilesDatabase.java new file mode 100644 index 0000000..d6fbf16 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/profiles/ProfilesDatabase.java @@ -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; +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/service/FirewallService.java b/app/src/main/java/dev/ukanth/ufirewall/service/FirewallService.java new file mode 100644 index 0000000..56a8e3e --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/service/FirewallService.java @@ -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; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/service/LogService.java b/app/src/main/java/dev/ukanth/ufirewall/service/LogService.java new file mode 100644 index 0000000..e57ff32 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/service/LogService.java @@ -0,0 +1,742 @@ +/** + * Background service to spool /proc/kmesg command output using klogripper + *

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

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

+ * 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 . + * + * @author Kevin Cernekee + * @version 1.0 + */ + +package dev.ukanth.ufirewall.service; + +import static dev.ukanth.ufirewall.service.RootShellService2.ShellState2.INIT; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.core.app.TaskStackBuilder; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Timer; +import java.util.TimerTask; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.MainActivity; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.util.G; +import eu.chainfire.libsuperuser.Debug; +import eu.chainfire.libsuperuser.Shell; + + +public class RootShellService2 extends Service { + + public static final String TAG = "AFWall6"; + public static final int NOTIFICATION_ID = 33347; + public static final int EXIT_NO_ROOT_ACCESS = -1; + public static final int NO_TOAST = -1; + /* write command completion times to logcat */ + private static final boolean enableProfiling = false; + //number of retries - increase the count + private final static int MAX_RETRIES = 10; + private static Shell.Interactive rootSession2; + private Context mContext; + private NotificationManager notificationManager; + private static ShellState2 rootState = INIT; + private final LinkedList waitQueue = new LinkedList<>(); + + private void complete(final RootCommand state, int exitCode) { + if (enableProfiling) { + Log.d(TAG, "RootShell6: " + state.getCommmands().size() + " commands completed in " + + (new Date().getTime() - state.startTime.getTime()) + " ms"); + } + state.exitCode = exitCode; + state.done = true; + if (state.cb != null) { + state.cb.cbFunc(state); + } + + if (exitCode == 0 && state.successToast != NO_TOAST) { + Api.sendToastBroadcast(this.mContext, mContext.getString(state.successToast)); + } else if (exitCode != 0 && state.failureToast != NO_TOAST) { + Api.sendToastBroadcast(mContext, mContext.getString(state.failureToast)); + } + + if (notificationManager != null) { + notificationManager.cancel(NOTIFICATION_ID); + } + } + + private void runNextSubmission() { + + do { + RootCommand state; + try { + state = waitQueue.remove(); + } catch (NoSuchElementException e) { + // nothing left to do + if (rootState == ShellState2.BUSY) { + rootState = ShellState2.READY; + } + break; + } + if (state != null) { + //same as last one. ignore it + if (enableProfiling) { + state.startTime = new Date(); + } + if (rootState == ShellState2.FAIL) { + // if we don't have root, abort all queued commands + complete(state, EXIT_NO_ROOT_ACCESS); + //continue; + } else if (rootState == ShellState2.READY) { + rootState = ShellState2.BUSY; + if (G.isRun()) { + createNotification(mContext); + } + processCommands(state); + } + } + } while (false); + } + + private void processCommands(final RootCommand state) { + if (state.commandIndex < state.getCommmands().size() && state.getCommmands().get(state.commandIndex) != null) { + String command = state.getCommmands().get(state.commandIndex); + //Log.i("AFWall", command); + //not to send conflicting status + sendUpdate(state); + + if (command != null) { + state.ignoreExitCode = false; + + if (command.startsWith("#NOCHK# ")) { + command = command.replaceFirst("#NOCHK# ", ""); + state.ignoreExitCode = true; + } + state.lastCommand = command; + state.lastCommandResult = new StringBuilder(); + try { + rootSession2.addCommand(command, 0, (Shell.OnCommandResultListener2) (commandCode, exitCode, output, STDERR) -> { + ListIterator iter = output.listIterator(); + while (iter.hasNext()) { + String line = iter.next(); + if (line != null && !line.equals("")) { + if (state.res != null) { + state.res.append(line).append("\n"); + } + state.lastCommandResult.append(line).append("\n"); + } + } + // Special handling for exit code 126 (command not executable) - fallback to system iptables + if (exitCode == 126 && shouldFallbackToSystem(state)) { + Log.w(TAG, "Built-in iptables failed with exit 126, attempting fallback to system iptables"); + // Remember that built-in iptables failed for future preference + G.setBuiltinIptablesFailed(true); + fallbackToSystemBinary(state); + processCommands(state); + return; + } + + if (exitCode >= 0 && exitCode == state.retryExitCode && state.retryCount < MAX_RETRIES) { + //lets wait for few ms before trying ? + state.retryCount++; + Log.d(TAG, "command '" + state.lastCommand + "' exited with status " + exitCode + + ", retrying (attempt " + state.retryCount + "/" + MAX_RETRIES + ")"); + processCommands(state); + return; + } + + state.commandIndex++; + state.retryCount = 0; + + boolean errorExit = exitCode != 0 && !state.ignoreExitCode; + if (state.commandIndex >= state.getCommmands().size() || errorExit) { + complete(state, exitCode); + if (exitCode < 0) { + rootState = ShellState2.FAIL; + Log.e(TAG, "libsuperuser error " + exitCode + " on command '" + state.lastCommand + "'"); + } else { + if (errorExit) { + Log.i(TAG, "command '" + state.lastCommand + "' exited with status " + exitCode + + "\nOutput:\n" + state.lastCommandResult); + } + rootState = ShellState2.READY; + } + runNextSubmission(); + } else { + processCommands(state); + } + }); + } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { + Log.e(TAG, e.getMessage(), e); + } + } + } else { + complete(state, 0); + } + } + + private void sendUpdate(final RootCommand state2) { + new Thread(() -> { + Intent broadcastIntent = new Intent(); + broadcastIntent.setAction("UPDATEUI6"); + broadcastIntent.putExtra("SIZE", state2.getCommmands().size()); + broadcastIntent.putExtra("INDEX", state2.commandIndex); + LocalBroadcastManager.getInstance(this.mContext).sendBroadcast(broadcastIntent); + }).start(); + } + + private void createNotification(Context context) { + + String CHANNEL_ID = "firewall.apply"; + notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID); + + Intent appIntent = new Intent(context, MainActivity.class); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + /* Create or update. */ + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.runNotification), + NotificationManager.IMPORTANCE_LOW); + channel.setDescription(""); + channel.setShowBadge(false); + channel.setSound(null, null); + channel.enableLights(false); + channel.enableVibration(false); + if(G.getNotificationPriority() == 0) { + channel.setImportance(NotificationManager.IMPORTANCE_DEFAULT); + } + notificationManager.createNotificationChannel(channel); + } + + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addParentStack(MainActivity.class); + stackBuilder.addNextIntent(appIntent); + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE); + builder.setContentIntent(resultPendingIntent); + + + int notifyType = G.getNotificationPriority(); + + Notification notification = builder.setSmallIcon(R.drawable.ic_apply) + .setAutoCancel(false) + .setContentTitle(context.getString(R.string.applying_rules)) + .setTicker(context.getString(R.string.app_name)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setChannelId(CHANNEL_ID) + .setCategory(Notification.CATEGORY_SERVICE) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setOnlyAlertOnce(true) + .setPriority(NotificationManager.IMPORTANCE_LOW) + .setContentText("").build(); + builder.setProgress(0, 0, true); + notificationManager.notify(NOTIFICATION_ID, notification); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null) { // if crash restart... + Log.i(TAG, "Restarting RootShell..."); + List cmds = new ArrayList<>(); + cmds.add("true"); + new RootCommand().setFailureToast(R.string.error_su) + .setReopenShell(true).run(getApplicationContext(), cmds); + } + return Service.START_STICKY; + } + + private void setupLogging() { + Debug.setDebug(false); + Debug.setLogTypeEnabled(Debug.LOG_ALL, false); + Debug.setLogTypeEnabled(Debug.LOG_GENERAL, false); + Debug.setSanityChecksEnabled(false); + Debug.setOnLogListener((type, typeIndicator, message) -> Log.i(TAG, "[libsuperuser] " + message)); + } + + + private void startShellInBackground() { + Log.d(TAG, "Starting root shell(6)..."); + setupLogging(); + //start only rootSession is null + if (rootSession2 == null) { + rootSession2 = new Shell.Builder(). + useSU(). + setWatchdogTimeout(5). + open((success, reason) -> { + if (reason < 0) { + Log.e(TAG, "Can't open root shell: exitCode " + reason); + rootState = ShellState2.FAIL; + } else { + Log.d(TAG, "Root shell(6) is open"); + rootState = ShellState2.READY; + } + runNextSubmission(); + }); + } + + } + + private void reOpenShell(Context context) { + if (rootState == null || rootState != ShellState2.READY || rootState == ShellState2.FAIL) { + if (notificationManager != null) { + notificationManager.cancel(NOTIFICATION_ID); + } + rootState = ShellState2.BUSY; + startShellInBackground(); + Intent intent = new Intent(context, RootShellService2.class); + context.startService(intent); + } + } + + + public void runScriptAsRoot(Context ctx, List cmds, RootCommand state) { + state.setCommmands(cmds); + state.commandIndex = 0; + state.retryCount = 0; + if (mContext == null) { + mContext = ctx.getApplicationContext(); + } + //already in memory and applied + //add it to queue + + waitQueue.add(state); + + if (rootState == INIT || (rootState == ShellState2.FAIL && state.reopenShell)) { + reOpenShell(ctx); + } else if (rootState != ShellState2.BUSY) { + runNextSubmission(); + } else { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + Log.i(TAG, "State of rootShell(6): " + rootState); + if (rootState == ShellState2.BUSY) { + //try resetting state to READY forcefully + Log.i(TAG, "Forcefully changing the state " + rootState); + rootState = ShellState2.READY; + } + runNextSubmission(); + } + }, 1000); + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * Check if fallback to system binary should be attempted for exit code 126 + * Only fallback once per command to avoid infinite loops + */ + private boolean shouldFallbackToSystem(RootCommand state) { + if (state.lastCommand == null) { + return false; + } + + // Check if command contains built-in iptables path and hasn't been fallback attempted + String builtinDir = getApplicationContext().getDir("bin", 0).getAbsolutePath(); + return state.lastCommand.contains(builtinDir) && + !state.lastCommand.contains("__FALLBACK_ATTEMPTED__"); + } + + /** + * Replace built-in iptables/ip6tables paths with system paths in the current command + */ + private void fallbackToSystemBinary(RootCommand state) { + if (state.lastCommand == null) { + return; + } + + String builtinDir = getApplicationContext().getDir("bin", 0).getAbsolutePath(); + String originalCommand = state.lastCommand; + + // Try to find system iptables + String systemIptables = Api.findSystemBinary("iptables"); + String systemIp6tables = Api.findSystemBinary("ip6tables"); + + if (systemIptables != null || systemIp6tables != null) { + String updatedCommand = originalCommand; + + // Replace built-in paths with system paths + if (systemIptables != null) { + updatedCommand = updatedCommand.replace(builtinDir + "/iptables", systemIptables); + } + if (systemIp6tables != null) { + updatedCommand = updatedCommand.replace(builtinDir + "/ip6tables", systemIp6tables); + } + + // Mark as fallback attempted to prevent infinite loops + updatedCommand += " # __FALLBACK_ATTEMPTED__"; + + // Update the command in the current state + List commands = state.getCommmands(); + if (state.commandIndex < commands.size()) { + commands.set(state.commandIndex, updatedCommand); + Log.i(TAG, "Fallback applied: " + originalCommand + " -> " + updatedCommand); + } + } else { + Log.w(TAG, "No system iptables found for fallback"); + } + } + + public enum ShellState2 { + INIT, + READY, + BUSY, + FAIL + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/service/RulesApplyService.java b/app/src/main/java/dev/ukanth/ufirewall/service/RulesApplyService.java new file mode 100644 index 0000000..51a9ab3 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/service/RulesApplyService.java @@ -0,0 +1,41 @@ +package dev.ukanth.ufirewall.service; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.InterfaceTracker; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.util.G; + +/** + * Created by ukanth on 14/11/16. + */ + +public class RulesApplyService extends IntentService { + + public RulesApplyService() { + super(RulesApplyService.class.getName()); + } + + @Override + protected void onHandleIntent(Intent intent) { + + Context context = RulesApplyService.this; + if(Api.isEnabled(context)) { + if(G.activeRules()) { + Log.d(Api.TAG, "Applying rules on connectivity change"); + InterfaceTracker.applyRulesOnChange(context, InterfaceTracker.CONNECTIVITY_CHANGE); + } + final Intent logIntent = new Intent(context, LogService.class); + if (G.enableLogService()) { + context.stopService(logIntent); + context.startService(logIntent); + } else { + context.stopService(logIntent); + //Api.cleanupUid(); + } + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/service/ToggleTileService.java b/app/src/main/java/dev/ukanth/ufirewall/service/ToggleTileService.java new file mode 100644 index 0000000..5668e00 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/service/ToggleTileService.java @@ -0,0 +1,117 @@ +package dev.ukanth.ufirewall.service; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.widget.Toast; + +import androidx.annotation.RequiresApi; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.util.G; + +@RequiresApi(api = Build.VERSION_CODES.N) +public class ToggleTileService extends TileService { + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public void onTileAdded() { + super.onTileAdded(); + } + + @Override + public void onTileRemoved() { + super.onTileRemoved(); + } + + @Override + public void onStartListening() { + super.onStartListening(); + boolean status = Api.isEnabled(this); + Tile tile = getQsTile(); // this is getQsTile() method form java, used in Kotlin as a property + if (tile != null) { + if (!status) { + tile.setLabel(getString(R.string.inactive)); + tile.setIcon(Icon.createWithResource(this, R.drawable.notification_error)); + tile.setState(Tile.STATE_INACTIVE); + } else { + tile.setLabel(getString(R.string.active)); + tile.setIcon(Icon.createWithResource(this, R.drawable.notification)); + tile.setState(Tile.STATE_ACTIVE); + } + tile.updateTile(); + } + } + + @Override + public void onStopListening() { + super.onStopListening(); + } + + + @Override + public void onClick() { + super.onClick(); + Context context = this; + //Start main activity + final SharedPreferences prefs = context.getSharedPreferences(Api.PREF_FIREWALL_STATUS, 0); + final boolean enabled = !prefs.getBoolean(Api.PREF_ENABLED, true); + + + if (!G.protectionLevel().equals("p0") || G.enableDeviceCheck()) { + Toast.makeText(context, R.string.widget_disable_fail, Toast.LENGTH_SHORT).show(); + return; + } + + Tile tile = getQsTile(); + + if (tile != null) { + if (enabled) { + Api.applySavedIptablesRules(context, true, new RootCommand() + .setSuccessToast(R.string.toast_enabled) + .setFailureToast(R.string.toast_error_enabling) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + // setEnabled always sends us a STATUS_CHANGED_MSG intent to update the icon + try { + Api.setEnabled(context, state.exitCode == 0, true); + } catch (Exception e) { + Log.e(G.TAG, e.getLocalizedMessage(), e ); + } + tile.setState(Tile.STATE_ACTIVE); + tile.setLabel(getString(R.string.active)); + tile.setIcon(Icon.createWithResource(context, R.drawable.notification)); + tile.updateTile(); + } + })); + } else { + Api.purgeIptables(context, true, new RootCommand() + .setSuccessToast(R.string.toast_disabled) + .setFailureToast(R.string.toast_error_disabling) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + try { + Api.setEnabled(context, state.exitCode != 0, true); + } catch (Exception e) { + Log.e(G.TAG, e.getLocalizedMessage(), e ); + } + tile.setState(Tile.STATE_INACTIVE);// e() method form java, used in Kotlin as a property + tile.setLabel(getString(R.string.inactive)); + tile.setIcon(Icon.createWithResource(context, R.drawable.notification_error)); + tile.updateTile(); + } + })); + } + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/ui/about/AboutFragment.java b/app/src/main/java/dev/ukanth/ufirewall/ui/about/AboutFragment.java new file mode 100644 index 0000000..9ce66c5 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/ui/about/AboutFragment.java @@ -0,0 +1,154 @@ +package dev.ukanth.ufirewall.ui.about; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.TextView; + +import androidx.fragment.app.Fragment; + +import com.google.android.material.snackbar.Snackbar; + +import java.io.IOException; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.BuildConfig; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.util.G; + + +public class AboutFragment extends Fragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup group, + Bundle saved) { + return inflater.inflate(R.layout.help_about_content, group, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + String version = BuildConfig.VERSION_NAME; + + TextView titleText = view.findViewById(R.id.afwall_title); + String versionText = getString(R.string.app_name) + " (v" + version + ")"; + if(G.isDoKey(requireContext()) || BuildConfig.APPLICATION_ID.equals("dev.ukanth.ufirewall.donate")) { + versionText = versionText + " (Donate) " + getString(R.string.donate_thanks) + " :)"; + } + titleText.setText(versionText); + + loadCreditsContent(view); + } + + private void loadCreditsContent(View view) { + WebView creditsWebView = view.findViewById(R.id.about_thirdsparty_credits); + + // Configure WebView for better user experience + creditsWebView.getSettings().setJavaScriptEnabled(false); + creditsWebView.getSettings().setBuiltInZoomControls(true); + creditsWebView.getSettings().setDisplayZoomControls(false); + + // Handle links to open in external browser + creditsWebView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + try { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(browserIntent); + return true; + } catch (Exception e) { + Log.e(Api.TAG, "Error opening URL: " + url, e); + return false; + } + } + }); + + try { + String data = Api.loadData(requireContext(), "about"); + // Enhance the HTML with modern styling that adapts to theme + String enhancedData = enhanceHTMLForModernUI(data); + creditsWebView.loadDataWithBaseURL(null, enhancedData, "text/html", "UTF-8", null); + } catch (IOException ioe) { + Log.e(Api.TAG, "Error reading about file!", ioe); + // Show error state + Snackbar.make(view, "Unable to load about content", Snackbar.LENGTH_LONG).show(); + } + } + + private String enhanceHTMLForModernUI(String originalHTML) { + // Replace the old styling with modern, theme-aware styling + String modernCSS = + ""; + + // Replace the existing style section with modern styling + if (originalHTML.contains("]*>.*?", modernCSS); + } else { + originalHTML = originalHTML.replace("", "" + modernCSS); + } + + return originalHTML; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/AppIconHelperV26.java b/app/src/main/java/dev/ukanth/ufirewall/util/AppIconHelperV26.java new file mode 100644 index 0000000..080e812 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/AppIconHelperV26.java @@ -0,0 +1,56 @@ +package dev.ukanth.ufirewall.util; + +import static dev.ukanth.ufirewall.Api.TAG; + +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import dev.ukanth.ufirewall.log.Log; + +public class AppIconHelperV26 { + + @RequiresApi(api = Build.VERSION_CODES.O) + public static Bitmap getAppIcon(PackageManager mPackageManager, String packageName) { + + try { + Drawable drawable = mPackageManager.getApplicationIcon(packageName); + + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } else if (drawable instanceof AdaptiveIconDrawable) { + Drawable backgroundDr = ((AdaptiveIconDrawable) drawable).getBackground(); + Drawable foregroundDr = ((AdaptiveIconDrawable) drawable).getForeground(); + + Drawable[] drr = new Drawable[2]; + drr[0] = backgroundDr; + drr[1] = foregroundDr; + + LayerDrawable layerDrawable = new LayerDrawable(drr); + + int width = layerDrawable.getIntrinsicWidth(); + int height = layerDrawable.getIntrinsicHeight(); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + + layerDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + layerDrawable.draw(canvas); + + return bitmap; + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG,e.getMessage(),e); + } + + return null; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java b/app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java new file mode 100644 index 0000000..2859bcf --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/AppListArrayAdapter.java @@ -0,0 +1,741 @@ +package dev.ukanth.ufirewall.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ScaleDrawable; +import android.os.AsyncTask; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.LinearLayout; + +import java.util.List; +import java.util.HashSet; +import java.util.Set; + +import com.raizlabs.android.dbflow.sql.language.SQLite; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.Api.PackageInfoData; +import dev.ukanth.ufirewall.MainActivity; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.activity.AppDetailActivity; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.log.LogPreference; +import dev.ukanth.ufirewall.log.LogPreference_Table; +import dev.ukanth.ufirewall.log.LogData; +import dev.ukanth.ufirewall.log.LogData_Table; +import dev.ukanth.ufirewall.util.G; +import dev.ukanth.ufirewall.util.DataUsageParser; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import androidx.core.content.ContextCompat; + +public class AppListArrayAdapter extends ArrayAdapter { + + public static final String TAG = "AFWall"; + private final Context context; + private final List listApps; + + private final Activity activity; + + private boolean useOld = false; + private Set expandedPositions = new HashSet<>(); + + //final int color = G.sysColor(); + //final int defaultColor = Color.WHITE; + + public AppListArrayAdapter(MainActivity activity, Context context, List apps, boolean useOld) { + super(context, R.layout.main_list_old, apps); + this.useOld = true; + this.activity = activity; + this.context = context; + this.listApps = apps; + } + public AppListArrayAdapter(MainActivity activity, Context context, List apps) { + super(context, R.layout.main_list, apps); + this.activity = activity; + this.context = context; + this.listApps = apps; + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + AppStateHolder holder; + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + if (convertView == null) { + // Inflate a new view + if(useOld) { + convertView = inflater.inflate(R.layout.main_list_old, parent, false); + } else{ + convertView = inflater.inflate(R.layout.main_list, parent, false); + } + holder = new AppStateHolder(); + holder.box_wifi = convertView.findViewById(R.id.itemcheck_wifi); + + if (Api.isMobileNetworkSupported(context)) { + holder.box_3g = addSupport(convertView, true, R.id.itemcheck_3g); + } else { + removeSupport(convertView, R.id.itemcheck_3g); + } + + if (G.enableRoam()) { + holder.box_roam = addSupport(convertView, true, R.id.itemcheck_roam); + } + if (G.enableVPN()) { + holder.box_vpn = addSupport(convertView, true, R.id.itemcheck_vpn); + } + if (G.enableTether()) { + holder.box_tether = addSupport(convertView, true, R.id.itemcheck_tether); + } + if (G.enableLAN()) { + holder.box_lan = addSupport(convertView, true, R.id.itemcheck_lan); + } + if (G.enableTor()) { + holder.box_tor = addSupport(convertView, true, R.id.itemcheck_tor); + } + + holder.text = convertView.findViewById(R.id.itemtext); + holder.icon = convertView.findViewById(R.id.itemicon); + + if (G.disableIcons()) { + holder.icon.setVisibility(View.GONE); + activity.findViewById(R.id.imageHolder).setVisibility(View.GONE); + } + convertView.setTag(holder); + } else { + // Convert an existing view + holder = (AppStateHolder) convertView.getTag(); + holder.box_wifi = convertView.findViewById(R.id.itemcheck_wifi); + if (Api.isMobileNetworkSupported(context)) { + holder.box_3g = addSupport(convertView, true, R.id.itemcheck_3g); + } else { + removeSupport(convertView, R.id.itemcheck_3g); + } + if (G.enableRoam()) { + addSupport(convertView, false, R.id.itemcheck_roam); + } + if (G.enableVPN()) { + addSupport(convertView, false, R.id.itemcheck_vpn); + } + if (G.enableTether()) { + addSupport(convertView, false, R.id.itemcheck_tether); + } + if (G.enableLAN()) { + addSupport(convertView, false, R.id.itemcheck_lan); + } + if (G.enableTor()) { + addSupport(convertView, false, R.id.itemcheck_tor); + } + + holder.text = convertView.findViewById(R.id.itemtext); + holder.icon = convertView.findViewById(R.id.itemicon); + if (G.disableIcons()) { + holder.icon.setVisibility(View.GONE); + activity.findViewById(R.id.imageHolder).setVisibility(View.GONE); + } + } + + + holder.app = listApps.get(position); + + if (G.showUid()) { + holder.text.setText(holder.app.toStringWithUID()); + } else { + holder.text.setText(holder.app.toString()); + } + + final int id = holder.app.uid; + final View finalConvertView = convertView; + final int finalPosition = position; + holder.icon.setOnClickListener(v -> toggleExpansion(finalConvertView, finalPosition)); + holder.text.setOnClickListener(v -> toggleExpansion(finalConvertView, finalPosition)); + + + ApplicationInfo info = holder.app.appinfo; + if (info != null && (info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + //user app + holder.text.setTextColor(G.userColor()); + } else { + //system app + holder.text.setTextColor(G.sysColor()); + } + + if (!G.disableIcons()) { + if(holder.app.pkgName.startsWith("dev.afwall.special.")) { + holder.icon.setImageDrawable(context.getDrawable(R.drawable.ic_unknown)); + } else { + holder.icon.setImageDrawable(holder.app.cached_icon); + if (!holder.app.icon_loaded && info != null) { + // this icon has not been loaded yet - load it on a + // separated thread + try { + new LoadIconTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, holder.app, + context.getPackageManager(), convertView); + } catch (Exception r) { + } + } + } + + } else { + holder.icon.setVisibility(View.GONE); + activity.findViewById(R.id.imageHolder).setVisibility(View.GONE); + } + + holder.box_wifi.setTag(holder.app); + holder.box_wifi.setChecked(holder.app.selected_wifi); + + + if (Api.isMobileNetworkSupported(context)) { + holder.box_3g.setTag(holder.app); + holder.box_3g.setChecked(holder.app.selected_3g); + } + + if (G.enableRoam()) { + holder.box_roam = addSupport(holder.box_roam, holder.app, 0); + } + if (G.enableVPN()) { + holder.box_vpn = addSupport(holder.box_vpn, holder.app, 1); + } + if (G.enableTether()) { + holder.box_tether = addSupport(holder.box_tether, holder.app, 6); + } + if (G.enableLAN()) { + holder.box_lan = addSupport(holder.box_lan, holder.app, 2); + } + if (G.enableTor()) { + holder.box_tor = addSupport(holder.box_tor, holder.app, 3); + } + + setupExpandableView(holder, convertView, position); + addEventListenter(holder); + + return convertView; + } + + private void toggleExpansion(View convertView, int position) { + AppStateHolder holder = (AppStateHolder) convertView.getTag(); + if (expandedPositions.contains(position)) { + expandedPositions.remove(position); + holder.expandedOptions.setVisibility(View.GONE); + } else { + expandedPositions.add(position); + holder.expandedOptions.setVisibility(View.VISIBLE); + updateLogStatistics(holder); + updateDataUsageStats(holder); + } + } + + private void setupExpandableView(AppStateHolder holder, View convertView, int position) { + holder.expandedOptions = convertView.findViewById(R.id.expanded_options); + holder.actionToggleLog = convertView.findViewById(R.id.action_toggle_log); + holder.actionOpenApp = convertView.findViewById(R.id.action_open_app); + holder.actionViewLogs = convertView.findViewById(R.id.action_view_logs); + holder.blockedCount = convertView.findViewById(R.id.blocked_count); + holder.lastActivity = convertView.findViewById(R.id.last_activity); + holder.lastBlockedDestination = convertView.findViewById(R.id.last_blocked_destination); + holder.dataUsage = convertView.findViewById(R.id.data_usage); + + if (expandedPositions.contains(position)) { + holder.expandedOptions.setVisibility(View.VISIBLE); + updateLogStatistics(holder); + updateDataUsageStats(holder); + } else { + holder.expandedOptions.setVisibility(View.GONE); + } + + updateLogNotificationIcon(holder); + updateLogsIconVisibility(holder); + applyThemeColors(holder); + + holder.actionToggleLog.setOnClickListener(v -> { + Log.d(TAG, "Notification toggle clicked for UID: " + holder.app.uid); + toggleLogNotification(holder); + }); + holder.actionOpenApp.setOnClickListener(v -> { + Log.d(TAG, "Open app settings clicked for: " + holder.app.pkgName); + openAppSettings(holder); + }); + holder.actionViewLogs.setOnClickListener(v -> { + Log.d(TAG, "View logs clicked for UID: " + holder.app.uid); + openFirewallLogs(holder); + }); + } + + private void updateLogNotificationIcon(AppStateHolder holder) { + try { + LogPreference logPreference = SQLite.select() + .from(LogPreference.class) + .where(LogPreference_Table.uid.eq(holder.app.uid)).querySingle(); + + boolean isDisabled = logPreference != null && logPreference.isDisable(); + + holder.actionToggleLog.setImageResource( + isDisabled ? R.drawable.ic_notifications_off_black_24dp + : R.drawable.ic_notifications_on_black_24dp + ); + } catch (Exception e) { + Log.e(TAG, "Error updating notification icon", e); + holder.actionToggleLog.setImageResource(R.drawable.ic_notifications_on_black_24dp); + } + } + + private void applyThemeColors(AppStateHolder holder) { + int iconColor = G.userColor(); + int textColor = G.userColor(); + + // Apply color filter to icons using setColorFilter on ImageView, not the Drawable + // This preserves click functionality + holder.actionToggleLog.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + holder.actionOpenApp.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + holder.actionViewLogs.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + + // Apply text colors + if (holder.blockedCount != null) { + holder.blockedCount.setTextColor(textColor); + } + if (holder.lastActivity != null) { + holder.lastActivity.setTextColor(textColor); + } + if (holder.lastBlockedDestination != null) { + holder.lastBlockedDestination.setTextColor(textColor); + } + if (holder.dataUsage != null) { + holder.dataUsage.setTextColor(textColor); + } + } + + private void toggleLogNotification(AppStateHolder holder) { + try { + LogPreference logPreference = SQLite.select() + .from(LogPreference.class) + .where(LogPreference_Table.uid.eq(holder.app.uid)).querySingle(); + + // Current state: if logPreference exists and isDisable() is true, notifications are disabled + boolean currentlyDisabled = logPreference != null && logPreference.isDisable(); + + // Toggle: if currently disabled, enable (false); if currently enabled, disable (true) + boolean newDisabledState = !currentlyDisabled; + + Log.d(TAG, "Toggling log notification for UID " + holder.app.uid + + ": currently disabled=" + currentlyDisabled + ", new disabled state=" + newDisabledState); + + G.updateLogNotification(holder.app.uid, newDisabledState); + updateLogNotificationIcon(holder); + applyThemeColors(holder); + } catch (Exception e) { + Log.e(TAG, "Error toggling log notification", e); + } + } + + private void openAppSettings(AppStateHolder holder) { + if (!holder.app.pkgName.startsWith("dev.afwall.special.")) { + Api.showInstalledAppDetails(context, holder.app.pkgName); + } + } + + private void updateLogsIconVisibility(AppStateHolder holder) { + try { + // Check if logs exist for this app + long logCount = SQLite.selectCountOf() + .from(LogData.class) + .where(LogData_Table.uid.eq(holder.app.uid)) + .count(); + + holder.actionViewLogs.setVisibility(logCount > 0 ? View.VISIBLE : View.GONE); + } catch (Exception e) { + Log.e(TAG, "Error checking log availability", e); + holder.actionViewLogs.setVisibility(View.GONE); + } + } + + private void openFirewallLogs(AppStateHolder holder) { + try { + Intent intent = new Intent(context, dev.ukanth.ufirewall.activity.LogDetailActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("DATA", holder.app.uid); + context.startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "Error opening firewall logs", e); + } + } + + private void updateLogStatistics(AppStateHolder holder) { + try { + int uid = holder.app.uid; + + // Get blocked count + long blockedCountValue = SQLite.selectCountOf() + .from(LogData.class) + .where(LogData_Table.uid.eq(uid)) + .count(); + + holder.blockedCount.setText("Blocked: " + blockedCountValue); + + // Get most recent log entry + LogData lastLogEntry = SQLite.select() + .from(LogData.class) + .where(LogData_Table.uid.eq(uid)) + .orderBy(LogData_Table.timestamp, false) + .querySingle(); + + if (lastLogEntry != null) { + // Format last activity time + SimpleDateFormat sdf = new SimpleDateFormat("MMM dd, HH:mm", Locale.getDefault()); + String formattedTime = sdf.format(new Date(lastLogEntry.getTimestamp())); + holder.lastActivity.setText("Last: " + formattedTime); + + // Show last blocked destination + String destination = lastLogEntry.getDst(); + if (destination != null && !destination.isEmpty()) { + String hostname = lastLogEntry.getHostname(); + String displayDestination = hostname != null && !hostname.isEmpty() ? + hostname : destination; + holder.lastBlockedDestination.setText("Last blocked: " + displayDestination); + } else { + holder.lastBlockedDestination.setText("Last blocked: -"); + } + } else { + holder.lastActivity.setText("Last activity: -"); + holder.lastBlockedDestination.setText("Last blocked: -"); + } + + } catch (Exception e) { + Log.e(TAG, "Error updating log statistics", e); + holder.blockedCount.setText("Blocked: -"); + holder.lastActivity.setText("Last activity: -"); + holder.lastBlockedDestination.setText("Last blocked: -"); + } + } + + private void updateDataUsageStats(AppStateHolder holder) { + // Run in background thread to avoid blocking UI + new Thread(() -> { + try { + DataUsageParser.DataUsageStats stats = DataUsageParser.getDataUsageForUID(holder.app.uid); + String dataUsageText = DataUsageParser.formatWifiMobileUsage(stats); + + // Update UI on main thread + if (activity != null) { + activity.runOnUiThread(() -> { + if (holder.dataUsage != null) { + holder.dataUsage.setText("Data: " + dataUsageText); + } + }); + } + } catch (Exception e) { + Log.e(TAG, "Error updating data usage stats", e); + if (activity != null) { + activity.runOnUiThread(() -> { + if (holder.dataUsage != null) { + holder.dataUsage.setText("Data: Not available"); + } + }); + } + } + }).start(); + } + + private void StartAppDetailActivityIntent(View v, AppStateHolder holder, Integer id) { + Intent intent = new Intent(context, AppDetailActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("package", holder.app.pkgName); + intent.putExtra("appid", id); + context.startActivity(intent); + } + + private void addEventListenter(final AppStateHolder holder) { + if (holder.box_lan != null) { + holder.box_lan.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if(compoundButton.isPressed()) { + if (holder.app.selected_lan != isChecked) { + holder.app.selected_lan = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + //Log.i(TAG, "Application state changed: " + holder.app.pkgName); + //MainActivity.addToQueue(holder.app); + } + } + + } + }); + } + + if (holder.box_wifi != null) { + holder.box_wifi.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if(compoundButton.isPressed()) { + if (holder.app.selected_wifi != isChecked) { + holder.app.selected_wifi = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + //Log.i(TAG, "Application state changed: " + holder.app.pkgName); + //MainActivity.addToQueue(holder.app); + } + } + } + }); + } + + if (holder.box_3g != null) { + holder.box_3g.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if(compoundButton.isPressed()) { + if (holder.app.selected_3g != isChecked) { + holder.app.selected_3g = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + //Log.i(TAG, "Application state changed: " + holder.app.pkgName); + //MainActivity.addToQueue(holder.app); + } + } + } + }); + } + + if (holder.box_roam != null) { + holder.box_roam.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if(compoundButton.isPressed()) { + if (holder.app.selected_roam != isChecked) { + holder.app.selected_roam = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + //Log.i(TAG, "Application state changed: " + holder.app.pkgName); + //MainActivity.addToQueue(holder.app); + } + } + } + }); + } + + if (holder.box_vpn != null) { + holder.box_vpn.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if(compoundButton.isPressed()) { + if (holder.app.selected_vpn != isChecked) { + holder.app.selected_vpn = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + //Log.i(TAG, "Application state changed: " + holder.app.pkgName); + //MainActivity.addToQueue(holder.app); + } + } + } + }); + } + + if (holder.box_tether != null) { + holder.box_tether.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if(compoundButton.isPressed()) { + if (holder.app.selected_tether != isChecked) { + holder.app.selected_tether = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + //Log.i(TAG, "Application state changed: " + holder.app.pkgName); + //MainActivity.addToQueue(holder.app); + } + } + } + }); + } + + if (holder.box_tor != null) { + holder.box_tor.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if(compoundButton.isPressed()) { + if (holder.app.selected_tor != isChecked) { + holder.app.selected_tor = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + //Log.i(TAG, "Application state changed: " + holder.app.pkgName); + //MainActivity.addToQueue(holder.app); + } + } + } + }); + } + } + + private CheckBox addSupport(CheckBox check, PackageInfoData app, int flag) { + if (check != null) { + check.setTag(app); + switch (flag) { + case 0: + check.setChecked(app.selected_roam); + break; + case 1: + check.setChecked(app.selected_vpn); + break; + case 6: + check.setChecked(app.selected_tether); + break; + case 2: + check.setChecked(app.selected_lan); + break; + case 3: + check.setChecked(app.selected_tor); + break; + } + } + return check; + } + + private CheckBox addSupport(View convertView, boolean action, int id) { + CheckBox check = convertView.findViewById(id); + check.setVisibility(View.VISIBLE); + /* if (action) { + check.setOnCheckedChangeListener(this); + }*/ + return check; + } + + private CheckBox removeSupport(View convertView, int id) { + CheckBox check = convertView.findViewById(id); + check.setVisibility(View.GONE); + return check; + } + + + static class AppStateHolder { + private CheckBox box_lan; + private CheckBox box_wifi; + private CheckBox box_3g; + private CheckBox box_roam; + private CheckBox box_vpn; + private CheckBox box_tether; + private CheckBox box_tor; + private TextView text; + private ImageView icon; + private PackageInfoData app; + private LinearLayout expandedOptions; + private ImageView actionToggleLog; + private ImageView actionOpenApp; + private ImageView actionViewLogs; + private TextView blockedCount; + private TextView lastActivity; + private TextView lastBlockedDestination; + private TextView dataUsage; + } + + /** + * Asynchronous task used to load icons in a background thread. + */ + private static class LoadIconTask extends AsyncTask { + @Override + protected View doInBackground(Object... params) { + try { + final PackageInfoData app = (PackageInfoData) params[0]; + final PackageManager pkgMgr = (PackageManager) params[1]; + final View viewToUpdate = (View) params[2]; + if (!app.icon_loaded) { + Drawable d = new ScaleDrawable(pkgMgr.getApplicationIcon(app.appinfo), 0, 32, 32).getDrawable(); + d.setBounds(0, 0, 32, 32); + app.cached_icon = d; + app.icon_loaded = true; + } + // Return the view to update at "onPostExecute" + // Note that we cannot be sure that this view still references + // "app" + return viewToUpdate; + } catch (Exception e) { + Log.e(TAG, "Error loading icon", e); + return null; + } + } + + protected void onPostExecute(View viewToUpdate) { + try { + // This is executed in the UI thread, so it is safe to use + // viewToUpdate.getTag() + // and modify the UI + final AppStateHolder entryToUpdate = (AppStateHolder) viewToUpdate.getTag(); + entryToUpdate.icon.setImageDrawable(entryToUpdate.app.cached_icon); + } catch (Exception e) { + Log.e(TAG, "Error showing icon", e); + } + } + + } + + /** + * Called an application is check/unchecked + */ + /*@Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + final PackageInfoData app = (PackageInfoData) buttonView.getTag(); + if (app != null) { + switch (buttonView.getId()) { + case R.id.itemcheck_wifi: + if (app.selected_wifi != isChecked) { + app.selected_wifi = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + } + break; + case R.id.itemcheck_3g: + if (app.selected_3g != isChecked) { + app.selected_3g = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + } + break; + case R.id.itemcheck_roam: + if (app.selected_roam != isChecked) { + app.selected_roam = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + } + break; + case R.id.itemcheck_vpn: + if (app.selected_vpn != isChecked) { + app.selected_vpn = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + } + break; + case R.id.itemcheck_lan: + if (app.selected_lan != isChecked) { + app.selected_lan = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + } + break; + case R.id.itemcheck_tor: + if (app.selected_tor != isChecked) { + app.selected_tor = isChecked; + MainActivity.dirty = true; + notifyDataSetChanged(); + } + break; + } + if(buttonView.isPressed()) { + Log.i(TAG, "Application state changed: " + app.pkgName); + MainActivity.addToQueue(app); + } + } + }*/ + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/BiometricUtil.java b/app/src/main/java/dev/ukanth/ufirewall/util/BiometricUtil.java new file mode 100644 index 0000000..5ffbdc3 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/BiometricUtil.java @@ -0,0 +1,394 @@ +package dev.ukanth.ufirewall.util; + +/** + * This file was created to simplify Biometric APIs and made specifically for (AFWall+) application. + * You are free to re-distributed this file anywhere you like. :) + * ---------------------------------------------- + * Created by ukanth + */ + +import static android.content.Context.BIOMETRIC_SERVICE; +import static android.content.Context.KEYGUARD_SERVICE; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Dialog; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Point; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.BiometricPrompt; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import android.view.WindowManager; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.ProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; + +public class BiometricUtil { + + final static String TAG = "AfWall-BiometricUtil"; + + // generate key based on pkg name + public static String GetKey(Context context){ + return Api.getCurrentPackage(context) + ":Biometric"; + } + + // safely check if device support fingerprint + public static boolean isAndroidSupport(){ + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } + + /* + * Dialog + **/ + public static class FingerprintDialog extends Dialog { + + private KeyStore keyStore; + // Variable used for storing the key in the Android Keystore container + private static String KEY_NAME = "Key will be determined at onCreate Event"; + private Cipher cipher; + TextView errorText; + + boolean isNotFirstWindowFocus = false; + + KeyguardManager keyguardManager; + BiometricManager biometricManager; + + BiometricHandler helper; + + BiometricPrompt.CryptoObject cryptoObject; + + // callbacks + OnFingerprintFailure failureCallback; + OnFingerprintSuccess successCallback; + + @RequiresApi(api = Build.VERSION_CODES.Q) + public FingerprintDialog(Context context) { + super(context); + + // Initializing both Android Keyguard Manager and Fingerprint Manager + keyguardManager = (KeyguardManager) getContext().getSystemService(KEYGUARD_SERVICE); + biometricManager = (BiometricManager) getContext().getSystemService(BIOMETRIC_SERVICE); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setCancelable(false); + setContentView(R.layout.fingerprint); + setTitle(R.string.fingerprint_required); + + errorText = findViewById(R.id.fingerprintErrorText); + + // choose key that depends on [pkg_name]:Fingerprint + KEY_NAME = GetKey(getContext()); + + setFullscreenDialog(); + } + + // Full screen + void setFullscreenDialog(){ + + try{ + + WindowManager manager = (WindowManager) getContext().getSystemService(Activity.WINDOW_SERVICE); + + Point point = new Point(); + manager.getDefaultDisplay().getSize(point); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + + lp.copyFrom(getWindow().getAttributes()); + lp.width = point.x; + lp.height = point.y; + getWindow().setAttributes(lp); + + }catch (NullPointerException ex){ + + Log.e(TAG, ex.getMessage()); + } + } + + @Override + protected void onStart() { + super.onStart(); + + startReadFingerTip(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + + if(failureCallback != null){ + + failureCallback.then(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + /* Focus on current window is required and critical, + * if focus interrupted by "Home Button" or "SuperSU Dialog" + */ + + if(hasFocus){ + + if(isNotFirstWindowFocus){ + + startReadFingerTip(); + } + + isNotFirstWindowFocus = true; + + }else{ + + stopReadFingerTip(); + } + } + + public void setOnFingerprintFailureListener(OnFingerprintFailure mayHappen){ + failureCallback = mayHappen; + } + + public void setOnFingerprintSuccess(OnFingerprintSuccess doSomething){ + successCallback = doSomething; + } + + /** + * Created by whit3hawks on 11/16/16. + * Modified by vzool on 1/14/17. + */ + @TargetApi(Build.VERSION_CODES.Q) + void startReadFingerTip(){ + + // Check whether the device has a Fingerprint sensor. + if(biometricManager.canAuthenticate() == BIOMETRIC_ERROR_NO_HARDWARE){ + /** + * This block will not be touched unless weird things happened, + * because we already checked if device support fingerprint before enable it. + * We just leave it as-is for the days, who knows! :) + */ + errorText.setText(R.string.device_with_no_fingerprint_sensor); + }else { + // Checks whether fingerprint permission is set on manifest + if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { + errorText.setText(R.string.fingerprint_permission_manifest_missing); + }else{ + // Check whether at least one fingerprint is registered + if (biometricManager.canAuthenticate() == BIOMETRIC_ERROR_NONE_ENROLLED) { + errorText.setText(R.string.register_at_least_one_fingerprint); + }else{ + // Checks whether lock screen security is enabled or not + if (!keyguardManager.isKeyguardSecure()) { + errorText.setText(R.string.lock_screen_not_enabled); + }else{ + generateKey(); + if (cipherInit()) { + cryptoObject = new BiometricPrompt.CryptoObject(cipher); + if(helper == null){ + helper = new BiometricHandler(); + } + helper.startAuth(biometricManager, cryptoObject); + } + } + } + } + } + } + + @TargetApi(Build.VERSION_CODES.Q) + void stopReadFingerTip(){ + + if(helper != null){ + + helper.stopAuth(); + } + } + + private void triggerSuccess(){ + if(successCallback != null){ + successCallback.then(); + } + if(isShowing()) { + dismiss(); + } + } + + @TargetApi(Build.VERSION_CODES.Q) + private void generateKey() { + try { + keyStore = KeyStore.getInstance("AndroidKeyStore"); + } catch (Exception e) { + Log.e(TAG, "Error(0): " + e.getMessage()); + } + + + KeyGenerator keyGenerator; + try { + keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException("Failed to get KeyGenerator instance", e); + } + + + try { + keyStore.load(null); + keyGenerator.init(new + KeyGenParameterSpec.Builder(KEY_NAME, + KeyProperties.PURPOSE_ENCRYPT | + KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setUserAuthenticationRequired(true) + .setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_PKCS7) + .build()); + keyGenerator.generateKey(); + } catch (NoSuchAlgorithmException | + InvalidAlgorithmParameterException + | CertificateException | ProviderException | IOException e) { + Log.e(TAG, "Error(1): " + e.getMessage()); + } + } + + @TargetApi(Build.VERSION_CODES.Q) + private boolean cipherInit() { + try { + cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new RuntimeException("Failed to get Cipher", e); + } + + + try { + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, + null); + cipher.init(Cipher.ENCRYPT_MODE, key); + return true; + } catch (KeyPermanentlyInvalidatedException e) { + return false; + } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("Failed to init Cipher", e); + } + } + + /** + * Created by whit3hawks on 11/16/16. + * Modified by vzool on 1/14/17. + */ + @RequiresApi(api = Build.VERSION_CODES.Q) + private class BiometricHandler extends BiometricPrompt.AuthenticationCallback { + + CancellationSignal cancellationSignal; + + BiometricHandler(){ + cancellationSignal = new CancellationSignal(); + } + + private void startAuth(BiometricManager manager, BiometricPrompt.CryptoObject cryptoObject) { + + if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.USE_BIOMETRIC) != PackageManager.PERMISSION_GRANTED) { + return; + } + //manager.(cryptoObject, cancellationSignal, 0, this, null); + } + + private void stopAuth(){ + + if(cancellationSignal != null){ + + cancellationSignal.cancel(); + + cancellationSignal = null; + } + } + + + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + + // First attempts always fail due to interruption by SuperSU dialog. + // So, this view should be first responder. + // We ignore any errors and repeat process again :) + // + // this.update(getContext().getString(R.string.fingerprint_authentication_error) + errString, false); + + startReadFingerTip(); + } + + + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + this.update(getContext().getString(R.string.fingerprint_authentication_help) + helpString, false); + } + + + @Override + public void onAuthenticationFailed() { + this.update(getContext().getString(R.string.fingerprint_authentication_failed), false); + } + + + @Override + public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { + this.update(getContext().getString(R.string.fingerprint_authentication_successded), true); + } + + + public void update(String e, Boolean success){ + + errorText.setText(e); + + if(success){ + errorText.setTextColor(getContext().getColor(R.color.dark_bg)); + triggerSuccess(); + } + } + } + } + + // interface for callback on failure + public interface OnFingerprintFailure{ + void then(); + } + + // interface for callback on Success + public interface OnFingerprintSuccess{ + void then(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java b/app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java new file mode 100644 index 0000000..b30cb57 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/BootRuleManager.java @@ -0,0 +1,215 @@ +/** + * Robust boot rule application manager to prevent race conditions + * and ensure proper rule application during system startup. + * + * Copyright (C) 2024 AFWall+ Team + * + * 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. + */ + +package dev.ukanth.ufirewall.util; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.InterfaceTracker; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.service.RootCommand; + +public class BootRuleManager { + + private static final String TAG = "AFWall.BootRuleManager"; + + // Boot state management + private static final AtomicBoolean isBootInProgress = new AtomicBoolean(false); + private static final AtomicBoolean initialBootRulesApplied = new AtomicBoolean(false); + private static final AtomicBoolean delayedBootRulesScheduled = new AtomicBoolean(false); + + // Handler for delayed rule application + private static final AtomicReference delayedBootRunnable = new AtomicReference<>(); + private static final Handler bootHandler = new Handler(Looper.getMainLooper()); + + // Synchronization for rule application + private static final Object ruleApplicationLock = new Object(); + + /** + * Initialize boot rule application process + * Should be called from OnBootReceiver + */ + public static void initializeBootRuleApplication(Context context) { + synchronized (ruleApplicationLock) { + Log.i(TAG, "Initializing boot rule application"); + + // Mark boot as in progress + isBootInProgress.set(true); + initialBootRulesApplied.set(false); + delayedBootRulesScheduled.set(false); + + // Cancel any existing delayed rule application + cancelDelayedBootRules(); + + // Check and copy fix leak script if needed + checkFixLeakScript(context); + + // Apply initial boot rules immediately + applyInitialBootRules(context); + + // Schedule delayed boot rules if enabled + if (G.startupDelay()) { + scheduleDelayedBootRules(context); + } else { + // Mark boot as complete if no delay is configured + markBootComplete(); + } + } + } + + /** + * Check and copy fix leak script if needed (optimized - only when required) + */ + private static void checkFixLeakScript(Context context) { + try { + if (G.initPath() != null && G.fixLeak()) { + Log.d(TAG, "Checking fix leak script requirement"); + Api.checkAndCopyFixLeak(context, "afwallstart"); + } else { + Log.d(TAG, "Fix leak script not required - skipping"); + } + } catch (Exception e) { + Log.e(TAG, "Error checking fix leak script: " + e.getMessage()); + } + } + + /** + * Apply initial boot rules immediately + */ + private static void applyInitialBootRules(Context context) { + Log.i(TAG, "Applying initial boot rules"); + + InterfaceTracker.applyBootRules(InterfaceTracker.BOOT_COMPLETED + "_INITIAL"); + initialBootRulesApplied.set(true); + + Log.i(TAG, "Initial boot rules applied"); + } + + /** + * Schedule delayed boot rule application + */ + private static void scheduleDelayedBootRules(Context context) { + if (delayedBootRulesScheduled.compareAndSet(false, true)) { + int delay = G.getCustomDelay(); + Log.i(TAG, "Scheduling delayed boot rules in " + delay + "ms"); + + Runnable delayedRules = () -> { + synchronized (ruleApplicationLock) { + if (isBootInProgress.get()) { + Log.i(TAG, "Applying delayed boot rules"); + try { + // Force interface configuration refresh for delayed rules + InterfaceTracker.getCurrentCfg(context, true); + InterfaceTracker.applyBootRules(InterfaceTracker.BOOT_COMPLETED + "_DELAYED"); + Log.i(TAG, "Delayed boot rules applied successfully"); + } catch (Exception e) { + Log.e(TAG, "Error applying delayed boot rules: " + e.getMessage()); + } finally { + markBootComplete(); + } + } else { + Log.d(TAG, "Boot process already completed, skipping delayed rules"); + } + } + }; + + delayedBootRunnable.set(delayedRules); + bootHandler.postDelayed(delayedRules, delay); + } + } + + /** + * Cancel any scheduled delayed boot rule application + */ + private static void cancelDelayedBootRules() { + Runnable existingRunnable = delayedBootRunnable.getAndSet(null); + if (existingRunnable != null) { + bootHandler.removeCallbacks(existingRunnable); + Log.d(TAG, "Cancelled existing delayed boot rules"); + } + delayedBootRulesScheduled.set(false); + } + + /** + * Mark boot process as complete + */ + private static void markBootComplete() { + Log.i(TAG, "Boot rule application process completed"); + isBootInProgress.set(false); + delayedBootRulesScheduled.set(false); + delayedBootRunnable.set(null); + } + + /** + * Handle network connectivity changes during boot + * Returns true if the network change should be processed, false if it should be ignored + */ + public static boolean shouldProcessNetworkChange(Context context, String reason) { + if (!isBootInProgress.get()) { + // Boot process is complete, allow normal network change processing + return true; + } + + // During boot process, be more selective about network changes + if (!initialBootRulesApplied.get()) { + // Initial boot rules haven't been applied yet, ignore network changes + Log.d(TAG, "Ignoring network change (" + reason + ") - initial boot rules not yet applied"); + return false; + } + + // If we have a delayed boot rule scheduled, we should be careful about network changes + if (delayedBootRulesScheduled.get()) { + Log.d(TAG, "Network change during boot delay period (" + reason + ") - allowing limited processing"); + // Allow processing but don't trigger a full rule reapplication + // The delayed boot rules will handle the final state + return false; + } + + // Boot rules applied but no delay configured, allow network change processing + return true; + } + + /** + * Force completion of boot rule application (for manual testing or emergency) + */ + public static void forceCompleteBootRuleApplication() { + synchronized (ruleApplicationLock) { + Log.i(TAG, "Forcing completion of boot rule application"); + cancelDelayedBootRules(); + markBootComplete(); + } + } + + /** + * Check if boot rule application is currently in progress + */ + public static boolean isBootInProgress() { + return isBootInProgress.get(); + } + + /** + * Get current boot state for debugging + */ + public static String getBootState() { + return String.format("Boot in progress: %s, Initial rules applied: %s, Delayed rules scheduled: %s", + isBootInProgress.get(), + initialBootRulesApplied.get(), + delayedBootRulesScheduled.get()); + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/CustomRuleOld.java b/app/src/main/java/dev/ukanth/ufirewall/util/CustomRuleOld.java new file mode 100644 index 0000000..31fcd22 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/CustomRuleOld.java @@ -0,0 +1,93 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Context; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.log.Log; + +/** + * Created by ukanth on 22/11/16. + */ + +public class CustomRuleOld { + + private static String loadAssetsFile(Context ctx, String inFile) { + String tContents = ""; + try { + InputStream stream = ctx.getAssets().open(inFile); + int size = stream.available(); + byte[] buffer = new byte[size]; + stream.read(buffer); + stream.close(); + tContents = new String(buffer); + } catch (IOException e) { + return null; + } + return tContents; + } + + public static List getRules(Context context) { + String jsonData = loadAssetsFile(context, "rules.json"); + List listRule = new ArrayList<>(); + try { + if (jsonData != null) { + JSONObject jsonObject = new JSONObject(jsonData); + JSONArray array = (JSONArray) jsonObject.get("rules"); + if (array != null) { + for (int i = 0; i < array.length(); i++) { + JSONObject row = array.getJSONObject(i); + Rule rule = new Rule(); + rule.setName(row.getString("name")); + rule.setDesc(row.getString("desc")); + JSONObject v4Obj = row.getJSONObject("v4"); + JSONObject v6Obj = row.getJSONObject("v6"); + List listv4On = new ArrayList<>(); + List listv4Off = new ArrayList<>(); + + List listv6On = new ArrayList<>(); + List listv6Off = new ArrayList<>(); + + for (int item = 0; item < v4Obj.getJSONArray("on").length(); item++) { + listv4On.add(v4Obj.getJSONArray("on").getString(item)); + } + for (int item = 0; item < v4Obj.getJSONArray("off").length(); item++) { + listv4Off.add(v4Obj.getJSONArray("off").getString(item)); + } + + rule.setIpv4On(listv4On); + rule.setIpv4Off(listv4Off); + + for (int item = 0; item < v6Obj.getJSONArray("on").length(); item++) { + listv6On.add(v6Obj.getJSONArray("on").getString(item)); + } + for (int item = 0; item < v6Obj.getJSONArray("off").length(); item++) { + listv6Off.add(v6Obj.getJSONArray("off").getString(item)); + } + + rule.setIpv6On(listv6On); + rule.setIpv6Off(listv6Off); + + listRule.add(rule); + } + } + } + } catch (JSONException e) { + Log.i(Api.TAG, "Exception in parsing json" + e.getMessage()); + } + return listRule; + } + + public static int getRulesSize(Context context) { + return getRules(context).size(); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/DataUsageParser.java b/app/src/main/java/dev/ukanth/ufirewall/util/DataUsageParser.java new file mode 100644 index 0000000..0884779 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/DataUsageParser.java @@ -0,0 +1,280 @@ +package dev.ukanth.ufirewall.util; + +import com.topjohnwu.superuser.Shell; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import dev.ukanth.ufirewall.log.Log; + +/** + * Utility class to parse network data usage from /proc/net/xt_qtaguid + * Provides WiFi/Mobile separation and caching for performance + */ +public class DataUsageParser { + private static final String TAG = "DataUsageParser"; + + // Cache for data usage stats to avoid frequent root shell calls + private static final Map dataUsageCache = new ConcurrentHashMap<>(); + private static long lastCacheUpdate = 0; + private static final long CACHE_VALIDITY_MS = 30000; // 30 seconds cache + + public static class DataUsageStats { + public long wifiRxBytes = 0; + public long wifiTxBytes = 0; + public long mobileRxBytes = 0; + public long mobileTxBytes = 0; + public long totalRxBytes = 0; + public long totalTxBytes = 0; + + public long getTotalWifiBytes() { + return wifiRxBytes + wifiTxBytes; + } + + public long getTotalMobileBytes() { + return mobileRxBytes + mobileTxBytes; + } + + public long getTotalBytes() { + return totalRxBytes + totalTxBytes; + } + } + + /** + * Get data usage statistics for a specific UID + * @param uid The application UID + * @return DataUsageStats object with WiFi/Mobile breakdown + */ + public static DataUsageStats getDataUsageForUID(int uid) { + // Check cache first + if (isCacheValid() && dataUsageCache.containsKey(uid)) { + return dataUsageCache.get(uid); + } + + // Refresh cache if needed + if (!isCacheValid()) { + refreshDataUsageCache(); + } + + DataUsageStats stats = dataUsageCache.get(uid); + return stats != null ? stats : new DataUsageStats(); + } + + /** + * Check if the cache is still valid + */ + private static boolean isCacheValid() { + return (System.currentTimeMillis() - lastCacheUpdate) < CACHE_VALIDITY_MS; + } + + /** + * Refresh the entire data usage cache by parsing /proc/net/xt_qtaguid + */ + private static void refreshDataUsageCache() { + try { + dataUsageCache.clear(); + + // Try different possible locations for xt_qtaguid + String[] possiblePaths = { + "/proc/net/xt_qtaguid/stats", + "/proc/net/xt_qtaguid/ctrl", + "/proc/net/xt_qtaguid", + "/sys/kernel/debug/xt_qtaguid/stats" + }; + + String qtaguidData = null; + for (String path : possiblePaths) { + Shell.Result result = Shell.cmd("test -f " + path + " && cat " + path).exec(); + if (result.isSuccess() && !result.getOut().isEmpty()) { + qtaguidData = String.join("\n", result.getOut()); + Log.d(TAG, "Found xt_qtaguid data at: " + path); + break; + } + } + + if (qtaguidData != null) { + parseQtaguidData(qtaguidData); + } else { + // Fallback to TrafficStats if xt_qtaguid is not available + Log.w(TAG, "xt_qtaguid not available, using fallback method"); + useFallbackMethod(); + } + + lastCacheUpdate = System.currentTimeMillis(); + + } catch (Exception e) { + Log.e(TAG, "Error refreshing data usage cache", e); + useFallbackMethod(); + } + } + + /** + * Parse the xt_qtaguid data format + * Format: idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets tx_tcp_packets rx_udp_bytes rx_udp_packets tx_udp_bytes tx_udp_packets rx_other_bytes rx_other_packets tx_other_bytes tx_other_packets + */ + private static void parseQtaguidData(String qtaguidData) { + String[] lines = qtaguidData.split("\n"); + + for (String line : lines) { + if (line.trim().isEmpty() || line.startsWith("idx")) { + continue; // Skip empty lines and header + } + + String[] parts = line.trim().split("\\s+"); + if (parts.length < 8) { + continue; // Skip malformed lines + } + + try { + // Parse fields based on xt_qtaguid format + String iface = parts[1]; + String uidTagStr = parts[3]; + long rxBytes = Long.parseLong(parts[5]); + long txBytes = Long.parseLong(parts[7]); + + // Extract UID from uid_tag_int (format: uid << 32 | tag) + long uidTag = Long.parseLong(uidTagStr); + int uid = (int) (uidTag >> 32); + + if (uid <= 0) { + continue; // Skip invalid UIDs + } + + // Get or create stats for this UID + DataUsageStats stats = dataUsageCache.get(uid); + if (stats == null) { + stats = new DataUsageStats(); + dataUsageCache.put(uid, stats); + } + + // Classify interface type and accumulate data + if (isWiFiInterface(iface)) { + stats.wifiRxBytes += rxBytes; + stats.wifiTxBytes += txBytes; + } else if (isMobileInterface(iface)) { + stats.mobileRxBytes += rxBytes; + stats.mobileTxBytes += txBytes; + } + + // Update totals + stats.totalRxBytes += rxBytes; + stats.totalTxBytes += txBytes; + + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse line: " + line); + } + } + + Log.d(TAG, "Parsed data usage for " + dataUsageCache.size() + " UIDs"); + } + + /** + * Check if interface is WiFi-related + */ + private static boolean isWiFiInterface(String iface) { + return iface != null && ( + iface.startsWith("wlan") || + iface.startsWith("wifi") || + iface.equals("wl0") || + iface.equals("eth0") // Sometimes WiFi appears as eth0 + ); + } + + /** + * Check if interface is mobile data-related + */ + private static boolean isMobileInterface(String iface) { + return iface != null && ( + iface.startsWith("rmnet") || + iface.startsWith("ccmni") || + iface.startsWith("pdp") || + iface.startsWith("ppp") || + iface.startsWith("mobile") || + iface.startsWith("radio") || + iface.matches("rmnet\\d+") || + iface.matches("rmnet_data\\d+") + ); + } + + /** + * Fallback method when xt_qtaguid is not available + * Uses /proc/uid_stat as AFWall+ already does + */ + private static void useFallbackMethod() { + // This provides total data only, no WiFi/Mobile separation + Shell.Result result = Shell.cmd("find /proc/uid_stat -name '[0-9]*' -type d 2>/dev/null").exec(); + + if (result.isSuccess()) { + for (String uidDir : result.getOut()) { + try { + String uidStr = uidDir.substring(uidDir.lastIndexOf('/') + 1); + int uid = Integer.parseInt(uidStr); + + // Read rx and tx bytes + Shell.Result rxResult = Shell.cmd("cat " + uidDir + "/tcp_rcv 2>/dev/null || echo 0").exec(); + Shell.Result txResult = Shell.cmd("cat " + uidDir + "/tcp_snd 2>/dev/null || echo 0").exec(); + + if (rxResult.isSuccess() && txResult.isSuccess()) { + long rxBytes = Long.parseLong(rxResult.getOut().get(0).trim()); + long txBytes = Long.parseLong(txResult.getOut().get(0).trim()); + + DataUsageStats stats = new DataUsageStats(); + stats.totalRxBytes = rxBytes; + stats.totalTxBytes = txBytes; + // Cannot separate WiFi/Mobile in fallback mode + + dataUsageCache.put(uid, stats); + } + + } catch (NumberFormatException e) { + Log.w(TAG, "Failed to parse UID directory: " + uidDir); + } + } + } + + Log.d(TAG, "Fallback method parsed " + dataUsageCache.size() + " UIDs"); + } + + /** + * Clear the cache to force refresh on next request + */ + public static void clearCache() { + dataUsageCache.clear(); + lastCacheUpdate = 0; + } + + /** + * Get human readable data usage string + */ + public static String formatDataUsage(long bytes) { + if (bytes < 0) return "0 B"; + if (bytes < 1024) return bytes + " B"; + + int unit = 1024; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = "KMGTPE".charAt(exp - 1) + ""; + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + /** + * Format WiFi/Mobile breakdown for display + */ + public static String formatWifiMobileUsage(DataUsageStats stats) { + if (stats.getTotalWifiBytes() == 0 && stats.getTotalMobileBytes() == 0) { + return "No data usage"; + } + + StringBuilder sb = new StringBuilder(); + if (stats.getTotalWifiBytes() > 0) { + sb.append("📶 ").append(formatDataUsage(stats.getTotalWifiBytes())); + } + if (stats.getTotalMobileBytes() > 0) { + if (sb.length() > 0) sb.append(" | "); + sb.append("📱 ").append(formatDataUsage(stats.getTotalMobileBytes())); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/DateComparator.java b/app/src/main/java/dev/ukanth/ufirewall/util/DateComparator.java new file mode 100644 index 0000000..4ad92b9 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/DateComparator.java @@ -0,0 +1,18 @@ +package dev.ukanth.ufirewall.util; + +import java.util.Comparator; + +import dev.ukanth.ufirewall.log.LogData; + +/** + * Created by ukanth on 27/7/16. + */ +public class DateComparator implements Comparator{ + + @Override + public int compare(LogData o1, LogData o2) { + Long o1_date = o1.getTimestamp(); + Long o2_date = o2.getTimestamp(); + return (o1_date > o2_date) ? -1: (o1_date < o2_date) ? 0 : 1; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/FileDialog.java b/app/src/main/java/dev/ukanth/ufirewall/util/FileDialog.java new file mode 100644 index 0000000..af60065 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/FileDialog.java @@ -0,0 +1,284 @@ +package dev.ukanth.ufirewall.util; + +import android.app.Activity; +import android.app.Dialog; +import android.os.Environment; +import android.util.Log; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.topjohnwu.superuser.Shell; + +import dev.ukanth.ufirewall.R; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by ukanth on 18/7/15. + */ +public class FileDialog { + private static final String PARENT_DIR = ".."; + private final String TAG = getClass().getName(); + public void setFlag(boolean flag) { + this.flag = flag; + } + + private boolean flag; + private String[] fileList; + private File currentPath; + + public interface FileSelectedListener { + void fileSelected(File file); + } + public interface DirectorySelectedListener { + void directorySelected(File directory); + } + private final ListenerList fileListenerList = new ListenerList<>(); + private final ListenerList dirListenerList = new ListenerList<>(); + private final Activity activity; + private boolean selectDirectoryOption; + //private String[] fileEndsWith; + + /** + * @param activity + * @param path + * + */ + public FileDialog(Activity activity, File path, boolean flag) { + this.activity = activity; + if (!path.exists()) path = Environment.getExternalStorageDirectory(); + setFlag(flag); + loadFileList(path,flag); + } + + /** + * @return file dialog + */ + public Dialog createFileDialog() { + Dialog dialog = null; + + //MaterialDialog.Builder + MaterialDialog.Builder builder; + try { + builder = new MaterialDialog.Builder(activity); + } catch (Exception e) { + android.util.Log.e(TAG, "MaterialDialog.Builder failed due to Android compatibility issue", e); + // Return null to indicate dialog creation failed + return null; + } + + builder.title(currentPath.getPath()); + if (selectDirectoryOption) { + builder.positiveText(R.string.select_dir); + builder.negativeText(R.string.Cancel); + builder.onPositive((dialog12, which) -> fireDirectorySelectedEvent(currentPath)); + } + + builder.items(fileList); + builder.itemsCallback((dialog1, view, which, text) -> { + try { + String fileChosen = fileList[which]; + File chosenFile = getChosenFile(fileChosen); + if (chosenFile != null && chosenFile.exists() && chosenFile.isDirectory()) { + loadFileList(chosenFile,flag); + dialog1.cancel(); + dialog1.dismiss(); + showDialog(); + } else if (chosenFile != null && chosenFile.exists()) { + fireFileSelectedEvent(chosenFile); + } + } catch (Exception e) { + Log.e(TAG, "Error in file selection callback", e); + } + }); + + try { + dialog = builder.show(); + } catch (Exception e) { + android.util.Log.e(TAG, "MaterialDialog.show() failed due to Android compatibility issue", e); + return null; + } + return dialog; + } + + + public void addFileListener(FileSelectedListener listener) { + fileListenerList.add(listener); + } + + public void removeFileListener(FileSelectedListener listener) { + fileListenerList.remove(listener); + } + + public void setSelectDirectoryOption(boolean selectDirectoryOption) { + this.selectDirectoryOption = selectDirectoryOption; + } + + public void addDirectoryListener(DirectorySelectedListener listener) { + dirListenerList.add(listener); + } + + public void removeDirectoryListener(DirectorySelectedListener listener) { + dirListenerList.remove(listener); + } + + /** + * Show file dialog + */ + public void showDialog() { + Dialog dialog = createFileDialog(); + if (dialog != null) { + dialog.show(); + } else { + android.util.Log.e(TAG, "Cannot show file dialog due to MaterialDialog compatibility issue"); + // Could implement alternative file picker here if needed + } + } + + private void fireFileSelectedEvent(final File file) { + fileListenerList.fireEvent(listener -> listener.fileSelected(file)); + } + + private void fireDirectorySelectedEvent(final File directory) { + dirListenerList.fireEvent(listener -> listener.directorySelected(directory)); + } + + private void loadFileList(File path,final boolean flag) { + this.currentPath = path; + List r = new ArrayList(); + if (path.exists()) { + if (path.getParentFile() != null) r.add(PARENT_DIR); + FilenameFilter filter = (dir, filename) -> { + File sel = new File(dir, filename); + if (!sel.canRead()) return false; + if (selectDirectoryOption) return sel.isDirectory(); + //backup.json - [a-z]+.json + else { + boolean endsWith; + if(flag) { + Pattern p1 = Pattern.compile("[a-z]+.json"); + Matcher m1 = p1.matcher(filename); + + Pattern p2 = Pattern.compile("[a-z]+-[a-z]+-\\d+-\\S*"); + Matcher m2 = p2.matcher(filename); + endsWith = m2.matches() || m1.matches(); + } else { + Pattern p1 = Pattern.compile("[a-z]+_[a-z]+.json"); + Matcher m1 = p1.matcher(filename); + + Pattern p2 = Pattern.compile("[a-z]+-[a-z]+-[a-z]+-\\d+-\\S*"); + Matcher m2 = p2.matcher(filename); + endsWith = m2.matches() || m1.matches(); + } + return endsWith || sel.isDirectory(); + } + }; + String[] fileList1 = path.list(filter); + if(fileList1 != null) { + r.addAll(Arrays.asList(fileList1)); + } + } + //copied ones from old afwall + File[] listFilesInDir = currentPath.listFiles(); + if(listFilesInDir !=null && listFilesInDir.length > 0){ + for(File files: listFilesInDir) { + String name = files.getName(); + boolean endsWith; + if(flag) { + Pattern p1 = Pattern.compile("[a-z]+.json"); + Matcher m1 = p1.matcher(name); + + Pattern p2 = Pattern.compile("[a-z]+-[a-z]+-\\d+-\\S*"); + Matcher m2 = p2.matcher(name); + endsWith = m2.matches() || m1.matches(); + } else { + Pattern p1 = Pattern.compile("[a-z]+_[a-z]+.json"); + Matcher m1 = p1.matcher(name); + + Pattern p2 = Pattern.compile("[a-z]+-[a-z]+-[a-z]+-\\d+-\\S*"); + Matcher m2 = p2.matcher(name); + endsWith = m2.matches() || m1.matches(); + } + if (!r.contains(files) && endsWith) { + r.add(name); + } + } + } + + + /*Shell.Result result = com.topjohnwu.superuser.Shell.cmd("ls " + currentPath).exec(); + List out = result.getOut(); + for(String files: out) { + boolean endsWith; + if(flag) { + Pattern p1 = Pattern.compile("[a-z]+.json"); + Matcher m1 = p1.matcher(files); + + Pattern p2 = Pattern.compile("[a-z]+-[a-z]+-\\d+-\\S*"); + Matcher m2 = p2.matcher(files); + endsWith = m2.matches() || m1.matches(); + } else { + Pattern p1 = Pattern.compile("[a-z]+_[a-z]+.json"); + Matcher m1 = p1.matcher(files); + + Pattern p2 = Pattern.compile("[a-z]+-[a-z]+-[a-z]+-\\d+-\\S*"); + Matcher m2 = p2.matcher(files); + endsWith = m2.matches() || m1.matches(); + } + if (!r.contains(files) && endsWith) { + r.add(files); + } + }*/ + + if(r != null && r.size() > 0) { + fileList = r.toArray(new String[]{}); + } + } + + private File getChosenFile(String fileChosen) { + if (currentPath == null) { + return null; + } + if (fileChosen.equals(PARENT_DIR)) { + return currentPath.getParentFile(); // Can return null if at root + } else { + return new File(currentPath, fileChosen); + } + } + + /*public void setFileEndsWith(String[] fileEndsWith,String notContains) { + this.fileEndsWith = fileEndsWith != null ? fileEndsWith : new String[]{ "" }; + }*/ +} + +class ListenerList { + private final List listenerList = new ArrayList(); + + public interface FireHandler { + void fireEvent(L listener); + } + + public void add(L listener) { + listenerList.add(listener); + } + + public void fireEvent(FireHandler fireHandler) { + List copy = new ArrayList(listenerList); + for (L l : copy) { + fireHandler.fireEvent(l); + } + } + + public void remove(L listener) { + listenerList.remove(listener); + } + + public List getListenerList() { + return listenerList; + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java b/app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java new file mode 100644 index 0000000..7b3117b --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/FingerprintUtil.java @@ -0,0 +1,397 @@ +package dev.ukanth.ufirewall.util; + +/** + * This file was created to simplify Fingerprint APIs and made specifically for (AFWall+) application. + * You are free to re-distributed this file anywhere you like. :) + * ---------------------------------------------- + * Created by vzool on 1/20/17. + */ + +import static android.content.Context.FINGERPRINT_SERVICE; +import static android.content.Context.KEYGUARD_SERVICE; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Dialog; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Point; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import android.view.WindowManager; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.ProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; + +public class FingerprintUtil { + + final static String TAG = "AfWall-FingerprintUtil"; + + // generate key based on pkg name + public static String GetKey(Context context){ + return Api.getCurrentPackage(context) + ":Fingerprint"; + } + + // safely check if device support fingerprint + public static boolean isAndroidSupport(){ + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; + } + + /* + * Dialog + **/ + public static class FingerprintDialog extends Dialog { + + private KeyStore keyStore; + // Variable used for storing the key in the Android Keystore container + private static String KEY_NAME = "Key will be determined at onCreate Event"; + private Cipher cipher; + TextView errorText; + + boolean isNotFirstWindowFocus = false; + + KeyguardManager keyguardManager; + FingerprintManager fingerprintManager; + + FingerprintHandler helper; + + FingerprintManager.CryptoObject cryptoObject; + + // callbacks + OnFingerprintFailure failureCallback; + OnFingerprintSuccess successCallback; + + @RequiresApi(api = Build.VERSION_CODES.M) + public FingerprintDialog(Context context) { + super(context); + + // Initializing both Android Keyguard Manager and Fingerprint Manager + keyguardManager = (KeyguardManager) getContext().getSystemService(KEYGUARD_SERVICE); + fingerprintManager = (FingerprintManager) getContext().getSystemService(FINGERPRINT_SERVICE); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setCancelable(false); + setContentView(R.layout.fingerprint); + setTitle(R.string.fingerprint_required); + + errorText = findViewById(R.id.fingerprintErrorText); + + // choose key that depends on [pkg_name]:Fingerprint + KEY_NAME = GetKey(getContext()); + + setFullscreenDialog(); + } + + // Full screen + void setFullscreenDialog(){ + + try{ + + WindowManager manager = (WindowManager) getContext().getSystemService(Activity.WINDOW_SERVICE); + + Point point = new Point(); + manager.getDefaultDisplay().getSize(point); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + + lp.copyFrom(getWindow().getAttributes()); + lp.width = point.x; + lp.height = point.y; + getWindow().setAttributes(lp); + + }catch (NullPointerException ex){ + + Log.e(TAG, ex.getMessage()); + } + } + + @Override + protected void onStart() { + super.onStart(); + + startReadFingerTip(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + + if(failureCallback != null){ + + failureCallback.then(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + /* Focus on current window is required and critical, + * if focus interrupted by "Home Button" or "SuperSU Dialog" + */ + + if(hasFocus){ + + if(isNotFirstWindowFocus){ + + startReadFingerTip(); + } + + isNotFirstWindowFocus = true; + + }else{ + + stopReadFingerTip(); + } + } + + public void setOnFingerprintFailureListener(OnFingerprintFailure mayHappen){ + failureCallback = mayHappen; + } + + public void setOnFingerprintSuccess(OnFingerprintSuccess doSomething){ + successCallback = doSomething; + } + + /** + * Created by whit3hawks on 11/16/16. + * Modified by vzool on 1/14/17. + */ + @TargetApi(Build.VERSION_CODES.M) + void startReadFingerTip(){ + + // Check whether the device has a Fingerprint sensor. + if(!fingerprintManager.isHardwareDetected()){ + /** + * This block will not be touched unless weird things happened, + * because we already checked if device support fingerprint before enable it. + * We just leave it as-is for the days, who knows! :) + */ + errorText.setText(R.string.device_with_no_fingerprint_sensor); + }else { + // Checks whether fingerprint permission is set on manifest + if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { + errorText.setText(R.string.fingerprint_permission_manifest_missing); + }else{ + // Check whether at least one fingerprint is registered + if (!fingerprintManager.hasEnrolledFingerprints()) { + errorText.setText(R.string.register_at_least_one_fingerprint); + }else{ + // Checks whether lock screen security is enabled or not + if (!keyguardManager.isKeyguardSecure()) { + errorText.setText(R.string.lock_screen_not_enabled); + }else{ + + generateKey(); + + if (cipherInit()) { + + cryptoObject = new FingerprintManager.CryptoObject(cipher); + + if(helper == null){ + + helper = new FingerprintHandler(); + } + + helper.startAuth(fingerprintManager, cryptoObject); + } + } + } + } + } + } + + @TargetApi(Build.VERSION_CODES.M) + void stopReadFingerTip(){ + + if(helper != null){ + + helper.stopAuth(); + } + } + + private void triggerSuccess(){ + if(successCallback != null){ + successCallback.then(); + } + if(isShowing()) { + dismiss(); + } + } + + @TargetApi(Build.VERSION_CODES.M) + private void generateKey() { + try { + keyStore = KeyStore.getInstance("AndroidKeyStore"); + } catch (Exception e) { + Log.e(TAG, "Error(0): " + e.getMessage()); + } + + + KeyGenerator keyGenerator; + try { + keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException("Failed to get KeyGenerator instance", e); + } + + + try { + keyStore.load(null); + keyGenerator.init(new + KeyGenParameterSpec.Builder(KEY_NAME, + KeyProperties.PURPOSE_ENCRYPT | + KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setUserAuthenticationRequired(true) + .setEncryptionPaddings( + KeyProperties.ENCRYPTION_PADDING_PKCS7) + .build()); + keyGenerator.generateKey(); + } catch (NoSuchAlgorithmException | + InvalidAlgorithmParameterException + | CertificateException | ProviderException | IOException e) { + Log.e(TAG, "Error(1): " + e.getMessage()); + } + } + + @TargetApi(Build.VERSION_CODES.M) + private boolean cipherInit() { + try { + cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new RuntimeException("Failed to get Cipher", e); + } + + + try { + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, + null); + cipher.init(Cipher.ENCRYPT_MODE, key); + return true; + } catch (KeyPermanentlyInvalidatedException e) { + return false; + } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("Failed to init Cipher", e); + } + } + + /** + * Created by whit3hawks on 11/16/16. + * Modified by vzool on 1/14/17. + */ + @RequiresApi(api = Build.VERSION_CODES.M) + private class FingerprintHandler extends FingerprintManager.AuthenticationCallback { + + CancellationSignal cancellationSignal; + + FingerprintHandler(){ + cancellationSignal = new CancellationSignal(); + } + + private void startAuth(FingerprintManager manager, FingerprintManager.CryptoObject cryptoObject) { + + if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { + return; + } + manager.authenticate(cryptoObject, cancellationSignal, 0, this, null); + } + + private void stopAuth(){ + + if(cancellationSignal != null){ + + cancellationSignal.cancel(); + + cancellationSignal = null; + } + } + + + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + + // First attempts always fail due to interruption by SuperSU dialog. + // So, this view should be first responder. + // We ignore any errors and repeat process again :) + // + // this.update(getContext().getString(R.string.fingerprint_authentication_error) + errString, false); + + startReadFingerTip(); + } + + + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + this.update(getContext().getString(R.string.fingerprint_authentication_help) + helpString, false); + } + + + @Override + public void onAuthenticationFailed() { + this.update(getContext().getString(R.string.fingerprint_authentication_failed), false); + } + + + @Override + public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { + this.update(getContext().getString(R.string.fingerprint_authentication_successded), true); + } + + + public void update(String e, Boolean success){ + + errorText.setText(e); + + if(success){ + errorText.setTextColor(getContext().getColor(R.color.dark_bg)); + triggerSuccess(); + } + } + } + } + + // interface for callback on failure + public interface OnFingerprintFailure{ + void then(); + } + + // interface for callback on Success + public interface OnFingerprintSuccess{ + void then(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/G.java b/app/src/main/java/dev/ukanth/ufirewall/util/G.java new file mode 100644 index 0000000..5189dba --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/G.java @@ -0,0 +1,1283 @@ +/** + * A place to store globals + *

+ * Copyright (C) 2013 Kevin Cernekee + * Copyright (C) 2016 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Kevin Cernekee + * @version 1.0 + */ + +package dev.ukanth.ufirewall.util; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.graphics.Color; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkRequest; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +import com.raizlabs.android.dbflow.config.FlowConfig; +import com.raizlabs.android.dbflow.config.FlowManager; +import com.raizlabs.android.dbflow.sql.language.SQLite; + +import dev.ukanth.ufirewall.MainActivity; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.BuildConfig; +import dev.ukanth.ufirewall.InterfaceTracker; +import dev.ukanth.ufirewall.MainActivity; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.log.LogPreference; +import dev.ukanth.ufirewall.log.LogPreferenceDB; +import dev.ukanth.ufirewall.log.LogPreference_Table; +import dev.ukanth.ufirewall.preferences.DefaultConnectionPref; +import dev.ukanth.ufirewall.preferences.DefaultConnectionPrefDB; + +public class G extends Application implements Application.ActivityLifecycleCallbacks{ + + private static G instance; + + private static boolean enabledPrivateLink = false; + + private static boolean isActivityVisible; + + private static Thread.UncaughtExceptionHandler defaultExceptionHandler; + + static { + com.topjohnwu.superuser.Shell.setDefaultBuilder(com.topjohnwu.superuser.Shell.Builder.create() + .setFlags(com.topjohnwu.superuser.Shell.FLAG_REDIRECT_STDERR) + .setTimeout(30) // 30 second timeout for shell operations + ); + } + + public static G getInstance() { + return instance; + } + + public static Context getContext() { + return instance; + } + + public static final String TAG = "AFWall"; + + private static final String HAS_ROOT = "hasRoot"; + private static final String FIX_START_LEAK = "fixLeak"; + private static final String DISABLE_TASKER_TOAST = "disableTaskerToast"; + private static final String REG_DO = "ipurchaseddonatekey"; + private static final String ENABLE_ROAM = "enableRoam"; + private static final String ENABLE_VPN = "enableVPN"; + private static final String ENABLE_TETHER = "enableTether"; + private static final String ENABLE_LAN = "enableLAN"; + private static final String ENABLE_TOR = "enableTor"; + private static final String ENABLE_IPV6 = "enableIPv6"; + private static final String CONTROL_IPV6 = "controlIPv6"; + private static final String SELECTED_FILTER = "selectedFilter"; + //private static final String BLOCK_IPV6 = "blockIPv6"; + private static final String ENABLE_INBOUND = "enableInbound"; + private static final String ENABLE_LOG_SERVICE = "enableLogService"; + private static final String LOG_PING_TIMEOUT = "logPingTime"; + private static final String ENABLE_ADMIN = "enableAdmin"; + private static final String DUAL_APPS = "supportDualApps"; + private static final String ENABLE_DEVICE_CHECK = "enableDeviceCheck"; + private static final String ENABLE_CONFIRM = "enableConfirm"; + private static final String ENABLE_MULTI_PROFILE = "enableMultiProfile"; + private static final String SHOW_UID = "showUid"; + private static final String NOTIFY_INSTALL = "notifyAppInstall"; + private static final String DISABLE_ICONS = "disableIcons"; + private static final String IPTABLES_PATH = "ipt_path"; + private static final String IPTABLES_BUILTIN_FAILED = "ipt_builtin_failed"; + private static final String PROTECTION_OPTION = "passSetting"; + private static final String BUSYBOX_PATH = "bb_path"; + private static final String TOAST_POS = "toast_pos"; + private static final String LANGUAGE = "locale"; + //private static final String LOG_DMESG = "logDmesg"; + private static final String SORT_BY = "sort"; + private static final String LAST_STORED_PROFILE = "storedProfile"; + private static final String STARTUP_DELAY = "addDelayStart"; + private static final String SYSTEM_APP_COLOR = "sysColor"; + + private static final String PRIMARY_COLOR = "primaryColor"; + private static final String PRIMARY_DARK_COLOR = "primaryColor"; + + private static final String ACTIVE_RULES = "activeRules"; + private static final String ADD_DELAY = "addDelay"; + + private static final String ACTIVE_NOTIFICATION = "activeNotification"; + private static final String PROFILE_SWITCH = "applyOnSwitchProfiles"; + private static final String LOG_TARGET = "logTarget"; + private static final String LOG_TARGETS = "logTargets"; + private static final String SHOW_HOST = "showHostName"; + private static final String APP_VERSION = "appVersion"; + private static final String MULTI_USER = "multiUser"; + private static final String MULTI_USER_ID = "multiUserId"; + private static final String IS_MIGRATED = "isMigrated"; + private static final String SHOW_FILTER = "showFilter"; + private static final String PATTERN_MAX_TRY = "patternMax"; + private static final String PATTERN_STEALTH = "stealthMode"; + private static final String ISKINGDETECT = "kingDetect"; + private static final String PWD_ENCRYPT = "pwdEncrypt"; + private static final String PROFILE_PWD = "profilePwd"; + private static final String FINGERPRINT_ENABLED = "fingerprintEnabled"; + private static final String CUSTOM_DELAY_SECONDS = "customDelay"; + private static final String NOTIFICATION_PRIORITY = "notification_priority"; + private static final String RUN_NOTIFICATION = "runNotification"; + private static final String COPIED_OLD_EXPORTS = "copyOldExports"; + + private static final String SHOW_ALL_APPS = "showAllApps"; + + private static final String THEME = "theme"; + private static final String FASTER_RULES = "fasterApplyRules"; + + private static boolean privateDns = false; + //private static final String QUICK_RULES = "quickApply"; + /** + * FIXME + **/ + private static final String AFWALL_STATUS = "AFWallStatus"; + //private static final String BLOCKED_NOTIFICATION = "block_filter_app"; + /* Profiles */ + private static final String ADDITIONAL_PROFILES = "plusprofiles"; + //private static final String PROFILES = "profiles_json"; + private static final String PROFILES_MIGRATED = "profilesmigrated"; + private static final String WIDGET_X = "widgetX"; + private static final String WIDGET_Y = "widgetY"; + //private static final String XPOSED_FIX_DM_LEAK = "fixDownloadManagerLeak"; + + //ippreference + private static final String IP4_INPUT = "input_chain"; + private static final String IP4_OUTPUT = "output_chain"; + private static final String IP4_FWD = "forward_chain"; + + private static final String IP6_INPUT = "input_chain_v6"; + private static final String IP6_OUTPUT = "output_chain_v6"; + private static final String IP6_FWD = "forward_chain_v6"; + + private static final String INITPATH = "initPath"; + + private static final String AFWALL_PROFILE = "AFWallProfile"; + public static String[] profiles = {"AFWallPrefs", AFWALL_PROFILE + 1, AFWALL_PROFILE + 2, AFWALL_PROFILE + 3}; + public static String[] default_profiles = {"AFWallProfile1", "AFWallProfile2", "AFWallProfile3"}; + public static Context ctx; + public static SharedPreferences gPrefs; + public static SharedPreferences pPrefs; + public static SharedPreferences sPrefs; + + public static Set storedPid() { + return gPrefs.getStringSet("storedPid", null); + } + + public static void storedPid(Set store) { + gPrefs.edit().putStringSet("storedPid", store).commit(); + } + + public static boolean supportDual() { + return gPrefs.getBoolean(DUAL_APPS, false); + } + + public static boolean supportDual(boolean val) { + gPrefs.edit().putBoolean(DUAL_APPS, val).commit(); + return val; + } + + + public static boolean isFaster() { + return gPrefs.getBoolean(FASTER_RULES, false); + } + + public static boolean isFaster(boolean val) { + gPrefs.edit().putBoolean(FASTER_RULES, val).commit(); + return val; + } + + + public static boolean isRun() { + return gPrefs.getBoolean(RUN_NOTIFICATION, true); + } + + public static boolean isRun(boolean val) { + gPrefs.edit().putBoolean(RUN_NOTIFICATION, val).commit(); + return val; + } + + + public static boolean hasCopyOld() { + return gPrefs.getBoolean(COPIED_OLD_EXPORTS, false); + } + + public static boolean hasCopyOldExports(boolean val) { + gPrefs.edit().putBoolean(COPIED_OLD_EXPORTS, val).commit(); + return val; + } + + + public static boolean showAllApps() { + return gPrefs.getBoolean(SHOW_ALL_APPS, false); + } + + public static boolean showAllApps(boolean val) { + gPrefs.edit().putBoolean(SHOW_ALL_APPS, val).commit(); + return val; + } + + + /* public static boolean showQuickButton() { + return gPrefs.getBoolean(QUICK_RULES, false); + }*/ + + public static boolean ipv4Input() { + return gPrefs.getBoolean(IP4_INPUT, true); + } + + public static boolean ipv4Input(boolean val) { + gPrefs.edit().putBoolean(IP4_INPUT, val).commit(); + return val; + } + + public static boolean ipv4Fwd() { + return gPrefs.getBoolean(IP4_FWD, true); + } + + public static boolean ipv4Fwd(boolean val) { + gPrefs.edit().putBoolean(IP4_FWD, val).commit(); + return val; + } + + public static boolean ipv4Output() { + return gPrefs.getBoolean(IP4_OUTPUT, true); + } + + public static boolean ipv4Output(boolean val) { + gPrefs.edit().putBoolean(IP4_OUTPUT, val).commit(); + return val; + } + + public static boolean ipv6Fwd() { + return gPrefs.getBoolean(IP6_FWD, true); + } + + public static boolean ipv6Fwd(boolean val) { + gPrefs.edit().putBoolean(IP6_FWD, val).commit(); + return val; + } + + public static boolean ipv6Input() { + return gPrefs.getBoolean(IP6_INPUT, true); + } + + public static boolean ipv6Input(boolean val) { + gPrefs.edit().putBoolean(IP6_INPUT, val).commit(); + return val; + } + + public static boolean ipv6Output() { + return gPrefs.getBoolean(IP6_OUTPUT, true); + } + + public static boolean ipv6Output(boolean val) { + gPrefs.edit().putBoolean(IP6_OUTPUT, val).commit(); + return val; + } + + + public static boolean isEnc() { + return gPrefs.getBoolean(PWD_ENCRYPT, false); + } + + public static boolean isEnc(boolean val) { + gPrefs.edit().putBoolean(PWD_ENCRYPT, val).commit(); + return val; + } + + public static String initPath() { + return gPrefs.getString(INITPATH, null); + } + + public static String initPath(String val) { + gPrefs.edit().putString(INITPATH, val).commit(); + return val; + } + + + public static String getSelectedTheme() { + return gPrefs.getString(THEME, "D"); + } + + public static String getSelectedTheme(String val) { + gPrefs.edit().putString(THEME, val).commit(); + return val; + } + + public static String profile_pwd() { + return gPrefs.getString(PROFILE_PWD, ""); + } + + public static String profile_pwd(String val) { + gPrefs.edit().putString(PROFILE_PWD, val).commit(); + return val; + } + + public static int getNotificationPriority() { + return Integer.parseInt(gPrefs.getString(NOTIFICATION_PRIORITY, "0")); + } + + + public static Boolean isFingerprintEnabled() { + return gPrefs.getBoolean(FINGERPRINT_ENABLED, false); + } + + public static Boolean isFingerprintEnabled(Boolean val) { + gPrefs.edit().putBoolean(FINGERPRINT_ENABLED, val).commit(); + return val; + } + + public static boolean isProfileMigrated() { + return gPrefs.getBoolean(PROFILES_MIGRATED, false); + } + + public static boolean isProfileMigrated(boolean val) { + gPrefs.edit().putBoolean(PROFILES_MIGRATED, val).commit(); + return val; + } + + /* public static boolean isXposedDM() { + return gPrefs.getBoolean(XPOSED_FIX_DM_LEAK, false); + } + + public static boolean isXposedDM(boolean val) { + gPrefs.edit().putBoolean(XPOSED_FIX_DM_LEAK, val).commit(); + return val; + }*/ + + public static boolean hasRoot() { + return gPrefs.getBoolean(HAS_ROOT, false); + } + + public static boolean hasRoot(boolean val) { + gPrefs.edit().putBoolean(HAS_ROOT, val).commit(); + return val; + } + + public static boolean activeNotification() { + return gPrefs.getBoolean(ACTIVE_NOTIFICATION, true); + } + + public static boolean activeNotification(boolean val) { + gPrefs.edit().putBoolean(ACTIVE_NOTIFICATION, val).commit(); + return val; + } + + + public static boolean fixLeak() { + return gPrefs.getBoolean(FIX_START_LEAK, false); + } + + public static boolean disableTaskerToast() { + return gPrefs.getBoolean(DISABLE_TASKER_TOAST, false); + } + + public static boolean enableIPv6() { + return gPrefs.getBoolean(ENABLE_IPV6, true); + } + + public static boolean enableIPv6(boolean val) { + gPrefs.edit().putBoolean(ENABLE_IPV6, val).commit(); + return val; + } + + public static boolean controlIPv6() { + return gPrefs.getBoolean(CONTROL_IPV6, false); + } + + + + /* public static boolean blockIPv6() { + return gPrefs.getBoolean(BLOCK_IPV6, false); + } + + public static boolean blockIPv6(boolean val) { + gPrefs.edit().putBoolean(BLOCK_IPV6, val).commit(); + return val; + }*/ + + public static boolean enableInbound() { + return gPrefs.getBoolean(ENABLE_INBOUND, false); + } + + public static boolean enableLogService() { + return gPrefs.getBoolean(ENABLE_LOG_SERVICE, false); + } + + public static boolean enableLogService(boolean val) { + gPrefs.edit().putBoolean(ENABLE_LOG_SERVICE, val).commit(); + return val; + } + + public static int logPingTimeout() { + return Integer.valueOf(gPrefs.getString(LOG_PING_TIMEOUT, "10")); + } + + /*public static void logPingTimeout(int logPingTimeout) { + gPrefs.edit().remove(LOG_PING_TIMEOUT); + gPrefs.edit().putString(LOG_PING_TIMEOUT, logPingTimeout+""); + }*/ + + public static boolean enableAdmin() { + return gPrefs.getBoolean(ENABLE_ADMIN, false); + } + + public static boolean enableAdmin(boolean val) { + gPrefs.edit().putBoolean(ENABLE_ADMIN, val).commit(); + return val; + } + + public static boolean showHost() { + return gPrefs.getBoolean(SHOW_HOST, false); + } + + public static boolean showHost(boolean val) { + gPrefs.edit().putBoolean(SHOW_HOST, val).commit(); + return val; + } + + + public static boolean enableDeviceCheck() { + return gPrefs.getBoolean(ENABLE_DEVICE_CHECK, false); + } + + public static boolean enableDeviceCheck(boolean val) { + gPrefs.edit().putBoolean(ENABLE_DEVICE_CHECK, val).commit(); + return val; + } + + public static boolean enableConfirm() { + return gPrefs.getBoolean(ENABLE_CONFIRM, false); + } + + public static boolean enableMultiProfile() { + return gPrefs.getBoolean(ENABLE_MULTI_PROFILE, false); + } + + public static boolean enableMultiProfile(boolean val) { + gPrefs.edit().putBoolean(ENABLE_MULTI_PROFILE, val).commit(); + return val; + } + + public static boolean showUid() { + return gPrefs.getBoolean(SHOW_UID, false); + } + + public static boolean showUid(boolean val) { + gPrefs.edit().putBoolean(SHOW_UID, val).commit(); + return val; + } + + public static boolean showFilter() { + return gPrefs.getBoolean(SHOW_FILTER, false); + } + + public static boolean showFilter(boolean val) { + gPrefs.edit().putBoolean(SHOW_FILTER, val).commit(); + return val; + } + + + public static boolean kingDetected() { + return gPrefs.getBoolean(ISKINGDETECT, false); + } + + public static boolean kingDetected(boolean val) { + gPrefs.edit().putBoolean(ISKINGDETECT, val).commit(); + return val; + } + + public static boolean disableIcons() { + return gPrefs.getBoolean(DISABLE_ICONS, false); + } + + public static String ip_path() { + return gPrefs.getString(IPTABLES_PATH, "system"); + } + + public static String ip_path(String val) { + gPrefs.edit().putString(IPTABLES_PATH, val).commit(); + return val; + } + + public static boolean isBuiltinIptablesFailed() { + return gPrefs.getBoolean(IPTABLES_BUILTIN_FAILED, false); + } + + public static void setBuiltinIptablesFailed(boolean failed) { + gPrefs.edit().putBoolean(IPTABLES_BUILTIN_FAILED, failed).commit(); + } + + + public static String bb_path() { + return gPrefs.getString(BUSYBOX_PATH, "builtin"); + } + + public static String bb_path(String val) { + gPrefs.edit().putString(BUSYBOX_PATH, val).commit(); + return val; + } + + public static String toast_pos() { + return gPrefs.getString(TOAST_POS, "bottom"); + } + + public static String locale() { + return PreferenceManager.getDefaultSharedPreferences(ctx).getString(LANGUAGE, "en"); + } + + public static String locale(String val) { + gPrefs.edit().putString(LANGUAGE, val).commit(); + return val; + } + + /*public static String logDmsg() { + return gPrefs.getString(LOG_DMESG, "OS"); + } + + public static String logDmsg(String val) { + gPrefs.edit().putString(LOG_DMESG, val).commit(); + return val; + }*/ + + public static String sortBy() { + return gPrefs.getString(SORT_BY, "s0"); + } + + public static void sortBy(String sort) { + gPrefs.edit().putString(SORT_BY, sort).commit(); + } + + public static String storedProfile() { + return gPrefs.getString(LAST_STORED_PROFILE, "AFWallPrefs"); + } + + public static String storedProfile(String val) { + gPrefs.edit().putString(LAST_STORED_PROFILE, val).commit(); + return val; + } + + public static int userColor() { + if (G.getSelectedTheme().equals("L")) { + return Color.parseColor("#000000"); + } else { + return Color.parseColor("#FFFFFF"); + } + } + + public static int sysColor() { + if (G.getSelectedTheme().equals("L")) { + return gPrefs.getInt(SYSTEM_APP_COLOR, Color.parseColor("#000000")); + } else { + return gPrefs.getInt(SYSTEM_APP_COLOR, Color.parseColor("#0F9D58")); + } + } + + /*public static int primaryColor() { + return gPrefs.getInt(PRIMARY_COLOR, Color.parseColor("#259b24")); + } + + public static int primaryDarkColor() { + return gPrefs.getInt(PRIMARY_DARK_COLOR, Color.parseColor("#0a7e07")); + }*/ + + public static boolean activeRules() { + return gPrefs.getBoolean(ACTIVE_RULES, true); + } + + public static boolean addDelay() { + //default enable add delay for Q + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){ + return gPrefs.getBoolean(ADD_DELAY, true); + } + return gPrefs.getBoolean(ADD_DELAY, false); + } + + public static boolean startupDelay() { + return gPrefs.getBoolean(STARTUP_DELAY, false); + } + + public static boolean isBootProcessActive() { + return BootRuleManager.isBootInProgress(); + } + + public static boolean enableStealthPattern() { + return gPrefs.getBoolean(PATTERN_STEALTH, false); + } + + public static int getMaxPatternTry() { + return Integer.parseInt(gPrefs.getString(PATTERN_MAX_TRY, "3")); + } + + public static boolean isMultiUser() { + return gPrefs.getBoolean(MULTI_USER, false); + } + + public static void setMultiUserId(int val) { + gPrefs.edit().putLong(MULTI_USER_ID, val).commit(); + } + + public static Long getMultiUserId() { + return gPrefs.getLong(MULTI_USER_ID, 0); + } + + public static boolean applyOnSwitchProfiles() { + return gPrefs.getBoolean(PROFILE_SWITCH, false); + } + + public static String logTargets() { + return gPrefs.getString(LOG_TARGETS, null); + } + + public static String logTargets(String val) { + gPrefs.edit().putString(LOG_TARGETS, val).commit(); + return val; + } + + public static String logTarget() { + return gPrefs.getString(LOG_TARGET, "").trim(); + } + + public static String logTarget(String val) { + gPrefs.edit().putString(LOG_TARGET, val).commit(); + return val; + } + + + public static void saveSelectedFilter(int i) { + gPrefs.edit().putInt(SELECTED_FILTER, i).commit(); + } + + public static int selectedFilter() { + return gPrefs.getInt(SELECTED_FILTER, 99); + } + + + + public static int appVersion() { + return gPrefs.getInt(APP_VERSION, 0); + } + + public static int appVersion(int val) { + gPrefs.edit().putInt(APP_VERSION, val).commit(); + return val; + } + + public static boolean isMigrated() { + return gPrefs.getBoolean(IS_MIGRATED, false); + } + + public static boolean isMigrated(boolean val) { + gPrefs.edit().putBoolean(IS_MIGRATED, val).commit(); + return val; + } + + + public static int ruleTextSize() { + return gPrefs.getInt("ruleTextSize", 32); + } + + public static int ruleTextSize(int val) { + gPrefs.edit().putInt("ruleTextSize", val).commit(); + return val; + } + + public static boolean oldLogView(boolean val) { + gPrefs.edit().putBoolean("oldLogView", val).commit(); + return val; + } + + public static boolean oldLogView() { + return gPrefs.getBoolean("oldLogView", false); + } + + public static boolean isDo(boolean val) { + gPrefs.edit().putBoolean(REG_DO, val).commit(); + return val; + } + + public static boolean enableRoam() { + return gPrefs.getBoolean(ENABLE_ROAM, false); + } + + public static boolean enableRoam(boolean val) { + gPrefs.edit().putBoolean(ENABLE_ROAM, val).commit(); + return val; + } + + public static boolean enableVPN() { + return gPrefs.getBoolean(ENABLE_VPN, false); + } + + public static boolean enableVPN(boolean val) { + gPrefs.edit().putBoolean(ENABLE_VPN, val).commit(); + return val; + } + + public static boolean enableTether() { + return gPrefs.getBoolean(ENABLE_TETHER, false); + } + + public static boolean enableTether(boolean val) { + gPrefs.edit().putBoolean(ENABLE_TETHER, val).commit(); + return val; + } + + public static boolean enableLAN() { + return gPrefs.getBoolean(ENABLE_LAN, true); + } + + public static boolean enableLAN(boolean val) { + gPrefs.edit().putBoolean(ENABLE_LAN, val).commit(); + return val; + } + + public static boolean enableTor() { + return gPrefs.getBoolean(ENABLE_TOR, false); + } + + public static boolean enableTor(boolean val) { + gPrefs.edit().putBoolean(ENABLE_TOR, val).commit(); + return val; + } + + private static Boolean ownerModuleAvailable = null; + + public static boolean hasOwnerModule() { + if (ownerModuleAvailable == null) { + // Test if owner module is available by attempting a simple command + // This will be cached for the lifetime of the application + try { + String testCmd = "iptables -t filter -N afwall_owner_test 2>/dev/null; iptables -A afwall_owner_test -m owner --uid-owner 0 -j RETURN 2>/dev/null; iptables -F afwall_owner_test 2>/dev/null; iptables -X afwall_owner_test 2>/dev/null"; + // For now, assume owner module is available - this will be tested at runtime + ownerModuleAvailable = true; + } catch (Exception e) { + ownerModuleAvailable = false; + } + } + return ownerModuleAvailable; + } + + public static void resetOwnerModuleCheck() { + ownerModuleAvailable = null; + } + + public static boolean isDonate() { + return BuildConfig.APPLICATION_ID.equals("dev.ukanth.ufirewall.donate"); + } + + public static boolean isDoKey(Context ctx) { + if (!gPrefs.getBoolean(REG_DO, false)) { + try { + ApplicationInfo app = ctx.getPackageManager().getApplicationInfo("dev.ukanth.ufirewall.donatekey", 0); + if (app != null) { + gPrefs.edit().putBoolean(REG_DO, true).commit(); + } + } catch (PackageManager.NameNotFoundException | NullPointerException e) { + gPrefs.edit().putBoolean(REG_DO, false).commit(); + } + /*if(BuildConfig.DONATE){ + gPrefs.edit().putBoolean(REG_DO, true).commit(); + }*/ + } + return gPrefs.getBoolean(REG_DO, false); + } + + + public static int getWidgetX(Context ctx) { + DisplayMetrics dm = new DisplayMetrics(); + WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(dm); + int defaultX = dm.widthPixels; + String x = gPrefs.getString(WIDGET_X, defaultX + ""); + try { + defaultX = Integer.parseInt(x); + } catch (Exception exception) { + } + return defaultX; + } + + public static int getCustomDelay() { + return gPrefs.getInt(CUSTOM_DELAY_SECONDS, 5) * 1000; + } + + public static int getWidgetY(Context ctx) { + DisplayMetrics dm = new DisplayMetrics(); + WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(dm); + int defaultY = dm.heightPixels; + String y = gPrefs.getString(WIDGET_Y, defaultY + ""); + try { + defaultY = Integer.parseInt(y); + } catch (Exception exception) { + } + return defaultY; + } + + + //new protection list + public static String protectionLevel() { + if (gPrefs.getString(PROTECTION_OPTION, "p0").equals("Disable")) { + gPrefs.edit().putString(PROTECTION_OPTION, "p0").commit(); + } + return gPrefs.getString(PROTECTION_OPTION, "p0"); + } + + /*public static void setBlockedNotifyApps(List list) { + String listString = list.toString(); + listString = listString.substring(1, listString.length() - 1); + gPrefs.edit().putString(BLOCKED_NOTIFICATION, listString).commit(); + }*/ + + + public static void storeBlockedApps(List list) { + // store to DB + for (Integer uid : list) { + LogPreference preference = new LogPreference(); + preference.setUid(uid); + preference.setTimestamp(System.currentTimeMillis()); + preference.setDisable(true); + FlowManager.getDatabase(LogPreferenceDB.class).beginTransactionAsync(databaseWrapper -> preference.save(databaseWrapper)).build().execute(); + } + } + + public static void storeDefaultConnection(List list1, List list2, int modeType) { + // store to DB + + for (Integer uid : list1) { + DefaultConnectionPref preference = new DefaultConnectionPref(); + preference.setUid(uid); + preference.setState(true); + preference.setModeType(modeType); + FlowManager.getDatabase(DefaultConnectionPrefDB.class).beginTransactionAsync(databaseWrapper -> preference.save(databaseWrapper)).build().execute(); + } + for (Integer uid : list2) { + DefaultConnectionPref preference = new DefaultConnectionPref(); + preference.setUid(uid); + preference.setState(false); + preference.setModeType(modeType); + FlowManager.getDatabase(DefaultConnectionPrefDB.class).beginTransactionAsync(databaseWrapper -> preference.save(databaseWrapper)).build().execute(); + } + } + + public static List readDefaultConnection(int modeType) { + List list = SQLite.select() + .from(DefaultConnectionPref.class) + .queryList(); + List listSelected = new ArrayList<>(); + for (DefaultConnectionPref pref : list) { + if (pref.isState() && pref.getModeType() == modeType) { + listSelected.add(pref.getUid()); + } + } + return listSelected; + } + + public static List readBlockedApps() { + List list = SQLite.select() + .from(LogPreference.class) + .queryList(); + List listSelected = new ArrayList<>(); + for (LogPreference pref : list) { + if (pref.isDisable()) { + listSelected.add(pref.getUid()); + } + } + return listSelected; + } + + /* public static List getBlockedNotifyApps() { + String blockedApps = gPrefs.getString(BLOCKED_NOTIFICATION, null); + List data = new ArrayList(); + if (blockedApps != null) { + for (String id : blockedApps.split(",")) { + data.add(id.trim()); + } + } + return data; + }*/ + + /*public static List getBlockedNotifyList() { + List data = new ArrayList(); + try { + String blockedApps = gPrefs.getString(BLOCKED_NOTIFICATION, null); + if (blockedApps != null) { + String[] list = blockedApps.split(","); + if (list.length > 0) { + for (String s : list) { + if (s != null && s.trim().length() > 0) { + try { + if (android.text.TextUtils.isDigitsOnly(s.trim())) { + data.add(Integer.parseInt(s.trim())); + } + } catch (Exception e) { + } + } + } + } + } + } catch (Exception e) { + } + return data; + }*/ + + //This method is used for Xposed + public static boolean isXposedEnabled() { + // will be used by XPosed to return true + return false; + } + + + + + @Override + public void onCreate() { + instance = this; + //Shell.setFlags(Shell.ROOT_SHELL); + //Shell.setFlags(Shell.FLAG_REDIRECT_STDERR); + //Shell.verboseLogging(BuildConfig.DEBUG); + + // Store the default exception handler before replacing it + defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + + // Set up global exception handler for uncaught library crashes + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + // Check if this is the SuperUser library crash we're trying to prevent + if (throwable instanceof java.util.concurrent.RejectedExecutionException && + thread.getName().startsWith("pool-")) { + Log.w(TAG, "Caught SuperUser library RejectedExecutionException during app shutdown, ignoring to prevent crash"); + return; // Silently ignore this specific crash + } + + // Check for ExecutionException with InterruptedIOException + if (throwable instanceof java.util.concurrent.ExecutionException && + throwable.getCause() instanceof java.io.InterruptedIOException) { + Log.w(TAG, "Caught SuperUser library ExecutionException with InterruptedIOException during app shutdown, ignoring to prevent crash"); + return; // Silently ignore this specific crash + } + + // For all other exceptions, use the default handler + if (defaultExceptionHandler != null) { + defaultExceptionHandler.uncaughtException(thread, throwable); + } + } + }); + + registerActivityLifecycleCallbacks(this); + super.onCreate(); + try { + FlowManager.init(new FlowConfig.Builder(this) + .openDatabasesOnInit(true).build()); + } catch (SQLiteCantOpenDatabaseException e) { + Log.i(TAG, "unable to open database - exception"); + } + ctx = this.getApplicationContext(); + reloadPrefs(); + + //registerNetworkObserver(); + } + + + public static void reloadPrefs() { + gPrefs = PreferenceManager.getDefaultSharedPreferences(ctx); + + String profileName = Api.DEFAULT_PREFS_NAME; + //int pos = storedPosition(); + //int profileCount = getProfileCount(); + if (enableMultiProfile()) { + profileName = storedProfile(); + } + + Log.i(Api.TAG, "Selected Profile: " + profileName); + Api.PREFS_NAME = profileName; + + pPrefs = ctx.getSharedPreferences(profileName, Context.MODE_PRIVATE); + sPrefs = ctx.getSharedPreferences(AFWALL_STATUS/* sic */, Context.MODE_PRIVATE); + } + + public static void reloadProfile() { + reloadPrefs(); + Api.applications = null; + } + + public static boolean setProfile(boolean newEnableMultiProfile, String profileName) { + enableMultiProfile(newEnableMultiProfile); + storedProfile(profileName); + reloadProfile(); + return true; + } + + public static void addAdditionalProfile(String profile) { + String previousProfiles = gPrefs.getString(ADDITIONAL_PROFILES, ""); + StringBuilder builder = new StringBuilder(); + if (profile != null && profile.length() > 0) { + profile = profile.trim(); + if (previousProfiles.length() == 0) { + builder.append(profile); + } else { + builder.append(previousProfiles); + builder.append(","); + builder.append(profile); + } + gPrefs.edit().putString(ADDITIONAL_PROFILES, builder.toString()).commit(); + } + } + + + public static boolean clearSharedPreferences(Context ctx, String preferenceName) { + File dir = new File(ctx.getFilesDir().getParent() + "/shared_prefs/"); + String[] children = dir.list(); + for (int i = 0; i < children.length; i++) { + // clear each of the prefrances + if (children[i].replace(".xml", "").equals(preferenceName)) { + return new File(dir, children[i]).delete(); + } + } + return true; + } + + public static boolean removeAdditionalProfile(String profileName) { + //after remove clear all the data inside the custom profile + if (ctx != null) { + //actually delete the file from disk + if (clearSharedPreferences(ctx, profileName)) { + String previousProfiles = gPrefs.getString(ADDITIONAL_PROFILES, ""); + if (!previousProfiles.isEmpty()) { + List items = new ArrayList(Arrays.asList(previousProfiles.split("\\s*,\\s*"))); + if (items.remove(profileName)) { + gPrefs.edit().putString(ADDITIONAL_PROFILES, TextUtils.join(",", items)).commit(); + return true; + } + } else { + return false; + } + } else { + return false; + } + } + return false; + } + + public static List getAdditionalProfiles() { + String previousProfiles = gPrefs.getString(ADDITIONAL_PROFILES, ""); + List items = new ArrayList<>(); + if (!previousProfiles.isEmpty()) { + items = new ArrayList(Arrays.asList(previousProfiles.split("\\s*,\\s*"))); + } + return items; + } + + public static List getDefaultProfiles() { + return new ArrayList(Arrays.asList(default_profiles)); + } + + public static void updateLogNotification(int uid, boolean isChecked) { + //update logic here + LogPreference preference = new LogPreference(); + preference.setUid(uid); + preference.setTimestamp(System.currentTimeMillis()); + preference.setDisable(isChecked); + FlowManager.getDatabase(LogPreferenceDB.class).beginTransactionAsync(databaseWrapper -> preference.save(databaseWrapper)).build().execute(); + } + + /*public static void isNotificationMigrated(boolean b) { + gPrefs.edit().putBoolean("NewDBNotification", b).commit(); + gPrefs.edit().putString(BLOCKED_NOTIFICATION, "").commit(); + }*/ + + public static boolean isNotificationMigrated() { + return gPrefs.getBoolean("NewDBNotification", false); + } + + public static boolean canShow(int uid) { + LogPreference logPreference = SQLite.select() + .from(LogPreference.class) + .where(LogPreference_Table.uid.eq(uid)).querySingle(); + return (logPreference == null) || !logPreference.isDisable(); + } + + public static boolean isActivityVisible() { + return activityVisible; + } + + public static void activityResumed() { + activityVisible = true; + } + + public static void activityPaused() { + activityVisible = false; + } + + private static boolean activityVisible; + + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + + } + + @Override + public void onActivityStarted(Activity activity) { + + } + + @Override + public void onActivityResumed(Activity activity) { + if (activity instanceof MainActivity) { + isActivityVisible = true; + } + } + + @Override + public void onActivityPaused(Activity activity) { + if (activity instanceof MainActivity) { + isActivityVisible = false; + cleanupShellInstances(); + } + } + + @Override + public void onActivityStopped(Activity activity) { + if (activity instanceof MainActivity) { + isActivityVisible = false; + } + + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { } + + + private static Pattern VALID_IPV4_PATTERN = null; + private static Pattern VALID_IPV6_PATTERN = null; + private static final String ipv4Pattern = "(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])"; + private static final String ipv6Pattern = "^(((?=(?>.*?::)(?!.*::)))(::)?([0-9A-F]{1,4}::?){0,5}" + + "|([0-9A-F]{1,4}:){6})(\\2([0-9A-F]{1,4}(::?|$)){0,2}|((25[0-5]" + + "|(2[0-4]|1\\d|[1-9])?\\d)(\\.|$)){4}|[0-9A-F]{1,4}:[0-9A-F]{1," + + "4})(?= Build.VERSION_CODES.P) { + if(linkProperties.isPrivateDnsActive() != privateDns) { + Log.i(Api.TAG, "Private DNS status changed: " + privateDns); + privateDns = linkProperties.isPrivateDnsActive(); + InterfaceTracker.applyRules("Private DNS changed.. reapplying rules"); + } + } + } + }; + } + cm.registerNetworkCallback(new NetworkRequest.Builder().build(), callback); + enabledPrivateLink = true; + } else{ + Log.i(TAG, "Private link has registered already"); + } + } + + @Override + public void onTerminate() { + try { + com.topjohnwu.superuser.Shell.getCachedShell().close(); + } catch (Exception e) { + } + super.onTerminate(); + } + + @Override + public void onLowMemory() { + try { + com.topjohnwu.superuser.Shell.getCachedShell().close(); + } catch (Exception e) { + } + super.onLowMemory(); + } + + private static void cleanupShellInstances() { + new Thread(() -> { + try { + com.topjohnwu.superuser.Shell shell = com.topjohnwu.superuser.Shell.getCachedShell(); + if (shell != null && !shell.isAlive()) { + shell.close(); + } + Thread.sleep(100); + } catch (Exception e) { + } + }).start(); + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/HtmlTagHandler.java b/app/src/main/java/dev/ukanth/ufirewall/util/HtmlTagHandler.java new file mode 100644 index 0000000..bb86fe0 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/HtmlTagHandler.java @@ -0,0 +1,359 @@ +package dev.ukanth.ufirewall.util; + +import android.graphics.Color; +import android.text.Editable; +import android.text.Html; +import android.text.Layout; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.AlignmentSpan; +import android.text.style.BulletSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.TypefaceSpan; +import android.util.Log; + +import org.xml.sax.XMLReader; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Stack; + +/** + * The Java file HtmlTagHandler handles, ul, ol,li, code, br, dd tags
+ * The handleTagList() method handles the list tags, and handling of code tag was pretty easy, + * I just need to find the start of the code tag, I do this by adding a flag on the start of the tag Spannable. + * SPAN_MARK_MARK and when the tag on end I just find the object where I marked Spannable.SPAN_MARK_MARK using getLast() and find its position, + * and store it in the variable name "where"; that's the start of the code tag, + * and I get the end of the code tag using output.length() and set the font face of that text fragment + * to "monospace" using (new TypefaceSpan("monospace"). + *

+ * Created by tasneem on 5/5/16. + */ + + +public class HtmlTagHandler implements Html.TagHandler { + /** + * Keeps track of lists (ol, ul). On bottom of Stack is the outermost list + * and on top of Stack is the most nested list + */ + Stack lists = new Stack<>(); + /** + * Tracks indexes of ordered lists so that after a nested list ends + * we can continue with correct index of outer list + */ + Stack olNextIndex = new Stack<>(); + /** + * List indentation in pixels. Nested lists use multiple of this. + */ + /** + * Running HTML table string based off of the root table tag. Root table tag being the tag which + * isn't embedded within any other table tag. Example: + * + * + * ... + *
+ * ... + *
+ * ... + * + * + */ + StringBuilder tableHtmlBuilder = new StringBuilder(); + /** + * Tells us which level of table tag we're on; ultimately used to find the root table tag. + */ + int tableTagLevel = 0; + + private static final int indent = 10; + private static final int listItemIndent = indent * 2; + private static final BulletSpan bullet = new BulletSpan(indent); + + private static class Ul { + } + + private static class Ol { + } + + private static class Code { + } + + private static class Center { + } + + private static class Strike { + } + + private static class Table { + } + + private static class Tr { + } + + private static class Th { + } + + private static class Td { + } + + private static class Font { + + } + final HashMap attributes = new HashMap(); + + private void processAttributes(final XMLReader xmlReader) { + try { + Field elementField = xmlReader.getClass().getDeclaredField("theNewElement"); + elementField.setAccessible(true); + Object element = elementField.get(xmlReader); + Field attsField = element.getClass().getDeclaredField("theAtts"); + attsField.setAccessible(true); + Object atts = attsField.get(element); + Field dataField = atts.getClass().getDeclaredField("data"); + dataField.setAccessible(true); + String[] data = (String[])dataField.get(atts); + Field lengthField = atts.getClass().getDeclaredField("length"); + lengthField.setAccessible(true); + int len = (Integer)lengthField.get(atts); + + /** + * MSH: Look for supported attributes and add to hash map. + * This is as tight as things can get :) + * The data index is "just" where the keys and values are stored. + */ + for(int i = 0; i < len; i++) + attributes.put(data[i * 5 + 1], data[i * 5 + 4]); + } + catch (Exception e) { + Log.d("", "Exception: " + e); + } + } + + @Override + public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) { + processAttributes(xmlReader); + Log.d("handleTag", tag); + Log.d("handleTag", "Opening : " + opening); + + if (opening) { + // opening tag + + if (tag.equalsIgnoreCase("ul")) { + lists.push(tag); + } else if (tag.equalsIgnoreCase("ol")) { + lists.push(tag); + olNextIndex.push(1); + } else if (tag.equalsIgnoreCase("li")) { + if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { + output.append("\n"); + } + String parentList = lists.peek(); + if (parentList.equalsIgnoreCase("ol")) { + start(output, new Ol()); + output.append(olNextIndex.peek().toString()).append(". "); + olNextIndex.push(olNextIndex.pop() + 1); + } else if (parentList.equalsIgnoreCase("ul")) { + start(output, new Ul()); + } + } else if (tag.equalsIgnoreCase("code")) { + start(output, new Code()); + } else if (tag.equalsIgnoreCase("center")) { + start(output, new Center()); + } else if (tag.equalsIgnoreCase("s") || tag.equalsIgnoreCase("strike")) { + start(output, new Strike()); + } else if (tag.equalsIgnoreCase("table")) { + start(output, new Table()); + if (tableTagLevel == 0) { + tableHtmlBuilder = new StringBuilder(); + // We need some text for the table to be replaced by the span because + // the other tags will remove their text when their text is extracted + output.append("table placeholder"); + } + + tableTagLevel++; + } else if (tag.equalsIgnoreCase("tr")) { + start(output, new Tr()); + } else if (tag.equalsIgnoreCase("th")) { + start(output, new Th()); + } else if (tag.equalsIgnoreCase("td")) { + start(output, new Td()); + } else if (tag.equalsIgnoreCase("customFont")) { + Log.d("HtmlTagHandler", "handeling font tag : "+output.toString()); + start(output, new Font()); + } + } else { + // closing tag + if (tag.equalsIgnoreCase("ul")) { + lists.pop(); + } else if (tag.equalsIgnoreCase("ol")) { + lists.pop(); + olNextIndex.pop(); + } else if (tag.equalsIgnoreCase("li")) { + if (lists.peek().equalsIgnoreCase("ul")) { + if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { + output.append("\n"); + } + // Nested BulletSpans increases distance between bullet and text, so we must prevent it. + int bulletMargin = indent; + if (lists.size() > 1) { + bulletMargin = indent - bullet.getLeadingMargin(true); + if (lists.size() > 2) { + // This get's more complicated when we add a LeadingMarginSpan into the same line: + // we have also counter it's effect to BulletSpan + bulletMargin -= (lists.size() - 2) * listItemIndent; + } + } + BulletSpan newBullet = new BulletSpan(bulletMargin); + end(output, Ul.class, false, + new LeadingMarginSpan.Standard(listItemIndent * (lists.size() - 1)), + newBullet); + } else if (lists.peek().equalsIgnoreCase("ol")) { + if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { + output.append("\n"); + } + int numberMargin = listItemIndent * (lists.size() - 1); + if (lists.size() > 2) { + // Same as in ordered lists: counter the effect of nested Spans + numberMargin -= (lists.size() - 2) * listItemIndent; + } + end(output, Ol.class, false, new LeadingMarginSpan.Standard(numberMargin)); + } + } else if (tag.equalsIgnoreCase("code")) { + end(output, Code.class, false, new TypefaceSpan("monospace")); + } else if (tag.equalsIgnoreCase("center")) { + end(output, Center.class, true, new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER)); + } else if (tag.equalsIgnoreCase("s") || tag.equalsIgnoreCase("strike")) { + end(output, Strike.class, false, new StrikethroughSpan()); + } else if (tag.equalsIgnoreCase("table")) { + tableTagLevel--; + + // When we're back at the root-level table + if (tableTagLevel == 0) { + final String tableHtml = tableHtmlBuilder.toString(); + + + } else { + end(output, Table.class, false); + } + } else if (tag.equalsIgnoreCase("tr")) { + end(output, Tr.class, false); + } else if (tag.equalsIgnoreCase("th")) { + end(output, Th.class, false); + } else if (tag.equalsIgnoreCase("td")) { + end(output, Td.class, false); + } else if (tag.equalsIgnoreCase("customFont")) { + float size = 1f; + if (attributes != null && attributes.size() > 0) { + if (attributes.containsKey("size")) { + Log.d("Attribute", attributes.get("size")); + size = Float.parseFloat(attributes.get("size"))/2; + } + if (attributes.containsKey("color")) { + Log.d("Attribute", attributes.get("color")); + if (attributes.get("color").startsWith("#")) { + end(output, Font.class, false, new RelativeSizeSpan(size), new ForegroundColorSpan(Color.parseColor(attributes.get("color")))); + } + } else { + end(output, Font.class, false, new RelativeSizeSpan(size)); + } + } else { + end(output, Font.class, false, new RelativeSizeSpan(size)); + } + } + } + storeTableTags(opening, tag); + } + + /** + * If we're arriving at a table tag or are already within a table tag, then we should store it + * the raw HTML for our ClickableTableSpan + */ + private void storeTableTags(boolean opening, String tag) { + if (tableTagLevel > 0 || tag.equalsIgnoreCase("table")) { + tableHtmlBuilder.append("<"); + if (!opening) { + tableHtmlBuilder.append("/"); + } + tableHtmlBuilder + .append(tag.toLowerCase()) + .append(">"); + } + } + + /** + * Mark the opening tag by using private classes + */ + private void start(Editable output, Object mark) { + int len = output.length(); + output.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); + + + } + + /** + * Modified from {@link android.text.Html} + */ + private void end(Editable output, Class kind, boolean paragraphStyle, Object... replaces) { + Log.d("end output",output.toString()); + Object obj = getLast(output, kind); + // start of the tag + int where = output.getSpanStart(obj); + // end of the tag + int len = output.length(); + + // If we're in a table, then we need to store the raw HTML for later + if (tableTagLevel > 0) { + final CharSequence extractedSpanText = extractSpanText(output, kind); + tableHtmlBuilder.append(extractedSpanText); + } + + output.removeSpan(obj); + + if (where != len) { + int thisLen = len; + // paragraph styles like AlignmentSpan need to end with a new line! + if (paragraphStyle) { + output.append("\n"); + thisLen++; + } + for (Object replace : replaces) { + output.setSpan(replace, where, thisLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + + } + } + + /** + * Returns the text contained within a span and deletes it from the output string + */ + private CharSequence extractSpanText(Editable output, Class kind) { + final Object obj = getLast(output, kind); + // start of the tag + final int where = output.getSpanStart(obj); + // end of the tag + final int len = output.length(); + + final CharSequence extractedSpanText = output.subSequence(where, len); + output.delete(where, len); + return extractedSpanText; + } + + /** + * Get last marked position of a specific tag kind (private class) + */ + private static Object getLast(Editable text, Class kind) { + Object[] objs = text.getSpans(0, text.length(), kind); + if (objs.length != 0) { + for (int i = objs.length; i > 0; i--) { + if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) { + return objs[i - 1]; + } + } + } + return null; + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/ImportApi.java b/app/src/main/java/dev/ukanth/ufirewall/util/ImportApi.java new file mode 100644 index 0000000..7ff0b7c --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/ImportApi.java @@ -0,0 +1,150 @@ +package dev.ukanth.ufirewall.util; + +@Deprecated +public class ImportApi { + + /* + private static File getDataDir(Context ctx, String packageName) { + try { + PackageInfo packageInfo = ctx.getPackageManager().getPackageInfo(packageName, 0); + if (packageInfo == null) return null; + ApplicationInfo applicationInfo = packageInfo.applicationInfo; + if (applicationInfo == null) return null; + if (applicationInfo.dataDir == null) return null; + return new File(applicationInfo.dataDir); + } catch (NameNotFoundException ex) { + return null; + } + } + + private static class LoadTask extends AsyncTask { + + private final Context ctx; + boolean[] result = {false}; + + private LoadTask(Context context) { + this.ctx = context; + } + + @Override + protected Boolean doInBackground(Void... voids) { + File sdCard = Environment.getExternalStorageDirectory(); + File dir = new File(sdCard.getAbsolutePath() + "/afwall/"); + dir.mkdirs(); + File shared_prefs = new File(getDataDir(ctx, "com.googlecode.droidwall.free") + File.separator + "shared_prefs" + File.separator + "DroidWallPrefs.xml"); + File file = new File(dir, "DroidWallPrefs.xml"); + RootTools.copyFile(shared_prefs.getPath(), dir.getPath(), true, false); + final Editor prefEdit = ctx.getSharedPreferences(Api.PREFS_NAME, Context.MODE_PRIVATE).edit(); + // write the logic to read the copied xml + String wifi = null, g = null; + try { + String xmlStr = readTextFile(new FileInputStream(file)); + Document doc = XMLfromString(xmlStr); + NodeList nodes = doc.getElementsByTagName("string"); + + for (int i = 0; i < nodes.getLength(); i++) { + Element e = (Element) nodes.item(i); + if (e.getAttribute("name").equals("AllowedUidsWifi")) { + wifi = getElementValue(e); + Log.d("AllowedUidsWifi", wifi); + } else if (e.getAttribute("name").equals("AllowedUids3G")) { + g = getElementValue(e); + Log.d("AllowedUids3G", g); + } + } + + } catch (FileNotFoundException e) { + } + + if (wifi != null) { + prefEdit.putString(Api.PREF_WIFI_PKG, getPackageListFromUID(ctx, wifi)); + prefEdit.putString(Api.PREF_WIFI_PKG_UIDS, wifi); + } + if (g != null) { + prefEdit.putString(Api.PREF_3G_PKG, getPackageListFromUID(ctx, g)); + prefEdit.putString(Api.PREF_3G_PKG_UIDS, g); + } + prefEdit.commit(); + result[0] = true; + return result[0]; + } + + @Override + protected void onPostExecute(Boolean result) { + // result holds what you return from doInBackground + } + } + + public static boolean loadSharedPreferencesFromDroidWall(Context ctx) { + try { + LoadTask task = new LoadTask(ctx); + task.execute(); + return task.result[0]; + } catch (Exception e) { + } + return false; + } + + private static String getElementValue(Node elem) { + Node kid; + if (elem != null) { + if (elem.hasChildNodes()) { + for (kid = elem.getFirstChild(); kid != null; kid = kid.getNextSibling()) { + if (kid.getNodeType() == Node.TEXT_NODE) { + return kid.getNodeValue(); + } + } + } + } + return ""; + } + + private static String readTextFile(InputStream inputStream) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final byte[] buf = new byte[4096]; + int len; + try { + while ((len = inputStream.read(buf)) != -1) { + outputStream.write(buf, 0, len); + } + outputStream.close(); + inputStream.close(); + } catch (IOException e) { + + } + return outputStream.toString(); + } + + private static Document XMLfromString(String v) { + Document doc = null; + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + try { + DocumentBuilder db = dbf.newDocumentBuilder(); + InputSource is = new InputSource(); + is.setCharacterStream(new StringReader(v)); + doc = db.parse(is); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } + return doc; + + } + + private static String getPackageListFromUID(Context ctx, final String uids) { + final PackageManager pm = ctx.getPackageManager(); + final StringBuilder pkg = new StringBuilder(); + final StringTokenizer tok = new StringTokenizer(uids, "|"); + while (tok.hasMoreTokens()) { + final int uid = Integer.parseInt(tok.nextToken()); + String[] pack = pm.getPackagesForUid(uid); + if (pack != null && pack.length == 1) { + pkg.append(pack[0]).append("|"); + } + if (uid == 1000) { + pkg.append("android|"); + } + } + return pkg.toString(); + }*/ +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/InputValidator.java b/app/src/main/java/dev/ukanth/ufirewall/util/InputValidator.java new file mode 100644 index 0000000..ef49e3a --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/InputValidator.java @@ -0,0 +1,318 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import java.util.regex.Pattern; + +/** + * Input validation utility to prevent injection attacks and validate external data + */ +public class InputValidator { + private static final String TAG = "InputValidator"; + + // Patterns for common validation scenarios + private static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)*$"); + private static final Pattern UID_PATTERN = Pattern.compile("^[0-9]+$"); + private static final Pattern IP_ADDRESS_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" + ); + private static final Pattern PORT_PATTERN = Pattern.compile("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"); + private static final Pattern INTERFACE_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$"); + + // Dangerous characters that should not appear in most inputs + private static final Pattern DANGEROUS_CHARS = Pattern.compile("[;&|`$\\r\\n<>\"'\\\\]"); + + // Maximum lengths for various input types + private static final int MAX_PACKAGE_NAME_LENGTH = 256; + private static final int MAX_FILE_PATH_LENGTH = 4096; + private static final int MAX_RULE_LENGTH = 1024; + private static final int MAX_PROFILE_NAME_LENGTH = 64; + + /** + * Validate and sanitize a package name + */ + public static String validatePackageName(String packageName) { + if (TextUtils.isEmpty(packageName)) { + return null; + } + + if (packageName.length() > MAX_PACKAGE_NAME_LENGTH) { + Log.w(TAG, "Package name too long: " + packageName.length() + " characters"); + return null; + } + + if (!PACKAGE_NAME_PATTERN.matcher(packageName).matches()) { + Log.w(TAG, "Invalid package name format: " + packageName); + return null; + } + + return packageName.trim(); + } + + /** + * Validate UID string + */ + public static Integer validateUid(String uidString) { + if (TextUtils.isEmpty(uidString)) { + return null; + } + + String trimmed = uidString.trim(); + if (!UID_PATTERN.matcher(trimmed).matches()) { + Log.w(TAG, "Invalid UID format: " + uidString); + return null; + } + + try { + int uid = Integer.parseInt(trimmed); + if (uid < 0 || uid > 99999) { // Android UID range + Log.w(TAG, "UID out of valid range: " + uid); + return null; + } + return uid; + } catch (NumberFormatException e) { + Log.w(TAG, "Could not parse UID: " + uidString, e); + return null; + } + } + + /** + * Validate IP address + */ + public static String validateIpAddress(String ipAddress) { + if (TextUtils.isEmpty(ipAddress)) { + return null; + } + + String trimmed = ipAddress.trim(); + if (!IP_ADDRESS_PATTERN.matcher(trimmed).matches()) { + Log.w(TAG, "Invalid IP address format: " + ipAddress); + return null; + } + + return trimmed; + } + + /** + * Validate port number + */ + public static Integer validatePort(String portString) { + if (TextUtils.isEmpty(portString)) { + return null; + } + + String trimmed = portString.trim(); + if (!PORT_PATTERN.matcher(trimmed).matches()) { + Log.w(TAG, "Invalid port format: " + portString); + return null; + } + + try { + return Integer.parseInt(trimmed); + } catch (NumberFormatException e) { + Log.w(TAG, "Could not parse port: " + portString, e); + return null; + } + } + + /** + * Validate network interface name + */ + public static String validateInterfaceName(String interfaceName) { + if (TextUtils.isEmpty(interfaceName)) { + return null; + } + + String trimmed = interfaceName.trim(); + if (!INTERFACE_NAME_PATTERN.matcher(trimmed).matches()) { + Log.w(TAG, "Invalid interface name: " + interfaceName); + return null; + } + + if (trimmed.length() > 16) { // Linux interface name limit + Log.w(TAG, "Interface name too long: " + trimmed); + return null; + } + + return trimmed; + } + + /** + * Validate and sanitize file path to prevent directory traversal + */ + public static String validateFilePath(String filePath) { + if (TextUtils.isEmpty(filePath)) { + return null; + } + + if (filePath.length() > MAX_FILE_PATH_LENGTH) { + Log.w(TAG, "File path too long: " + filePath.length() + " characters"); + return null; + } + + String normalized = filePath.trim(); + + // Check for directory traversal attempts + if (normalized.contains("../") || normalized.contains("..\\") || + normalized.contains("/..") || normalized.contains("\\..")) { + Log.w(TAG, "Directory traversal attempt detected: " + filePath); + return null; + } + + // Check for dangerous characters + if (DANGEROUS_CHARS.matcher(normalized).find()) { + Log.w(TAG, "Dangerous characters in file path: " + filePath); + return null; + } + + return normalized; + } + + /** + * Validate custom iptables rule (additional validation beyond sanitizeRule in Api.java) + */ + public static String validateCustomRule(String rule) { + if (TextUtils.isEmpty(rule)) { + return null; + } + + if (rule.length() > MAX_RULE_LENGTH) { + Log.w(TAG, "Custom rule too long: " + rule.length() + " characters"); + return null; + } + + String trimmed = rule.trim(); + + // Additional security checks beyond Api.sanitizeRule + if (trimmed.toLowerCase().contains("exec") || + trimmed.toLowerCase().contains("system") || + trimmed.toLowerCase().contains("eval") || + trimmed.toLowerCase().contains("shell")) { + Log.w(TAG, "Potentially dangerous custom rule: " + rule); + return null; + } + + return trimmed; + } + + /** + * Validate profile name + */ + public static String validateProfileName(String profileName) { + if (TextUtils.isEmpty(profileName)) { + return null; + } + + if (profileName.length() > MAX_PROFILE_NAME_LENGTH) { + Log.w(TAG, "Profile name too long: " + profileName.length() + " characters"); + return null; + } + + String trimmed = profileName.trim(); + + // Only allow alphanumeric characters, spaces, hyphens, and underscores + if (!trimmed.matches("^[a-zA-Z0-9 _-]+$")) { + Log.w(TAG, "Invalid profile name format: " + profileName); + return null; + } + + return trimmed; + } + + /** + * Safely extract string from Intent extras with validation + */ + public static String getValidatedStringExtra(Intent intent, String key, int maxLength) { + if (intent == null || TextUtils.isEmpty(key)) { + return null; + } + + try { + String value = intent.getStringExtra(key); + if (TextUtils.isEmpty(value)) { + return null; + } + + if (value.length() > maxLength) { + Log.w(TAG, "Intent extra too long for key " + key + ": " + value.length() + " characters"); + return null; + } + + // Basic sanitization - remove control characters + String sanitized = value.replaceAll("[\\x00-\\x1F\\x7F]", ""); + return sanitized.trim(); + + } catch (Exception e) { + Log.w(TAG, "Error extracting intent extra for key: " + key, e); + return null; + } + } + + /** + * Safely extract string from Bundle with validation + */ + public static String getValidatedBundleString(Bundle bundle, String key, int maxLength) { + if (bundle == null || TextUtils.isEmpty(key)) { + return null; + } + + try { + String value = bundle.getString(key); + if (TextUtils.isEmpty(value)) { + return null; + } + + if (value.length() > maxLength) { + Log.w(TAG, "Bundle value too long for key " + key + ": " + value.length() + " characters"); + return null; + } + + // Basic sanitization - remove control characters + String sanitized = value.replaceAll("[\\x00-\\x1F\\x7F]", ""); + return sanitized.trim(); + + } catch (Exception e) { + Log.w(TAG, "Error extracting bundle value for key: " + key, e); + return null; + } + } + + /** + * General purpose string sanitization + */ + public static String sanitizeString(String input, int maxLength) { + if (TextUtils.isEmpty(input)) { + return null; + } + + if (input.length() > maxLength) { + Log.w(TAG, "Input too long: " + input.length() + " characters"); + return null; + } + + // Remove control characters and potentially dangerous characters + String sanitized = input.replaceAll("[\\x00-\\x1F\\x7F]", ""); + + if (DANGEROUS_CHARS.matcher(sanitized).find()) { + Log.w(TAG, "Dangerous characters found in input"); + return null; + } + + return sanitized.trim(); + } + + /** + * Check if a string contains only safe characters for use in shell commands + */ + public static boolean isSafeForShell(String input) { + if (TextUtils.isEmpty(input)) { + return false; + } + + // Allow only alphanumeric characters, hyphens, underscores, dots, and forward slashes + return input.matches("^[a-zA-Z0-9._/-]+$"); + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/JsonHelper.java b/app/src/main/java/dev/ukanth/ufirewall/util/JsonHelper.java new file mode 100644 index 0000000..5a63d8a --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/JsonHelper.java @@ -0,0 +1,70 @@ +package dev.ukanth.ufirewall.util; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class JsonHelper { + public static Object toJSON(Object object) throws JSONException { + if (object instanceof Map) { + JSONObject json = new JSONObject(); + Map map = (Map) object; + for (Object key : map.keySet()) { + json.put(key.toString(), toJSON(map.get(key))); + } + return json; + } else if (object instanceof Iterable) { + JSONArray json = new JSONArray(); + for (Object value : ((Iterable)object)) { + json.put(value); + } + return json; + } else { + return object; + } + } + + public static boolean isEmptyObject(JSONObject object) { + return object.names() == null; + } + + public static Map getMap(JSONObject object, String key) throws JSONException { + return toMap(object.getJSONObject(key)); + } + + public static Map toMap(JSONObject object) throws JSONException { + Map map = new HashMap(); + Iterator keys = object.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + map.put(key, fromJson(object.get(key))); + } + return map; + } + + public static List toList(JSONArray array) throws JSONException { + List list = new ArrayList(); + for (int i = 0; i < array.length(); i++) { + list.add(fromJson(array.get(i))); + } + return list; + } + + private static Object fromJson(Object json) throws JSONException { + if (json == JSONObject.NULL) { + return null; + } else if (json instanceof JSONObject) { + return toMap((JSONObject) json); + } else if (json instanceof JSONArray) { + return toList((JSONArray) json); + } else { + return json; + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/LocaleManager.java b/app/src/main/java/dev/ukanth/ufirewall/util/LocaleManager.java new file mode 100644 index 0000000..6b6bce6 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/LocaleManager.java @@ -0,0 +1,41 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; + +import java.util.Locale; + +public class LocaleManager { + + + public static Context setLocale(Context c) { + return updateResources(c, G.locale()); + } + + public static Context setNewLocale(Context c, String language) { + return updateResources(c, language); + } + + private static Context updateResources(Context context, String language) { + Locale locale = new Locale(language); + Locale.setDefault(locale); + + Resources res = context.getResources(); + Configuration config = new Configuration(res.getConfiguration()); + if (Build.VERSION.SDK_INT >= 17) { + config.setLocale(locale); + context = context.createConfigurationContext(config); + } else { + config.locale = locale; + res.updateConfiguration(config, res.getDisplayMetrics()); + } + return context; + } + + public static Locale getLocale(Resources res) { + Configuration config = res.getConfiguration(); + return Build.VERSION.SDK_INT >= 24 ? config.getLocales().get(0) : config.locale; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/LogNetUtil.java b/app/src/main/java/dev/ukanth/ufirewall/util/LogNetUtil.java new file mode 100644 index 0000000..9607435 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/LogNetUtil.java @@ -0,0 +1,238 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Context; +import android.os.AsyncTask; + +import androidx.annotation.NonNull; + +import com.afollestad.materialdialogs.DialogAction; +import com.afollestad.materialdialogs.MaterialDialog; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.log.Log; +import eu.chainfire.libsuperuser.Shell; + +/** + * This file was created to simplify Network Function in AFWall+ log system + * Created by vzool on 1/20/17. + */ + +public class LogNetUtil { + + final static String TAG = "AFWall-LogNetUtil"; + + public static class NetTask extends AsyncTask { + long start_time; + OnFinishRequest onFinishRequest; + MaterialDialog progress; + Context context; + String output_result = ""; + + private static final String PING_CMD = "%s ping -w 1 -W %d %s"; + + public NetTask(Context context) { + this.context = context; + } + + long finish_time() { + return System.currentTimeMillis() - start_time; + } + + public NetTask setOnFinishRequest(OnFinishRequest when) { + onFinishRequest = when; + return this; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + progress = new MaterialDialog.Builder(context) + .title(R.string.searching) + .content(R.string.looking_for_data) + .progress(true, 0) + .progressIndeterminateStyle(true) + .show(); + } + + @Override + protected String doInBackground(NetParam... params) { + start_time = System.currentTimeMillis(); + try { + switch (params[0].type) { + case PING: + // Ping + try { + String shell_result = ""; + String command = ""; + try { + // This command needs permission to allow + // AFWall+ itself to has access to network + // to work probably + command = String.format(PING_CMD, "", G.logPingTimeout(), params[0].address); + Log.d(TAG, "Execute CMD: " + command); + Process process = Runtime.getRuntime().exec(command); + process.waitFor(); + Log.d(TAG, "CMD exit code: " + process.exitValue()); + // check if ping command does not encounter any errors + if (process.exitValue() == 0) { + //The ping was succeeded. + shell_result = parse(process); + } else { + shell_result = su_busyboox_ping(params[0].address); + } + } catch (Exception ping_cmd_ex) { + Log.e(TAG, "Exception(00): " + ping_cmd_ex.getMessage()); + shell_result = su_busyboox_ping(params[0].address); + } + return shell_result; + } catch (Exception eex) { + Log.e(TAG, "Exception(01): " + eex.getMessage()); + // final choice is to use Android API + return normal_ping(params[0].address); + } + case RESOLVE: + // Resolve + try { + InetAddress inetAddress = InetAddress.getByName(params[0].address); + // String name = Address.getHostName(InetAddress.getByName(params[0].address)); + if (inetAddress != null) { + return inetAddress.getHostName(); + } else { + return ""; + } + } catch (UnknownHostException ex) { + Log.e(TAG, "Exception(02): " + ex.getMessage()); + return String.format("Currently can not resolve Host for IP(%s), timeout: %d ms", params[0].address, finish_time()); + } + } + } catch (Exception e) { + Log.e(TAG, "Exception(03): " + e.getMessage()); + } + return context.getString(R.string.error_or_unknown_category); + } + + + @Override + protected void onPostExecute(String s) { + super.onPostExecute(s); + + if (onFinishRequest != null) { + onFinishRequest.then(s); + } + + try { + if ((progress != null) && progress.isShowing()) { + progress.dismiss(); + } + } catch (IllegalArgumentException e) { + Log.e(TAG, e.getMessage()); + // Handle or log or ignore + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + // Handle or log or ignore + } finally { + progress = null; + } + output_result = s; + new MaterialDialog.Builder(context) + .content(s) + .title(R.string.result) + .neutralText(R.string.OK) + .positiveText(R.string.copy_text) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + Api.copyToClipboard(context, output_result); + Api.toast(context, context.getString(R.string.result_copied_to_clipboard)); + } + }).show(); + } + + private String normal_ping(String ip) { + String result = ""; + try { + if (InetAddress.getByAddress(ip.getBytes()).isReachable(G.logPingTimeout() * 1000)) { // isReachable expect timeout in millisecond + result = String.format(context.getString(R.string.reachable_timeout), finish_time()); + } + } catch (Exception e) { + Log.e(TAG, "Exception(04): " + e.getMessage()); + result = String.format("Currently IP(%s) is not Reachable, timeout: %d ms", ip, finish_time()); + } + return result; + } + + private String su_busyboox_ping(String ip) { + // using libsuperuser to perform ping by Busybox, + // This will need permission in AFWall+ + // "0:(root) Apps running as root" + String result = ""; + String command = String.format(PING_CMD, Api.getBusyBoxPath(context, true), G.logPingTimeout(), ip); + Log.d(TAG, "Execute CMD: " + command); + result = parse(Shell.run("su", new String[]{command}, null, true)); + if (result.isEmpty()) { + + return context.getString(R.string.network_connection_not_available); + } + return result; + } + + private String parse(List output) { + StringBuilder resultBuilder = new StringBuilder(); + for (String line : output) { + resultBuilder.append(line).append(" "); + } + String result = resultBuilder.toString(); + if (result.isEmpty()) { + return context.getString(R.string.output_is_empty); + } + return result; + } + + private String parse(Process process) { + try { + BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(process.getInputStream())); + // Grab the results + StringBuilder log = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + log.append(line).append("\n"); + } + return log.toString(); + } catch (IOException e) { + Log.e(TAG, "Exception(05): " + e.getMessage()); + } + return context.getString(R.string.output_is_empty); + } + } + + public enum JobType { + PING, + RESOLVE + } + + public static class NetParam { + + public JobType type; + public String address; + + public NetParam(JobType type, String address) { + this.type = type; + this.address = address; + } + } + + public interface OnFinishRequest { + void then(String result); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java b/app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java new file mode 100644 index 0000000..ff2202b --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/PackageComparator.java @@ -0,0 +1,36 @@ +package dev.ukanth.ufirewall.util; + +import java.util.Comparator; + +import dev.ukanth.ufirewall.Api; + +/** + * Created by ukanth on 11/8/15. + */ +public class PackageComparator implements Comparator { + + @Override + public int compare(Api.PackageInfoData o1, Api.PackageInfoData o2) { + if (o1.firstseen != o2.firstseen) { + return (o1.firstseen ? -1 : 1); + } + boolean o1_selected = o1.selected_3g || o1.selected_wifi || o1.selected_roam || + o1.selected_vpn || o1.selected_tether || o1.selected_lan || o1.selected_tor; + boolean o2_selected = o2.selected_3g || o2.selected_wifi || o2.selected_roam || + o2.selected_vpn || o2.selected_tether || o2.selected_lan || o2.selected_tor; + + if (o1_selected == o2_selected) { + switch (G.sortBy()) { + case "s0": + return String.CASE_INSENSITIVE_ORDER.compare(o1.names.get(0), o2.names.get(0)); + case "s1": + return (o1.installTime > o2.installTime) ? -1: (o1.installTime < o2.installTime) ? 1 : 0; + case "s2": + return (o2.uid > o1.uid) ? -1: (o2.uid < o1.uid) ? 0 : 1; + } + } + if (o1_selected) + return -1; + return 1; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/Rule.java b/app/src/main/java/dev/ukanth/ufirewall/util/Rule.java new file mode 100644 index 0000000..f470124 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/Rule.java @@ -0,0 +1,65 @@ +package dev.ukanth.ufirewall.util; + +import java.util.List; + +/** + * Created by ukanth on 22/11/16. + */ + +public class Rule { + + String name; + String desc; + List ipv4On; + List ipv4Off; + List ipv6On; + List ipv6Off; + + public List getIpv4On() { + return ipv4On; + } + + public void setIpv4On(List ipv4On) { + this.ipv4On = ipv4On; + } + + public List getIpv4Off() { + return ipv4Off; + } + + public void setIpv4Off(List ipv4Off) { + this.ipv4Off = ipv4Off; + } + + public List getIpv6On() { + return ipv6On; + } + + public void setIpv6On(List ipv6On) { + this.ipv6On = ipv6On; + } + + public List getIpv6Off() { + return ipv6Off; + } + + public void setIpv6Off(List ipv6Off) { + this.ipv6Off = ipv6Off; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/SecureCrypto.java b/app/src/main/java/dev/ukanth/ufirewall/util/SecureCrypto.java new file mode 100644 index 0000000..45ab209 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/SecureCrypto.java @@ -0,0 +1,248 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Base64; +import android.util.Log; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.security.spec.KeySpec; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Secure cryptographic utility with backward compatibility for DES-encrypted passwords. + * + * This class provides: + * - Modern AES-256-GCM encryption for new passwords + * - Backward compatibility for existing DES-encrypted passwords + * - Automatic migration from DES to AES when passwords are verified + * - Secure key derivation using PBKDF2 + */ +public class SecureCrypto { + private static final String TAG = "SecureCrypto"; + + // Modern encryption constants + private static final String AES_ALGORITHM = "AES/GCM/NoPadding"; + private static final String KEY_DERIVATION = "PBKDF2WithHmacSHA256"; + private static final int AES_KEY_LENGTH = 256; + private static final int GCM_IV_LENGTH = 12; + private static final int GCM_TAG_LENGTH = 16; + private static final int PBKDF2_ITERATIONS = 100000; + private static final int SALT_LENGTH = 32; + + // Legacy DES constants (for backward compatibility) + private static final String DES_ALGORITHM = "DES"; + private static final String CHARSET_NAME = "UTF-8"; + private static final int BASE64_MODE = Base64.DEFAULT; + + // Version identifiers for encrypted data + private static final String AES_PREFIX = "AES:"; + private static final String DES_PREFIX = "DES:"; + + private static final String PREF_ENCRYPTION_VERSION = "encryption_version"; + private static final String PREF_PASSWORD_SALT = "password_salt"; + private static final int ENCRYPTION_VERSION_AES = 2; + private static final int ENCRYPTION_VERSION_DES = 1; + + /** + * Encrypt data using modern AES-256-GCM encryption + */ + public static String encryptSecure(Context context, String masterKey, String data) { + if (masterKey == null || data == null) { + return null; + } + + try { + SharedPreferences prefs = context.getSharedPreferences("secure_crypto", Context.MODE_PRIVATE); + + // Generate or retrieve salt + String saltBase64 = prefs.getString(PREF_PASSWORD_SALT, null); + byte[] salt; + if (saltBase64 == null) { + salt = new byte[SALT_LENGTH]; + new SecureRandom().nextBytes(salt); + saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP); + prefs.edit().putString(PREF_PASSWORD_SALT, saltBase64).apply(); + } else { + salt = Base64.decode(saltBase64, Base64.NO_WRAP); + } + + // Derive key using PBKDF2 + SecretKey key = deriveKey(masterKey, salt); + + // Generate random IV + byte[] iv = new byte[GCM_IV_LENGTH]; + new SecureRandom().nextBytes(iv); + + // Encrypt data + Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); + cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec); + + byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + + // Combine IV and encrypted data + byte[] combined = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length); + + // Mark encryption version and store + prefs.edit().putInt(PREF_ENCRYPTION_VERSION, ENCRYPTION_VERSION_AES).apply(); + + return AES_PREFIX + Base64.encodeToString(combined, Base64.NO_WRAP); + + } catch (Exception e) { + Log.e(TAG, "AES encryption failed", e); + return null; + } + } + + /** + * Decrypt data - handles both AES and legacy DES formats + */ + public static String decryptSecure(Context context, String masterKey, String encryptedData) { + if (masterKey == null || encryptedData == null) { + return null; + } + + try { + if (encryptedData.startsWith(AES_PREFIX)) { + // Modern AES decryption + return decryptAES(context, masterKey, encryptedData.substring(AES_PREFIX.length())); + } else if (encryptedData.startsWith(DES_PREFIX)) { + // Legacy DES decryption (marked format) + return decryptDES(masterKey, encryptedData.substring(DES_PREFIX.length())); + } else { + // Assume legacy DES format (no prefix) + return decryptDES(masterKey, encryptedData); + } + } catch (Exception e) { + Log.e(TAG, "Decryption failed", e); + return null; + } + } + + /** + * Decrypt using modern AES-256-GCM + */ + private static String decryptAES(Context context, String masterKey, String encryptedData) throws Exception { + SharedPreferences prefs = context.getSharedPreferences("secure_crypto", Context.MODE_PRIVATE); + + // Get salt + String saltBase64 = prefs.getString(PREF_PASSWORD_SALT, null); + if (saltBase64 == null) { + throw new IllegalStateException("Salt not found for AES decryption"); + } + byte[] salt = Base64.decode(saltBase64, Base64.NO_WRAP); + + // Derive key + SecretKey key = deriveKey(masterKey, salt); + + // Decode encrypted data + byte[] combined = Base64.decode(encryptedData, Base64.NO_WRAP); + + // Extract IV and encrypted data + byte[] iv = new byte[GCM_IV_LENGTH]; + byte[] encrypted = new byte[combined.length - GCM_IV_LENGTH]; + System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH); + System.arraycopy(combined, GCM_IV_LENGTH, encrypted, 0, encrypted.length); + + // Decrypt + Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); + cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec); + + byte[] decrypted = cipher.doFinal(encrypted); + return new String(decrypted, StandardCharsets.UTF_8); + } + + /** + * Legacy DES decryption for backward compatibility + */ + private static String decryptDES(String masterKey, String encryptedData) throws Exception { + byte[] dataBytes = Base64.decode(encryptedData, BASE64_MODE); + DESKeySpec desKeySpec = new DESKeySpec(masterKey.getBytes(CHARSET_NAME)); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(DES_ALGORITHM); + SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec); + Cipher cipher = Cipher.getInstance(DES_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + byte[] dataBytesDecrypted = cipher.doFinal(dataBytes); + return new String(dataBytesDecrypted, CHARSET_NAME); + } + + /** + * Derive AES key using PBKDF2 + */ + private static SecretKey deriveKey(String password, byte[] salt) throws Exception { + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, AES_KEY_LENGTH); + SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION); + byte[] keyBytes = factory.generateSecret(spec).getEncoded(); + return new SecretKeySpec(keyBytes, "AES"); + } + + /** + * Check if data is encrypted with legacy DES + */ + public static boolean isLegacyEncryption(String encryptedData) { + return encryptedData != null && + !encryptedData.startsWith(AES_PREFIX) && + !encryptedData.startsWith(DES_PREFIX); + } + + /** + * Migrate password from DES to AES encryption + * This should be called when a legacy password is successfully verified + */ + public static String migrateToAES(Context context, String masterKey, String plaintext) { + Log.i(TAG, "Migrating password from DES to AES encryption"); + return encryptSecure(context, masterKey, plaintext); + } + + /** + * Get current encryption version + */ + public static int getCurrentEncryptionVersion(Context context) { + SharedPreferences prefs = context.getSharedPreferences("secure_crypto", Context.MODE_PRIVATE); + return prefs.getInt(PREF_ENCRYPTION_VERSION, ENCRYPTION_VERSION_DES); + } + + /** + * Validate that the crypto system is working correctly + */ + public static boolean validateCrypto(Context context) { + try { + String testData = "AFWall+ Security Test"; + String testKey = "TestKey123"; + + String encrypted = encryptSecure(context, testKey, testData); + if (encrypted == null) return false; + + String decrypted = decryptSecure(context, testKey, encrypted); + return testData.equals(decrypted); + + } catch (Exception e) { + Log.e(TAG, "Crypto validation failed", e); + return false; + } + } + + /** + * Clear all crypto preferences (for testing or reset purposes) + */ + public static void clearCryptoPreferences(Context context) { + SharedPreferences prefs = context.getSharedPreferences("secure_crypto", Context.MODE_PRIVATE); + prefs.edit().clear().apply(); + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/SecurePasswordManager.java b/app/src/main/java/dev/ukanth/ufirewall/util/SecurePasswordManager.java new file mode 100644 index 0000000..436776a --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/SecurePasswordManager.java @@ -0,0 +1,160 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Context; +import android.util.Log; + +/** + * Secure password manager that handles encryption, storage, and migration + * with backward compatibility for existing DES-encrypted passwords + */ +public class SecurePasswordManager { + private static final String TAG = "SecurePasswordManager"; + private static final String MASTER_KEY = "AFW@LL_P@SSWORD_PR0T3CTI0N"; + + /** + * Store a password securely using modern AES encryption + * + * @param context Application context + * @param password Plain text password to store + * @return true if password was stored successfully + */ + public static boolean storePasswordSecure(Context context, String password) { + if (password == null || password.isEmpty()) { + return false; + } + + try { + String encrypted = SecureCrypto.encryptSecure(context, MASTER_KEY, password); + if (encrypted != null) { + // Use G's helper method to store password + G.profile_pwd(encrypted); + G.gPrefs.edit() + .putBoolean("enc", true) + .putBoolean("secure_enc", true) // Flag for new encryption + .apply(); + Log.i(TAG, "Password stored with secure encryption"); + return true; + } + } catch (Exception e) { + Log.e(TAG, "Failed to store password securely", e); + } + return false; + } + + /** + * Verify a password against stored encrypted password with automatic migration + * + * @param context Application context + * @param enteredPassword Password entered by user + * @return true if password matches + */ + public static boolean verifyPassword(Context context, String enteredPassword) { + if (enteredPassword == null) { + return false; + } + + String storedPassword = G.profile_pwd(); + if (storedPassword == null || storedPassword.isEmpty()) { + return false; + } + + try { + if (G.isEnc()) { + // Check if using new secure encryption + if (G.gPrefs.getBoolean("secure_enc", false)) { + String decrypted = SecureCrypto.decryptSecure(context, MASTER_KEY, storedPassword); + return enteredPassword.equals(decrypted); + } else { + // Try new secure decryption first (for migrated passwords) + String decrypted = SecureCrypto.decryptSecure(context, MASTER_KEY, storedPassword); + if (decrypted != null && enteredPassword.equals(decrypted)) { + return true; + } + + // Fallback to legacy DES decryption + decrypted = dev.ukanth.ufirewall.Api.unhideCrypt(MASTER_KEY, storedPassword); + if (decrypted != null && enteredPassword.equals(decrypted)) { + // Auto-migrate to secure encryption + if (migrateToSecureEncryption(context, enteredPassword)) { + Log.i(TAG, "Successfully migrated password from DES to AES"); + } + return true; + } + } + } else { + // Plain text password - migrate to secure encryption + if (enteredPassword.equals(storedPassword)) { + if (migrateToSecureEncryption(context, enteredPassword)) { + Log.i(TAG, "Successfully migrated plaintext password to AES"); + } + return true; + } + } + } catch (Exception e) { + Log.e(TAG, "Error during password verification", e); + } + + return false; + } + + /** + * Migrate existing password to secure AES encryption + */ + private static boolean migrateToSecureEncryption(Context context, String plainPassword) { + try { + return storePasswordSecure(context, plainPassword); + } catch (Exception e) { + Log.e(TAG, "Failed to migrate password to secure encryption", e); + return false; + } + } + + /** + * Check if password is encrypted with legacy DES + */ + public static boolean isLegacyEncryption() { + return G.isEnc() && !G.gPrefs.getBoolean("secure_enc", false); + } + + /** + * Get encryption status information for debugging + */ + public static String getEncryptionStatus(Context context) { + StringBuilder status = new StringBuilder(); + status.append("Encrypted: ").append(G.isEnc()).append("\n"); + status.append("Secure encryption: ").append(G.gPrefs.getBoolean("secure_enc", false)).append("\n"); + status.append("Legacy encryption: ").append(isLegacyEncryption()).append("\n"); + + if (G.isEnc()) { + String stored = G.profile_pwd(); + status.append("Uses AES prefix: ").append(stored != null && stored.startsWith("AES:")).append("\n"); + status.append("Crypto validation: ").append(SecureCrypto.validateCrypto(context)).append("\n"); + } + + return status.toString(); + } + + /** + * Force password re-encryption (for testing or security updates) + */ + public static boolean reencryptPassword(Context context, String currentPassword) { + if (!verifyPassword(context, currentPassword)) { + Log.w(TAG, "Cannot re-encrypt: current password verification failed"); + return false; + } + + return storePasswordSecure(context, currentPassword); + } + + /** + * Clear all password data (for reset/logout) + */ + public static void clearPassword() { + G.profile_pwd(""); // Clear password using G's method + G.gPrefs.edit() + .remove("enc") + .remove("secure_enc") + .apply(); + Log.i(TAG, "Password data cleared"); + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java b/app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java new file mode 100644 index 0000000..71cc501 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/SecurityUtil.java @@ -0,0 +1,202 @@ +package dev.ukanth.ufirewall.util; + +import static dev.ukanth.ufirewall.util.G.isDonate; +import static haibison.android.lockpattern.LockPatternActivity.ACTION_COMPARE_PATTERN; +import static haibison.android.lockpattern.LockPatternActivity.EXTRA_PATTERN; + +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.text.InputType; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.RequiresApi; + +import com.afollestad.materialdialogs.MaterialDialog; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.R; +import haibison.android.lockpattern.LockPatternActivity; + +/** + * Created by ukanth on 17/3/18. + */ + +public class SecurityUtil { + + private final Context context; + + public static final int REQ_ENTER_PATTERN = 9755; + public static final int LOCK_VERIFICATION = 1212; + + private final Activity activity; + + public SecurityUtil(Activity activity) { + this.activity = activity; + this.context = activity.getApplicationContext(); + } + + private void deviceCheck() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if ((G.isDoKey(context) || isDonate())) { + KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + if (keyguardManager.isKeyguardSecure()) { + Intent createConfirmDeviceCredentialIntent = keyguardManager.createConfirmDeviceCredentialIntent(null, null); + if (createConfirmDeviceCredentialIntent != null) { + try { + activity.startActivityForResult(createConfirmDeviceCredentialIntent, LOCK_VERIFICATION); + } catch (ActivityNotFoundException e) { + } + } + } else { + Toast.makeText(activity, context.getText(R.string.android_version), Toast.LENGTH_SHORT).show(); + } + } else { + Api.donateDialog(activity, true); + } + } + } + + public boolean passCheck() { + if (G.enableDeviceCheck()) { + deviceCheck(); + return true; + } else { + switch (G.protectionLevel()) { + case "p0": + return true; + case "p1": + final String oldpwd = G.profile_pwd(); + if (oldpwd.length() == 0) { + return true; + } else { + // Check the password + requestPassword(); + return true; + } + case "p2": + final String pwd = G.sPrefs.getString("LockPassword", ""); + if (pwd.length() == 0) { + return true; + } else { + requestPassword(); + return true; + } + case "p3": + /* TODO: Testing is required before enabling this. + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){ + if (BiometricUtil.isAndroidSupport() && G.isFingerprintEnabled()) { + requestFingerprintQ(); + return true; + } + } else {*/ + if (FingerprintUtil.isAndroidSupport() && G.isFingerprintEnabled()) { + requestFingerprint(); + return true; + } + //} + break; + } + } + return false; + } + + + public boolean isPasswordProtected() { + return (G.enableDeviceCheck() || (G.protectionLevel().equals("p1") && G.profile_pwd().length() > 0) + || G.sPrefs.getString("LockPassword", "").length() > 0 || (FingerprintUtil.isAndroidSupport() && G.isFingerprintEnabled())); + + } + + @RequiresApi(api = Build.VERSION_CODES.M) + private void requestFingerprint() { + FingerprintUtil.FingerprintDialog dialog = new FingerprintUtil.FingerprintDialog(activity); + dialog.setOnFingerprintFailureListener(() -> { + gracefulShutdown(); + }); + dialog.show(); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + private void requestFingerprintQ() { + BiometricUtil.FingerprintDialog dialog = new BiometricUtil.FingerprintDialog(activity); + dialog.setOnFingerprintFailureListener(() -> { + gracefulShutdown(); + }); + dialog.show(); + } + + private void requestPassword() { + switch (G.protectionLevel()) { + case "p1": + + MaterialDialog.Builder builder = new MaterialDialog.Builder(activity).cancelable(false) + .title(R.string.pass_titleget).autoDismiss(false) + .inputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD) + .positiveText(R.string.submit) + .negativeText(R.string.Cancel) + .onNegative((dialog, which) -> { + gracefulShutdown(); + }) + .input(R.string.enterpass, R.string.password_empty, (dialog, input) -> { + String pass = InputValidator.sanitizeString(input.toString(), 256); + if (pass == null) { + Api.toast(activity, context.getString(R.string.wrong_password)); + return; + } + + // Use secure password manager for verification and auto-migration + boolean isAllowed = SecurePasswordManager.verifyPassword(context, pass); + + if (isAllowed) { + dialog.dismiss(); + } else { + Api.toast(activity, context.getString(R.string.wrong_password)); + } + }); + + builder.show(); + break; + case "p2": + Intent intent = new Intent(ACTION_COMPARE_PATTERN, null, context, LockPatternActivity.class); + String savedPattern = G.sPrefs.getString("LockPassword", ""); + intent.putExtra(EXTRA_PATTERN, savedPattern.toCharArray()); + activity.startActivityForResult(intent, REQ_ENTER_PATTERN); + break; + } + + } + + /** + * Perform graceful shutdown instead of abrupt process termination + * This ensures proper cleanup and prevents firewall rules from being left in inconsistent state + */ + private void gracefulShutdown() { + try { + // Give some time for any pending operations to complete + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + try { + // Attempt to save any pending state or cleanup + activity.moveTaskToBack(true); + activity.finishAndRemoveTask(); + } catch (Exception e) { + // If graceful methods fail, fall back to finish() + activity.finish(); + } finally { + // Only use process kill as absolute last resort after cleanup attempt + new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { + android.os.Process.killProcess(android.os.Process.myPid()); + }, 500); // 500ms delay to allow cleanup + } + }); + } catch (Exception e) { + Log.e("SecurityUtil", "Error during graceful shutdown", e); + // Emergency fallback + activity.finish(); + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabLayout.java b/app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabLayout.java new file mode 100644 index 0000000..75dcf1c --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabLayout.java @@ -0,0 +1,310 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import dev.ukanth.ufirewall.R; + +/** + * Created by ukanth on 2/5/15. + */ +public class SlidingTabLayout extends HorizontalScrollView { + + /**G + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + int getIndicatorColor(int position); + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + + private final int mTitleOffset; + + private int mTabViewLayoutId; + private int mTabViewTextViewId; + private boolean mDistributeEvenly; + + private final Context context; + private ViewPager mViewPager; + private final SparseArray mContentDescriptions = new SparseArray(); + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private int count; + private final SlidingTabStrip mTabStrip; + + public SlidingTabLayout(Context context) { + this(context, null); + } + + public SlidingTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new SlidingTabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple custmisation then you can use + * {@link #setSelectedIndicatorColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + public void setDistributeEvenly(boolean distributeEvenly) { + mDistributeEvenly = distributeEvenly; + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are + * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + mTabViewLayoutId = layoutResId; + mTabViewTextViewId = textViewId; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + if (viewPager != null) { + viewPager.setOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(Context context) { + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + textView.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + textView.setAllCaps(true); + + int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); + textView.setPadding(padding, padding, padding, padding); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final View.OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + TextView tabTitleView = null; + + if (mTabViewLayoutId != 0) { + // If there is a custom tab view layout id set, try and inflate it + tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, + false); + tabTitleView = tabView.findViewById(mTabViewTextViewId); + } + + if (tabView == null) { + tabView = createDefaultTabView(getContext()); + } + + if (tabTitleView == null && tabView instanceof TextView) { + tabTitleView = (TextView) tabView; + } + + if (mDistributeEvenly) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams(); + lp.width = 0; + lp.weight = 1; + } + + tabTitleView.setText(adapter.getPageTitle(i)); + tabView.setOnClickListener(tabClickListener); + String desc = mContentDescriptions.get(i, null); + if (desc != null) { + tabView.setContentDescription(desc); + } + + mTabStrip.addView(tabView); + if (i == mViewPager.getCurrentItem()) { + tabView.setSelected(true); + } + + tabTitleView.setTextColor(ContextCompat.getColor(context,R.color.white)); + tabTitleView.setTextSize(14); + } + } + + public void setContentDescription(int i, String desc) { + mContentDescriptions.put(i, desc); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) + ? (int) (positionOffset * selectedTitle.getWidth()) + : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, + positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + mTabStrip.getChildAt(i).setSelected(position == i); + } + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements View.OnClickListener { + @Override + public void onClick(View v) { + /*count++; + if(!G.isDo()) { + if(count < 20 && count > 15) { + Toast.makeText(context, (7 - count) + context.getString(R.string.unlock_donate), Toast.LENGTH_SHORT).show(); + count++; + } + if(count >= 20){ + G.isDo(true); + Toast.makeText(context, context.getString(R.string.donate_support), Toast.LENGTH_LONG).show(); + } + } else { + Toast.makeText(context, context.getString(R.string.donate_support), Toast.LENGTH_LONG).show(); + }*/ + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabStrip.java b/app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabStrip.java new file mode 100644 index 0000000..1a51221 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/SlidingTabStrip.java @@ -0,0 +1,154 @@ +package dev.ukanth.ufirewall.util; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +/** + * Created by ukanth on 2/5/15. + */ +public class SlidingTabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private int mSelectedPosition; + private float mSelectionOffset; + + private SlidingTabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + SlidingTabStrip(Context context) { + this(context, null); + } + + SlidingTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + //context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + } + + void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { + private int[] mIndicatorColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + } +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/UidCorrelator.java b/app/src/main/java/dev/ukanth/ufirewall/util/UidCorrelator.java new file mode 100644 index 0000000..b277610 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/UidCorrelator.java @@ -0,0 +1,277 @@ +/** + * Enhanced UID correlation system for AFWall+ + * Attempts to resolve Unknown UID (-100) entries by correlating + * netfilter logs with active network connections + * + * Copyright (C) 2024 AFWall+ Contributors + */ +package dev.ukanth.ufirewall.util; + +import android.util.Log; +import com.topjohnwu.superuser.Shell; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class UidCorrelator { + private static final String TAG = "UidCorrelator"; + + // Cache active connections for correlation + private static final Map activeConnections = new ConcurrentHashMap<>(); + private static final Map recentConnections = new ConcurrentHashMap<>(); + private static long lastRefresh = 0; + private static final long REFRESH_INTERVAL = 5000; // 5 seconds + private static final long CORRELATION_WINDOW = 10000; // 10 seconds + + public static class ConnectionInfo { + public final int uid; + public final String localAddress; + public final String remoteAddress; + public final int localPort; + public final int remotePort; + public final String protocol; + public final long timestamp; + + public ConnectionInfo(int uid, String localAddr, String remoteAddr, + int localPort, int remotePort, String protocol) { + this.uid = uid; + this.localAddress = localAddr; + this.remoteAddress = remoteAddr; + this.localPort = localPort; + this.remotePort = remotePort; + this.protocol = protocol; + this.timestamp = System.currentTimeMillis(); + } + + public String getConnectionKey() { + return protocol + ":" + remoteAddress + ":" + remotePort; + } + } + + /** + * Attempt to correlate unknown UID with active/recent connections + * + * @param srcIp Source IP from netfilter log + * @param dstIp Destination IP from netfilter log + * @param dstPort Destination port from netfilter log + * @param srcPort Source port from netfilter log + * @param protocol Protocol (TCP/UDP) + * @param logTimestamp Timestamp of the log entry + * @return UID if found, -100 if still unknown + */ + public static int correlateUid(String srcIp, String dstIp, int dstPort, + int srcPort, String protocol, long logTimestamp) { + + refreshConnectionCache(); + + // Try exact match first (outbound connection) + String connectionKey = protocol.toUpperCase() + ":" + dstIp + ":" + dstPort; + ConnectionInfo conn = activeConnections.get(connectionKey); + + // Also try reverse lookup (for return traffic where src/dst are swapped) + if (conn == null) { + String reverseKey = protocol.toUpperCase() + ":" + srcIp + ":" + srcPort; + conn = activeConnections.get(reverseKey); + } + + if (conn != null && isWithinTimeWindow(conn.timestamp, logTimestamp)) { + Log.d(TAG, "Found exact match for " + connectionKey + " -> UID " + conn.uid); + return conn.uid; + } + + // Try recent connections cache + Integer recentUid = recentConnections.get(connectionKey); + if (recentUid != null) { + Log.d(TAG, "Found recent connection for " + connectionKey + " -> UID " + recentUid); + return recentUid; + } + + // Fallback: scan all connections for partial matches + for (ConnectionInfo connection : activeConnections.values()) { + if (isPartialMatch(connection, srcIp, dstIp, dstPort, srcPort, protocol, logTimestamp)) { + Log.d(TAG, "Found partial match -> UID " + connection.uid); + // Cache for future lookups + recentConnections.put(connectionKey, connection.uid); + return connection.uid; + } + } + + Log.d(TAG, "No correlation found for " + connectionKey); + return -100; // Still unknown + } + + /** + * Refresh the connection cache by parsing /proc/net files + */ + private static void refreshConnectionCache() { + long now = System.currentTimeMillis(); + if (now - lastRefresh < REFRESH_INTERVAL) { + return; // Cache still fresh + } + + try { + // Clear old data + activeConnections.clear(); + cleanupOldRecentConnections(now); + + // Parse TCP connections + parseNetworkConnections("/proc/net/tcp", "TCP"); + parseNetworkConnections("/proc/net/tcp6", "TCP"); + + // Parse UDP connections + parseNetworkConnections("/proc/net/udp", "UDP"); + parseNetworkConnections("/proc/net/udp6", "UDP"); + + lastRefresh = now; + Log.d(TAG, "Refreshed connection cache: " + activeConnections.size() + " active connections"); + + } catch (Exception e) { + Log.e(TAG, "Error refreshing connection cache", e); + } + } + + /** + * Parse network connection files from /proc/net + */ + private static void parseNetworkConnections(String filePath, String protocol) { + try { + Shell.Result result = Shell.cmd("cat " + filePath).exec(); + if (!result.isSuccess()) { + return; + } + + String output = String.join("\n", result.getOut()); + BufferedReader reader = new BufferedReader(new StringReader(output)); + String line; + boolean firstLine = true; + + while ((line = reader.readLine()) != null) { + if (firstLine) { + firstLine = false; + continue; // Skip header + } + + ConnectionInfo conn = parseConnectionLine(line, protocol); + if (conn != null && conn.uid > 0) { + activeConnections.put(conn.getConnectionKey(), conn); + } + } + + } catch (Exception e) { + Log.w(TAG, "Failed to parse " + filePath, e); + } + } + + /** + * Parse a single line from /proc/net/tcp or /proc/net/udp + * Format: sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + */ + private static ConnectionInfo parseConnectionLine(String line, String protocol) { + try { + String[] parts = line.trim().split("\\s+"); + if (parts.length < 8) { + return null; + } + + // Parse local address (IP:PORT in hex) + String[] localAddr = parts[1].split(":"); + String localIp = hexToIp(localAddr[0]); + int localPort = Integer.parseInt(localAddr[1], 16); + + // Parse remote address + String[] remoteAddr = parts[2].split(":"); + String remoteIp = hexToIp(remoteAddr[0]); + int remotePort = Integer.parseInt(remoteAddr[1], 16); + + // Get UID (column 7) + int uid = Integer.parseInt(parts[7]); + + // Only interested in established connections or UDP sockets + // TCP state 01 = ESTABLISHED, for UDP we take all + if (protocol.equals("TCP")) { + String state = parts[3]; + if (!"01".equals(state)) { + return null; // Not established + } + } + + return new ConnectionInfo(uid, localIp, remoteIp, localPort, remotePort, protocol); + + } catch (Exception e) { + Log.w(TAG, "Failed to parse connection line: " + line, e); + return null; + } + } + + /** + * Convert hex IP address to dotted decimal + * /proc/net format uses little-endian hex representation + */ + private static String hexToIp(String hexIp) { + if (hexIp.length() == 8) { + // IPv4 - /proc/net uses little-endian format + long ip = Long.parseLong(hexIp, 16); + // Convert from little-endian: reverse byte order + return (ip & 0xFF) + "." + ((ip >> 8) & 0xFF) + "." + + ((ip >> 16) & 0xFF) + "." + ((ip >> 24) & 0xFF); + } else if (hexIp.length() == 32) { + // IPv6 - check if it's an IPv4-mapped IPv6 address + // Format: 0000000000000000FFFF0000XXXXXXXX where XXXXXXXX is the IPv4 in hex + if (hexIp.startsWith("0000000000000000FFFF0000")) { + // Extract the IPv4 part (last 8 characters) + String ipv4Hex = hexIp.substring(24); + long ip = Long.parseLong(ipv4Hex, 16); + // Convert from big-endian for IPv6 mapped addresses + return ((ip >> 24) & 0xFF) + "." + ((ip >> 16) & 0xFF) + "." + + ((ip >> 8) & 0xFF) + "." + (ip & 0xFF); + } + } + // IPv6 or unknown format - return as is for now + return hexIp; + } + + /** + * Check if connection matches the netfilter log entry + */ + private static boolean isPartialMatch(ConnectionInfo conn, String srcIp, String dstIp, + int dstPort, int srcPort, String protocol, long logTime) { + + // Protocol must match + if (!conn.protocol.equalsIgnoreCase(protocol)) { + return false; + } + + // Time window check + if (!isWithinTimeWindow(conn.timestamp, logTime)) { + return false; + } + + // Check if this is an outbound connection matching the log + boolean outboundMatch = conn.remoteAddress.equals(dstIp) && + conn.remotePort == dstPort; + + // Check if local port matches (if available) + boolean portMatch = srcPort == 0 || conn.localPort == srcPort; + + return outboundMatch && portMatch; + } + + private static boolean isWithinTimeWindow(long connTime, long logTime) { + return Math.abs(connTime - logTime) <= CORRELATION_WINDOW; + } + + private static void cleanupOldRecentConnections(long now) { + // Remove entries older than correlation window + Iterator> iterator = recentConnections.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (now - lastRefresh > CORRELATION_WINDOW) { + iterator.remove(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/UidResolver.java b/app/src/main/java/dev/ukanth/ufirewall/util/UidResolver.java new file mode 100644 index 0000000..241109f --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/UidResolver.java @@ -0,0 +1,667 @@ +/** + * UID Resolution Helper - Provides fallback mechanisms for identifying UIDs + * + * Copyright (C) 2024 AFWall+ Contributors + * + * 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. + */ + +package dev.ukanth.ufirewall.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; +import android.util.LruCache; +import android.util.SparseArray; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import dev.ukanth.ufirewall.R; + +public class UidResolver { + + private static final String TAG = "AFWall"; + + // System UID database - well-known Android system UIDs + private static final SparseArray SYSTEM_UIDS = new SparseArray<>(); + + static { + // Core system UIDs + SYSTEM_UIDS.put(0, "root"); + SYSTEM_UIDS.put(1000, "system"); + SYSTEM_UIDS.put(1001, "radio"); + SYSTEM_UIDS.put(1002, "bluetooth"); + SYSTEM_UIDS.put(1003, "graphics"); + SYSTEM_UIDS.put(1004, "input"); + SYSTEM_UIDS.put(1005, "audio"); + SYSTEM_UIDS.put(1006, "camera"); + SYSTEM_UIDS.put(1007, "log"); + SYSTEM_UIDS.put(1008, "compass"); + SYSTEM_UIDS.put(1009, "mount"); + SYSTEM_UIDS.put(1010, "wifi"); + SYSTEM_UIDS.put(1011, "adb"); + SYSTEM_UIDS.put(1012, "install"); + SYSTEM_UIDS.put(1013, "media"); + SYSTEM_UIDS.put(1014, "dhcp"); + SYSTEM_UIDS.put(1015, "sdcard_rw"); + SYSTEM_UIDS.put(1016, "vpn"); + SYSTEM_UIDS.put(1017, "keystore"); + SYSTEM_UIDS.put(1018, "usb"); + SYSTEM_UIDS.put(1019, "drm"); + SYSTEM_UIDS.put(1020, "mdnsr"); + SYSTEM_UIDS.put(1021, "gps"); + SYSTEM_UIDS.put(1023, "media_rw"); + SYSTEM_UIDS.put(1024, "mtp"); + SYSTEM_UIDS.put(1025, "unused1"); + SYSTEM_UIDS.put(1026, "unused2"); + SYSTEM_UIDS.put(1027, "unused3"); + SYSTEM_UIDS.put(1028, "unused4"); + SYSTEM_UIDS.put(1029, "clat"); + SYSTEM_UIDS.put(1030, "hsm"); + SYSTEM_UIDS.put(1031, "reserved"); + + // Shell and nobody + SYSTEM_UIDS.put(2000, "shell"); + SYSTEM_UIDS.put(9999, "nobody"); + + // Android framework UIDs (1000-1999 range commonly used) + for (int uid = 1032; uid <= 1099; uid++) { + SYSTEM_UIDS.put(uid, "system_" + uid); + } + } + + // Cache for resolved UIDs with timestamp + private static final ConcurrentHashMap UID_CACHE = new ConcurrentHashMap<>(); + private static final long CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes TTL + private static final int MAX_CACHE_SIZE = 500; // Maximum cache entries + + // LRU cache for frequently accessed UIDs + private static final LruCache FREQUENT_UID_CACHE = new LruCache(100) { + @Override + protected void entryRemoved(boolean evicted, Integer key, String oldValue, String newValue) { + if (evicted) { + Log.d(TAG, "LRU cache evicted UID " + key + " -> " + oldValue); + } + } + }; + + // Cache entry with TTL + private static class CacheEntry { + final String name; + final long timestamp; + final ResolutionMethod method; + + CacheEntry(String name, ResolutionMethod method) { + this.name = name; + this.timestamp = System.currentTimeMillis(); + this.method = method; + } + + boolean isExpired() { + return System.currentTimeMillis() - timestamp > CACHE_TTL_MS; + } + } + + // Track which resolution method was used + private enum ResolutionMethod { + SYSTEM_DB, PACKAGE_MANAGER, PROC_FS, PACKAGES_LIST, UNKNOWN + } + + // Process information from /proc/[pid]/status + private static class ProcessInfo { + String name; + int uid; + int pid; + String state; + + ProcessInfo(String name, int uid, int pid, String state) { + this.name = name; + this.uid = uid; + this.pid = pid; + this.state = state; + } + } + + /** + * Resolve UID to app name using fallback chain with caching + * + * @param ctx Context for accessing resources + * @param uid UID to resolve + * @return Resolved name or "Unknown" if all methods fail + */ + public static String resolveUid(Context ctx, int uid) { + // Check LRU cache first (most frequently accessed) + String frequent = FREQUENT_UID_CACHE.get(uid); + if (frequent != null) { + Log.d(TAG, "UID " + uid + " resolved from LRU cache: " + frequent); + return frequent; + } + + // Check main cache + CacheEntry cached = UID_CACHE.get(uid); + if (cached != null && !cached.isExpired()) { + // Move to frequent cache if accessed multiple times + FREQUENT_UID_CACHE.put(uid, cached.name); + Log.d(TAG, "UID " + uid + " resolved from cache (" + cached.method + "): " + cached.name); + return cached.name; + } + + // Clean expired entries periodically + if (UID_CACHE.size() > MAX_CACHE_SIZE) { + cleanExpiredEntries(); + } + + String result; + ResolutionMethod method = ResolutionMethod.UNKNOWN; + + // Method 1: Check system UID database + result = resolveSystemUid(uid); + if (result != null) { + method = ResolutionMethod.SYSTEM_DB; + cacheResult(uid, result, method); + Log.d(TAG, "UID " + uid + " resolved via system database: " + result); + return result; + } + + // Method 2: Use PackageManager + result = resolveViaPackageManager(ctx, uid); + if (result != null) { + method = ResolutionMethod.PACKAGE_MANAGER; + cacheResult(uid, result, method); + Log.d(TAG, "UID " + uid + " resolved via PackageManager: " + result); + return result; + } + + // Method 3: Check running processes + result = resolveViaProc(uid); + if (result != null) { + method = ResolutionMethod.PROC_FS; + cacheResult(uid, result, method); + Log.d(TAG, "UID " + uid + " resolved via /proc: " + result); + return result; + } + + // Method 4: Parse packages.list (requires root) + result = resolveViaPackagesList(uid); + if (result != null) { + method = ResolutionMethod.PACKAGES_LIST; + cacheResult(uid, result, method); + Log.d(TAG, "UID " + uid + " resolved via packages.list: " + result); + return result; + } + + // Cache unknown result with shorter TTL + String unknown = ctx.getString(R.string.unknown_item); + UID_CACHE.put(uid, new CacheEntry(unknown, ResolutionMethod.UNKNOWN)); + Log.w(TAG, "UID " + uid + " could not be resolved by any method"); + return unknown; + } + + /** + * Resolve UID using system UID database + */ + private static String resolveSystemUid(int uid) { + return SYSTEM_UIDS.get(uid); + } + + /** + * Resolve UID using PackageManager with multi-user support + */ + private static String resolveViaPackageManager(Context ctx, int uid) { + try { + PackageManager pm = ctx.getPackageManager(); + + // Try direct lookup first + String[] packages = pm.getPackagesForUid(uid); + if (packages != null && packages.length > 0) { + String packageName = packages[0]; + + // For multi-user UIDs, add user context + if (isMultiUserUid(uid)) { + int userId = getUserId(uid); + int appId = getAppId(uid); + return packageName + " (User " + userId + ")"; + } + return packageName; + } + + // Try getNameForUid as fallback + String name = pm.getNameForUid(uid); + if (name != null && !name.isEmpty()) { + if (isMultiUserUid(uid)) { + int userId = getUserId(uid); + return name + " (User " + userId + ")"; + } + return name; + } + + // For multi-user UIDs, try looking up the base app UID + if (isMultiUserUid(uid)) { + int appId = getAppId(uid); + int userId = getUserId(uid); + + Log.d(TAG, "Trying base UID lookup for multi-user UID " + uid + " (user:" + userId + ", app:" + appId + ")"); + + // Try to resolve the base app UID + String[] basePackages = pm.getPackagesForUid(appId); + if (basePackages != null && basePackages.length > 0) { + return basePackages[0] + " (User " + userId + ")"; + } + + String baseName = pm.getNameForUid(appId); + if (baseName != null && !baseName.isEmpty()) { + return baseName + " (User " + userId + ")"; + } + } + + } catch (Exception e) { + Log.w(TAG, "PackageManager resolution failed for UID " + uid, e); + } + return null; + } + + /** + * Resolve UID by checking running processes in /proc (enhanced version) + */ + private static String resolveViaProc(int uid) { + try { + File procDir = new File("/proc"); + if (!procDir.exists() || !procDir.canRead()) { + Log.d(TAG, "/proc directory not accessible"); + return null; + } + + File[] pidDirs = procDir.listFiles(file -> { + try { + Integer.parseInt(file.getName()); + return file.isDirectory(); + } catch (NumberFormatException e) { + return false; + } + }); + + if (pidDirs == null) return null; + + // Track best match found + String bestMatch = null; + int bestScore = 0; + + for (File pidDir : pidDirs) { + try { + File statusFile = new File(pidDir, "status"); + if (!statusFile.exists() || !statusFile.canRead()) continue; + + // Check if this process belongs to our UID + ProcessInfo procInfo = parseProcessStatus(statusFile); + if (procInfo != null && procInfo.uid == uid) { + + // Try multiple sources for process name + String processName = null; + int score = 0; + + // Method 1: Get from cmdline (highest priority) + File cmdlineFile = new File(pidDir, "cmdline"); + if (cmdlineFile.exists() && cmdlineFile.canRead()) { + String cmdline = readFirstLine(cmdlineFile); + if (cmdline != null && !cmdline.isEmpty()) { + processName = cleanProcessName(cmdline); + score = 3; // Highest score for cmdline + } + } + + // Method 2: Get from comm file (medium priority) + if (processName == null || processName.isEmpty()) { + File commFile = new File(pidDir, "comm"); + if (commFile.exists() && commFile.canRead()) { + String comm = readFirstLine(commFile); + if (comm != null && !comm.isEmpty()) { + processName = comm.trim(); + score = 2; + } + } + } + + // Method 3: Get from status Name field (lowest priority) + if (processName == null || processName.isEmpty()) { + if (procInfo.name != null && !procInfo.name.isEmpty()) { + processName = procInfo.name; + score = 1; + } + } + + // Track the best match found + if (processName != null && score > bestScore) { + bestMatch = processName; + bestScore = score; + } + } + } catch (Exception e) { + // Skip this process and continue + continue; + } + } + + if (bestMatch != null) { + Log.d(TAG, "Found process for UID " + uid + ": " + bestMatch + " (score: " + bestScore + ")"); + } + return bestMatch; + + } catch (Exception e) { + Log.w(TAG, "Failed to resolve UID via /proc", e); + } + return null; + } + + /** + * Parse comprehensive process information from /proc/[pid]/status file + */ + private static ProcessInfo parseProcessStatus(File statusFile) { + try (BufferedReader reader = new BufferedReader(new FileReader(statusFile))) { + String line; + String name = null; + int uid = -1; + int pid = -1; + String state = null; + + while ((line = reader.readLine()) != null) { + if (line.startsWith("Name:")) { + String[] parts = line.split("\\s+", 2); + if (parts.length >= 2) { + name = parts[1].trim(); + } + } else if (line.startsWith("Pid:")) { + String[] parts = line.split("\\s+"); + if (parts.length >= 2) { + try { + pid = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + // Ignore + } + } + } else if (line.startsWith("State:")) { + String[] parts = line.split("\\s+", 2); + if (parts.length >= 2) { + state = parts[1].trim(); + } + } else if (line.startsWith("Uid:")) { + String[] parts = line.split("\\s+"); + if (parts.length >= 2) { + try { + uid = Integer.parseInt(parts[1]); // Real UID + } catch (NumberFormatException e) { + // Ignore + } + } + } + } + + if (uid != -1) { + return new ProcessInfo(name, uid, pid, state); + } + + } catch (IOException e) { + // Ignore, process might have died + } + return null; + } + + /** + * Read first line from file + */ + private static String readFirstLine(File file) { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line = reader.readLine(); + return line != null ? line.trim() : null; + } catch (IOException e) { + return null; + } + } + + /** + * Clean process name from cmdline + */ + private static String cleanProcessName(String cmdline) { + if (cmdline == null || cmdline.isEmpty()) return null; + + // Replace null bytes with spaces + cmdline = cmdline.replace('\0', ' ').trim(); + + // Extract just the command name + String[] parts = cmdline.split("\\s+"); + if (parts.length > 0) { + String cmd = parts[0]; + // Remove path if present + int lastSlash = cmd.lastIndexOf('/'); + if (lastSlash >= 0) { + cmd = cmd.substring(lastSlash + 1); + } + return cmd; + } + return cmdline; + } + + /** + * Resolve UID by parsing /data/system/packages.list (requires root) - enhanced version + */ + private static String resolveViaPackagesList(int uid) { + // Try multiple possible locations for packages list + String[] possiblePaths = { + "/data/system/packages.list", + "/system/etc/packages.list", // Some ROMs + "/data/data/packages.list" // Alternative location + }; + + for (String path : possiblePaths) { + String result = tryReadPackagesList(path, uid); + if (result != null) { + Log.d(TAG, "Found UID " + uid + " in " + path + ": " + result); + return result; + } + } + + return null; + } + + /** + * Try to read packages list from specific path + */ + private static String tryReadPackagesList(String path, int uid) { + try { + File packagesFile = new File(path); + if (!packagesFile.exists() || !packagesFile.canRead()) { + return null; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(packagesFile))) { + String line; + while ((line = reader.readLine()) != null) { + // Skip comments and empty lines + if (line.trim().isEmpty() || line.startsWith("#")) { + continue; + } + + String[] parts = line.split("\\s+"); + // Format: package_name uid debuggable data_dir selinux_label + if (parts.length >= 2) { + try { + int packageUid = Integer.parseInt(parts[1]); + + // Handle multi-user UIDs (user ID is in higher bits) + int baseUid = packageUid % 100000; // Remove user ID part + + if (packageUid == uid || baseUid == (uid % 100000)) { + String packageName = parts[0]; + + // Validate package name format + if (isValidPackageName(packageName)) { + return packageName; + } + } + } catch (NumberFormatException e) { + // Skip malformed line + Log.d(TAG, "Malformed line in " + path + ": " + line); + continue; + } + } + } + } + } catch (IOException e) { + Log.d(TAG, "Failed to read " + path + ": " + e.getMessage()); + } catch (SecurityException e) { + Log.d(TAG, "No permission to read " + path + " (expected on non-root)"); + } + return null; + } + + /** + * Validate package name format + */ + private static boolean isValidPackageName(String packageName) { + if (packageName == null || packageName.trim().isEmpty()) { + return false; + } + + // Basic package name validation (at least one dot, reasonable length) + return packageName.contains(".") && + packageName.length() > 3 && + packageName.length() < 256 && + packageName.matches("^[a-zA-Z0-9._]+$"); + } + + /** + * Check if UID is in system range + */ + public static boolean isSystemUid(int uid) { + return uid >= 0 && uid < 10000; + } + + /** + * Check if UID is in app range + */ + public static boolean isAppUid(int uid) { + return uid >= 10000; + } + + /** + * Check if UID is in multi-user range + */ + public static boolean isMultiUserUid(int uid) { + return uid >= 100000; // User 1 and above + } + + /** + * Extract user ID from multi-user UID + * @param uid the multi-user UID + * @return user ID (0 for primary user, 1+ for secondary users) + */ + public static int getUserId(int uid) { + return uid / 100000; + } + + /** + * Extract app ID from multi-user UID + * @param uid the multi-user UID + * @return app ID (the base UID without user component) + */ + public static int getAppId(int uid) { + return uid % 100000; + } + + /** + * Create multi-user UID from user ID and app ID + * @param userId user ID (0, 1, 2, etc.) + * @param appId app ID (10000+) + * @return multi-user UID + */ + public static int createMultiUserUid(int userId, int appId) { + return userId * 100000 + appId; + } + + /** + * Get user-friendly description of UID type + */ + public static String getUidTypeDescription(int uid) { + if (uid < 0) { + return "invalid"; + } else if (uid == 0) { + return "root"; + } else if (uid < 1000) { + return "system_low"; + } else if (uid < 10000) { + return "system"; + } else if (uid < 100000) { + return "app_primary"; + } else { + int userId = getUserId(uid); + int appId = getAppId(uid); + return String.format("app_user%d(app:%d)", userId, appId); + } + } + + /** + * Cache a resolved UID result + */ + private static void cacheResult(int uid, String name, ResolutionMethod method) { + UID_CACHE.put(uid, new CacheEntry(name, method)); + + // Add system UIDs to frequent cache immediately (they're stable) + if (method == ResolutionMethod.SYSTEM_DB) { + FREQUENT_UID_CACHE.put(uid, name); + } + } + + /** + * Clean expired entries from cache + */ + public static void cleanExpiredEntries() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + UID_CACHE.entrySet().removeIf(entry -> entry.getValue().isExpired()); + } else { + // Fallback for older APIs + Iterator> iterator = UID_CACHE.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().isExpired()) { + iterator.remove(); + } + } + } + } + /** + * Clear all caches (useful for testing or when packages change) + */ + public static void clearCache() { + UID_CACHE.clear(); + FREQUENT_UID_CACHE.evictAll(); + Log.i(TAG, "UID resolution caches cleared"); + } + + /** + * Invalidate cache entry for specific UID + */ + public static void invalidateUid(int uid) { + UID_CACHE.remove(uid); + FREQUENT_UID_CACHE.remove(uid); + Log.d(TAG, "Cache invalidated for UID: " + uid); + } + + /** + * Get cache statistics for debugging + */ + public static String getCacheStats() { + return String.format("Cache: %d entries, LRU: %d entries", + UID_CACHE.size(), FREQUENT_UID_CACHE.size()); + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/util/XPreferenceProvider.java b/app/src/main/java/dev/ukanth/ufirewall/util/XPreferenceProvider.java new file mode 100644 index 0000000..c24692d --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/util/XPreferenceProvider.java @@ -0,0 +1,12 @@ +package dev.ukanth.ufirewall.util; + +/*import com.crossbowffs.remotepreferences.RemotePreferenceProvider; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.BuildConfig; + +public class XPreferenceProvider extends RemotePreferenceProvider { + public XPreferenceProvider() { + super(BuildConfig.APPLICATION_ID, new String[] {Api.PREFS_NAME, BuildConfig.APPLICATION_ID+ "_preferences"}); + } +}*/ \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/RadialMenuWidget.java b/app/src/main/java/dev/ukanth/ufirewall/widget/RadialMenuWidget.java new file mode 100644 index 0000000..b3cca7a --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/RadialMenuWidget.java @@ -0,0 +1,1188 @@ +package dev.ukanth.ufirewall.widget; + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationSet; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.TranslateAnimation; + +import java.util.ArrayList; +import java.util.List; + +import dev.ukanth.ufirewall.util.G; + +public class RadialMenuWidget extends View { + + //Defines the interface + public interface RadialMenuEntry { + String getName(); + String getLabel(); + int getIcon(); + List getChildren(); + void menuActiviated(); + } + + + private final List menuEntries = new ArrayList(); + private RadialMenuEntry centerCircle = null; + + private final float screen_density = getContext().getResources().getDisplayMetrics().density; + + private int defaultColor = Color.rgb(0, 0, 0); //default color of wedge pieces + private int defaultAlpha = 180; //transparency of the colors, 255=Opague, 0=Transparent + private int wedge2Color = Color.rgb(85, 85, 85); //default color of wedge pieces + private int wedge2Alpha = 210; + private int outlineColor = Color.rgb(255, 255, 255); //color of outline + private int outlineAlpha = 255; //transparency of outline + private int selectedColor = Color.rgb(0, 0, 0); //color to fill when something is selected + private int selectedAlpha = 210; //transparency of fill when something is selected + + private int disabledColor = Color.rgb(85, 85, 85); //color to fill when something is selected + private int disabledAlpha = 100; //transparency of fill when something is selected + + private final int pictureAlpha = 255; //transparency of images + + private int textColor = Color.rgb(255, 255, 255); //color to fill when something is selected + private int textAlpha = 255; //transparency of fill when something is selected + + private int headerTextColor = Color.rgb(255, 255, 255); //color of header text + private int headerTextAlpha = 255; //transparency of header text + private int headerBackgroundColor = Color.rgb(0, 0, 0); //color of header background + private int headerBackgroundAlpha = 180; //transparency of header background + + private int wedgeQty = 1; //Number of wedges + private Wedge[] Wedges = new Wedge[wedgeQty]; + private Wedge selected = null; //Keeps track of which wedge is selected + private Wedge enabled = null; //Keeps track of which wedge is enabled for outer ring + private Rect[] iconRect = new Rect[wedgeQty]; + + + private int wedgeQty2 = 1; //Number of wedges + private Wedge[] Wedges2 = new Wedge[wedgeQty2]; + private Wedge selected2 = null; //Keeps track of which wedge is selected + private Rect[] iconRect2 = new Rect[wedgeQty2]; + private RadialMenuEntry wedge2Data = null; //Keeps track off which menuItem data is being used for the outer ring + + private int MinSize = scalePX(35); //Radius of inner ring size + private int MaxSize = scalePX(90); //Radius of outer ring size + private int r2MinSize = MaxSize+scalePX(5); //Radius of inner second ring size + private int r2MaxSize = r2MinSize+scalePX(45); //Radius of outer second ring size + private int MinIconSize = scalePX(15); //Min Size of Image in Wedge + private int MaxIconSize = scalePX(35); //Max Size of Image in Wedge + //private int BitmapSize = scalePX(40); //Size of Image in Wedge + private int cRadius = MinSize-scalePX(7); //Inner Circle Radius + private int textSize = scalePX(15); //TextSize + private int animateTextSize = textSize; + + private int xPosition = getSizeX(); //Center X location of Radial Menu + private int yPosition = getSizeY(); //Center Y location of Radial Menu + + private int xSource = 0; //Source X of clicked location + private int ySource = 0; //Center Y of clicked location + private boolean showSource = false; //Display icon where at source location + + private boolean inWedge = false; //Identifies touch event was in first wedge + private boolean inWedge2 = false; //Identifies touch event was in second wedge + private boolean inCircle = false; //Identifies touch event was in middle circle + + private boolean Wedge2Shown = false; //Identifies 2nd wedge is drawn + private boolean HeaderBoxBounded = false; //Identifies if header box is drawn + + private String headerString = null; + private int headerTextSize = textSize; //TextSize + private final int headerBuffer = scalePX(8); + private final Rect textRect = new Rect(); + private final RectF textBoxRect = new RectF(); + private int headerTextLeft; + private int headerTextBottom; + + //private RotateAnimation rotate; + //private AlphaAnimation blend; + private ScaleAnimation scale; + private TranslateAnimation move; + private AnimationSet spriteAnimation; + private long animationSpeed = 400L; + + + private static final int ANIMATE_IN = 1; + private static final int ANIMATE_OUT = 2; + + private final int animateSections = 4; + private int r2VariableSize; + private boolean animateOuterIn = false; + private boolean animateOuterOut = false; + + + + @SuppressLint("NewApi") + public RadialMenuWidget(Context context) { + super(context); + + // Gets screen specs and defaults to center of screen + DisplayMetrics dm = new DisplayMetrics(); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(dm); + + /*Display mdisp = wm.getDefaultDisplay(); + + Point mdispSize = new Point(); + mdisp.getSize(mdispSize); + int maxX = mdispSize.x; + int maxY = mdispSize.y; */ + + + this.xPosition = G.getWidgetX(context) / 2; + this.yPosition = G.getWidgetY(context) / 2; + + + + determineWedges(); + onOpenAnimation(); + } + + + @Override + public boolean onTouchEvent(MotionEvent e) { + int state = e.getAction(); + int eventX = (int) e.getX(); + int eventY = (int) e.getY(); + if (state == MotionEvent.ACTION_DOWN) { + //selected = null; + //selected2 = null; + inWedge = false; + inWedge2 = false; + inCircle = false; + + + //Checks if a pie slice is selected in first Wedge + for (int i = 0; i < Wedges.length; i++) { + Wedge f = Wedges[i]; + double slice = (2*Math.PI) / wedgeQty; + double start = (2*Math.PI)*(0.75) - (slice/2); //this is done so top slice is the centered on top of the circle + + inWedge = pntInWedge(eventX, eventY, + xPosition, yPosition, + MinSize, MaxSize, + (i* slice)+start, slice); + + if (inWedge) { + selected = f; + break; + } + } + + + //Checks if a pie slice is selected in second Wedge + if (Wedge2Shown) { + for (int i = 0; i < Wedges2.length; i++) { + Wedge f = Wedges2[i]; + double slice = (2*Math.PI) / wedgeQty2; + double start = (2*Math.PI)*(0.75) - (slice/2); //this is done so top slice is the centered on top of the circle + + inWedge2 = pntInWedge(eventX, eventY, + xPosition, yPosition, + r2MinSize, r2MaxSize, + (i* slice)+start, slice); + + if (inWedge2) { + selected2 = f; + break; + } + } + + } + + //Checks if center circle is selected + inCircle = pntInCircle(eventX, eventY, xPosition,yPosition,cRadius); + + } else if (state == MotionEvent.ACTION_UP) { + //execute commands... + //put in stuff here to "return" the button that was pressed. + if (inCircle) { + if (Wedge2Shown) { + enabled = null; + animateOuterIn = true; //sets Wedge2Shown = false; + } + selected = null; + //Toast.makeText(getContext(), centerCircle.getName() + " pressed.", Toast.LENGTH_SHORT).show(); + centerCircle.menuActiviated(); + + } else if (selected != null){ + for (int i = 0; i < Wedges.length; i++) { + Wedge f = Wedges[i]; + if (f == selected) { + + //Checks if a inner ring is enabled if so closes the outer ring an + if (enabled != null) { + //Toast.makeText(getContext(), "Closing outer ring", Toast.LENGTH_SHORT).show(); + enabled = null; + animateOuterIn = true; //sets Wedge2Shown = false; + + //If outer ring is not enabled, then executes event + } else { + //Toast.makeText(getContext(), menuEntries.get(i).getName() + " pressed.", Toast.LENGTH_SHORT).show(); + menuEntries.get(i).menuActiviated(); + + //Figures out how many outer rings + if (menuEntries.get(i).getChildren() != null) { + determineOuterWedges(menuEntries.get(i)); + enabled = f; + animateOuterOut = true; //sets Wedge2Shown = true;m + + } else { + Wedge2Shown = false; + } + + } + selected = null; + } + } + } else if (selected2 != null){ + for (int i = 0; i < Wedges2.length; i++) { + Wedge f = Wedges2[i]; + if (f == selected2) { + //Toast.makeText(getContext(), wedge2Data.getChildren().get(i).getName() + " pressed.", Toast.LENGTH_SHORT).show(); + animateOuterIn = true; //sets Wedge2Shown = false; + wedge2Data.getChildren().get(i).menuActiviated(); + enabled = null; + selected = null; + } + } + } else { + //This is when something outside the circle or any of the rings is selected + //selected = null; + //enabled = null; + } + //selected = null; + selected2 = null; + inCircle = false; + } + invalidate(); + return true; + } + + + @Override + protected void onDraw(Canvas c) { + + + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setStrokeWidth(3); + + // draws a dot at the source of the press + if (showSource) { + paint.setColor(outlineColor); + paint.setAlpha(outlineAlpha); + paint.setStyle(Paint.Style.STROKE); + c.drawCircle(xSource, ySource, cRadius/10, paint); + + paint.setColor(selectedColor); + paint.setAlpha(selectedAlpha); + paint.setStyle(Paint.Style.FILL); + c.drawCircle(xSource, ySource, cRadius/10, paint); + } + + + for (int i = 0; i < Wedges.length; i++) { + Wedge f = Wedges[i]; + paint.setColor(outlineColor); + paint.setAlpha(outlineAlpha); + paint.setStyle(Paint.Style.STROKE); + c.drawPath(f, paint); + if (f == enabled && Wedge2Shown) { + paint.setColor(wedge2Color); + paint.setAlpha(wedge2Alpha); + paint.setStyle(Paint.Style.FILL); + c.drawPath(f, paint); + } else if (f != enabled && Wedge2Shown) { + paint.setColor(disabledColor); + paint.setAlpha(disabledAlpha); + paint.setStyle(Paint.Style.FILL); + c.drawPath(f, paint); + } else if (f == enabled && !Wedge2Shown) { + paint.setColor(wedge2Color); + paint.setAlpha(wedge2Alpha); + paint.setStyle(Paint.Style.FILL); + c.drawPath(f, paint); + } else if (f == selected) { + paint.setColor(wedge2Color); + paint.setAlpha(wedge2Alpha); + paint.setStyle(Paint.Style.FILL); + c.drawPath(f, paint); + } else { + paint.setColor(defaultColor); + paint.setAlpha(defaultAlpha); + paint.setStyle(Paint.Style.FILL); + c.drawPath(f, paint); + } + + Rect rf = iconRect[i]; + + if ((menuEntries.get(i).getIcon() != 0) && (menuEntries.get(i).getLabel() != null)) { + + //This will look for a "new line" and split into multiple lines + String menuItemName = menuEntries.get(i).getLabel(); + String[] stringArray = menuItemName.split("\n"); + + paint.setColor(textColor); + if (f != enabled && Wedge2Shown) { + paint.setAlpha(disabledAlpha); + } else { + paint.setAlpha(textAlpha); + } + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(textSize); + + Rect rect = new Rect(); + float textHeight = 0; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + textHeight = textHeight+(rect.height()+3); + } + + Rect rf2 = new Rect(); + rf2.set(rf.left, rf.top-((int)textHeight/2), rf.right, rf.bottom-((int)textHeight/2)); + + float textBottom = rf2.bottom; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + float textLeft = rf.centerX() - rect.width()/2; + textBottom = textBottom + (rect.height()+3); + c.drawText(stringArray[j], textLeft-rect.left, textBottom-rect.bottom, paint); + } + + //Puts in the Icon + Drawable drawable = getResources().getDrawable(menuEntries.get(i).getIcon()); + drawable.setBounds(rf2); + if (f != enabled && Wedge2Shown) { + drawable.setAlpha(disabledAlpha); + } else { + drawable.setAlpha(pictureAlpha); + } + drawable.draw(c); + + //Icon Only + } else if (menuEntries.get(i).getIcon() != 0) { + //Puts in the Icon + Drawable drawable = getResources().getDrawable(menuEntries.get(i).getIcon()); + drawable.setBounds(rf); + if (f != enabled && Wedge2Shown) { + drawable.setAlpha(disabledAlpha); + } else { + drawable.setAlpha(pictureAlpha); + } + drawable.draw(c); + + + //Text Only + } else { + //Puts in the Text if no Icon + paint.setColor(textColor); + if (f != enabled && Wedge2Shown) { + paint.setAlpha(disabledAlpha); + } else { + paint.setAlpha(textAlpha); + } + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(textSize); + + //This will look for a "new line" and split into multiple lines + String menuItemName = menuEntries.get(i).getLabel(); + String[] stringArray = menuItemName.split("\n"); + + //gets total height + Rect rect = new Rect(); + float textHeight = 0; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + textHeight = textHeight+(rect.height()+3); + } + + float textBottom = rf.centerY()-(textHeight/2); + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + float textLeft = rf.centerX() - rect.width()/2; + textBottom = textBottom + (rect.height()+3); + c.drawText(stringArray[j], textLeft-rect.left, textBottom-rect.bottom, paint); + } + } + + } + + + //Animate the outer ring in/out + if (animateOuterIn) { + animateOuterWedges(ANIMATE_IN); + } + else if (animateOuterOut) { + animateOuterWedges(ANIMATE_OUT); + } + + if (Wedge2Shown) { + + for (int i = 0; i < Wedges2.length; i++) { + Wedge f = Wedges2[i]; + paint.setColor(outlineColor); + paint.setAlpha(outlineAlpha); + paint.setStyle(Paint.Style.STROKE); + c.drawPath(f, paint); + if (f == selected2) { + paint.setColor(selectedColor); + paint.setAlpha(selectedAlpha); + paint.setStyle(Paint.Style.FILL); + c.drawPath(f, paint); + } else { + paint.setColor(wedge2Color); + paint.setAlpha(wedge2Alpha); + paint.setStyle(Paint.Style.FILL); + c.drawPath(f, paint); + } + + Rect rf = iconRect2[i]; + if(wedge2Data.getChildren().size() > 0) { + if ((wedge2Data.getChildren().get(i).getIcon() != 0) && (wedge2Data.getChildren().get(i).getLabel() != null)) { + + //This will look for a "new line" and split into multiple lines + String menuItemName = wedge2Data.getChildren().get(i).getLabel(); + String[] stringArray = menuItemName.split("\n"); + + paint.setColor(textColor); + paint.setAlpha(textAlpha); + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(animateTextSize); + + Rect rect = new Rect(); + float textHeight = 0; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + textHeight = textHeight+(rect.height()+3); + } + + Rect rf2 = new Rect(); + rf2.set(rf.left, rf.top-((int)textHeight/2), rf.right, rf.bottom-((int)textHeight/2)); + + float textBottom = rf2.bottom; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + float textLeft = rf.centerX() - rect.width()/2; + textBottom = textBottom + (rect.height()+3); + c.drawText(stringArray[j], textLeft-rect.left, textBottom-rect.bottom, paint); + } + + + //Puts in the Icon + Drawable drawable = getResources().getDrawable(wedge2Data.getChildren().get(i).getIcon()); + drawable.setBounds(rf2); + drawable.setAlpha(pictureAlpha); + drawable.draw(c); + + //Icon Only + } else if (wedge2Data.getChildren().get(i).getIcon() != 0) { + //Puts in the Icon + Drawable drawable = getResources().getDrawable(wedge2Data.getChildren().get(i).getIcon()); + drawable.setBounds(rf); + drawable.setAlpha(pictureAlpha); + drawable.draw(c); + + //Text Only + } else { + //Puts in the Text if no Icon + paint.setColor(textColor); + paint.setAlpha(textAlpha); + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(animateTextSize); + + //This will look for a "new line" and split into multiple lines + String menuItemName = wedge2Data.getChildren().get(i).getLabel(); + String[] stringArray = menuItemName.split("\n"); + + //gets total height + Rect rect = new Rect(); + float textHeight = 0; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + textHeight = textHeight+(rect.height()+3); + } + + float textBottom = rf.centerY()-(textHeight/2); + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + float textLeft = rf.centerX() - rect.width()/2; + textBottom = textBottom + (rect.height()+3); + c.drawText(stringArray[j], textLeft-rect.left, textBottom-rect.bottom, paint); + } + } + } + + } + } + + //Draws the Middle Circle + paint.setColor(outlineColor); + paint.setAlpha(outlineAlpha); + paint.setStyle(Paint.Style.STROKE); + c.drawCircle(xPosition, yPosition, cRadius, paint); + if (inCircle) { + paint.setColor(selectedColor); + paint.setAlpha(selectedAlpha); + paint.setStyle(Paint.Style.FILL); + c.drawCircle(xPosition, yPosition, cRadius, paint); + onCloseAnimation(); + } else { + paint.setColor(defaultColor); + paint.setAlpha(defaultAlpha); + paint.setStyle(Paint.Style.FILL); + c.drawCircle(xPosition, yPosition, cRadius, paint); + } + + + // Draw the circle picture + if ((centerCircle.getIcon() != 0) && (centerCircle.getLabel() != null)) { + + //This will look for a "new line" and split into multiple lines + String menuItemName = centerCircle.getLabel(); + String[] stringArray = menuItemName.split("\n"); + + paint.setColor(textColor); + paint.setAlpha(textAlpha); + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(textSize); + + Rect rectText = new Rect(); + Rect rectIcon = new Rect(); + Drawable drawable = getResources().getDrawable(centerCircle.getIcon()); + + int h = getIconSize(drawable.getIntrinsicHeight(),MinIconSize,MaxIconSize); + int w = getIconSize(drawable.getIntrinsicWidth(),MinIconSize,MaxIconSize); + rectIcon.set(xPosition-w/2, yPosition-h/2, xPosition+w/2, yPosition+h/2); + + + float textHeight = 0; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rectText); + textHeight = textHeight+(rectText.height()+3); + } + + rectIcon.set(rectIcon.left, rectIcon.top-((int)textHeight/2), rectIcon.right, rectIcon.bottom-((int)textHeight/2)); + + float textBottom = rectIcon.bottom; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rectText); + float textLeft = xPosition - rectText.width()/2; + textBottom = textBottom + (rectText.height()+3); + c.drawText(stringArray[j], textLeft-rectText.left, textBottom-rectText.bottom, paint); + } + + + //Puts in the Icon + drawable.setBounds(rectIcon); + drawable.setAlpha(pictureAlpha); + drawable.draw(c); + + //Icon Only + } else if (centerCircle.getIcon() != 0) { + + Rect rect = new Rect(); + + Drawable drawable = getResources().getDrawable(centerCircle.getIcon()); + + int h = getIconSize(drawable.getIntrinsicHeight(),MinIconSize,MaxIconSize); + int w = getIconSize(drawable.getIntrinsicWidth(),MinIconSize,MaxIconSize); + rect.set(xPosition-w/2, yPosition-h/2, xPosition+w/2, yPosition+h/2); + + drawable.setBounds(rect); + drawable.setAlpha(pictureAlpha); + drawable.draw(c); + + //Text Only + } else { + //Puts in the Text if no Icon + paint.setColor(textColor); + paint.setAlpha(textAlpha); + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(textSize); + + //This will look for a "new line" and split into multiple lines + String menuItemName = centerCircle.getLabel(); + String[] stringArray = menuItemName.split("\n"); + + //gets total height + Rect rect = new Rect(); + float textHeight = 0; + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + textHeight = textHeight+(rect.height()+3); + } + + float textBottom = yPosition-(textHeight/2); + for (int j = 0; j < stringArray.length; j++) + { + paint.getTextBounds(stringArray[j],0,stringArray[j].length(),rect); + float textLeft = xPosition - rect.width()/2; + textBottom = textBottom + (rect.height()+3); + c.drawText(stringArray[j], textLeft-rect.left, textBottom-rect.bottom, paint); + } + + + } + + // Draws Text in TextBox + if (headerString != null) { + + paint.setTextSize(headerTextSize); + paint.getTextBounds(headerString,0,headerString.length(),this.textRect); + if (!HeaderBoxBounded) { + determineHeaderBox(); + HeaderBoxBounded = true; + } + + paint.setColor(outlineColor); + paint.setAlpha(outlineAlpha); + paint.setStyle(Paint.Style.STROKE); + c.drawRoundRect(this.textBoxRect, scalePX(5), scalePX(5), paint); + paint.setColor(headerBackgroundColor); + paint.setAlpha(headerBackgroundAlpha); + paint.setStyle(Paint.Style.FILL); + c.drawRoundRect(this.textBoxRect, scalePX(5), scalePX(5), paint); + + paint.setColor(headerTextColor); + paint.setAlpha(headerTextAlpha); + paint.setStyle(Paint.Style.FILL); + paint.setTextSize(headerTextSize); + c.drawText(headerString, headerTextLeft, headerTextBottom, paint); + } + + } + + + private int getIconSize(int iconSize, int minSize, int maxSize) { + + if (iconSize > minSize) { + if (iconSize > maxSize) { + return maxSize; + } else { //iconSize < maxSize + return iconSize; + } + } else { //iconSize < minSize + return minSize; + } + + } + + + + private void onOpenAnimation() { + + //rotate = new RotateAnimation(0, 360, xPosition, yPosition); + //rotate.setRepeatMode(Animation.REVERSE); + //rotate.setRepeatCount(Animation.INFINITE); + scale = new ScaleAnimation(0, 1, 0, 1, xPosition, yPosition); + //scale.setRepeatMode(Animation.REVERSE); + //scale.setRepeatCount(Animation.INFINITE); + scale.setInterpolator(new DecelerateInterpolator()); + move = new TranslateAnimation(xSource-xPosition, 0, ySource-yPosition, 0); + + spriteAnimation = new AnimationSet(true); + //spriteAnimation.addAnimation(rotate); + spriteAnimation.addAnimation(scale); + spriteAnimation.addAnimation(move); + spriteAnimation.setDuration(animationSpeed); + + startAnimation(spriteAnimation); + + } + private void onCloseAnimation() { + + //rotate = new RotateAnimation(360, 0, xPosition, yPosition); + scale = new ScaleAnimation(1, 0, 1, 0, xPosition, yPosition); + scale.setInterpolator(new AccelerateInterpolator()); + move = new TranslateAnimation(0, xSource-xPosition, 0, ySource-yPosition); + + spriteAnimation = new AnimationSet(true); + //spriteAnimation.addAnimation(rotate); + spriteAnimation.addAnimation(scale); + spriteAnimation.addAnimation(move); + spriteAnimation.setDuration(animationSpeed); + + startAnimation(spriteAnimation); + + } + + private boolean pntInCircle(double px, double py, double x1, double y1, double radius) { + double diffX = x1 - px; + double diffY = y1 - py; + double dist = diffX*diffX + diffY*diffY; + return dist < radius*radius; + } + + + private boolean pntInWedge(double px, double py, + float xRadiusCenter, float yRadiusCenter, + int innerRadius, int outerRadius, + double startAngle, double sweepAngle) { + double diffX = px-xRadiusCenter; + double diffY = py-yRadiusCenter; + + double angle = Math.atan2(diffY,diffX); + if (angle < 0) + angle += (2*Math.PI); + + if (startAngle >= (2*Math.PI)) { + startAngle = startAngle-(2*Math.PI); + } + + //checks if point falls between the start and end of the wedge + if ((angle >= startAngle && angle <= startAngle + sweepAngle) || + (angle+(2*Math.PI) >= startAngle && (angle+(2*Math.PI)) <= startAngle + sweepAngle)) { + + // checks if point falls inside the radius of the wedge + double dist = diffX*diffX + diffY*diffY; + return dist < outerRadius * outerRadius && dist > innerRadius * innerRadius; + } + return false; + } + + + public boolean addMenuEntry( RadialMenuEntry entry ) + { + menuEntries.add( entry ); + determineWedges(); + return true; + } + + public boolean setCenterCircle( RadialMenuEntry entry ) + { + centerCircle = entry; + return true; + } + + + public void setInnerRingRadius( int InnerRadius, int OuterRadius ) + { + this.MinSize = scalePX(InnerRadius); + this.MaxSize = scalePX(OuterRadius); + determineWedges(); + } + + public void setOuterRingRadius( int InnerRadius, int OuterRadius ) + { + this.r2MinSize = scalePX(InnerRadius); + this.r2MaxSize = scalePX(OuterRadius); + determineWedges(); + } + + public void setCenterCircleRadius( int centerRadius ) + { + this.cRadius = scalePX(centerRadius); + determineWedges(); + } + + public void setTextSize( int TextSize ) + { + this.textSize = scalePX(TextSize); + this.animateTextSize = this.textSize; + } + + public void setIconSize( int minIconSize, int maxIconSize ) + { + this.MinIconSize = scalePX(minIconSize); + this.MaxIconSize = scalePX(maxIconSize); + determineWedges(); + } + + + public void setCenterLocation( int x, int y ) + { + this.xPosition = x; + this.yPosition = y; + determineWedges(); + onOpenAnimation(); + } + + public void setSourceLocation( int x, int y ) + { + this.xSource = x; + this.ySource = y; + onOpenAnimation(); + } + + public void setShowSourceLocation( boolean showSourceLocation ) + { + this.showSource = showSourceLocation; + onOpenAnimation(); + } + + public void setAnimationSpeed( long millis ) + { + this.animationSpeed = millis; + onOpenAnimation(); + } + + public void setInnerRingColor( int color, int alpha ) + { + this.defaultColor = color; + this.defaultAlpha = alpha; + } + public void setOuterRingColor( int color, int alpha ) + { + this.wedge2Color = color; + this.wedge2Alpha = alpha; + } + public void setOutlineColor( int color, int alpha ) + { + this.outlineColor = color; + this.outlineAlpha = alpha; + } + public void setSelectedColor( int color, int alpha ) + { + this.selectedColor = color; + this.selectedAlpha = alpha; + } + + public void setDisabledColor( int color, int alpha ) + { + this.disabledColor = color; + this.disabledAlpha = alpha; + } + + public void setTextColor( int color, int alpha ) + { + this.textColor = color; + this.textAlpha = alpha; + } + + public void setHeader( String header, int TextSize ) + { + this.headerString = header; + this.headerTextSize = scalePX(TextSize); + HeaderBoxBounded = false; + } + public void setHeaderColors( int TextColor, int TextAlpha, int BgColor, int BgAlpha ) + { + this.headerTextColor = TextColor; + this.headerTextAlpha = TextAlpha; + this.headerBackgroundColor = BgColor; + this.headerBackgroundAlpha = BgAlpha; + + } + + + private int scalePX( int dp_size ) + { + return (int) (dp_size * screen_density + 0.5f); + } + + private int getSizeX() + { + DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); + Float f = displayMetrics.widthPixels / displayMetrics.density; + return f.intValue(); + } + + private int getSizeY() + { + DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); + Float f = displayMetrics.heightPixels / displayMetrics.density; + return f.intValue(); + } + + + private void animateOuterWedges( int animation_direction) { + + boolean animationComplete = false; + + + //Wedge 2 + float slice2 = 360 / wedgeQty2; + float start_slice2 = 270 - (slice2/2); + //calculates where to put the images + double rSlice2 = (2*Math.PI) / wedgeQty2; + double rStart2 = (2*Math.PI)*(0.75) - (rSlice2/2); + + this.Wedges2 = new Wedge[wedgeQty2]; + this.iconRect2 = new Rect[wedgeQty2]; + + Wedge2Shown = true; + + int wedgeSizeChange = (r2MaxSize-r2MinSize)/animateSections; + + if (animation_direction==ANIMATE_OUT) { + if ( r2MinSize+r2VariableSize+wedgeSizeChange < r2MaxSize) { + r2VariableSize += wedgeSizeChange; + } else { + animateOuterOut = false; + r2VariableSize = r2MaxSize - r2MinSize; + animationComplete = true; + } + + //animates text size change + this.animateTextSize = (textSize/animateSections) * (r2VariableSize/wedgeSizeChange); + + //calculates new wedge sizes + for (int i = 0; i < Wedges2.length; i++) { + this.Wedges2[i] = new Wedge(xPosition, yPosition, r2MinSize, r2MinSize + r2VariableSize, (i + * slice2) + start_slice2, slice2); + float xCenter = (float)(Math.cos(((rSlice2*i)+(rSlice2*0.5))+rStart2) * (r2MinSize+r2VariableSize+r2MinSize)/2)+xPosition; + float yCenter = (float)(Math.sin(((rSlice2*i)+(rSlice2*0.5))+rStart2) * (r2MinSize+r2VariableSize+r2MinSize)/2)+yPosition; + + int h = MaxIconSize; + int w = MaxIconSize; + if(wedge2Data.getChildren().size() > 0) { + if ( wedge2Data.getChildren().get(i).getIcon() != 0 ) { + Drawable drawable = getResources().getDrawable(wedge2Data.getChildren().get(i).getIcon()); + h = getIconSize(drawable.getIntrinsicHeight(),MinIconSize,MaxIconSize); + w = getIconSize(drawable.getIntrinsicWidth(),MinIconSize,MaxIconSize); + } + } + + + if (r2VariableSize < h) { + h = r2VariableSize; + } + if (r2VariableSize < w) { + w = r2VariableSize; + } + + this.iconRect2[i] = new Rect((int) xCenter-w/2, (int) yCenter-h/2, (int) xCenter+w/2, (int) yCenter+h/2); + + + int widthOffset = MaxSize; + if (widthOffset < this.textRect.width()/2) { + widthOffset = this.textRect.width()/2+scalePX(3); + } + this.textBoxRect.set((xPosition - (widthOffset)), + yPosition - (r2MinSize+r2VariableSize) - headerBuffer-this.textRect.height()-scalePX(3), + (xPosition + (widthOffset)), + (yPosition - (r2MinSize+r2VariableSize) - headerBuffer+scalePX(3))); + this.headerTextBottom = yPosition - (r2MinSize+r2VariableSize) - headerBuffer-this.textRect.bottom; + + } + + } + else if (animation_direction==ANIMATE_IN) { + if ( r2MinSize < r2MaxSize-r2VariableSize-wedgeSizeChange) { + r2VariableSize += wedgeSizeChange; + } else { + animateOuterIn = false; + r2VariableSize = r2MaxSize; + animationComplete = true; + } + + //animates text size change + this.animateTextSize = textSize - ((textSize/animateSections) * (r2VariableSize/wedgeSizeChange)); + + + for (int i = 0; i < Wedges2.length; i++) { + this.Wedges2[i] = new Wedge(xPosition, yPosition, r2MinSize, r2MaxSize - r2VariableSize, (i + * slice2) + start_slice2, slice2); + + float xCenter = (float)(Math.cos(((rSlice2*i)+(rSlice2*0.5))+rStart2) * (r2MaxSize-r2VariableSize+r2MinSize)/2)+xPosition; + float yCenter = (float)(Math.sin(((rSlice2*i)+(rSlice2*0.5))+rStart2) * (r2MaxSize-r2VariableSize+r2MinSize)/2)+yPosition; + + int h = MaxIconSize; + int w = MaxIconSize; + if ( wedge2Data.getChildren().size() > 0 && wedge2Data.getChildren().get(i).getIcon() != 0 ) { + Drawable drawable = getResources().getDrawable(wedge2Data.getChildren().get(i).getIcon()); + h = getIconSize(drawable.getIntrinsicHeight(),MinIconSize,MaxIconSize); + w = getIconSize(drawable.getIntrinsicWidth(),MinIconSize,MaxIconSize); + } + + if (r2MaxSize-r2MinSize-r2VariableSize < h) { + h = r2MaxSize-r2MinSize-r2VariableSize; + } + if (r2MaxSize-r2MinSize-r2VariableSize < w) { + w = r2MaxSize-r2MinSize-r2VariableSize; + } + + this.iconRect2[i] = new Rect((int) xCenter-w/2, (int) yCenter-h/2, (int) xCenter+w/2, (int) yCenter+h/2); + + + //computes header text box + int heightOffset = r2MaxSize-r2VariableSize; + int widthOffset = MaxSize; + if (MaxSize > r2MaxSize-r2VariableSize) {heightOffset = MaxSize;} + if (widthOffset < this.textRect.width()/2) { + widthOffset = this.textRect.width()/2+scalePX(3); + } + this.textBoxRect.set((xPosition - (widthOffset)), + yPosition - (heightOffset) - headerBuffer-this.textRect.height()-scalePX(3), + (xPosition + (widthOffset)), + (yPosition - (heightOffset) - headerBuffer+scalePX(3))); + this.headerTextBottom = yPosition - (heightOffset) - headerBuffer-this.textRect.bottom; + + } + } + + if (animationComplete) { + r2VariableSize = 0; + this.animateTextSize = textSize; + if (animation_direction==ANIMATE_IN) { + Wedge2Shown = false; + } + } + + invalidate(); //re-draws the picture + } + + private void determineWedges() { + + int entriesQty = menuEntries.size(); + if ( entriesQty > 0) { + wedgeQty = entriesQty; + + float degSlice = 360 / wedgeQty; + float start_degSlice = 270 - (degSlice/2); + //calculates where to put the images + double rSlice = (2*Math.PI) / wedgeQty; + double rStart = (2*Math.PI)*(0.75) - (rSlice/2); + + this.Wedges = new Wedge[wedgeQty]; + this.iconRect = new Rect[wedgeQty]; + + for (int i = 0; i < Wedges.length; i++) { + this.Wedges[i] = new Wedge(xPosition, yPosition, MinSize, MaxSize, (i + * degSlice) + start_degSlice, degSlice); + float xCenter = (float)(Math.cos(((rSlice*i)+(rSlice*0.5))+rStart) * (MaxSize+MinSize)/2)+xPosition; + float yCenter = (float)(Math.sin(((rSlice*i)+(rSlice*0.5))+rStart) * (MaxSize+MinSize)/2)+yPosition; + + int h = MaxIconSize; + int w = MaxIconSize; + if ( menuEntries.get(i).getIcon() != 0 ) { + Drawable drawable = getResources().getDrawable(menuEntries.get(i).getIcon()); + h = getIconSize(drawable.getIntrinsicHeight(),MinIconSize,MaxIconSize); + w = getIconSize(drawable.getIntrinsicWidth(),MinIconSize,MaxIconSize); + } + + this.iconRect[i] = new Rect( (int) xCenter-w/2, (int) yCenter-h/2, (int) xCenter+w/2, (int) yCenter+h/2); + } + + invalidate(); //re-draws the picture + } + } + + private void determineOuterWedges(RadialMenuEntry entry) { + + int entriesQty = entry.getChildren().size(); + wedgeQty2 = entriesQty; + + //if only default profile + if(entriesQty == 0 ) { + wedgeQty2 = 1; + } + //Wedge 2 + float degSlice2 = 360 / wedgeQty2; + float start_degSlice2 = 270 - (degSlice2/2); + //calculates where to put the images + double rSlice2 = (2*Math.PI) / wedgeQty2; + double rStart2 = (2*Math.PI)*(0.75) - (rSlice2/2); + + this.Wedges2 = new Wedge[wedgeQty2]; + this.iconRect2 = new Rect[wedgeQty2]; + + for (int i = 0; i < Wedges2.length; i++) { + this.Wedges2[i] = new Wedge(xPosition, yPosition, r2MinSize, r2MaxSize, (i + * degSlice2) + start_degSlice2, degSlice2); + float xCenter = (float)(Math.cos(((rSlice2*i)+(rSlice2*0.5))+rStart2) * (r2MaxSize+r2MinSize)/2)+xPosition; + float yCenter = (float)(Math.sin(((rSlice2*i)+(rSlice2*0.5))+rStart2) * (r2MaxSize+r2MinSize)/2)+yPosition; + + int h = MaxIconSize; + int w = MaxIconSize; + if(wedgeQty2 > 1) { + if ( entry.getChildren().get(i).getIcon() != 0 ) { + Drawable drawable = getResources().getDrawable(entry.getChildren().get(i).getIcon()); + h = getIconSize(drawable.getIntrinsicHeight(),MinIconSize,MaxIconSize); + w = getIconSize(drawable.getIntrinsicWidth(),MinIconSize,MaxIconSize); + this.iconRect2[i] = new Rect((int) xCenter-w/2, (int) yCenter-h/2, (int) xCenter+w/2, (int) yCenter+h/2); + } + } + } + this.wedge2Data = entry; + invalidate(); //re-draws the picture + } + + private void determineHeaderBox() { + + this.headerTextLeft = xPosition - this.textRect.width()/2; + this.headerTextBottom = yPosition - (MaxSize) - headerBuffer-this.textRect.bottom; + int offset = MaxSize; + if (offset < this.textRect.width()/2) { + offset = this.textRect.width()/2+scalePX(3); + } + + this.textBoxRect.set((xPosition - (offset)), + yPosition - (MaxSize) - headerBuffer-this.textRect.height()-scalePX(3), + (xPosition + (offset)), + (yPosition - (MaxSize) - headerBuffer+scalePX(3))); + + } + + public static class Wedge extends Path { + private final int x; + private final int y; + private final int InnerSize; + private final int OuterSize; + private final float StartArc; + private final float ArcWidth; + + private Wedge(int x, int y, int InnerSize, int OuterSize, float StartArc, float ArcWidth) { + super(); + + if (StartArc >= 360) { + StartArc = StartArc-360; + } + + this.x = x; this.y = y; + this.InnerSize = InnerSize; + this.OuterSize = OuterSize; + this.StartArc = StartArc; + this.ArcWidth = ArcWidth; + this.buildPath(); + } + + private void buildPath() { + + final RectF rect = new RectF(); + final RectF rect2 = new RectF(); + + //Rectangles values + rect.set(this.x-this.InnerSize, this.y-this.InnerSize, this.x+this.InnerSize, this.y+this.InnerSize); + rect2.set(this.x-this.OuterSize, this.y-this.OuterSize, this.x+this.OuterSize, this.y+this.OuterSize); + + this.reset(); + //this.moveTo(100, 100); + this.arcTo(rect2, StartArc, ArcWidth); + this.arcTo(rect, StartArc+ArcWidth, -ArcWidth); + + this.close(); + + + } + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/StatusWidget.java b/app/src/main/java/dev/ukanth/ufirewall/widget/StatusWidget.java new file mode 100644 index 0000000..7de89e1 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/StatusWidget.java @@ -0,0 +1,207 @@ +/** + * ON/OFF Widget implementation + *

+ * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro + * Copyright (C) 2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Rodrigo Zechin Rosauro, Umakanthan Chandran + * @version 1.1 + */ + +package dev.ukanth.ufirewall.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.widget.RemoteViews; + +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.R; +import dev.ukanth.ufirewall.service.RootCommand; +import dev.ukanth.ufirewall.util.G; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.TransitionDrawable; + +/** + * ON/OFF Widget implementation + */ +public class StatusWidget extends AppWidgetProvider { + @Override + public void onReceive(final Context context, final Intent intent) { + super.onReceive(context, intent); + if (Api.STATUS_CHANGED_MSG.equals(intent.getAction())) { + // Broadcast sent when the DroidWall status has changed + final Bundle extras = intent.getExtras(); + if (extras != null && extras.containsKey(Api.STATUS_EXTRA)) { + final boolean firewallEnabled = extras.getBoolean(Api.STATUS_EXTRA); + final AppWidgetManager manager = AppWidgetManager.getInstance(context); + final int[] widgetIds = manager.getAppWidgetIds(new ComponentName(context, StatusWidget.class)); + showWidget(context, manager, widgetIds, firewallEnabled); + } + } else if (Api.TOGGLE_REQUEST_MSG.equals(intent.getAction())) { + // Broadcast sent to request toggling DroidWall's status + + /*final String oldPwd = G.profile_pwd(); + final String newPwd = context.getSharedPreferences(Api.PREF_FIREWALL_STATUS, 0).getString("LockPassword", ""); + */ + final SharedPreferences prefs = context.getSharedPreferences(Api.PREF_FIREWALL_STATUS, 0); + final boolean enabled = !prefs.getBoolean(Api.PREF_ENABLED, true); + final AppWidgetManager manager = AppWidgetManager.getInstance(context); + final int[] widgetIds = manager.getAppWidgetIds(new ComponentName(context, StatusWidget.class)); + + // Show immediate pending state + showPendingState(context, manager, widgetIds, enabled); + + Log.d(Api.TAG, "Protection Level: " + G.protectionLevel()); + if (!G.protectionLevel().equals("p0") || G.enableDeviceCheck()) { + + //Toast.makeText(context, R.string.widget_disable_fail, Toast.LENGTH_SHORT).show(); + //return; + } else { + if (enabled) { + Api.applySavedIptablesRules(context, true, new RootCommand() + .setSuccessToast(R.string.toast_enabled) + .setFailureToast(R.string.toast_error_enabling) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + boolean status = (state.exitCode == 0); + if (state.exitCode != 0) { + // Show error state on failure + showErrorState(context, manager, widgetIds); + } else { + // Show success state briefly before final state + showSuccessState(context, manager, widgetIds, status); + } + // setEnabled always sends us a STATUS_CHANGED_MSG intent to update the icon + Api.setEnabled(context, status, true); + } + })); + } else { + Api.purgeIptables(context, true, new RootCommand() + .setSuccessToast(R.string.toast_disabled) + .setFailureToast(R.string.toast_error_disabling) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + boolean status = (state.exitCode != 0); + if (state.exitCode != 0 && !status) { + // Show error state on failure (when status doesn't match expected) + showErrorState(context, manager, widgetIds); + } else if (state.exitCode == 0) { + // Show success state briefly before final state + showSuccessState(context, manager, widgetIds, status); + } + Api.setEnabled(context, status, true); + } + })); + } + } + } + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] ints) { + super.onUpdate(context, appWidgetManager, ints); + final SharedPreferences prefs = context.getSharedPreferences(Api.PREF_FIREWALL_STATUS, 0); + boolean enabled = prefs.getBoolean(Api.PREF_ENABLED, true); + showWidget(context, appWidgetManager, ints, enabled); + } + + private void showWidget(Context context, AppWidgetManager manager, + int[] widgetIds, boolean enabled) { + final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.onoff_widget); + final int iconId = enabled ? R.drawable.widget_on : R.drawable.widget_off; + views.setInt(R.id.widgetCanvas, "setBackgroundResource", iconId); + + // Note: Animation support removed for compatibility + + final Intent msg = new Intent(context, StatusWidget.class); + msg.setAction(Api.TOGGLE_REQUEST_MSG); + final PendingIntent intent = PendingIntent.getBroadcast(context, -1, msg, PendingIntent.FLAG_IMMUTABLE); + views.setOnClickPendingIntent(R.id.widgetCanvas, intent); + manager.updateAppWidget(widgetIds, views); + } + + private void showPendingState(Context context, AppWidgetManager manager, int[] widgetIds, boolean willEnable) { + final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.onoff_widget); + try { + final int iconId = willEnable ? R.drawable.widget_enabling : R.drawable.widget_disabling; + views.setInt(R.id.widgetCanvas, "setBackgroundResource", iconId); + } catch (Exception e) { + // Fallback to original icons if enhanced resources fail + final int iconId = willEnable ? R.drawable.widget_on : R.drawable.widget_off; + views.setInt(R.id.widgetCanvas, "setBackgroundResource", iconId); + } + final Intent msg = new Intent(context, StatusWidget.class); + msg.setAction(Api.TOGGLE_REQUEST_MSG); + final PendingIntent intent = PendingIntent.getBroadcast(context, -1, msg, PendingIntent.FLAG_IMMUTABLE); + views.setOnClickPendingIntent(R.id.widgetCanvas, intent); + manager.updateAppWidget(widgetIds, views); + } + + private void showErrorState(Context context, AppWidgetManager manager, int[] widgetIds) { + final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.onoff_widget); + try { + views.setInt(R.id.widgetCanvas, "setBackgroundResource", R.drawable.widget_error); + } catch (Exception e) { + // Fallback to off icon if error resource fails + views.setInt(R.id.widgetCanvas, "setBackgroundResource", R.drawable.widget_off); + } + final Intent msg = new Intent(context, StatusWidget.class); + msg.setAction(Api.TOGGLE_REQUEST_MSG); + final PendingIntent intent = PendingIntent.getBroadcast(context, -1, msg, PendingIntent.FLAG_IMMUTABLE); + views.setOnClickPendingIntent(R.id.widgetCanvas, intent); + manager.updateAppWidget(widgetIds, views); + + // Auto-revert to normal state after 2 seconds + android.os.Handler handler = new android.os.Handler(android.os.Looper.getMainLooper()); + handler.postDelayed(() -> { + final SharedPreferences prefs = context.getSharedPreferences(Api.PREF_FIREWALL_STATUS, 0); + boolean currentEnabled = prefs.getBoolean(Api.PREF_ENABLED, true); + showWidget(context, manager, widgetIds, currentEnabled); + }, 2000); + } + + private void showSuccessState(Context context, AppWidgetManager manager, int[] widgetIds, boolean finalState) { + final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.onoff_widget); + try { + views.setInt(R.id.widgetCanvas, "setBackgroundResource", R.drawable.widget_success); + } catch (Exception e) { + // Fallback to final state icon if success resource fails + final int iconId = finalState ? R.drawable.widget_on : R.drawable.widget_off; + views.setInt(R.id.widgetCanvas, "setBackgroundResource", iconId); + } + final Intent msg = new Intent(context, StatusWidget.class); + msg.setAction(Api.TOGGLE_REQUEST_MSG); + final PendingIntent intent = PendingIntent.getBroadcast(context, -1, msg, PendingIntent.FLAG_IMMUTABLE); + views.setOnClickPendingIntent(R.id.widgetCanvas, intent); + manager.updateAppWidget(widgetIds, views); + + // Auto-revert to final state after 1 second + android.os.Handler handler = new android.os.Handler(android.os.Looper.getMainLooper()); + handler.postDelayed(() -> { + showWidget(context, manager, widgetIds, finalState); + }, 1000); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidget.java b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidget.java new file mode 100644 index 0000000..eab3972 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidget.java @@ -0,0 +1,57 @@ +/** + * ON/OFF Widget implementation + * + * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro + * Copyright (C) 2012 Umakanthan Chandran + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Rodrigo Zechin Rosauro, Umakanthan Chandran + * @version 1.1 + */ + +package dev.ukanth.ufirewall.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; + +import dev.ukanth.ufirewall.R; + +/** + * ON/OFF Widget implementation + */ +public class ToggleWidget extends AppWidgetProvider { + @Override + public void onReceive(final Context context, final Intent intent) { + super.onReceive(context, intent); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + + RemoteViews remoteViews = new RemoteViews(context.getPackageName(),R.layout.toggle_widget_layout); + Intent configIntent = new Intent(context, ToggleWidgetActivity.class); + + PendingIntent configPendingIntent = PendingIntent.getActivity(context,0, configIntent, PendingIntent.FLAG_MUTABLE); + remoteViews.setOnClickPendingIntent(R.id.toggle_widget_icon,configPendingIntent); + appWidgetManager.updateAppWidget(appWidgetIds, remoteViews); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java new file mode 100644 index 0000000..995f534 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetActivity.java @@ -0,0 +1,568 @@ +package dev.ukanth.ufirewall.widget; + +import static dev.ukanth.ufirewall.util.SecurityUtil.LOCK_VERIFICATION; +import static dev.ukanth.ufirewall.util.SecurityUtil.REQ_ENTER_PATTERN; +import static haibison.android.lockpattern.LockPatternActivity.RESULT_FAILED; +import static haibison.android.lockpattern.LockPatternActivity.RESULT_FORGOT_PATTERN; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.List; + +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; +import dev.ukanth.ufirewall.util.SecurityUtil; +import dev.ukanth.ufirewall.widget.RadialMenuWidget.RadialMenuEntry; + +public class ToggleWidgetActivity extends Activity { + + private RadialMenuWidget pieMenu; + private RelativeLayout relativeLayout; + + private int actionType = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.toggle_widget_view); + + relativeLayout = this.findViewById(R.id.widgetCircle); + pieMenu = new RadialMenuWidget(getBaseContext()); + + pieMenu.setAnimationSpeed(0L); + + int xLayoutSize = relativeLayout.getWidth(); + int yLayoutSize = relativeLayout.getHeight(); + pieMenu.setSourceLocation(xLayoutSize, yLayoutSize); + pieMenu.setIconSize(15, 30); + pieMenu.setTextSize(13); + + pieMenu.setCenterCircle(new Close()); + pieMenu.addMenuEntry(new Status()); + pieMenu.addMenuEntry(new EnableFirewall()); + pieMenu.addMenuEntry(new DisableFirewall()); + + if (G.enableMultiProfile()) { + pieMenu.addMenuEntry(new Profiles()); + } + + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); + + relativeLayout.addView(pieMenu, params); + + } + + + public class Close implements RadialMenuEntry { + + public String getName() { + return "Close"; + } + + public String getLabel() { + return null; + } + + public int getIcon() { + return android.R.drawable.ic_menu_close_clear_cancel; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + relativeLayout = findViewById(R.id.widgetCircle); + relativeLayout.removeAllViews(); + finish(); + } + } + + public class EnableFirewall implements RadialMenuEntry { + public String getName() { + return ""; + } + + public String getLabel() { + return getString(R.string.enable); + } + + public int getIcon() { + return 0; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + actionType = 1; + startAction(1); + } + } + + public class Status implements RadialMenuEntry { + public String getName() { + if (G.enableMultiProfile()) { + switch (G.storedProfile()) { + case Api.DEFAULT_PREFS_NAME: + return G.gPrefs.getString("default", getApplicationContext().getString(R.string.defaultProfile)); + case "AFWallProfile1": + return G.gPrefs.getString("profile1", getApplicationContext().getString(R.string.profile1)); + case "AFWallProfile2": + return G.gPrefs.getString("profile2", getApplicationContext().getString(R.string.profile2)); + case "AFWallProfile3": + return G.gPrefs.getString("profile3", getApplicationContext().getString(R.string.profile3)); + default: + return G.storedProfile(); + } + } else { + return ""; + } + } + + public String getLabel() { + if (G.enableMultiProfile()) { + switch (G.storedProfile()) { + case Api.DEFAULT_PREFS_NAME: + return G.gPrefs.getString("default", getApplicationContext().getString(R.string.defaultProfile)); + case "AFWallProfile1": + return G.gPrefs.getString("profile1", getApplicationContext().getString(R.string.profile1)); + case "AFWallProfile2": + return G.gPrefs.getString("profile2", getApplicationContext().getString(R.string.profile2)); + case "AFWallProfile3": + return G.gPrefs.getString("profile3", getApplicationContext().getString(R.string.profile3)); + default: + return G.storedProfile(); + } + } else { + return ""; + } + } + + public int getIcon() { + return (Api.isEnabled(getApplicationContext()) ? R.drawable.widget_on : R.drawable.widget_off); + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + + } + } + + public class DisableFirewall implements RadialMenuEntry { + public String getName() { + return ""; + } + + public String getLabel() { + return getString(R.string.disable); + } + + public int getIcon() { + return 0; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + actionType = 2; + startAction(2); + } + } + + + public class Profiles implements RadialMenuEntry { + public String getName() { + return getString(R.string.profiles); + } + + public String getLabel() { + return getString(R.string.profiles); + } + + public int getIcon() { + return 0; + } + + private final List children = new ArrayList(); + + public List getChildren() { + return children; + } + + public Profiles() { + if (!G.isProfileMigrated()) { + children.add(new DefaultProfile()); + children.add(new Profile1()); + children.add(new Profile2()); + children.add(new Profile3()); + for (String profileName : G.getAdditionalProfiles()) { + RadialMenuEntry entry = new GenericProfile(profileName); + children.add(entry); + } + } else { + children.add(new DefaultProfile()); + for (ProfileData data : ProfileHelper.getProfiles()) { + RadialMenuEntry entry = new GenericProfile(data.getName()); + children.add(entry); + } + } + } + + public void menuActiviated() { + } + } + + public class GenericProfile implements RadialMenuEntry { + public String getName() { + return profileName; + } + + public String getLabel() { + return profileName; + } + + public int getIcon() { + return 0; + } + + private final String profileName; + + public GenericProfile(String profileName) { + this.profileName = profileName; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + final Handler toaster = new Handler() { + public void handleMessage(Message msg) { + if (msg.arg1 != 0) + Toast.makeText(getApplicationContext(), msg.arg1, Toast.LENGTH_SHORT).show(); + } + }; + final Context context = getApplicationContext(); + new Thread() { + @Override + public void run() { + if (G.isProfileMigrated()) { + ProfileData data = ProfileHelper.getProfileByName(profileName); + G.setProfile(true, data.getIdentifier()); + } else { + G.setProfile(true, profileName); + } + Api.applySavedIptablesRules(context, true, new RootCommand() + .setSuccessToast(R.string.rules_applied) + .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; + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + } + toaster.sendMessage(msg); + } + })); + //Api.showNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + Api.updateNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + } + }.start(); + } + } + + public class DefaultProfile implements RadialMenuEntry { + public String getName() { + return G.gPrefs.getString("default", getApplicationContext().getString(R.string.defaultProfile)); + } + + public String getLabel() { + return G.gPrefs.getString("default", getApplicationContext().getString(R.string.defaultProfile)); + } + + public int getIcon() { + return 0; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + startAction(3); + } + } + + public class Profile1 implements RadialMenuEntry { + public String getName() { + if (!G.isProfileMigrated()) { + return G.gPrefs.getString("profile1", getString(R.string.profile1)); + } else { + return "AFWallProfile1"; + } + } + + public String getLabel() { + if (!G.isProfileMigrated()) { + return G.gPrefs.getString("profile1", getString(R.string.profile1)); + } else { + return "AFWallProfile1"; + } + } + + public int getIcon() { + return 0; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + startAction(4); + } + } + + public class Profile2 implements RadialMenuEntry { + public String getName() { + if (!G.isProfileMigrated()) { + return G.gPrefs.getString("profile2", getString(R.string.profile2)); + } else { + return "AFWallProfile2"; + } + } + + public String getLabel() { + if (!G.isProfileMigrated()) { + return G.gPrefs.getString("profile2", getString(R.string.profile2)); + } else { + return "AFWallProfile2"; + } + } + + public int getIcon() { + return 0; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + startAction(5); + } + } + + public class Profile3 implements RadialMenuEntry { + public String getName() { + if (!G.isProfileMigrated()) { + return G.gPrefs.getString("profile3", getString(R.string.profile3)); + } else { + return "AFWallProfile3"; + } + } + + public String getLabel() { + if (!G.isProfileMigrated()) { + return G.gPrefs.getString("profile3", getString(R.string.profile3)); + } else { + return "AFWallProfile3"; + } + } + + public int getIcon() { + return 0; + } + + public List getChildren() { + return null; + } + + public void menuActiviated() { + startAction(6); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case LOCK_VERIFICATION: { + switch (resultCode) { + case RESULT_OK: + invokeAction(); + break; + default: + ToggleWidgetActivity.this.finish(); + android.os.Process.killProcess(android.os.Process.myPid()); + break; + } + } + break; + case REQ_ENTER_PATTERN: { + switch (resultCode) { + case RESULT_OK: + invokeAction(); + break; + case RESULT_CANCELED: + case RESULT_FAILED: + case RESULT_FORGOT_PATTERN: + default: + ToggleWidgetActivity.this.finish(); + break; + } + } + break; + } + } + + + private void startAction(final int i) { + actionType = i; + SecurityUtil util = new SecurityUtil(ToggleWidgetActivity.this); + boolean isProtected = util.isPasswordProtected(); + if (!isProtected) { + invokeAction(); + } else { + util.passCheck(); + } + } + + private void invokeAction() { + final Handler toaster = new Handler(getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.arg1 != 0) { + runOnUiThread(() -> Toast.makeText(getApplicationContext(),msg.arg1,Toast.LENGTH_SHORT).show()); + } + } + }; + final Context context = getApplicationContext(); + new Thread() { + @Override + public void run() { + Looper.prepare(); + if (actionType < 7) { + switch (actionType) { + case 1: + Api.applySavedIptablesRules(context, true, new RootCommand() + .setSuccessToast(R.string.rules_applied) + .setFailureToast(R.string.error_apply) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + final 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; + } + toaster.sendMessage(msg); + } + })); + break; + case 2: + //validation, check for password + Api.purgeIptables(context, true, new RootCommand() + .setSuccessToast(R.string.toast_disabled) + .setFailureToast(R.string.toast_error_disabling) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + final Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.toast_disabled; + Api.setEnabled(context, false, false); + } else { + // error details are already in logcat + msg.arg1 = R.string.toast_error_disabling; + } + toaster.sendMessage(msg); + } + })); + break; + case 3: + G.setProfile(G.enableMultiProfile(), "AFWallPrefs"); + break; + case 4: + G.setProfile(true, "AFWallProfile1"); + break; + case 5: + G.setProfile(true, "AFWallProfile2"); + break; + case 6: + G.setProfile(true, "AFWallProfile3"); + break; + } + if (actionType > 2) { + final Message msg = new Message(); + Api.applySavedIptablesRules(context, true, new RootCommand() + .setSuccessToast(R.string.rules_applied) + .setFailureToast(R.string.error_apply) + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + if (state.exitCode == 0) { + msg.arg1 = R.string.rules_applied; + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + } + toaster.sendMessage(msg); + } + })); + G.reloadPrefs(); + } + } + //Api.showNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + Api.updateNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + } + }.start(); + } + + /*private boolean applyProfileRules(final Context context, final Message msg, final Handler toaster) { + boolean ret = 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) { + msg.arg1 = R.string.rules_applied; + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + } + } + })); + return ret; + }*/ +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOld.java b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOld.java new file mode 100644 index 0000000..633477a --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOld.java @@ -0,0 +1,59 @@ +/** + * ON/OFF Widget implementation + *

+ * Copyright (C) 2009-2011 Rodrigo Zechin Rosauro + * Copyright (C) 2012 Umakanthan Chandran + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @author Rodrigo Zechin Rosauro, Umakanthan Chandran + * @version 1.1 + */ + +package dev.ukanth.ufirewall.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; + +import dev.ukanth.ufirewall.R; + +/** + * ON/OFF Widget implementation + */ +public class ToggleWidgetOld extends AppWidgetProvider { + @Override + public void onReceive(final Context context, final Intent intent) { + super.onReceive(context, intent); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + + RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.toggle_widget_old_layout); + + Intent configIntent = new Intent(context, ToggleWidgetOldActivity.class); + + PendingIntent configPendingIntent = PendingIntent.getActivity(context, 0, configIntent, PendingIntent.FLAG_MUTABLE); + + remoteViews.setOnClickPendingIntent(R.id.toggle_widget_icon_old, configPendingIntent); + appWidgetManager.updateAppWidget(appWidgetIds, remoteViews); + } + +} diff --git a/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java new file mode 100644 index 0000000..91d7bc8 --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/widget/ToggleWidgetOldActivity.java @@ -0,0 +1,486 @@ +package dev.ukanth.ufirewall.widget; + +import static dev.ukanth.ufirewall.util.SecurityUtil.LOCK_VERIFICATION; +import static dev.ukanth.ufirewall.util.SecurityUtil.REQ_ENTER_PATTERN; +import static haibison.android.lockpattern.LockPatternActivity.RESULT_FAILED; +import static haibison.android.lockpattern.LockPatternActivity.RESULT_FORGOT_PATTERN; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.Toast; + +import java.util.List; + +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; +import dev.ukanth.ufirewall.util.SecurityUtil; + +public class ToggleWidgetOldActivity extends Activity implements + OnClickListener { + + private static Button enableButton; + private static Button disableButton; + private static Button defaultButton; + private static Button profButton1; + private static Button profButton2; + private static Button profButton3; + + private String profileName; + private int buttonId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.toggle_widget_old_view); + + enableButton = this.findViewById(R.id.toggle_enable_firewall); + disableButton = this + .findViewById(R.id.toggle_disable_firewall); + defaultButton = this.findViewById(R.id.toggle_default_profile); + + enableButton.setOnClickListener(this); + disableButton.setOnClickListener(this); + defaultButton.setOnClickListener(this); + + profButton1 = this.findViewById(R.id.toggle_profile1); + profButton2 = this.findViewById(R.id.toggle_profile2); + profButton3 = this.findViewById(R.id.toggle_profile3); + + if (Api.isEnabled(getApplicationContext())) { + enableOthers(); + } else { + disableOthers(); + } + + if (!G.isProfileMigrated()) { + profButton1.setText(G.gPrefs.getString("profile1", getApplicationContext().getString(R.string.profile1))); + profButton2.setText(G.gPrefs.getString("profile2", getApplicationContext().getString(R.string.profile2))); + profButton3.setText(G.gPrefs.getString("profile3", getApplicationContext().getString(R.string.profile3))); + } else { + //hide by default + profButton1.setVisibility(View.INVISIBLE); + profButton2.setVisibility(View.INVISIBLE); + profButton3.setVisibility(View.INVISIBLE); + + if (ProfileHelper.getProfileByIdentifier("AFWallProfile1") != null) { + profButton1.setVisibility(View.VISIBLE); + } + if (ProfileHelper.getProfileByIdentifier("AFWallProfile2") != null) { + profButton2.setVisibility(View.VISIBLE); + } + if (ProfileHelper.getProfileByIdentifier("AFWallProfile3") != null) { + profButton3.setVisibility(View.VISIBLE); + } + List listData = ProfileHelper.getProfiles(); + //worst case 10 ! + if (listData.size() <= 20) { + switch (listData.size()) { + case 1: + profButton1.setText(listData.get(0).getName()); + profButton1.setVisibility(View.VISIBLE); + break; + case 2: + profButton1.setText(listData.get(0).getName()); + profButton1.setVisibility(View.VISIBLE); + profButton2.setText(listData.get(1).getName()); + profButton2.setVisibility(View.VISIBLE); + case 3: + profButton1.setText(listData.get(0).getName()); + profButton1.setVisibility(View.VISIBLE); + profButton2.setText(listData.get(1).getName()); + profButton2.setVisibility(View.VISIBLE); + profButton3.setText(listData.get(2).getName()); + profButton3.setVisibility(View.VISIBLE); + default: + //enable first 3 + profButton1.setText(listData.get(0).getName()); + profButton1.setVisibility(View.VISIBLE); + profButton2.setText(listData.get(1).getName()); + profButton2.setVisibility(View.VISIBLE); + profButton3.setText(listData.get(2).getName()); + profButton3.setVisibility(View.VISIBLE); + + } + } + } + + profButton1.setOnClickListener(this); + profButton2.setOnClickListener(this); + profButton3.setOnClickListener(this); + + if (!G.enableMultiProfile()) { + profButton1.setEnabled(false); + profButton2.setEnabled(false); + profButton3.setEnabled(false); + } else { + if (Api.isEnabled(getApplicationContext())) { + String profileName = G.storedProfile(); + if (profileName.equals(Api.DEFAULT_PREFS_NAME)) { + disableDefault(); + } else { + disableCustom(profileName); + } + } + } + } + + private void switchAction() { + if(buttonId == R.id.toggle_enable_firewall) { + startAction(1); + } else if(buttonId == R.id.toggle_disable_firewall) { + startAction(2); + } else if(buttonId == R.id.toggle_default_profile) { + startAction(3); + } else if(buttonId == R.id.toggle_profile1) { + if (!G.isProfileMigrated()) { + startAction(4); + } else { + runProfile(profileName); + } + } else if(buttonId == R.id.toggle_profile2) { + if (!G.isProfileMigrated()) { + startAction(5); + } else { + runProfile(profileName); + } + } else if(buttonId == R.id.toggle_profile3) { + if (!G.isProfileMigrated()) { + startAction(6); + } else { + runProfile(profileName); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case LOCK_VERIFICATION: { + switch (resultCode) { + case RESULT_OK: + switchAction(); + break; + default: + ToggleWidgetOldActivity.this.finish(); + android.os.Process.killProcess(android.os.Process.myPid()); + break; + } + } + break; + case REQ_ENTER_PATTERN: { + switch (resultCode) { + case RESULT_OK: + switchAction(); + break; + case RESULT_CANCELED: + case RESULT_FAILED: + case RESULT_FORGOT_PATTERN: + default: + ToggleWidgetOldActivity.this.finish(); + break; + } + } + break; + } + } + + @Override + public void onClick(View button) { + profileName = ((Button) button).getText().toString(); + buttonId = button.getId(); + + SecurityUtil util = new SecurityUtil(ToggleWidgetOldActivity.this); + boolean passCheck = util.isPasswordProtected(); + if (!passCheck) { + switchAction(); + } else { + util.passCheck(); + } + } + + private void runProfile(final String profileName) { + final Handler toaster = new Handler() { + public void handleMessage(Message msg) { + if (msg.arg1 != 0) + Toast.makeText(getApplicationContext(), msg.arg1, Toast.LENGTH_SHORT).show(); + } + }; + + final Context context = getApplicationContext(); + new Thread() { + @Override + public void run() { + Looper.prepare(); + ProfileData data = ProfileHelper.getProfileByName(profileName); + G.setProfile(true, data.getIdentifier()); + Api.applySavedIptablesRules(context, false, new RootCommand() + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.rules_applied; + toaster.sendMessage(msg); + enableOthers(); + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + toaster.sendMessage(msg); + } + } + })); + //Api.showNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + Api.updateNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + } + }.start(); + defaultButton.setEnabled(true); + if (profButton1.getText().equals(profileName)) { + profButton1.setEnabled(false); + profButton2.setEnabled(true); + profButton3.setEnabled(true); + } else if (profButton2.getText().equals(profileName)) { + profButton1.setEnabled(true); + profButton2.setEnabled(false); + profButton3.setEnabled(true); + } else if (profButton3.getText().equals(profileName)) { + profButton1.setEnabled(true); + profButton2.setEnabled(true); + profButton3.setEnabled(false); + } + } + + private void startAction(final int i) { + + final Handler toaster = new Handler() { + public void handleMessage(Message msg) { + if (msg.arg1 != 0) + Toast.makeText(getApplicationContext(), msg.arg1, + Toast.LENGTH_SHORT).show(); + } + }; + final Context context = getApplicationContext(); + new Thread() { + @Override + public void run() { + Looper.prepare(); + switch (i) { + case 1: + Api.applySavedIptablesRules(context, false, new RootCommand() + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.rules_applied; + toaster.sendMessage(msg); + enableOthers(); + Api.setEnabled(context, true, false); + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + toaster.sendMessage(msg); + } + } + })); + break; + case 2: + // validation, check for password + Api.purgeIptables(context, true, new RootCommand() + .setSuccessToast(R.string.toast_disabled) + .setFailureToast(R.string.toast_error_disabling) + .setReopenShell(true) + .setCallback(new RootCommand.Callback() { + public void cbFunc(RootCommand state) { + final Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.toast_disabled; + Api.setEnabled(context, false, false); + } else { + // error details are already in logcat + msg.arg1 = R.string.toast_error_disabling; + } + toaster.sendMessage(msg); + } + })); + break; + case 3: + G.setProfile(G.enableMultiProfile(), "AFWallPrefs"); + Api.applySavedIptablesRules(context, false, new RootCommand() + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.rules_applied; + toaster.sendMessage(msg); + enableOthers(); + disableDefault(); + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + toaster.sendMessage(msg); + } + } + })); + /* if (applyProfileRules(context, msg, toaster)) { + disableDefault(); + }*/ + break; + case 4: + G.setProfile(true, "AFWallProfile1"); + Api.applySavedIptablesRules(context, false, new RootCommand() + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.rules_applied; + toaster.sendMessage(msg); + enableOthers(); + disableCustom("AFWallProfile1"); + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + toaster.sendMessage(msg); + } + } + })); + /*if (applyProfileRules(context, msg, toaster)) { + disableCustom("AFWallProfile1"); + }*/ + break; + case 5: + G.setProfile(true, "AFWallProfile2"); + Api.applySavedIptablesRules(context, false, new RootCommand() + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.rules_applied; + toaster.sendMessage(msg); + enableOthers(); + disableCustom("AFWallProfile2"); + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + toaster.sendMessage(msg); + } + } + })); + /*if (applyProfileRules(context, msg, toaster)) { + disableCustom("AFWallProfile2"); + }*/ + break; + case 6: + G.setProfile(true, "AFWallProfile3"); + Api.applySavedIptablesRules(context, false, new RootCommand() + .setCallback(new RootCommand.Callback() { + @Override + public void cbFunc(RootCommand state) { + Message msg = new Message(); + if (state.exitCode == 0) { + msg.arg1 = R.string.rules_applied; + toaster.sendMessage(msg); + enableOthers(); + disableCustom("AFWallProfile3"); + } else { + // error details are already in logcat + msg.arg1 = R.string.error_apply; + toaster.sendMessage(msg); + } + } + })); + /* if (applyProfileRules(context, msg, toaster)) { + disableCustom("AFWallProfile3"); + }*/ + break; + } + //Api.showNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + Api.updateNotification(Api.isEnabled(getApplicationContext()), getApplicationContext()); + } + }.start(); + } + + private void enableOthers() { + runOnUiThread(new Runnable() { + public void run() { + enableButton.setEnabled(false); + disableButton.setEnabled(true); + defaultButton.setEnabled(true); + if (G.enableMultiProfile()) { + profButton1.setEnabled(true); + profButton2.setEnabled(true); + profButton3.setEnabled(true); + } + } + }); + + } + + private void disableOthers() { + runOnUiThread(new Runnable() { + public void run() { + enableButton.setEnabled(true); + disableButton.setEnabled(false); + defaultButton.setEnabled(false); + profButton1.setEnabled(false); + profButton2.setEnabled(false); + profButton3.setEnabled(false); + } + }); + } + + private void disableDefault() { + runOnUiThread(new Runnable() { + public void run() { + defaultButton.setEnabled(false); + if (G.enableMultiProfile()) { + profButton1.setEnabled(true); + profButton2.setEnabled(true); + profButton3.setEnabled(true); + } + } + }); + } + + private void disableCustom(final String code) { + runOnUiThread(new Runnable() { + public void run() { + switch (code) { + case "AFWallProfile1": + defaultButton.setEnabled(true); + profButton1.setEnabled(false); + profButton2.setEnabled(true); + profButton3.setEnabled(true); + break; + case "AFWallProfile2": + defaultButton.setEnabled(true); + profButton1.setEnabled(true); + profButton2.setEnabled(false); + profButton3.setEnabled(true); + break; + case "AFWallProfile3": + defaultButton.setEnabled(true); + profButton1.setEnabled(true); + profButton2.setEnabled(true); + profButton3.setEnabled(false); + } + } + }); + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/ukanth/ufirewall/xposed/XposedInit.java b/app/src/main/java/dev/ukanth/ufirewall/xposed/XposedInit.java new file mode 100644 index 0000000..53b03fc --- /dev/null +++ b/app/src/main/java/dev/ukanth/ufirewall/xposed/XposedInit.java @@ -0,0 +1,175 @@ +package dev.ukanth.ufirewall.xposed; + +/* +import android.app.Activity; +import android.app.AndroidAppHelper; +import android.app.DownloadManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.net.Uri; +import android.os.Build; +import android.widget.Toast; + +import com.crossbowffs.remotepreferences.RemotePreferences; + +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.IXposedHookZygoteInit; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XSharedPreferences; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import dev.ukanth.ufirewall.Api; +import dev.ukanth.ufirewall.BuildConfig; +import dev.ukanth.ufirewall.log.Log; +import dev.ukanth.ufirewall.preferences.SharePreference; + +import static de.robv.android.xposed.XposedHelpers.callStaticMethod; +import static de.robv.android.xposed.XposedHelpers.findClass;*/ + + +/** + * Created by ukanth on 6/7/16. + */ +/*public class XposedInit implements IXposedHookZygoteInit, IXposedHookLoadPackage { + + private final String MY_APP = BuildConfig.APPLICATION_ID; + private final String TAG = "AFWallXPosed"; + private String MODULE_PATH = null; + private Context context; + private XSharedPreferences prefs; + private SharedPreferences pPrefs; + private SharedPreferences sharedPreferences; + private Activity activity; + + public Activity getActivity() { + return activity; + } + + public void setActivity(Activity activity) { + this.activity = activity; + } + + @Override + public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { + try { + //Log.i(TAG,"Looking for AFWall: " + loadPackageParam.packageName); + if (loadPackageParam.packageName.equals(MY_APP)) { + Log.i(TAG, "Matched Package and now hooking: " + loadPackageParam.packageName); + reloadPreference(); + interceptAFWall(loadPackageParam); + } + interceptDownloadManager(loadPackageParam); + } catch (XposedHelpers.ClassNotFoundError e) { + Log.d(TAG, e.getLocalizedMessage()); + } + } + + //Check if AFWall is hooked to make sure XPosed works fine. + private void interceptAFWall(XC_LoadPackage.LoadPackageParam loadPackageParam) { + Class afwallHook = findClass("dev.ukanth.ufirewall.util.G", loadPackageParam.classLoader); + XC_MethodHook xposedResult = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + Log.i(TAG, "Util.isXposedEnabled hooked"); + param.setResult(true); + } + + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + } + }; + XposedBridge.hookAllMethods(afwallHook, "isXposedEnabled", xposedResult); + } + + private void reloadPreference() { + try { + if (context == null) { + Object activityThread = callStaticMethod( + findClass("android.app.ActivityThread", null), "currentActivityThread"); + context = AndroidAppHelper.currentApplication(); + } + if (prefs == null) { + prefs = new XSharedPreferences(MY_APP); + prefs.makeWorldReadable(); + prefs.reload(); + sharedPreferences = new RemotePreferences(context, BuildConfig.APPLICATION_ID, Api.PREFS_NAME); + } else { + prefs.makeWorldReadable(); + prefs.reload(); + } + //pPrefs = context.getSharedPreferences(Api.PREFS_NAME,Context.MODE_PRIVATE); + pPrefs = new SharePreference(context, MY_APP, Api.PREFS_NAME); + Log.d(TAG, "Reloaded preferences from AFWall"); + } catch (Exception e) { + Log.d(TAG, "Exception in reloading preferences" + e.getLocalizedMessage()); + } + } + + + private void interceptDownloadManager(XC_LoadPackage.LoadPackageParam loadPackageParam) { + final ApplicationInfo applicationInfo = loadPackageParam.appInfo; + Class downloadManager = findClass("android.app.DownloadManager", loadPackageParam.classLoader); + Class downloadManagerRequest = findClass("android.app.DownloadManager.Request", loadPackageParam.classLoader); + + XC_MethodHook dmSingleResult = new XC_MethodHook() { + + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + reloadPreference(); + final boolean isAppAllowed = Api.isAppAllowed(context, applicationInfo, sharedPreferences, pPrefs); + Log.d(TAG, "DM Calling Application: " + applicationInfo.packageName + ", Allowed: " + isAppAllowed); + if (!isAppAllowed) { + if (param.getResult() != null) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + dm.remove((Long) param.getResult()); + } + param.setResult(0L); + if (getActivity() != null) { + getActivity().runOnUiThread(() -> Toast.makeText(getActivity().getApplicationContext(), "AFWall+ denied access to Download Manager for package(uid) : " + applicationInfo.packageName + "(" + applicationInfo.uid + ")", Toast.LENGTH_LONG).show()); + } + } + } + }; + + XC_MethodHook hookDM = new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + reloadPreference(); + + final boolean isAppAllowed = Api.isAppAllowed(context, applicationInfo, sharedPreferences, pPrefs); + Log.d(TAG, "DM Calling Application: " + applicationInfo.packageName + ", Allowed: " + isAppAllowed); + if (!isAppAllowed) { + final Uri uri = (Uri) param.args[0]; + Log.d(TAG, "Attempted URL via DM Leak : " + uri.toString()); + XposedHelpers.setObjectField(param.thisObject, "mUri", Uri.parse("http://localhost/dummy.txt")); + if (getActivity() != null) { + getActivity().runOnUiThread(() -> Toast.makeText(getActivity().getApplicationContext(), "Download Manager is attempting to download : " + uri.toString(), Toast.LENGTH_LONG).show()); + } + } + } + }; + + XposedBridge.hookAllMethods(downloadManager, "enqueue", dmSingleResult); + XposedBridge.hookAllConstructors(downloadManagerRequest, hookDM); + + Class instrumentation = findClass("android.app.Instrumentation", loadPackageParam.classLoader); + XposedBridge.hookAllMethods(instrumentation, "newActivity", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Activity mCurrentActivity = (Activity) param.getResult(); + if (mCurrentActivity != null) { + setActivity(mCurrentActivity); + } + } + }); + + } + + @Override + public void initZygote(StartupParam startupParam) throws Throwable { + MODULE_PATH = startupParam.modulePath; + Log.d(TAG, "MyPackage: " + MY_APP); + } +}*/ diff --git a/app/src/main/java/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java b/app/src/main/java/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java new file mode 100644 index 0000000..c92aa17 --- /dev/null +++ b/app/src/main/java/net/margaritov/preference/colorpicker/AlphaPatternDrawable.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2010 Daniel Nilsson + * + * 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 net.margaritov.preference.colorpicker; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * This drawable that draws a simple white and gray chessboard pattern. + * It's pattern you will often see as a background behind a + * partly transparent image in many applications. + * + * @author Daniel Nilsson + */ +public class AlphaPatternDrawable extends Drawable { + + private int mRectangleSize = 10; + + private final Paint mPaint = new Paint(); + private final Paint mPaintWhite = new Paint(); + private final Paint mPaintGray = new Paint(); + + private int numRectanglesHorizontal; + private int numRectanglesVertical; + + /** + * Bitmap in which the pattern will be cahched. + */ + private Bitmap mBitmap; + + public AlphaPatternDrawable(int rectangleSize) { + mRectangleSize = rectangleSize; + mPaintWhite.setColor(0xffffffff); + mPaintGray.setColor(0xffcbcbcb); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); + } + + @Override + public int getOpacity() { + return PixelFormat.UNKNOWN; + } + + @Override + public void setAlpha(int alpha) { + throw new UnsupportedOperationException("Alpha is not supported by this drawwable."); + } + + @Override + public void setColorFilter(ColorFilter cf) { + throw new UnsupportedOperationException("ColorFilter is not supported by this drawwable."); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + int height = bounds.height(); + int width = bounds.width(); + + numRectanglesHorizontal = (int) Math.ceil((width / mRectangleSize)); + numRectanglesVertical = (int) Math.ceil(height / mRectangleSize); + + generatePatternBitmap(); + + } + + /** + * This will generate a bitmap with the pattern + * as big as the rectangle we were allow to draw on. + * We do this to chache the bitmap so we don't need to + * recreate it each time draw() is called since it + * takes a few milliseconds. + */ + private void generatePatternBitmap() { + + if (getBounds().width() <= 0 || getBounds().height() <= 0) { + return; + } + + mBitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888); + Canvas canvas = new Canvas(mBitmap); + + Rect r = new Rect(); + boolean verticalStartWhite = true; + for (int i = 0; i <= numRectanglesVertical; i++) { + + boolean isWhite = verticalStartWhite; + for (int j = 0; j <= numRectanglesHorizontal; j++) { + + r.top = i * mRectangleSize; + r.left = j * mRectangleSize; + r.bottom = r.top + mRectangleSize; + r.right = r.left + mRectangleSize; + + canvas.drawRect(r, isWhite ? mPaintWhite : mPaintGray); + + isWhite = !isWhite; + } + + verticalStartWhite = !verticalStartWhite; + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java new file mode 100644 index 0000000..f2d1072 --- /dev/null +++ b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerDialog.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2010 Daniel Nilsson + * + * 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 net.margaritov.preference.colorpicker; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.text.InputFilter; +import android.text.InputType; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.Locale; + +import dev.ukanth.ufirewall.R; + +public class ColorPickerDialog + extends + Dialog + implements + ColorPickerView.OnColorChangedListener, + View.OnClickListener { + + private ColorPickerView mColorPicker; + + private ColorPickerPanelView mOldColor; + private ColorPickerPanelView mNewColor; + + private EditText mHexVal; + private boolean mHexValueEnabled = false; + private ColorStateList mHexDefaultTextColor; + + private OnColorChangedListener mListener; + + public interface OnColorChangedListener { + void onColorChanged(int color); + } + + public ColorPickerDialog(Context context, int initialColor) { + super(context); + + init(initialColor); + } + + private void init(int color) { + // To fight color banding. + getWindow().setFormat(PixelFormat.RGBA_8888); + + setUp(color); + + } + + private void setUp(int color) { + + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + View layout = inflater.inflate(R.layout.dialog_color_picker, null); + + setContentView(layout); + + setTitle(R.string.dialog_color_picker); + + mColorPicker = layout.findViewById(R.id.color_picker_view); + mOldColor = layout.findViewById(R.id.old_color_panel); + mNewColor = layout.findViewById(R.id.new_color_panel); + + mHexVal = layout.findViewById(R.id.hex_val); + mHexVal.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + mHexDefaultTextColor = mHexVal.getTextColors(); + + mHexVal.setOnEditorActionListener(new TextView.OnEditorActionListener() { + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + String s = mHexVal.getText().toString(); + if (s.length() > 5 || s.length() < 10) { + try { + int c = ColorPickerPreference.convertToColorInt(s); + mColorPicker.setColor(c, true); + mHexVal.setTextColor(mHexDefaultTextColor); + } catch (IllegalArgumentException e) { + mHexVal.setTextColor(Color.RED); + } + } else { + mHexVal.setTextColor(Color.RED); + } + return true; + } + return false; + } + }); + + ((LinearLayout) mOldColor.getParent()).setPadding( + Math.round(mColorPicker.getDrawingOffset()), + 0, + Math.round(mColorPicker.getDrawingOffset()), + 0 + ); + + mOldColor.setOnClickListener(this); + mNewColor.setOnClickListener(this); + mColorPicker.setOnColorChangedListener(this); + mOldColor.setColor(color); + mColorPicker.setColor(color, true); + + } + + @Override + public void onColorChanged(int color) { + + mNewColor.setColor(color); + + if (mHexValueEnabled) + updateHexValue(color); + + /* + if (mListener != null) { + mListener.onColorChanged(color); + } + */ + + } + + public void setHexValueEnabled(boolean enable) { + mHexValueEnabled = enable; + if (enable) { + mHexVal.setVisibility(View.VISIBLE); + updateHexLengthFilter(); + updateHexValue(getColor()); + } else + mHexVal.setVisibility(View.GONE); + } + + public boolean getHexValueEnabled() { + return mHexValueEnabled; + } + + private void updateHexLengthFilter() { + if (getAlphaSliderVisible()) + mHexVal.setFilters(new InputFilter[]{new InputFilter.LengthFilter(9)}); + else + mHexVal.setFilters(new InputFilter[]{new InputFilter.LengthFilter(7)}); + } + + private void updateHexValue(int color) { + if (getAlphaSliderVisible()) { + mHexVal.setText(ColorPickerPreference.convertToARGB(color).toUpperCase(Locale.getDefault())); + } else { + mHexVal.setText(ColorPickerPreference.convertToRGB(color).toUpperCase(Locale.getDefault())); + } + mHexVal.setTextColor(mHexDefaultTextColor); + } + + public void setAlphaSliderVisible(boolean visible) { + mColorPicker.setAlphaSliderVisible(visible); + if (mHexValueEnabled) { + updateHexLengthFilter(); + updateHexValue(getColor()); + } + } + + public boolean getAlphaSliderVisible() { + return mColorPicker.getAlphaSliderVisible(); + } + + /** + * Set a OnColorChangedListener to get notified when the color + * selected by the user has changed. + * + * @param listener + */ + public void setOnColorChangedListener(OnColorChangedListener listener) { + mListener = listener; + } + + public int getColor() { + return mColorPicker.getColor(); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.new_color_panel) { + if (mListener != null) { + mListener.onColorChanged(mNewColor.getColor()); + } + } + dismiss(); + } + + @Override + public Bundle onSaveInstanceState() { + Bundle state = super.onSaveInstanceState(); + state.putInt("old_color", mOldColor.getColor()); + state.putInt("new_color", mNewColor.getColor()); + return state; + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mOldColor.setColor(savedInstanceState.getInt("old_color")); + mColorPicker.setColor(savedInstanceState.getInt("new_color"), true); + } +} diff --git a/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPanelView.java b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPanelView.java new file mode 100644 index 0000000..6a211b2 --- /dev/null +++ b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPanelView.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2010 Daniel Nilsson + * + * 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 net.margaritov.preference.colorpicker; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +/** + * This class draws a panel which which will be filled with a color which can be set. + * It can be used to show the currently selected color which you will get from + * the {@link ColorPickerView}. + * + * @author Daniel Nilsson + */ +public class ColorPickerPanelView extends View { + + /** + * The width in pixels of the border + * surrounding the color panel. + */ + private final static float BORDER_WIDTH_PX = 1; + + private float mDensity = 1f; + + private int mBorderColor = 0xff6E6E6E; + private int mColor = 0xff000000; + + private Paint mBorderPaint; + private Paint mColorPaint; + + private RectF mDrawingRect; + private RectF mColorRect; + + private AlphaPatternDrawable mAlphaPattern; + + + public ColorPickerPanelView(Context context) { + this(context, null); + } + + public ColorPickerPanelView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorPickerPanelView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mBorderPaint = new Paint(); + mColorPaint = new Paint(); + mDensity = getContext().getResources().getDisplayMetrics().density; + } + + + @Override + protected void onDraw(Canvas canvas) { + + final RectF rect = mColorRect; + + if (BORDER_WIDTH_PX > 0) { + mBorderPaint.setColor(mBorderColor); + canvas.drawRect(mDrawingRect, mBorderPaint); + } + + if (mAlphaPattern != null) { + mAlphaPattern.draw(canvas); + } + + mColorPaint.setColor(mColor); + + canvas.drawRect(rect, mColorPaint); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + mDrawingRect = new RectF(); + mDrawingRect.left = getPaddingLeft(); + mDrawingRect.right = w - getPaddingRight(); + mDrawingRect.top = getPaddingTop(); + mDrawingRect.bottom = h - getPaddingBottom(); + + setUpColorRect(); + + } + + private void setUpColorRect() { + final RectF dRect = mDrawingRect; + + float left = dRect.left + BORDER_WIDTH_PX; + float top = dRect.top + BORDER_WIDTH_PX; + float bottom = dRect.bottom - BORDER_WIDTH_PX; + float right = dRect.right - BORDER_WIDTH_PX; + + mColorRect = new RectF(left, top, right, bottom); + + mAlphaPattern = new AlphaPatternDrawable((int) (5 * mDensity)); + + mAlphaPattern.setBounds( + Math.round(mColorRect.left), + Math.round(mColorRect.top), + Math.round(mColorRect.right), + Math.round(mColorRect.bottom) + ); + + } + + /** + * Set the color that should be shown by this view. + * + * @param color + */ + public void setColor(int color) { + mColor = color; + invalidate(); + } + + /** + * Get the color currently show by this view. + * + * @return + */ + public int getColor() { + return mColor; + } + + /** + * Set the color of the border surrounding the panel. + * + * @param color + */ + public void setBorderColor(int color) { + mBorderColor = color; + invalidate(); + } + + /** + * Get the color of the border surrounding the panel. + */ + public int getBorderColor() { + return mBorderColor; + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java new file mode 100644 index 0000000..c575151 --- /dev/null +++ b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerPreference.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2011 Sergey Margaritov + * + * 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 net.margaritov.preference.colorpicker; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +/** + * A preference type that allows a user to choose a time + * + * @author Sergey Margaritov + */ +public class ColorPickerPreference + extends + Preference + implements + Preference.OnPreferenceClickListener, + ColorPickerDialog.OnColorChangedListener { + + View mView; + ColorPickerDialog mDialog; + private int mValue = Color.BLACK; + private float mDensity = 0; + private boolean mAlphaSliderEnabled = false; + private boolean mHexValueEnabled = false; + + public ColorPickerPreference(Context context) { + super(context); + init(context, null); + } + + public ColorPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public ColorPickerPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getColor(index, Color.BLACK); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + onColorChanged(restoreValue ? getPersistedInt(mValue) : (Integer) defaultValue); + } + + private void init(Context context, AttributeSet attrs) { + mDensity = getContext().getResources().getDisplayMetrics().density; + setOnPreferenceClickListener(this); + if (attrs != null) { + mAlphaSliderEnabled = attrs.getAttributeBooleanValue(null, "alphaSlider", false); + mHexValueEnabled = attrs.getAttributeBooleanValue(null, "hexValue", false); + } + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + mView = view; + setPreviewColor(); + } + + private void setPreviewColor() { + if (mView == null) return; + ImageView iView = new ImageView(getContext()); + LinearLayout widgetFrameView = mView.findViewById(android.R.id.widget_frame); + if (widgetFrameView == null) return; + widgetFrameView.setVisibility(View.VISIBLE); + widgetFrameView.setPadding( + widgetFrameView.getPaddingLeft(), + widgetFrameView.getPaddingTop(), + (int) (mDensity * 8), + widgetFrameView.getPaddingBottom() + ); + // remove already create preview image + int count = widgetFrameView.getChildCount(); + if (count > 0) { + widgetFrameView.removeViews(0, count); + } + widgetFrameView.addView(iView); + widgetFrameView.setMinimumWidth(0); + iView.setBackgroundDrawable(new AlphaPatternDrawable((int) (5 * mDensity))); + iView.setImageBitmap(getPreviewBitmap()); + } + + private Bitmap getPreviewBitmap() { + int d = (int) (mDensity * 31); //30dip + int color = mValue; + Bitmap bm = Bitmap.createBitmap(d, d, Config.ARGB_8888); + int w = bm.getWidth(); + int h = bm.getHeight(); + int c = color; + for (int i = 0; i < w; i++) { + for (int j = i; j < h; j++) { + c = (i <= 1 || j <= 1 || i >= w - 2 || j >= h - 2) ? Color.GRAY : color; + bm.setPixel(i, j, c); + if (i != j) { + bm.setPixel(j, i, c); + } + } + } + + return bm; + } + + @Override + public void onColorChanged(int color) { + if (isPersistent()) { + persistInt(color); + } + mValue = color; + setPreviewColor(); + try { + getOnPreferenceChangeListener().onPreferenceChange(this, color); + } catch (NullPointerException e) { + + } + } + + public boolean onPreferenceClick(Preference preference) { + showDialog(null); + return false; + } + + protected void showDialog(Bundle state) { + mDialog = new ColorPickerDialog(getContext(), mValue); + mDialog.setOnColorChangedListener(this); + if (mAlphaSliderEnabled) { + mDialog.setAlphaSliderVisible(true); + } + if (mHexValueEnabled) { + mDialog.setHexValueEnabled(true); + } + if (state != null) { + mDialog.onRestoreInstanceState(state); + } + mDialog.show(); + } + + /** + * Toggle Alpha Slider visibility (by default it's disabled) + * + * @param enable + */ + public void setAlphaSliderEnabled(boolean enable) { + mAlphaSliderEnabled = enable; + } + + /** + * Toggle Hex Value visibility (by default it's disabled) + * + * @param enable + */ + public void setHexValueEnabled(boolean enable) { + mHexValueEnabled = enable; + } + + /** + * For custom purposes. Not used by ColorPickerPreferrence + * + * @param color + * @author Unknown + */ + public static String convertToARGB(int color) { + String alpha = Integer.toHexString(Color.alpha(color)); + String red = Integer.toHexString(Color.red(color)); + String green = Integer.toHexString(Color.green(color)); + String blue = Integer.toHexString(Color.blue(color)); + + if (alpha.length() == 1) { + alpha = "0" + alpha; + } + + if (red.length() == 1) { + red = "0" + red; + } + + if (green.length() == 1) { + green = "0" + green; + } + + if (blue.length() == 1) { + blue = "0" + blue; + } + + return "#" + alpha + red + green + blue; + } + + /** + * For custom purposes. Not used by ColorPickerPreference + * + * @param color + * @return A string representing the hex value of color, + * without the alpha value + * @author Charles Rosaaen + */ + public static String convertToRGB(int color) { + String red = Integer.toHexString(Color.red(color)); + String green = Integer.toHexString(Color.green(color)); + String blue = Integer.toHexString(Color.blue(color)); + + if (red.length() == 1) { + red = "0" + red; + } + + if (green.length() == 1) { + green = "0" + green; + } + + if (blue.length() == 1) { + blue = "0" + blue; + } + + return "#" + red + green + blue; + } + + /** + * For custom purposes. Not used by ColorPickerPreferrence + * + * @param argb + * @throws NumberFormatException + * @author Unknown + */ + public static int convertToColorInt(String argb) throws IllegalArgumentException { + + if (!argb.startsWith("#")) { + argb = "#" + argb; + } + + return Color.parseColor(argb); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (mDialog == null || !mDialog.isShowing()) { + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.dialogBundle = mDialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !(state instanceof SavedState)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + showDialog(myState.dialogBundle); + } + + private static class SavedState extends BaseSavedState { + Bundle dialogBundle; + + public SavedState(Parcel source) { + super(source); + dialogBundle = source.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeBundle(dialogBundle); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java new file mode 100644 index 0000000..4d872ef --- /dev/null +++ b/app/src/main/java/net/margaritov/preference/colorpicker/ColorPickerView.java @@ -0,0 +1,945 @@ +/* + * Copyright (C) 2010 Daniel Nilsson + * + * 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 net.margaritov.preference.colorpicker; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ComposeShader; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.Shader.TileMode; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +/** + * Displays a color picker to the user and allow them + * to select a color. A slider for the alpha channel is + * also available. Enable it by setting + * setAlphaSliderVisible(boolean) to true. + * + * @author Daniel Nilsson + */ +public class ColorPickerView extends View { + + private final static int PANEL_SAT_VAL = 0; + private final static int PANEL_HUE = 1; + private final static int PANEL_ALPHA = 2; + + /** + * The width in pixels of the border + * surrounding all color panels. + */ + private final static float BORDER_WIDTH_PX = 1; + + /** + * The width in dp of the hue panel. + */ + private float HUE_PANEL_WIDTH = 30f; + /** + * The height in dp of the alpha panel + */ + private float ALPHA_PANEL_HEIGHT = 20f; + /** + * The distance in dp between the different + * color panels. + */ + private float PANEL_SPACING = 10f; + /** + * The radius in dp of the color palette tracker circle. + */ + private float PALETTE_CIRCLE_TRACKER_RADIUS = 5f; + /** + * The dp which the tracker of the hue or alpha panel + * will extend outside of its bounds. + */ + private float RECTANGLE_TRACKER_OFFSET = 2f; + + + private float mDensity = 1f; + + private OnColorChangedListener mListener; + + private Paint mSatValPaint; + private Paint mSatValTrackerPaint; + + private Paint mHuePaint; + private Paint mHueTrackerPaint; + + private Paint mAlphaPaint; + private Paint mAlphaTextPaint; + + private Paint mBorderPaint; + + private Shader mValShader; + private Shader mSatShader; + private Shader mHueShader; + private Shader mAlphaShader; + + private int mAlpha = 0xff; + private float mHue = 360f; + private float mSat = 0f; + private float mVal = 0f; + + private String mAlphaSliderText = ""; + private int mSliderTrackerColor = 0xff1c1c1c; + private int mBorderColor = 0xff6E6E6E; + private boolean mShowAlphaPanel = false; + + /* + * To remember which panel that has the "focus" when + * processing hardware button data. + */ + private int mLastTouchedPanel = PANEL_SAT_VAL; + + /** + * Offset from the edge we must have or else + * the finger tracker will get clipped when + * it is drawn outside of the view. + */ + private float mDrawingOffset; + + + /* + * Distance form the edges of the view + * of where we are allowed to draw. + */ + private RectF mDrawingRect; + + private RectF mSatValRect; + private RectF mHueRect; + private RectF mAlphaRect; + + private AlphaPatternDrawable mAlphaPattern; + + private Point mStartTouchPoint = null; + + public interface OnColorChangedListener { + void onColorChanged(int color); + } + + public ColorPickerView(Context context) { + this(context, null); + } + + public ColorPickerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorPickerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mDensity = getContext().getResources().getDisplayMetrics().density; + PALETTE_CIRCLE_TRACKER_RADIUS *= mDensity; + RECTANGLE_TRACKER_OFFSET *= mDensity; + HUE_PANEL_WIDTH *= mDensity; + ALPHA_PANEL_HEIGHT *= mDensity; + PANEL_SPACING = PANEL_SPACING * mDensity; + + mDrawingOffset = calculateRequiredOffset(); + + initPaintTools(); + + //Needed for receiving trackball motion events. + setFocusable(true); + setFocusableInTouchMode(true); + } + + private void initPaintTools() { + + mSatValPaint = new Paint(); + mSatValTrackerPaint = new Paint(); + mHuePaint = new Paint(); + mHueTrackerPaint = new Paint(); + mAlphaPaint = new Paint(); + mAlphaTextPaint = new Paint(); + mBorderPaint = new Paint(); + + + mSatValTrackerPaint.setStyle(Style.STROKE); + mSatValTrackerPaint.setStrokeWidth(2f * mDensity); + mSatValTrackerPaint.setAntiAlias(true); + + mHueTrackerPaint.setColor(mSliderTrackerColor); + mHueTrackerPaint.setStyle(Style.STROKE); + mHueTrackerPaint.setStrokeWidth(2f * mDensity); + mHueTrackerPaint.setAntiAlias(true); + + mAlphaTextPaint.setColor(0xff1c1c1c); + mAlphaTextPaint.setTextSize(14f * mDensity); + mAlphaTextPaint.setAntiAlias(true); + mAlphaTextPaint.setTextAlign(Align.CENTER); + mAlphaTextPaint.setFakeBoldText(true); + + + } + + private float calculateRequiredOffset() { + float offset = Math.max(PALETTE_CIRCLE_TRACKER_RADIUS, RECTANGLE_TRACKER_OFFSET); + offset = Math.max(offset, BORDER_WIDTH_PX * mDensity); + + return offset * 1.5f; + } + + private int[] buildHueColorArray() { + + int[] hue = new int[361]; + + int count = 0; + for (int i = hue.length - 1; i >= 0; i--, count++) { + hue[count] = Color.HSVToColor(new float[]{i, 1f, 1f}); + } + + return hue; + } + + + @Override + protected void onDraw(Canvas canvas) { + + if (mDrawingRect.width() <= 0 || mDrawingRect.height() <= 0) return; + + drawSatValPanel(canvas); + drawHuePanel(canvas); + drawAlphaPanel(canvas); + + } + + private void drawSatValPanel(Canvas canvas) { + + final RectF rect = mSatValRect; + + if (BORDER_WIDTH_PX > 0) { + mBorderPaint.setColor(mBorderColor); + canvas.drawRect(mDrawingRect.left, mDrawingRect.top, rect.right + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, mBorderPaint); + } + + if (mValShader == null) { + mValShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, + 0xffffffff, 0xff000000, TileMode.CLAMP); + } + + int rgb = Color.HSVToColor(new float[]{mHue, 1f, 1f}); + + mSatShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, + 0xffffffff, rgb, TileMode.CLAMP); + ComposeShader mShader = new ComposeShader(mValShader, mSatShader, PorterDuff.Mode.MULTIPLY); + mSatValPaint.setShader(mShader); + + canvas.drawRect(rect, mSatValPaint); + + Point p = satValToPoint(mSat, mVal); + + mSatValTrackerPaint.setColor(0xff000000); + canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS - 1f * mDensity, mSatValTrackerPaint); + + mSatValTrackerPaint.setColor(0xffdddddd); + canvas.drawCircle(p.x, p.y, PALETTE_CIRCLE_TRACKER_RADIUS, mSatValTrackerPaint); + + } + + private void drawHuePanel(Canvas canvas) { + + final RectF rect = mHueRect; + + if (BORDER_WIDTH_PX > 0) { + mBorderPaint.setColor(mBorderColor); + canvas.drawRect(rect.left - BORDER_WIDTH_PX, + rect.top - BORDER_WIDTH_PX, + rect.right + BORDER_WIDTH_PX, + rect.bottom + BORDER_WIDTH_PX, + mBorderPaint); + } + + if (mHueShader == null) { + mHueShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, buildHueColorArray(), null, TileMode.CLAMP); + mHuePaint.setShader(mHueShader); + } + + canvas.drawRect(rect, mHuePaint); + + float rectHeight = 4 * mDensity / 2; + + Point p = hueToPoint(mHue); + + RectF r = new RectF(); + r.left = rect.left - RECTANGLE_TRACKER_OFFSET; + r.right = rect.right + RECTANGLE_TRACKER_OFFSET; + r.top = p.y - rectHeight; + r.bottom = p.y + rectHeight; + + + canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint); + + } + + private void drawAlphaPanel(Canvas canvas) { + + if (!mShowAlphaPanel || mAlphaRect == null || mAlphaPattern == null) return; + + final RectF rect = mAlphaRect; + + if (BORDER_WIDTH_PX > 0) { + mBorderPaint.setColor(mBorderColor); + canvas.drawRect(rect.left - BORDER_WIDTH_PX, + rect.top - BORDER_WIDTH_PX, + rect.right + BORDER_WIDTH_PX, + rect.bottom + BORDER_WIDTH_PX, + mBorderPaint); + } + + + mAlphaPattern.draw(canvas); + + float[] hsv = new float[]{mHue, mSat, mVal}; + int color = Color.HSVToColor(hsv); + int acolor = Color.HSVToColor(0, hsv); + + mAlphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, + color, acolor, TileMode.CLAMP); + + + mAlphaPaint.setShader(mAlphaShader); + + canvas.drawRect(rect, mAlphaPaint); + + if (mAlphaSliderText != null && !mAlphaSliderText.equals("")) { + canvas.drawText(mAlphaSliderText, rect.centerX(), rect.centerY() + 4 * mDensity, mAlphaTextPaint); + } + + float rectWidth = 4 * mDensity / 2; + + Point p = alphaToPoint(mAlpha); + + RectF r = new RectF(); + r.left = p.x - rectWidth; + r.right = p.x + rectWidth; + r.top = rect.top - RECTANGLE_TRACKER_OFFSET; + r.bottom = rect.bottom + RECTANGLE_TRACKER_OFFSET; + + canvas.drawRoundRect(r, 2, 2, mHueTrackerPaint); + + } + + + private Point hueToPoint(float hue) { + + final RectF rect = mHueRect; + final float height = rect.height(); + + Point p = new Point(); + + p.y = (int) (height - (hue * height / 360f) + rect.top); + p.x = (int) rect.left; + + return p; + } + + private Point satValToPoint(float sat, float val) { + + final RectF rect = mSatValRect; + final float height = rect.height(); + final float width = rect.width(); + + Point p = new Point(); + + p.x = (int) (sat * width + rect.left); + p.y = (int) ((1f - val) * height + rect.top); + + return p; + } + + private Point alphaToPoint(int alpha) { + + final RectF rect = mAlphaRect; + final float width = rect.width(); + + Point p = new Point(); + + p.x = (int) (width - (alpha * width / 0xff) + rect.left); + p.y = (int) rect.top; + + return p; + + } + + private float[] pointToSatVal(float x, float y) { + + final RectF rect = mSatValRect; + float[] result = new float[2]; + + float width = rect.width(); + float height = rect.height(); + + if (x < rect.left) { + x = 0f; + } else if (x > rect.right) { + x = width; + } else { + x = x - rect.left; + } + + if (y < rect.top) { + y = 0f; + } else if (y > rect.bottom) { + y = height; + } else { + y = y - rect.top; + } + + + result[0] = 1.f / width * x; + result[1] = 1.f - (1.f / height * y); + + return result; + } + + private float pointToHue(float y) { + + final RectF rect = mHueRect; + + float height = rect.height(); + + if (y < rect.top) { + y = 0f; + } else if (y > rect.bottom) { + y = height; + } else { + y = y - rect.top; + } + + return 360f - (y * 360f / height); + } + + private int pointToAlpha(int x) { + + final RectF rect = mAlphaRect; + final int width = (int) rect.width(); + + if (x < rect.left) { + x = 0; + } else if (x > rect.right) { + x = width; + } else { + x = x - (int) rect.left; + } + + return 0xff - (x * 0xff / width); + + } + + + @Override + public boolean onTrackballEvent(MotionEvent event) { + + float x = event.getX(); + float y = event.getY(); + + boolean update = false; + + + if (event.getAction() == MotionEvent.ACTION_MOVE) { + + switch (mLastTouchedPanel) { + + case PANEL_SAT_VAL: + + float sat, val; + + sat = mSat + x / 50f; + val = mVal - y / 50f; + + if (sat < 0f) { + sat = 0f; + } else if (sat > 1f) { + sat = 1f; + } + + if (val < 0f) { + val = 0f; + } else if (val > 1f) { + val = 1f; + } + + mSat = sat; + mVal = val; + + update = true; + + break; + + case PANEL_HUE: + + float hue = mHue - y * 10f; + + if (hue < 0f) { + hue = 0f; + } else if (hue > 360f) { + hue = 360f; + } + + mHue = hue; + + update = true; + + break; + + case PANEL_ALPHA: + + if (!mShowAlphaPanel || mAlphaRect == null) { + update = false; + } else { + + int alpha = (int) (mAlpha - x * 10); + + if (alpha < 0) { + alpha = 0; + } else if (alpha > 0xff) { + alpha = 0xff; + } + + mAlpha = alpha; + + + update = true; + } + + break; + } + + + } + + + if (update) { + + if (mListener != null) { + mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal})); + } + + invalidate(); + return true; + } + + + return super.onTrackballEvent(event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + + boolean update = false; + + switch (event.getAction()) { + + case MotionEvent.ACTION_DOWN: + + mStartTouchPoint = new Point((int) event.getX(), (int) event.getY()); + + update = moveTrackersIfNeeded(event); + + break; + + case MotionEvent.ACTION_MOVE: + + update = moveTrackersIfNeeded(event); + + break; + + case MotionEvent.ACTION_UP: + + mStartTouchPoint = null; + + update = moveTrackersIfNeeded(event); + + break; + + } + + if (update) { + + if (mListener != null) { + mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal})); + } + + invalidate(); + return true; + } + + + return super.onTouchEvent(event); + } + + private boolean moveTrackersIfNeeded(MotionEvent event) { + + if (mStartTouchPoint == null) return false; + + boolean update = false; + + int startX = mStartTouchPoint.x; + int startY = mStartTouchPoint.y; + + + if (mHueRect.contains(startX, startY)) { + mLastTouchedPanel = PANEL_HUE; + + mHue = pointToHue(event.getY()); + + update = true; + } else if (mSatValRect.contains(startX, startY)) { + + mLastTouchedPanel = PANEL_SAT_VAL; + + float[] result = pointToSatVal(event.getX(), event.getY()); + + mSat = result[0]; + mVal = result[1]; + + update = true; + } else if (mAlphaRect != null && mAlphaRect.contains(startX, startY)) { + + mLastTouchedPanel = PANEL_ALPHA; + + mAlpha = pointToAlpha((int) event.getX()); + + update = true; + } + + + return update; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int width = 0; + int height = 0; + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int widthAllowed = MeasureSpec.getSize(widthMeasureSpec); + int heightAllowed = MeasureSpec.getSize(heightMeasureSpec); + + widthAllowed = chooseWidth(widthMode, widthAllowed); + heightAllowed = chooseHeight(heightMode, heightAllowed); + + if (!mShowAlphaPanel) { + + height = (int) (widthAllowed - PANEL_SPACING - HUE_PANEL_WIDTH); + + //If calculated height (based on the width) is more than the allowed height. + if (height > heightAllowed || getTag().equals("landscape")) { + height = heightAllowed; + width = (int) (height + PANEL_SPACING + HUE_PANEL_WIDTH); + } else { + width = widthAllowed; + } + } else { + + width = (int) (heightAllowed - ALPHA_PANEL_HEIGHT + HUE_PANEL_WIDTH); + + if (width > widthAllowed) { + width = widthAllowed; + height = (int) (widthAllowed - HUE_PANEL_WIDTH + ALPHA_PANEL_HEIGHT); + } else { + height = heightAllowed; + } + + } + + setMeasuredDimension(width, height); + } + + private int chooseWidth(int mode, int size) { + if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) { + return size; + } else { // (mode == MeasureSpec.UNSPECIFIED) + return getPrefferedWidth(); + } + } + + private int chooseHeight(int mode, int size) { + if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) { + return size; + } else { // (mode == MeasureSpec.UNSPECIFIED) + return getPrefferedHeight(); + } + } + + private int getPrefferedWidth() { + + int width = getPrefferedHeight(); + + if (mShowAlphaPanel) { + width -= (PANEL_SPACING + ALPHA_PANEL_HEIGHT); + } + + + return (int) (width + HUE_PANEL_WIDTH + PANEL_SPACING); + + } + + private int getPrefferedHeight() { + + int height = (int) (200 * mDensity); + + if (mShowAlphaPanel) { + height += PANEL_SPACING + ALPHA_PANEL_HEIGHT; + } + + return height; + } + + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + mDrawingRect = new RectF(); + mDrawingRect.left = mDrawingOffset + getPaddingLeft(); + mDrawingRect.right = w - mDrawingOffset - getPaddingRight(); + mDrawingRect.top = mDrawingOffset + getPaddingTop(); + mDrawingRect.bottom = h - mDrawingOffset - getPaddingBottom(); + + setUpSatValRect(); + setUpHueRect(); + setUpAlphaRect(); + } + + private void setUpSatValRect() { + + final RectF dRect = mDrawingRect; + float panelSide = dRect.height() - BORDER_WIDTH_PX * 2; + + if (mShowAlphaPanel) { + panelSide -= PANEL_SPACING + ALPHA_PANEL_HEIGHT; + } + + float left = dRect.left + BORDER_WIDTH_PX; + float top = dRect.top + BORDER_WIDTH_PX; + float bottom = top + panelSide; + float right = left + panelSide; + + mSatValRect = new RectF(left, top, right, bottom); + } + + private void setUpHueRect() { + final RectF dRect = mDrawingRect; + + float left = dRect.right - HUE_PANEL_WIDTH + BORDER_WIDTH_PX; + float top = dRect.top + BORDER_WIDTH_PX; + float bottom = dRect.bottom - BORDER_WIDTH_PX - (mShowAlphaPanel ? (PANEL_SPACING + ALPHA_PANEL_HEIGHT) : 0); + float right = dRect.right - BORDER_WIDTH_PX; + + mHueRect = new RectF(left, top, right, bottom); + } + + private void setUpAlphaRect() { + + if (!mShowAlphaPanel) return; + + final RectF dRect = mDrawingRect; + + float left = dRect.left + BORDER_WIDTH_PX; + float top = dRect.bottom - ALPHA_PANEL_HEIGHT + BORDER_WIDTH_PX; + float bottom = dRect.bottom - BORDER_WIDTH_PX; + float right = dRect.right - BORDER_WIDTH_PX; + + mAlphaRect = new RectF(left, top, right, bottom); + + mAlphaPattern = new AlphaPatternDrawable((int) (5 * mDensity)); + mAlphaPattern.setBounds( + Math.round(mAlphaRect.left), + Math.round(mAlphaRect.top), + Math.round(mAlphaRect.right), + Math.round(mAlphaRect.bottom) + ); + + } + + + /** + * Set a OnColorChangedListener to get notified when the color + * selected by the user has changed. + * + * @param listener + */ + public void setOnColorChangedListener(OnColorChangedListener listener) { + mListener = listener; + } + + /** + * Set the color of the border surrounding all panels. + * + * @param color + */ + public void setBorderColor(int color) { + mBorderColor = color; + invalidate(); + } + + /** + * Get the color of the border surrounding all panels. + */ + public int getBorderColor() { + return mBorderColor; + } + + /** + * Get the current color this view is showing. + * + * @return the current color. + */ + public int getColor() { + return Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal}); + } + + /** + * Set the color the view should show. + * + * @param color The color that should be selected. + */ + public void setColor(int color) { + setColor(color, false); + } + + /** + * Set the color this view should show. + * + * @param color The color that should be selected. + * @param callback If you want to get a callback to + * your OnColorChangedListener. + */ + public void setColor(int color, boolean callback) { + + int alpha = Color.alpha(color); + + float[] hsv = new float[3]; + + Color.colorToHSV(color, hsv); + + mAlpha = alpha; + mHue = hsv[0]; + mSat = hsv[1]; + mVal = hsv[2]; + + if (callback && mListener != null) { + mListener.onColorChanged(Color.HSVToColor(mAlpha, new float[]{mHue, mSat, mVal})); + } + + invalidate(); + } + + /** + * Get the drawing offset of the color picker view. + * The drawing offset is the distance from the side of + * a panel to the side of the view minus the padding. + * Useful if you want to have your own panel below showing + * the currently selected color and want to align it perfectly. + * + * @return The offset in pixels. + */ + public float getDrawingOffset() { + return mDrawingOffset; + } + + /** + * Set if the user is allowed to adjust the alpha panel. Default is false. + * If it is set to false no alpha will be set. + * + * @param visible + */ + public void setAlphaSliderVisible(boolean visible) { + + if (mShowAlphaPanel != visible) { + mShowAlphaPanel = visible; + + /* + * Reset all shader to force a recreation. + * Otherwise they will not look right after + * the size of the view has changed. + */ + mValShader = null; + mSatShader = null; + mHueShader = null; + mAlphaShader = null; + + requestLayout(); + } + + } + + public boolean getAlphaSliderVisible() { + return mShowAlphaPanel; + } + + public void setSliderTrackerColor(int color) { + mSliderTrackerColor = color; + + mHueTrackerPaint.setColor(mSliderTrackerColor); + + invalidate(); + } + + public int getSliderTrackerColor() { + return mSliderTrackerColor; + } + + /** + * Set the text that should be shown in the + * alpha slider. Set to null to disable text. + * + * @param res string resource id. + */ + public void setAlphaSliderText(int res) { + String text = getContext().getString(res); + setAlphaSliderText(text); + } + + /** + * Set the text that should be shown in the + * alpha slider. Set to null to disable text. + * + * @param text Text that should be shown. + */ + public void setAlphaSliderText(String text) { + mAlphaSliderText = text; + invalidate(); + } + + /** + * Get the current value of the text + * that will be shown in the alpha + * slider. + * + * @return + */ + public String getAlphaSliderText() { + return mAlphaSliderText; + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/widget_pulse.xml b/app/src/main/res/anim/widget_pulse.xml new file mode 100644 index 0000000..9a3d265 --- /dev/null +++ b/app/src/main/res/anim/widget_pulse.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/widget_scale_in.xml b/app/src/main/res/anim/widget_scale_in.xml new file mode 100644 index 0000000..4a1a6df --- /dev/null +++ b/app/src/main/res/anim/widget_scale_in.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/convert.properties b/app/src/main/res/convert.properties new file mode 100755 index 0000000..2f22c58 --- /dev/null +++ b/app/src/main/res/convert.properties @@ -0,0 +1,47 @@ +ast=values-ast-rES +ar=values-ar +az=values-az +he=values-he +bg=values-bg +bn=values-bn +bs=values-bs +ca=values-ca +cs=values-cs +da=values-da +de=values-de +el=values-el +es-ES=values-es +eu=values-eu +fa=values-fa +fi=values-fi +fr=values-fr +hi=values-hi +hr=values-hr +hu=values-hu +id=values-in +it=values-it +ja=values-ja +kn=values-kn +ko=values-ko +ml-IN=values-ml-rIN +ms=values-ms +nb=values-nb +nl=values-nl +pl=values-pl +pt-BR=values-pt-rBR +pt-PT=values-pt +ro=values-ro +ru=values-ru +sl=values-sl +sr=values-sr +sk=values-sk +sr-CS=values-sr-rCS +sv-SE=values-sv +ta=values-ta +th=values-th +tr=values-tr +uk=values-uk +ur-PK=values-ur-rPK +vi=values-vi +zh-TW=values-zh-rTW +zh-CN=values-zh-rCN diff --git a/app/src/main/res/convert.sh b/app/src/main/res/convert.sh new file mode 100755 index 0000000..1d91f3a --- /dev/null +++ b/app/src/main/res/convert.sh @@ -0,0 +1,15 @@ +# Convert exported crowdin files to android format +for d in */ ; do + while read line + do + key=`echo "$line" | cut -d'=' -f1` + value=`echo "$line" | cut -d'=' -f2` + if [ $key"/" == $d ] + then + mv $d $value + fi + done < convert.properties +done +#delete Playstore.txt files +find . -name "Playstore.txt" -print0 | xargs -0 rm -rf + diff --git a/app/src/main/res/drawable-hdpi-v11/active.png b/app/src/main/res/drawable-hdpi-v11/active.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a0341d29aa865ef4f37c41ab9c90fcdecd0130 GIT binary patch literal 686 zcmV;f0#W^mP)Px#v`|b`MF0Q*OiWBJE-nTJ21Z6k000000000006;)M zySuv+6B7Xe0h^nfU0q#~k&&vZsy{zJX=!PwsHkLQWN>hBFE1~QjEn;V105Y592^`g zD=SV;PI`KJ51m;5|NkAKVW70D6P{bU#K5w|y3EYXFs5xeu61g|m#MX{K&N%;_VN4u z{DZKX!sgVj!@01ruxzi7V62Cdy{OCT+oIXNpqf-n#OU`2z58}Fl#CI%%I%h77JegF3=S`0COwlm#7Op(9~S(Xif8 zhEJ0Lq?AD0nvB{?N0PcS8qj?ClzmbPVH6!h2%$Ap^wKDhk}PG&%Oj&OI)4#MqYw@h zUa3n0?;4Ck#s@*z?{T!^(;g-vE=SMx$U7w7V-y6xKsWIZX!C&|qRoqcQTI@8{O!6H zAuO9JLc4b?84pq<;ZW|7`Pp*?_C)CPAxO8KRxDxYtC?0d95mtW*Sgi4unHpI=imWN`7T}Pi&ZMye zgvNB?0m}p@wkb9!=q%$?aBG62vFre`g@L*-$N>remBiLU1=3TXYo@(Q3Wn0qFg3mU zYyfrU+T@dA%%Ysk<{9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi-v11/error.png b/app/src/main/res/drawable-hdpi-v11/error.png new file mode 100644 index 0000000000000000000000000000000000000000..815bf872f8397ffc38137ba0485d68512c2bd1b6 GIT binary patch literal 628 zcmV-)0*n2LP)Px#nNUnrMF0Q*dwY8f3=C&yXC);i0RaJ3RaLsWx=&9} zM@L7>$;pU_h@YRIva+%@H8oRHQ;v>~7Z(=*007)XH~;_t&bhb0s;AseLd?v}-&IQ7 zM>_AQqWIX=y|Ay%z`xaJUHReP|Nj2*x3bqz9z>T~f?@ zbkmTE?3|Usp`OpKtJ0g6^~}h^w6bpJeT)DA026dlPE!EFCZbCVd;W25{>SF>VTsp0 z1gt-<0{{R4T1iAfR5;7E)ai19Fc1J>fxs%4IzbZxBq)biulD_4t{@=62HWYh`)6VJ zhGa9@oz4C}bYvNh>wyiPyV48`=}*8!hHF#GZ`Hd^0gB?90Km#X=3tOVd6f%->!>Ic zbt;KC6$!ZLf#y6baTkJu=%33hSqx}{AHjIShPSO6;d73^M_Kj*G>TuNOVsA$ei|Kj zK_q7tVsQ`z5yO{V5FEm(Kaoc5quQk5^q#^rxX0n`Rdr!JGI%>z9YNHfJ$s3o{m4o} z)uuc|9v$ko%(6%312O|B6pTS3Zd(8q_!d O0000Px#hfqvZMF0Q*0000~R8(bUWg{aagM)(s0Rc)%O1iqb zQc_ZokdU&nvNbg|2L}hCp`mtmcE`xf4Gj$t5D-I!_5c6>yu;DU%*?5?!cL3wM~C!b znfG_5_^`FV_xk>lxcRlf(}1AYiK*aQk?WMH&7ZZ|Xqf6`ljNYm`o!7%=%t9%L$;UTJ zkBhrI8PTTFO;*rCJkiFC?kSbYJSy+GCkyDTwl?#ovSenY?M_U8%f@&FGHsz&TG55b z%!om#Y8(f3N?E2pH-f5hFX+wX)J0mm*w~m%4YuBQJ~sTF@ETli(wz=g?bNgvaJ*>` zTOq`ac8aWqAhdqWf`g0clkqx--N7_5EaGC^51fy(zg=Q|7%Y;-n95nP`$!(Kw|&>% cul_5(0jX*{+UR7bMF0Q*07*qoM6N<$f_&H#)Bpeg literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_apply.png b/app/src/main/res/drawable-hdpi/ic_apply.png new file mode 100644 index 0000000000000000000000000000000000000000..bcf6c7afa1e70277fdb3fe1c7466d61078ff7a45 GIT binary patch literal 326 zcmV-M0lEH(P)|hx6`;H9J}}9ug?pNE%8*k%1`B4yB>UK$QQV4%yxSMv`WcmGAkl7_z+sJOKN^ zp`?#xp;iuLdlQ%fC%`H2F3EoaWP1eM1INIvq}OFZzUEeD$o4ky3>*RHlHP{Gl=(qG zw#)n#unSBiEy_`-Lr Y6NYa)C8c2{ssI2007*qoM6N<$f{fyb{r~^~ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3c57d87e5640c8f9c7ed52dedad8cd3a6c631995 GIT binary patch literal 1271 zcmVPx#{7_6(MF0Q*00000004t@H2?qrz^hWyyI2be1ONa4 z0000B3ImaVJOAc}>(qA>4+U^zETD}*#I91z-VV&FAtqq|NsB#&S3xH zXZ+q}9anBt=WB6ck?+*yRDx|a${fRzx5rQ_!uAwR=o9+W2XM-; zt>QrkUErv_stGjF#!#**Df3YYwZ+73{!vktyid(SldJ&lzELZprqVJmR2f=ODZiT8j2#ZE2m^acK4MZ?G=~&6 h_tyBY$O^f?mOt}*`Px#%uq~JMF0Q*si~|IEzH$jHdLy1KWxw;rNmn!v4%vY`~8 zTe7sXRJw?=;L1OzbceH@lD()wuzKf36&lw{%x@Bjb+B6LztQvm5O{t6)K0~7)N=z9I|{Ga+W z)Pc%Wr~Tz<$mGeN+T=O~@|Ju600S&ZL_t(o!{yjnccL&90AMRPV8yy`)!H5;ED{@} zE@kW3`v3paB)Cgbg5}J^%zZ;oKJHCOLMRmezbKa_N|ei)4BVpm*zWaOtF5f*`_$=_`hLgSY#Q6NjDg-0Nt`qwlQWD1%N z%^V}_%;-40=Aawgpj zxx_h>ZiU>B$u2&B2F>30&a3$&X!dsPa6zt_?(jhF`>UDia6s<+Wed8)fF8j7+`(<^ z6i0PZpu_Nd9$IY=&gW&lywKtPv&rH5YROH5t|UfXJkW*2b~vCjY0Gvn)JTI)^J+rx zXKv_P`c2!~xS&gk>Tp8)$MPG830bUp%5WH?Mp;Jo(yN_uJBpHP&aNSZ8t823&L#FYE=egln7I{EqGDt3gRjo<-#GQqV$v$kXAXE++k!eRZNh~Q<+~Jm)?wC_!wWuj2k$Uzc(x9P( z$W46`H`uALoLK617El1rw@I0S0hiTIm+25mF4jqTk;k5?K2E1ms~2Uq6HB-}oRnrU z>O)fH?TkrZ930h7=2m9k?XeXr{9VjSOO%Ns6@D7=C$e(k auf-onZ>TwnI3p?m0000|l3?zm1T2})pmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIDl0X`wFK>9xsV4ba(B~U3(NswPK!<+9{5^9u8{1)%y+UB}qGEn@wr;B5V z#`)v~1*r{N&lz2@RM}&s#HjWlvzU#G&;KTa*sKyZ&WxtklnJeTmqK482((SG=;oVs z;UH6+go$Dy8=qv$gjKu|fx?H@IP;xMaJAxCIGHEMOu*G+mBb{)t_4mG*96(Nv9PI6 zkZn(JN@tRgh$snY?K5%^acn*Cj;le#sq9stdP@Fnh4v$f8!pJCES#L6D4()WIbq@1 z6HeC>6w8(*uvT&NY!Yxfy+E_UnnP{TF2-p-H?BLl_=ze^;$y#&+b%s}vheXm#_3z6 jE=-EpkjeCpg@a+@C;sN<72AA)US#le^>bP0l+XkK7lD-F literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/notification_error.png b/app/src/main/res/drawable-hdpi/notification_error.png new file mode 100644 index 0000000000000000000000000000000000000000..e3cbbec817bb88f84f6e8237c59f3c51d3dc8edc GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|l3?zm1T2})pmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIB<0X`wFK>9y%pl$aleV}UIk|4iehIhA4d<$1u%UL&D%*<>4X;q-aJ5LwK z5RLQ62?|mhT*b71#R+bjHH$&?LEojl+^o~5x0XhU zEbQ_vf4{0wkvlf)!L_|p4kUE1$zpV#B*m=Wbu60a4ku4>U~bGohOic$lF5u~I3hiI z8xxJ4PKck_wWo1mqjbi@yV?bc*=*~$HW=-=u#}f4dwuiN*v8qS3?-GUtM$+51e}PE z<^Gb+q4DLQ)3y_eg_1fU-8v<*IwC+)ROd>I&JxDX?AVqQsu?xv$}MLWXf`Bs`5f8I zIL)VG!Mhzb3=>Nt4kk@!o-kQh+hFYu_64dNPfp|9A*{gg!As@o7yEo$pwAgRUHx3v IIVCg!073$)P5=M^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/notification_quest.png b/app/src/main/res/drawable-hdpi/notification_quest.png new file mode 100644 index 0000000000000000000000000000000000000000..1d2c70ea9c16107e63b7af2066e12167ffc0ca56 GIT binary patch literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|l3?zm1T2})pmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UV>Bx_=LCu>Hmbl7S3LfdhU`SzhH)U(ce$q*I3TiWWVTsM=enJx~Gd{h{pNk z1O=%LnbTOJ^2JK$Phk*!5YT^*mo>Uu>VVVH-4h!uBRr%cGnDVPs7zN4UCfosvTHVH zXokA;WF}|Rt_yl?H)l(132fQc7WLjmn2AZd?Pi9PwCIf`7h4WVOjF9)!p5!3usiX4 zTFZrJY+@w~?>1fXa7y@=rW&eZ@8pf~!U)4ha+ePSCfh;0ZkEqai{&Yh{uoy)|% zRe`(G;gOY;;wDk4`HE>=z+47K?L&-r8XKdffL>(qboFyt=akR{0Mt8`<^TWy literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/notification_warn.png b/app/src/main/res/drawable-hdpi/notification_warn.png new file mode 100644 index 0000000000000000000000000000000000000000..32bed43a8a35c98d615f490c3521746cf20b847b GIT binary patch literal 302 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|l3?zm1T2})pmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIICa0X`wFK>9yka9;8CPoN^Mk|4ieh7V~cmuTJ#;A^qUve^a{o$Bf07@~1L zIYB{cLztENx+v8&BU=Wo0}Cf@<>H!VY1+^#6RyZ8xM70GrVCELICoA@;LtSS^;6vF zl4N7DSIAR?`{tU|1VwFw=*65qx@_E)inF(}=qzc~*}|c7rAH@)vGcV~%L${58g}It zpG3_BF`txFW}BG~cQ$4fGAO;=(0o)(TtQvEY{BDcA`4VEo}9+GLs)@9p!|~~=kY(A QfDUBvboFyt=akR{0HrN*(*OVf literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_dontsave.png b/app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_dontsave.png new file mode 100644 index 0000000000000000000000000000000000000000..0fc43ca48ce7a02df01e437b05314f2c27161785 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|l3?zm1T2})pmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIID%0X`wFK>9xt=y$r80Tf~_3GxeOIJ1ODEzUB-2`JF*>EaloaXxp_McxAn z94@|ZnM`CC9AlMI+wfaIR69&zj{LI;>Pi!YBC5BFZQo~me69M9oUiaFIXFtuXhnob?I5t^&81VC+ac-*QkUiS1P~*e2 zM9Q9JpKLd?mjpNOEy3ArQQT|{J)9D1CPj&RFK?6;|2pAv9?)3~p00i_>zopr0LzM9 A^8f$< literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_help.png b/app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_help.png new file mode 100644 index 0000000000000000000000000000000000000000..9f07cd8996b85c76b4e3d9930a9f66d060a59429 GIT binary patch literal 322 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|l3?zm1T2})pmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIDN0X`wFK>9xp@Y>hz5Kx-4B*-tA;Y@$zYC*I8YNcUT8bHAfo-U3d8t0P} z7@E`E9Scp_QUp{unDRW7&a@^dIx@FRP;_w#kO=lLVAMOpA{^Afz0r!xYWYltRuuuO z+pSCz5+Z>-$!sqZ1Vu!W99YlHaPpbq^iaadA$wvkd!yI_WtTvg8(e{NxIRy*VA#aL ze!^ZUtP;2oxGn=21Zgt@m?E80nA22)OBCICjvFVlh*|7o o?GP$*=$_}~bA3C9)!`Wo3FpOvZR1y82fCfX)78&qol`;+0G|wRjsO4v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_save.png b/app/src/main/res/drawable-hdpi/twofortyfouram_locale_ic_menu_save.png new file mode 100644 index 0000000000000000000000000000000000000000..e666a91d82796f612c273a012fdd5a9d72630f2a GIT binary patch literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skw2Ka=y0_p#F!Fk2kKY@z4OM?7@8BSkgW|Gb__1HM6zbaA=D16z|#WBR< z^wrCI`I-y_*gtr4wSAo>5xKiFSX_MhIlt1DlOI@qvAs~9FvCJv{>r6E5~ubzE1Q3P zJ^l69<_z75OVxb1o{0IsIL+SIJ%?Z7idS4>+q|b@xhuogtT~v#u4UNEY|eY&K?8%P zpTq=@#$u)m2YMW~1)i}_kh0$DYt3BwUx*`M@hXQeGB4D<{asj2e6=mqUZlHqUWXEs z-E!q$8&@2x2r?*ME?ye??Oe<;*ZwEE;tB$s4i?-D%MNQ?wC#zsD&g;Y!&>)HmqoAd g%lGvQLXV3dYf*o*!({Spp!XO&UHx3vIVCg!0Od%VdH?_b literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi-v11/active.png b/app/src/main/res/drawable-mdpi-v11/active.png new file mode 100644 index 0000000000000000000000000000000000000000..725251edc1dd5ee5fb0dd14b74d74b51223fe1a4 GIT binary patch literal 544 zcmV+*0^j|KP)Px#u24)=MF0Q*u)4LItfT+{0GXwqJUl!jBO?F+00000 zsI;#N3JOC*Lt$ZIjggOzpO<}oeEP6^J!Fj*9U(ToG z20FjKl>{VfPAQcaAoaqRyAF8?T%#ryJG=S8Wg)~Dkw=w?;%X74Dy|Yy%F7gs_!$%0oe7FY#3udG8+LHVhMeORNIe#>JKsZw$L`IDsRNd`tuB i^%3g#NY$j^Vv+~^>L*7rd#Zi_0000Px#fly3TMF0Q*2L}hTqoTF4vA?vlU0q$LrKLJLI;@_Z zysfNQSXg^|dkF~%%EiP10Rby3D*ylhYHDhlnVA3p03IG5+(bA3|Nq=dJ=#x0*I!h} zi-y%{V85uQ&bPJo$;Ie~eebBE>y(V`o|n>+jsE@p=6G)R)za~^uEUv@(wddfrlL0% zE;s-H02OpnPE!CI_wxFd>}2%xdA%e43Q_{6<^e|JrF;DV007rXL_t(2&xO+27J?uU z24GErN=&mD7FU#PdH)AYlVdI||Amh8!9maHIKOkEXR_}?&h+%|QWD_&aesvVP2IBO zNL&kElM2Qd4}IR>Wg9G2e> RpQZo+002ovPDHLkV1l^*&5i&7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi-v11/question.png b/app/src/main/res/drawable-mdpi-v11/question.png new file mode 100644 index 0000000000000000000000000000000000000000..7c7564c5818b2954e2691712d839c740cf57cf22 GIT binary patch literal 472 zcmV;}0Vn>6P)Px#jZjQfMF0Q*va_{ZTwEn3B{(=ZrKYC|2??L3udTDb zw7J2ftF{LR2WMwzyTZ*yMMYd(Tmbi_@$_WAy% zu)}7X_h^^qR+9FTuHBHg`Ju!5;OhO8sm+R|(zL(Rd#d=+;rzAF`^4D%LxuFFxYx+a z&4{DZn6cZNvfA%iCKvz!02OpnPE!E#luTmmBJ}k7_!^}BZF>sB=CU)LpDwQe0078I zL_t(2&xMld4uUWcggIIi0YOZ+E%&8z>H9yPhL(U>!~EIZ`LfwGQ%&2EWg_4A&&aga z=J|@f{?!k5RaH^wB1Zx}OMnmpgFTg`#u6Nf(6g4!VFM`r1G-KLplI7%1@4S<_V@)* zmTAnm@&?%gd-h8vj-0+h%K43`K>R3(8Wj2F9T=wZ#_qz0P=ZS$<}?)`xk!K-AS*$H z=0FUOEo_5SV75?duJLRvaZ~TA$Pa O0000(E#Rab=P((J=ETneI>h-aYS~my9U< zR5+go5H;K=DyPVH3usGv{G(*upb8%X6JR6h{tHqYU_0~-fF*DS4*#~u0rY@f2nM#2 zp4p0QcYp$CCu$Qz>nfz0e1PH~p v`@l`os|cV@=yz@M1`QzBK(w#9K~&Kj-BK(W0r?0t00000NkvXXu0mjfZ;fBN literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..15d04ed1563916c98af378631dd3a2db163ac0ae GIT binary patch literal 990 zcmV<410np0P)Px$4p2-~MF0Q*0000K5eD?vV(!vo*S}f;0RR9103{v| z0001WW-r~uTy0=1qmV-J+l6&!FEuU{wVz3EVk~!QFz?!g|LBYE*@yMsgYMXX{N;)3 z({Q?_Pg6)B@!5Xw*?;ibfb-jJo{dC#YBPd!GhkIJ=FolQ$z5AcB8YlET2Ln5%6ku; zSpWb3!o*(G)npN#S?SJT|KVsDpjlk7TkQDq_x$=Hqh2?sTJ+py+re47!(LRmh-kr< z>e6oiJh9)&t;%9|x95!j001j=QchC<6*Kql z?IjKZ2)*?0{^Z+D{-^o9@%WpD#u?pfHEmT2#i9E}XzggBR_ArxQZWDk0yjxSK~zY` z&DK{_;!qfXVG|{72{C|xirp=<2RJF1LQfHuCP>r&|3@~3S#wB;WM(h+y9@I^q>{tI z!GAv27njBSY3Agl*DDkXnM}S|`hG#}h|jKyr%u@EbVPRqciQdtV35g|uC7CM$%&n` z_Axf(p*nKGSRKl?HL5ynP{?1N1$!#f>u4)$p#Xn|E>Jfc%EB0#?ZNNhq0vW6SptFS zib5l}Ey9d81|FigzPe=kUGwC!_#tHJvd_yn-#|dn|d}ExU{>|T!KlWqmon0Ww%K!iX M07*qoM6N<$f~iN~Gynhq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_free.png b/app/src/main/res/drawable-mdpi/ic_launcher_free.png new file mode 100644 index 0000000000000000000000000000000000000000..6ee1fa64a15b2a1d9fbca2e84422105ebfaa8139 GIT binary patch literal 948 zcmV;l155mgP)Px#?NCfqMF0Q*0000G5D>DmvaPMHzP`TJ*46+300000 z0001CVPOIS0$p8Q!otFWf`VOLT{AN?%*@OH002NhKvq^(kB^Vf&(Frj#>dCU&CShK zRaISGUDDFh*4Ea`%ge>Z#mC3T*x1;Ulap+0Y@D2&dU|?=g@q5DSpWb3&CSgbo?5HK zxSh4Cy1Ke9rE3!`t!Zva+(QtgOoF+F-trnANv$#G9DEt#)>Hp`oFf znVI5>uf6~P04H=(PE!CCGxP23{SE^O_$R&e{@hCEA!hOZzZP6@rlyn0HIK9G*`ejd z#rnyVG5`PqGD$>1R7l6o)>(I=Fc=14p(^WuxYecGv@;VyfhbEWP^ntA*5&{IoC(OB z1`(7wb1~l?ocBoxgd~N+|6X_Po93hx^uDTK2kia~matQc;7gAnv{aVo8{0C#p=JTlq6=eYJFW>!GzOT` z+|mJ7V_6;H*Sh5v84OURK8Teqt6u!hph#r_1G+{o0309W1J*1j07_l}{t_rgKA=qB zwOnA@>qX)sprAYw#--qp?@cp%Fuq==vJUf~nPV_kYbBO{`8@rWzKz4X+N#%!#8DoM zu1)}b6@=EoL|2}}{0GPc;NvBTJ?lU}&ZhwIdJx7T{|e3m@Hz~l!$dw6L?S@3d^!M= zgBVUg7QnxBD28v@)B6ga8b~Da$DlaJSDCl>L|%7)#K5DhW&NLM4ycbfV1CJ7_8;o0 zKk_Br9*JLIq!2X@cLu3}Lx@p1?tYA;mrPm6Vmhb{ite#P$c8 zB1Hjiv{QM_S!_j;q6$1zattUQ!=kC}JcbpGIwI&K?--|t$Vhxttdaatw2jd{f zF5582p8mCsj21$@LlVsCZ4q@+;Srhp;aD_#uoUWi<#h9ZI^hrzmT>IbBX_^8^EEE5 z`KXhCbVatY*a@uIsh~K0W^1Pzs!G;)7Bx;*sk8BDECffA96`|+pCD)k75#5cBfkMy W{nc$^y(MJ;0000NS%G}U;vjb? zhIQv;UIIDl0X`wFK>9xsV4ba(B~U3(NswPK!lsxAuXvr5u@0~GJ_ba4!k zxSX8ez?vH^dR`{v?lp!a>$!~$&U_AiJX4C@8bZ#!lu+qw7MxQ2;mk~B9?O6}_p>uu z%vQ{M%=Y%UL`?!4OR-KuNS%G}U;vjb? zhIQv;UIIA=0X`wFK>9x^z}lC^0H}w*B*-tA;mfU~3cuQy@W1jYW;aOYI%GEoC^g&D z#W6(Ua&m$LYu(9NA19uwR_!!I;U dHo<|Nq4sM}{q*FBPe5lfc)I$ztaD0e0s!Xko2~!= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/notification_quest.png b/app/src/main/res/drawable-mdpi/notification_quest.png new file mode 100644 index 0000000000000000000000000000000000000000..7795a7b87ce94cd2b4d910cd04eb1f8a92c4e83f GIT binary patch literal 305 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaN3?zjj6;1;wmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIC~0X`wFK>9z?V4=tA*+30^B|(0{3}3GPTISWr^K4(fO59|zMJMk8WjZ`v z977~7Cnq?tR_AHmip;oolOaicZ*zk?r$e92l+S(*Dt$NK39zUyoN|!=oafi*0zS|4 zjinrYlI8L($?Q*bIv9l50?q~5YOpUkvzy?S`6fsrBeXjj~OKTD7|89ZJ6T-G@y GGywp1#er%7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/notification_warn.png b/app/src/main/res/drawable-mdpi/notification_warn.png new file mode 100644 index 0000000000000000000000000000000000000000..b71482369243c5786a1875df3d7c6a11a7c86b81 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaN3?zjj6;1;wmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIDl0X`wFK>9xsV4ba(B~U3(NswPK!>61d9kEak7 zaXC4`fpzvCrd*!IHs%dC)}<>vn87?jnfd7)L51GOZ<~(h($ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-v21/ic_android_white_24dp.xml b/app/src/main/res/drawable-v21/ic_android_white_24dp.xml new file mode 100644 index 0000000..d92a98a --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_android_white_24dp.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable-xhdpi-v11/active.png b/app/src/main/res/drawable-xhdpi-v11/active.png new file mode 100644 index 0000000000000000000000000000000000000000..3cfefcd5502602a481219614edaef0856add0aa9 GIT binary patch literal 784 zcmV+r1MmEaP)Px#rBF;%MF0Q*00024uCBwz#5_DarKP0;0s;U400000 z0000#KR<|wh?0_$85tQE7#Ir+3u|j@LqkKFnwn8jQG0uPBqStdWo2Y!WMX1sZf|G2ogySuwYr*8R)$;cd`VB7HKGN^8^wX)Fc-lf8{T&;#ps(t$X z`)$OUlD4Fmy{m!Dr*N&1XSI)iu$Yz9wYJB<=JoE!=-GRkU(o;n02*{sPE!B~_5FSF zAp{TssP@@RWjf|^{mH|Hp~#P+L104-0005wNkl$wjR%*$f&-^(=NcbTR-3y~lGSUdVsOH+cEy0d-> zT?3lqVZ;6Dkn~oJ{4Hl4`Z6+&(9!DHwmcO{Mh`V8XqM#1QZ(qrg%&H?163EtCC^b`y#mm5?02uK)fwxf)k$! zJay~93 z;Ap-1x##l#Dwn`IrRo`X7G4*CS5~M&xpccLA9&I#Wj@0;>H_HN>RXh;0=?xMSwx!_ z{EThCw|Xz2@k-cfB2PPx#fly3TMF0Q*1_lNb6B8vRC9|`$hK7btPEJ5TK&-5+ zxwyDtVPTb(l>h($ZEbC#p`m+wd%?lM+(bA3|NpwWy3EYX(rRJPv#{JoIm*Vx{`dCX zN4|{tpP9p(nC5(Q{O#=Er=p8$ z>=yt401%y&x zF3*9FqHD4yq?ah_jg}Y2n$Tmy7&~i28o+zs+%`ZSSC_ws{@XJDS z?S=bBVc4n zkkPl1zPgWbv%*3VY@E%W&h8yYrX&QIEFaMl$akfSt3^ITzH%7@TrBWOt*ol1d6ZUbH=Qg{3NdtvW@SdW%wA!sf=@gEay0z ziH-=liqJP+B5*R6VpR9e;x9!z7n+K%Bimd{Ufr|0yz6q!Hjf*^T6g>|b`OEMIp7V? eKe@X0PwE%XkzhZ4T>=*X0000Px#a!^cEMF0Q*6%`c-2nd&#m$bCBgoK1$U0u4mx0HMn5)W~LD@zC000eiQchCQf1@*IoOB0004Zy*7=?q%;zEcZXk4l$`u?w2NaD0$G+R|z%JMWD75%4 z5ZUX%wd9fWP+9xPXmWs1$bI0B6AOS$?E|d?WURKFp9VZ*H1wzKI zb|w6-GYh4gF-LUB4L{Lh>==T*7!8n)y1X~FNS!*`o_tE)0GTKq`bVz8B=rdEfP|0O kjWggK@BoDw2~Ph+-$m+4C@mj+NB{r;07*qoM6N<$g2d7-;s5{u literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_apply.png b/app/src/main/res/drawable-xhdpi/ic_apply.png new file mode 100644 index 0000000000000000000000000000000000000000..af91264daac8f314c190fa1fc91ea71964b35ef7 GIT binary patch literal 456 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tmUKs7M+SzC{oH>NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yLU`q0KcVYP7-hXC4kjGx) z>Fdh=luLreR7?BtlRrSA3!W~HArXh)US99j94K+%;bECBW1kQHvya|waPBt$*m+Z@ zCe<v3G#xDCleqVssqXpHr?#4{0!|$N=gQ7j z{oI}verxF`cDA^?ucw@RFIn+<&8AqT`(=;sdatbUzJDnsb+yaBmD`MhZ=RT#ZBhQ( z<*|BwZtgp!>w9*$+1XlN_3`uua*dvQ_4U+Ll%&Pq*DAJKIz>i0x}qRGb6T$D{&_E? zB7Yc#PW`n#HhHqo^v};1Rk^;iYY}id@laYurnYzIwEet5zp0kEMwFx^mZVxG7o`Fz z1|tI_LtR4yT_cka0~0G#V=H4zZ36=<0|R@3TbEHZFVdQ I&MBb@016_g6aWAK literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..553243fa7241fcf9e52c63192a36ac0437f6e0d9 GIT binary patch literal 1450 zcmV;b1y%ZqP)Px#x=>71MF0Q*)4Ep8wO59AHWm{J4-Eyqs8Rv~00000 z0001~l|+?;JT)&AfpRp=wN-a%Fz?!g`{jtxylvjfcj(i8UsNTxrCfJvH0#!Z*28tI znM;_6MYp3)51m;5|NrRCU;p7}v|QZ8TkF(y|KxhYt4>|Lj%C16O#7I5ACwwnZe zY?jM7p=!1k*`rhgvbISz2P>lBY!{)+*eZK}%Q;eY*E@asAT2=x<$ z&rJOyfNN?J+5U?~6x9j|9RCG}{B@c<{{;cj^GFDXjPscn5Pvw2r}VkbIG^D=imE@e z0j(q;*28aZJ|zILRC4GS2Sg{GL%#$dc4y)0mjNURnfiQy$+rQx@PcX^#IKH=J|kGZ4=|yh*G2Uu6Jv4qX3=a?1u-+R0%*r)++4^_`o#`^g6U zv^2|RKWXv(EB1~D(7e(wnB?aYH52frYMNi^4GO7h>x}_;l7u&1)7(k`7XuhSsXcXt zd5e9F@)exY{hal)V^n%4-aBx=4-+;a6J2dE`3T6hkX*$$>mSN_K!54p_oe_6QqtfYm+9l zBkwpNyp;g^?U~q-qd=g9&{njWz0}5h-MG!@?5O!K8}Dk<}i&5RR{BR$=?yUjlocE z%McC8rZ)Ll8@OsUpV-7hAJlv8@()I1>LKI*_g^dj04gzlV2@o3A^-pY07*qoM6N<$ Eg6$lx$N&HU literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_free.png b/app/src/main/res/drawable-xhdpi/ic_launcher_free.png new file mode 100644 index 0000000000000000000000000000000000000000..5759417eba39ee739f5f7357a9229fc131ccaff5 GIT binary patch literal 1473 zcmV;y1wQ(TP)Px#w@^$}MF0Q*n3$NUsi^}40{{R33kwUVsHgw{00RR9 z0000S930Zp(tLb;Y;0_6YipdGoIpT8Vq#)mUS4EmWY*T!&d$!eyStW_mdD4(#l^+E zyu7HWsDy-sv$L~`ii)+hwGW+G|NsBGy1LBF%(l0-$jQjU!^3>Amx{Tf8K7Q9sd@VS z`(M6~t+cRIv4n51kS?Tanwpw`xt;U-_kql)n!Ky&_V91Tn~aQ%wZ^`x+{BjEwyDIo z4LAkJ0000WbW%=J0O0*30}$f@2pBW{`tQ^1QkQnJ{pXG?*uSt>vvk~I{xMxS000Dh zNklkKosT2flEPik=bfwK7YYq?xMuNI5N_s7S_@49I&7OU0sdcAzz zJc*1`n!jA zTTiPmfT*44<%V{W$)MI>pBFK4^Ws|v*Kr8(BY;H+aa?z6*+vYqTEEgp`0ha+tVh}x zf9nuW3CoE9B>I~NAj`gkEY=%|N9al4BI_65bv&%Z3SX`OJNbb}ZWM$bc!Zw#&19kY zE=foPb+`iDv=1?HeMT@I#B@Mk8UO%Uj;C;nTmgjl1<%>q02oMpKEP7=MKc0`k2Air ztpiY825^}%&@5p0uBr(_qX6fms!2kVfWUu(&>$f6Pixl#;9mbv z3mtIu8w2ca`ftZttH{fluc{vFTxj@s`Y7YSG~h@TO874TIK=$FsVRgE{|xY>C;`_p1AHNTWUS9hfCS;* z|Bj~^pVX6+zuoXP<7XxS zVg61i-@k%w&CUS4AYJz=KVY(5j;7f!830Y!Nv$U+qG??opwkmy3|)6?0bBq~MeaMh zS(m?ziWMxP{frM7!jw&~hQ4ROjE7tR3;{Aa)Ov{3y7GYcS`(&BGcv1>nHV%7-i&ER z!cSO7m#QIoHZ(92MDDv~$Y{EUOX0H`!SmrFI8V6=(U;faQ0}6NJuv;f3?t^hfQJ?) zBSt7c2VztG@g$=G*1}*m+G{ZBEiNUGU6^n8Jf022%oBvZBnlIm5_G>zOrE7teR(2? zQuZlGPZ9@1`D_3IIfeBdo9d6`;Xup^=th{Iy9i>kiZ$WE59FU literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/notification.png b/app/src/main/res/drawable-xhdpi/notification.png new file mode 100644 index 0000000000000000000000000000000000000000..1e1a4f20bfec4d389197e6dc0d986a915ad278dc GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?smG1o(uw0_p#Rz!uJ4kb3TtAirRS=PIA)?+l9*1skqzKIA>_#8pzFZfxBH^XHZ22#R>rw1y?Tt77V%wfP-47thyg@hV!GU* z1>`98YBBOoQrN+^Wy7siO1)}~-kpsvG$%4EXO%ms>@ZL%b5w~iP}%12+0ks53scA; zN#B~y%ppxjr9uy}%)2TRIC(a+5`USpw~69YKR=bjI)R%uvmGktS|NFtciJYI3*8L- v8#2ZCX0$eo85ArP*&r%CwRbKnj|c;U)SlxKM_o69;)TJ})z4*}Q$iB}tVOQH literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/notification_error.png b/app/src/main/res/drawable-xhdpi/notification_error.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e699880658de3295713c289bcccfe6b9190713 GIT binary patch literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?smG1o(uw0_p#Rz!uJ4kb3TtAirRS7oWUt>PVef)0Y$6rSSl0q`9YyV~EA+ z3i&>FI1QTpL(#nD8>6Iix5g(KU(jMpy5XY0p^L bfUzc1x%ZIqOsy%vkY(_6^>bP0l+XkKvuC+c literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/notification_quest.png b/app/src/main/res/drawable-xhdpi/notification_quest.png new file mode 100644 index 0000000000000000000000000000000000000000..64bc04c55e872d171fa603648a2e71a547a6a17a GIT binary patch literal 437 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?smG1o(uw0_p#Rz!uJ4kb3TtAirRSS1w;Od)MAMsxJ78^H~PaNC{6D#}JFt z$q5R=1*=-Na=*W)b5Cqhr_!6Sz0!QAY!VN}tnxUd5U`42V#57izA^_D2}2b#zSPdM zinFxt-cYe>^byPS>3qxWkdy1vxt51#9T$(x458T)5#`c`8xp*7xOv%MB^`WsM|X$H z5{VldymG=1JkwG*k@R57F}01ojRysfK0gz{>Aj$N;lgIEKn8!Y4538}58hA}NHAXY z)Yq$P#YaZtos8>JtX-Ozotb6{-CS@-NIboz-=RVD@Tau`oN7Fsz87jP^y*z?m>S^X zZ+qd-!2>D1T8S*@vtDUDx-BYDX&@b`B&=EPBrGM-u^{DnX2Hi?;Q-@ZY&s?)7rGft wH(Wj=V!++L(xKW#Jwe-S+LGlu2@8PXb(``0>!(ixbbw3-Pgg&ebxsLQ017m|eGF(JB@Z(?mXHp|^^IYS+cRfecdPx#qEJj!MF0Q*0RaKCwX~(Br7W3*z)@Jp}MYUuZ%*c zc9XiNOssu%!JLTCtcbFmu*JI3?BA5rw3*eoX`6ydNYGLM001R)QchCwe|5sh!seDIxd!3s~r41z^$eh1n`0z)koTj#000qzx z;U}9(vNC`FXbrE7R~VF}d6fc2>RN53Cn)W_&F8M1*~`06p}_mR{7y%)+3oHKC zJ_riK2SL30$DrNeG0^VQK0E@-KW)2}ZijZ%X{DQ?+Oh53eNf~44)22Uv-2HJY0Y(c zdA=uIHFv*vIZikQa`(3V+C1xDJJ0hM+c+&2`2>Z6?Nbp9EcIIx<%UTR`j{U9_sV)yDUNu}*-ouwryZd(#Z;mm}9mL5u}mV{}G)6IBCyUOvXj zA&;(#jLxd<8d8mo=N|_D2*Sj{k**?{y^)*|kaS_|#{+{3GG`Ml!6BSIBy47pwv6!g zP+hHbWp zl!YESo}Z;mhQ>i6k%o5vFenjeD?WCqAraHZ+$=B1wjsBHiA(x*QeYM1=RCOk7z94pjX^xW!J%757X^zPL;)&cJBoZ(e T&M8t%00000NkvXXu0mjfbkq^8 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi-v11/error.png b/app/src/main/res/drawable-xxhdpi-v11/error.png new file mode 100644 index 0000000000000000000000000000000000000000..30220f568a57595184405c7d7e5d109de42d30e8 GIT binary patch literal 876 zcmV-y1C#uTP)Px#W>8F2MF0Q*b#-;Nv$BeciXkB(%f7u>SXebRHA6!~ z4-XFm0|T0xnyRX*TU%S)L^uEc|IEzHy1Kg8VORe9``B4e+(|s-Ze#uO@z8{R)NN(! zl8fkueZ;J(!O7!eZvj1>`r;jsm>?ALF*~ z7pUvH)?c;Mi;SJcVe%k)Su-xOq528x)N8?6ExUHXf)KRG!(cpHtDYdHeZ<+K`N#mZ zQvbtP0xPH$&mS|XECNGy29#8ggubbb5QBcs6~v)$m>ZO}1#Pn~U4 zg2d$Hm%Hu^?@Q;7sznmIb9JAiMN;zwI`*r!NdK;&<8T};66`n}*$)YHOi%=zBD(bA zWSr8-64Ins#|g^r3v?4-LCbonwG&@Jngd8?i0?VCk~MT)=DC6;TWCF=OIWgm9!{g5 zEy8)Sx91lqSwZ}^_M=PH_IhN|(Il-`#7WW}6ZE{%Qj&gURno>WZM0BPYZzxPAv@$`Gz>+4FQ7NI@~vnY-ni+XxMU(#9DJh<5^N*tI^deaYg35MpS5&Zw~?? zRbq?wk{|I^MtU<*yi*Q&lOf0Ox7U=AYFNSBWP1+r?o8Dxrum^w;6`IpJ-B>Kjfy77 zGVhN~0o26UwKLCDFy`C7*5|4o8S2F+mm(8|<-CGiPH{LO&QBv|Y6#4n&&_zaMmri1I-maPfcg&|fUhBHQso2y0000P)Px#V^B;~MF0Q*udlDg#>oHx08dX(SXfwfb#*v6I3y$_ z5D*ZWnwp4+hy(-#LxuIay1LBF%>V!YUyWYb$IO7HOKGd}kSoQAG8d@S7AaLH93}+{z6j>bj1tAjZaV zOG`6nWqDGph>i)~N?Iu~17iA_l7oVmDP{90X3VSNVC#H1b{M45iPCW zMLP`||M|HQnw7yIUI7vsB%n9k32LFg6AdEJo5l}Rbu}KVD?p-#JM#v~2+K=A6=T&k zAl_UAs_0dq>OTWXDd}mD?C0l0={ZcNK>b278su?M|Jdp&kAhy0cg^%5Xt0$Gvn9O_ zf{a0fs$;T`$3W7QA%EPOkAY~fW!jlcljkSKM=zhIv>Yj-2waHwFoksj<|@SgLdh;{%X-u@Z_Js zy*Z^iOW+6*IZoQnfP(fS0$R7zre<^(q=s8H7wzhfV@D8KNjfwN z`VB*cPDa_M=!Zg`k-iv3m@8pzebV7C`c%|X4lchY+k2t|VCQs->zEcMdjdCF5-GfV zjqVB;XlKw~imF))WA>F}ohnVfc2;nxO z`(upjY4mmzk)q(J?481g+k*WaqBN{=k)k*_tr%TF`Bf170UVv9I|#5G1^@s607*qo IM6N<$f?}$Bx&QzG literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_apply.png b/app/src/main/res/drawable-xxhdpi/ic_apply.png new file mode 100644 index 0000000000000000000000000000000000000000..98443d74bfaf2c32fda150d81c20d2856fa04a50 GIT binary patch literal 492 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/notification.png b/app/src/main/res/drawable-xxhdpi/notification.png new file mode 100644 index 0000000000000000000000000000000000000000..7adf41509406ee5a4fe4609295095fe16432a1af GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xZ3?!EyURMI7SkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skw2Ka=y0_p#F!Fk2kKY@z4N`m}?8E(Hyc5Xkhl3%@{Wb1yQaWg$#978;g zzrA+eo5@jx{R8{1r*ey5JTTRqF}bwk%M`c$d2EpnG))X`L_C4H+ zlz@fyvZ_Vt`yL9%d)G@f#JdD;xqGQ;-p8Xljng?V3Cw0;-10_2ZsrOVJsWEdXCo~} z-4kw~%8XVmWWU+5{>1550d@NdxIS*$apvR#o5g{DKJPu3Y0_uZEFhBJBw*5cso6l} zwxh(PK(`5Pm%e+tTgY?#}#G zW7jiAG~FpQdV>O!il5m z8E;c(Z{=;-oR_8->u@VcO)zW4FZX&*-AmHu?7mTtq{<7nd|P9+pEF0^RoJN_#K(i( zIeVA0j+{$*U;l!R+{FS3&PT5P*(JDFpV8+itD}+^uMsa}_a-LQ6D^BMirXtM8wtrS zy|dGM(V;(_^ENy$I;a(~@p&C%(tNi`l1DChm3-m#_*YP=WHo1vySwSPCo2Q39yzA> zXHDsgnDADAm)DfG3W3;MQB^NzX(ch~{;ao3=H8D^Pjj=>GxOV;a`n=Rd1ij=KfRvx zHNNl(+a-Z3m!95d;r;o;tY2l;;|%3MZg=g@Hj7`=UNE?q3r#H$NpVk;HTHhWz-uU? z-JT@r9Q?Ioo_KH2xp@9<{IBMn3@OWII{e2qDmNkX@Pv2zTj#!ClpWl8@s3$?@!Cs& zS{1h~Iwq$XvLlg|^Z&7@I{VwQ;u&S;iiJj<`&~T!&Gx?xFMciEt2yz@IbgIhc)I$z JtaD0e0sz4-4`%=X literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/notification_quest.png b/app/src/main/res/drawable-xxhdpi/notification_quest.png new file mode 100644 index 0000000000000000000000000000000000000000..70b3d10d1da0c61056c8ce77a199ebe2b78e023a GIT binary patch literal 549 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xZ3?!EyURMI7SkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skw2Ka=y0_p#F!Fk2kKY@z4N`m}?8SX4N_%2DsnY+pO*mgOfaXp?cjv*e$ z-(K78eb_;U{lomJY`Z1pIsB$RT0FC9qsQ(jSEEoCZNG&HZ0fnX9l`Yy0zvjhRi@aCAuWnOUxqf)-jFW+1o7AHX zr%%f(I?N&}8CN%*_`XtRspVfzt<%S+sO*}0Uhkyg&-zS_ zfENbNhb~CJ%vj^_{zH<@6Peiv@O+H@h>so_Y0i-D|0xYki7a^$)fzl9ut+j5;#WGmz7rxyz&^ zUWxsR@_d8oNsP{^-@43>sGW$pu6lLD09bI}#GW(isfYup-QafW){~+1Y$(C-rKS{m%*%0D7eU+ED^i|mz`F0G}?8mL@ TmYs73#ukI8tDnm{r-UW|74!LI literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/notification_warn.png b/app/src/main/res/drawable-xxhdpi/notification_warn.png new file mode 100644 index 0000000000000000000000000000000000000000..0173da784b315396b1c4e11297f5791f3f691c87 GIT binary patch literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xZ3?!EyURMI7SkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?smm1^9%x0_p#_flAHnsX!H+B|(0{47U!voLi*o$vffj;vYbx1U+3GLp+YZ zy>^zH(NTi!gJ)T>Xm!El^D#@GJd4iD3A@0ZBJ0K1agKcM?tQl`HaSk2 z>F2H>^~k;8QAXclV|PZGqn1GhR~~w}zHgo7vhVX`1CeG4t#0LAywbN#*NNWTHdjX> z_=``mXv77xy~gn$WtrdpIDPm=_pE@U5xacF-f2C$ohB8Q!Ll?m--@m8dhBp*ONedmr1Q_1cyuajGh7|Z8Wp^p)w_9PH2;HpXCeDL}4B)_aZcgTe~DWM4f`h(fq literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/card_border.xml b/app/src/main/res/drawable/card_border.xml new file mode 100644 index 0000000..7581dae --- /dev/null +++ b/app/src/main/res/drawable/card_border.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_background.xml b/app/src/main/res/drawable/circle_background.xml new file mode 100644 index 0000000..3953e54 --- /dev/null +++ b/app/src/main/res/drawable/circle_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action.xml b/app/src/main/res/drawable/ic_action.xml new file mode 100644 index 0000000..a58fc30 --- /dev/null +++ b/app/src/main/res/drawable/ic_action.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_action_fingerprint.xml b/app/src/main/res/drawable/ic_action_fingerprint.xml new file mode 100644 index 0000000..0cc10ac --- /dev/null +++ b/app/src/main/res/drawable/ic_action_fingerprint.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_allow.xml b/app/src/main/res/drawable/ic_allow.xml new file mode 100644 index 0000000..75c7392 --- /dev/null +++ b/app/src/main/res/drawable/ic_allow.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_android_white_24dp.xml b/app/src/main/res/drawable/ic_android_white_24dp.xml new file mode 100644 index 0000000..669d4ee --- /dev/null +++ b/app/src/main/res/drawable/ic_android_white_24dp.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_apply_menu.xml b/app/src/main/res/drawable/ic_apply_menu.xml new file mode 100644 index 0000000..53d34ba --- /dev/null +++ b/app/src/main/res/drawable/ic_apply_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_apply_notification.xml b/app/src/main/res/drawable/ic_apply_notification.xml new file mode 100644 index 0000000..f5d1c73 --- /dev/null +++ b/app/src/main/res/drawable/ic_apply_notification.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_block_black_24dp.xml b/app/src/main/res/drawable/ic_block_black_24dp.xml new file mode 100644 index 0000000..2cc82ad --- /dev/null +++ b/app/src/main/res/drawable/ic_block_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bluetooth.xml b/app/src/main/res/drawable/ic_bluetooth.xml new file mode 100644 index 0000000..c3dc57f --- /dev/null +++ b/app/src/main/res/drawable/ic_bluetooth.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_clean.xml b/app/src/main/res/drawable/ic_clean.xml new file mode 100644 index 0000000..f4e74ac --- /dev/null +++ b/app/src/main/res/drawable/ic_clean.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_clearlog.xml b/app/src/main/res/drawable/ic_clearlog.xml new file mode 100644 index 0000000..fd5ed1c --- /dev/null +++ b/app/src/main/res/drawable/ic_clearlog.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_clone.xml b/app/src/main/res/drawable/ic_clone.xml new file mode 100644 index 0000000..62ad953 --- /dev/null +++ b/app/src/main/res/drawable/ic_clone.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 0000000..d83f998 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_custom_rules.xml b/app/src/main/res/drawable/ic_custom_rules.xml new file mode 100644 index 0000000..01c3559 --- /dev/null +++ b/app/src/main/res/drawable/ic_custom_rules.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_deny.xml b/app/src/main/res/drawable/ic_deny.xml new file mode 100644 index 0000000..8f83467 --- /dev/null +++ b/app/src/main/res/drawable/ic_deny.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_exit.xml b/app/src/main/res/drawable/ic_exit.xml new file mode 100644 index 0000000..f975fbe --- /dev/null +++ b/app/src/main/res/drawable/ic_exit.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_export.xml b/app/src/main/res/drawable/ic_export.xml new file mode 100644 index 0000000..f0bab5f --- /dev/null +++ b/app/src/main/res/drawable/ic_export.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_flow.xml b/app/src/main/res/drawable/ic_flow.xml new file mode 100644 index 0000000..c465042 --- /dev/null +++ b/app/src/main/res/drawable/ic_flow.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100644 index 0000000..1f2ca59 --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml new file mode 100644 index 0000000..c5c42f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_import.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_invert.xml b/app/src/main/res/drawable/ic_invert.xml new file mode 100644 index 0000000..53a34a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_invert.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_lan.xml b/app/src/main/res/drawable/ic_lan.xml new file mode 100644 index 0000000..c628228 --- /dev/null +++ b/app/src/main/res/drawable/ic_lan.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_legend.xml b/app/src/main/res/drawable/ic_legend.xml new file mode 100644 index 0000000..df887b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_legend.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_log.xml b/app/src/main/res/drawable/ic_log.xml new file mode 100644 index 0000000..7ae2af0 --- /dev/null +++ b/app/src/main/res/drawable/ic_log.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mail.xml b/app/src/main/res/drawable/ic_mail.xml new file mode 100644 index 0000000..caa40f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_mail.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_binary.xml b/app/src/main/res/drawable/ic_menu_binary.xml new file mode 100644 index 0000000..a450938 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_binary.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_exp.xml b/app/src/main/res/drawable/ic_menu_exp.xml new file mode 100644 index 0000000..5b7fa96 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_exp.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_pref.xml b/app/src/main/res/drawable/ic_menu_pref.xml new file mode 100644 index 0000000..e5f2393 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_pref.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_profile.xml b/app/src/main/res/drawable/ic_menu_profile.xml new file mode 100644 index 0000000..504104a --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_profile.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_save.xml b/app/src/main/res/drawable/ic_menu_save.xml new file mode 100644 index 0000000..21fcfbc --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_save.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_menu_secure.xml b/app/src/main/res/drawable/ic_menu_secure.xml new file mode 100644 index 0000000..4bd3cfb --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_secure.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_translate.xml b/app/src/main/res/drawable/ic_menu_translate.xml new file mode 100644 index 0000000..8615577 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_translate.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_widget.xml b/app/src/main/res/drawable/ic_menu_widget.xml new file mode 100644 index 0000000..e826521 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_widget.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mobiledata.xml b/app/src/main/res/drawable/ic_mobiledata.xml new file mode 100644 index 0000000..555a580 --- /dev/null +++ b/app/src/main/res/drawable/ic_mobiledata.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_notifications_off_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_off_black_24dp.xml new file mode 100644 index 0000000..a7916a8 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_off_black_24dp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_on_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_on_black_24dp.xml new file mode 100644 index 0000000..838c288 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_on_black_24dp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_open_in_new_black_24dp.xml b/app/src/main/res/drawable/ic_open_in_new_black_24dp.xml new file mode 100644 index 0000000..ecadb90 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in_new_black_24dp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_preference.xml b/app/src/main/res/drawable/ic_preference.xml new file mode 100644 index 0000000..4f24c6a --- /dev/null +++ b/app/src/main/res/drawable/ic_preference.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..e93847e --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_roam.xml b/app/src/main/res/drawable/ic_roam.xml new file mode 100644 index 0000000..0a54ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_roam.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_rules.xml b/app/src/main/res/drawable/ic_rules.xml new file mode 100644 index 0000000..43379db --- /dev/null +++ b/app/src/main/res/drawable/ic_rules.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_script.xml b/app/src/main/res/drawable/ic_script.xml new file mode 100644 index 0000000..8baed90 --- /dev/null +++ b/app/src/main/res/drawable/ic_script.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..8fd93e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..1cf289f --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml new file mode 100644 index 0000000..8555ccc --- /dev/null +++ b/app/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_tether.xml b/app/src/main/res/drawable/ic_tether.xml new file mode 100644 index 0000000..9e9e8ed --- /dev/null +++ b/app/src/main/res/drawable/ic_tether.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_theme.xml b/app/src/main/res/drawable/ic_theme.xml new file mode 100644 index 0000000..8dd27d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_theme.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_tor.xml b/app/src/main/res/drawable/ic_tor.xml new file mode 100644 index 0000000..c254644 --- /dev/null +++ b/app/src/main/res/drawable/ic_tor.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_unknown.xml b/app/src/main/res/drawable/ic_unknown.xml new file mode 100644 index 0000000..d4b9f96 --- /dev/null +++ b/app/src/main/res/drawable/ic_unknown.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_vpn.xml b/app/src/main/res/drawable/ic_vpn.xml new file mode 100644 index 0000000..eb2d4fd --- /dev/null +++ b/app/src/main/res/drawable/ic_vpn.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_wifi.xml b/app/src/main/res/drawable/ic_wifi.xml new file mode 100644 index 0000000..9667550 --- /dev/null +++ b/app/src/main/res/drawable/ic_wifi.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/list_divider.xml b/app/src/main/res/drawable/list_divider.xml new file mode 100644 index 0000000..5d8ab8b --- /dev/null +++ b/app/src/main/res/drawable/list_divider.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/drawable/plus.xml b/app/src/main/res/drawable/plus.xml new file mode 100644 index 0000000..747f7cb --- /dev/null +++ b/app/src/main/res/drawable/plus.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/preview_new.png b/app/src/main/res/drawable/preview_new.png new file mode 100644 index 0000000000000000000000000000000000000000..ff4f26ed2868801c553b7e849a41f9fd4ffbf282 GIT binary patch literal 3223 zcmV;I3~2L-P)r004>z0{{R3LRlhK00004XF*Lt006O% z3;baP00001b5ch_0Itp)=>Px#W>8F2MF0Q*#KgqL#>PfQMx30SrKP2_v$Kkdija_y zxw*L^At3+&07pnjOH52)VPSJ~b7^X6goK1qQBhb}SS&0oe0+SMprAH3HV6m^mX?;L zrlucKIzB!=oSd8y5fQGgt{E8_!otER)8iQc000VfQchCePvmE{WgtjkdegFUq zxk*GpRCwC$Txol%IuJe@ZIDdXN!SXA|Nl?k04hjg4_Mp#<>cvR`xIVgd1uMQKY#wM zf9v1+pVr6zbUGf!bv*6&AO9DOW21%PK*j~!2;Ce{zYXW(bfCydxB&IxwEI0EyJLbt zo-QsAza*ReL2~k!W#}(}*&P&+6^rZR&w}|l%2hx{Fh41o6Xj&XGKc+-S*v(*&u5YX z`7>JDS1PzylJ)2kH@qk@1fvf>;Mph1li*GbRHhUcl{g8qxdd1t0e5=0y&eo9E>KWI znnqWRapwk_rU?`!{eOt#o00VP8j8hBC6l5N*}3Fd2n9+QtoMxuvrERCxTQ6enGlt9 z=G>@EJ1Lh!88yb=hh;~vkIU&6U%m=u5a;gANW4RsEl`x4%y;)@BtEumfs(+?l|J5K zv2$bGLbt-rmGyml8GYxzP;U^*!+(C?c8}((>wmS-Eh5r4`Q;@3a)Tk0^ozriR03U! zZ}ILIxL%`t|$}?1~73l zR4?XIA8|f|gdim>r5RpPOv|duwaicxNhn6OeY{OQ!=shT1EF~uU8HnvD;s>0W4`1? zJN2xvu!SrqAUWZPq@E;1GW1${@*FRD(E*;&d0{g?(+PZj@=yOclZ+_4j>hEzFIetA zekmy!mUA1ipVu|lQl907iznr3yIA6O=Qwln(Nbej(lkLPqgddunek&n0Y?grX#|Sm z3n-}*Oo_LMB`Oq27&S}({p99TiKSH1R4FF$4b~F16opKifJ#0;rBI(Cl+>G5l;}2! z(Jf}c312VmZ za*zIB+(ml~95l~{>$xt~GL%YcMkGA^ixy4G{XmMg*&E99}B}< zS_1%~OrY1MFs&x%%4MdMu56MlJ9{V_*#B@Xh%2YcDiJ*tms&YiR}q`YZRB^=@XNC0 z*btKOiYgB!5$K`t)5bK?5H3I%j^1R9VpnHCovvEd7%;txw2!NbNC!Ug&XA`~(4CeV6<0#F;C<_1zevt6XK08mH7a(tEaK^F7mlHc9PONHlNF8mQn zojn9;7CxI^&u@#UVGTYea+9rCp4X}IXzDt@D#%t39wjXg9!~6DG!@76GL`$vd9wPE zcXSbyXY1TKG!-sv&`%Q2kt{QslonwJ{P>!ws8lW88}!{Uh%B2jQFlqGKw{j~;w57V zStZcVPz{ZSA+n)ZEoyzuy<^ckqM=pHat!XO0{z?~KwjchGD~Xje81FCur*-zuUIXT z=q{A!j=JeQh8%@J>wmC~wNIoG2f(z{f zts+DVMHJprXGlC2$r!aUD6$i<-Gu^Q7P*L);<{j}We>|5mPUD)bHle&|0#gqMq3I- zFiY5CiGt~vSaOvwC5!v*wBjOxb;ui-SR!F9vN{$LtV`8)Vp$D^wI+b#(G^KFF12nu zu^iX=G6=@6mQvimc#pO$n)`;-4=+HGEY_1VoZ-!HiqUz77lz50vFP9;FF~0e2-H0Z z683H?w)g7p`T`mH7;Wu{Cm%irC@)AQK(AV7F4aJMF6nEUYvM3jE%=0HG8XRB2UCv- zd_e%I9XQTy?`@pR#nPR_1j~y%ClxIldKTwKaxaF-fuxw(E#W;Bi)cNYKn=k2UMf|y zSbvA&3**P-rGl3VGm4qUs=1u?*|Yc@N-pmDxk|;Syg+

(g++9?DBniP;J!glEpz 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/preview_toggle.png b/app/src/main/res/drawable/preview_toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..982d2955ff45a690334f3e688dc459b0d9b8d5e6 GIT binary patch literal 2903 zcmV-d3#jyoP)r004>z0{{R3LRlhK00004XF*Lt006O% z3;baP00001b5ch_0Itp)=>Px#^H5AwMF0Q*-rnBO(9q1x%-7e~(9qDixVOB#yuG}= z%*@QCrKN?1g}Au5xVX5FkB^|Bpv=t7(b3V(&CS}{+Ro0-ySux&xw*2kvaheN&CSiS zva+J0qOGm1qobpViHXI<#oRye7n?cn&@*!9cDt=`7Z zvaplWw7})kKWr|N0000WbW%=J01?$QN{0Lh{_lts=g2Wl;cB8Ex0bBEo5_S^ba-{< z%!07%000TrNklG1$G560)zyWW%wPL!f9@%jcKCw7*88>`nqh* zmj|nAlPeJp2jj6jbP+gYTm92T>8ki=Sm#`%VZX~b0%36uR+A>TYr6%+=nrceAcPDJ zMH__~M1RR?D&Sl>;_R)A0w9ll5Cba_eHf}g+9+2w$}fpRfP-n+)ltOjp+MV%eAn?ym}pMo;q{zoWi@zwRB zpn9EJGiVz}LF(;Gv@AgNm)qWkQIPs@cXz8rN#1VWdmBYTy@#7hmeDmM@Qw;SICL3n9TQFB?+D)r36tT}3FM30b{+ zLYqN>hgPO6Z#@)Qti1Qoh8(2b-UL|S9aIl2shbPix?ck9jR4$3q@-JCUh#{7S^Ph~ zYnx4R^%uV`UL3;)r<~2!oA{BRrQ-7r?UcCY*C9t^xw)mDS20Qo$9M5jjj7$9?iM7m zQGToq1s)J)i6hwn3hKQzd?a@-afScdQE=?u_#?K5xGzFM^{1dJx9S(6;8$@tR(h+V zQ(`K`yrZDwX33htDXRyk?cAA_9``%ykWzr4IS z_#TG>#VswbJoM=2M~sqF9@R38+Tgv|B7@JT-&yG^G+i)C zH3lWSjqeQ3w!VqsPPrItkGEn_eB~3=PcL5hLTMY_(s3x+qaC0icLwg`#a64;Y_`NH zN(YQ#Vo^|#qz7BcSFJ=!TRKW^8($lE6hf;rnpk69R}{r+xhRZc!q2*HO_~%G6wu@b zrxX9Srj^-Il#-tV9%;@Rer-^+U3b5S0 zP&ab`W&7BP$+<&~W+p*7L57!pr089W3i?1Z0%!f~oA zi;o1t!)0ki&Z~UbBk)R01F5fp)0@B|o*1qP}P}@IFM~MKV<&aM_W6L2rsepW0uFX?0QWue2_p z93(^GCn6j8fC8K9Q9~`z(!rJH^!U!3~OvCBT{L0Yg}R&bwEf?qoWatQH=KUD`63dlQfh727z<>xKz)d zP=+)TqDV&jwNZI(%cg~6SGZ0j4AKTTV5|8uZIUJ#3XnYGTIjhg8{-Z+uYC@*#bBnA zFonw^?Ie~A#UUXqvNCDe!Ys6v5UP<$kt%Z%GtQF)MVgWT%NW2Q!C+w?`;cSMoNG-8 zS0>&$Nl|?3*B?KhL5lN197#=gqYmGLQ3 z&F{^a{8}6ZNGhHt?mLofB?pG)CK9YR2+Ndb4t>#3WNM(a?>!RT_YuRgTMwd$12`ZJ zo(F?0Z!3a*Gqbf5MUmRSWQjsD)uiznI5^2V+xpj;3d2EDNA7(Pa5y1C((Al2xF|~d z{{EK{A+cgKAt>x=>CA-@9|x*EAg2vojw)L?m_N!HMj?KDQnf~>V zO(nV}dSqugD5Pn0?6zuHN08IS2RDN#-d}&;mig!^Xu`*%koM*-fgvbtelN)YX#ho1 zywve*Il2UP@tB{DLi$*{5W_4}C|@KS9Su3i$KvQBdzv(8BoP$SW-Og2AMXu?t)#qq z$-ND+EC0HmO+rIi&UOw%o7w#BixG=^$E@ON-6(!t8XxA&>TcaAo-U1FrnDK9 zEulpZ1V@)fw2q?HD=DGG>{G%5n#?P|t+Y_i1TijUm>;y-l7@1U2FmuaAXsuN%X|{v zn52dx3X|C98S)~nIZ6j<89Vwe3LTSQ!OTudDA`?c_Jz=V6sF3Y=TbrWCWvEs1r!~Q z!WSu!WOAiLVMeel7AmRiWwM)vrYS8i3L9+(uMoGwXlYsG5%}jala65z#iy2ejVKGw zJB5RkP>3uoeHj)BXWAhJPNzz0on;VZ$gr|ou{*tAS}UbtGF1v`C9JINNN)q#+SRYu}XYs8RF z-+3{!Hal`6rZs|)kzy3lM^3ALU6M>Eu+Bt#%4bYbhA7gp+rbyb%$hBW!#;2lgB!499IP|Z4R-=dk_>%fgRuF%_4>v?zG#OG9@8Vz01g(U<0w+z6o}pN zbd<3t^|zZ;BiQ@RkGPG{|As>}S5laz~}(L-J24 zsw|T~IoQu-zRo}z+_lFMQv6B|Pn)mRLT!=(TK002ovPDHLkV1n)( Bi9rAW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/question_widget.png b/app/src/main/res/drawable/question_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..39df4748350a386d774d41d37a65c448332de45e GIT binary patch literal 3615 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 nIzxx2)BmG0`lnsmoI0cMeof%7@qhLUK{{L + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/toast.xml b/app/src/main/res/drawable/toast.xml new file mode 100644 index 0000000..0e93e4e --- /dev/null +++ b/app/src/main/res/drawable/toast.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_dontsave.xml b/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_dontsave.xml new file mode 100644 index 0000000..31fe615 --- /dev/null +++ b/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_dontsave.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_help.xml b/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_help.xml new file mode 100644 index 0000000..4f2cd47 --- /dev/null +++ b/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_help.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_save.xml b/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_save.xml new file mode 100644 index 0000000..b64acb6 --- /dev/null +++ b/app/src/main/res/drawable/twofortyfouram_locale_ic_menu_save.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/widget_bg.xml b/app/src/main/res/drawable/widget_bg.xml new file mode 100644 index 0000000..782b458 --- /dev/null +++ b/app/src/main/res/drawable/widget_bg.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/widget_bg_focus.png b/app/src/main/res/drawable/widget_bg_focus.png new file mode 100644 index 0000000000000000000000000000000000000000..419394f8ea1f74b707e95210b4b55bdd31b71651 GIT binary patch literal 823 zcmV-71IYY|P)Px#Do{*RMgL_0|78IGWdQ$X0RLqG|78IGcL4u=0RN2u z|APSkasdBr0RM;p|7HNPWywqc000JbQchFU_yaqYd4MTUYybcPnMp)JR7l6|mN9PQ zFc3w#z`6_&AHa=sgCGZ3AQ$i{EWihlp&O`BF9>j*!nr{N^)jF$t_|KLGxLYkP_m50 z*82;?QC|LhD&%n7_GS0W`rCJHK}CTley3Ek^9~ zv5z{am-p-6ukP*1vk^1=-3(2M$jQ3`UF?hJ;+O;@D!OOhJlXY(>#sFk*&*h9w+RumVIzuxV6vfr`Uv7`92&nnk?>8dxSn^Ymci&Ws4gg|^&G!RSsMTG5Ctn) zU)z!sA{E9*y=o2BMJVwhW)LfdLhb-n!_bFxts2-QrC5D8AEXATQJ zGwr^ok+I!(v5(ia?c)+E(+h!n_+@P45-Gh~tkL{#IqQX(GUk@jdg0m(gfOV)Px#Hc(7dMf<4$|G5DF!T|rT0Q{x^|EmE1v;h960RN}} z{igu`zX1Qe0RP1R|FHo7wgCUT0RN`|^rQgCh1`t*000VfQchF&|Ns9w|Np%OeAUa< zKL7v%~yl|s{URim58rCK$!#(rfKTu zF?@YnVVwH}37L~Y=|}h)-uoPnmPIEiOnt(Hecy+yZs@w0K93k5R`Jor*d?XsPNjuu z`PfgPpAQOfx~zE0;hZWDAt|xuKBTxbCsRB_>5mLWq!eA?oPjVGLe#)XJU<0POEx6| z6P_}LID~<<4NB26ijfeM{V+^kjvS%^2|b;`Nlq-BhFOtH|JqD+45a)t7jish5G4rg z6updyMWkXWgD5U^L#x*$&C0GQx&y>FY*U<7NRc+ab$h_Tcy=s=!+54JH~@+KAZ8Il zl*khYks)Qr1EDEtsetm13C`k5kYMKkWDM0gNHUfJ89Fmv{FGI#5TUXscn2)V_^E-4 zCk29+x&#KC))kQ;OXXDxm?AJ#B5T_!iSnH-5vaV>U}(AEpr-=bAT|a%BfI4g2{w*{ zD6&-s_GFU5ts=GziUPz8JtgQVEQx(PKrBTs36dNl0qDh(bOF*FBz+8W^d?Ad+6`!x z`ycr3zdi%K`2PO~{oA0QeIY*rHNW~5agUMKq4>v0=bT6CV6(s3gDt*sLbm2uyEq}$ z*ZQY!02^DgshHUO@$fi<5I#q3@Iu`Y@t!lHS_<5Zj zS)C0nM2s=o4#F1xyoZQb`D0T9Ag31*6w`}DXpg8nir?vuqUw%Bzjo8EQNt!Wze)#L zXKmYc2Wd$ji?zq5&)(hc*tY7rWmmoNuICwYd{Alo)d+!s{(2IDAZIpmhY@}V#61D5 z3};9WHmi|-aHv5f1(Q+`<|mYCO$Fu4V9sgHIPOf0< + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/widget_disabling.xml b/app/src/main/res/drawable/widget_disabling.xml new file mode 100644 index 0000000..667d464 --- /dev/null +++ b/app/src/main/res/drawable/widget_disabling.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/widget_enabling.xml b/app/src/main/res/drawable/widget_enabling.xml new file mode 100644 index 0000000..ed4d41c --- /dev/null +++ b/app/src/main/res/drawable/widget_enabling.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/widget_error.xml b/app/src/main/res/drawable/widget_error.xml new file mode 100644 index 0000000..40f28f3 --- /dev/null +++ b/app/src/main/res/drawable/widget_error.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/widget_off.png b/app/src/main/res/drawable/widget_off.png new file mode 100644 index 0000000000000000000000000000000000000000..3a70aef082057d76cf00c91404758a27af9fa232 GIT binary patch literal 4260 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/widget_on.png b/app/src/main/res/drawable/widget_on.png new file mode 100644 index 0000000000000000000000000000000000000000..63f03a91e8de3578b5b30116b9cd9c03dc628d9d GIT binary patch literal 3951 zcma)9dsvL=`@f%e-Z?bWVLEP7)3i!OQC1Y;Ewzme4N^IzX>(}5Eyi&N9iGW4mJDmv zW@YHOLa`)@-lS4mwOCSULL}8pD0G_dYp?6~-|u%_zxVy){@nNHxj*-PJ@<1x*Y)g^ z&JQx;+wcLvXpSUcAppTZ0!S8PIAOK5j3L+f&GQ4ea7Zu|&1QVGX{O+`S}N^!ePX%7<)@ z_E^aN(UYI{~M4)-dJo4WHBl`zm59ym5*-)ZS7|J{7caiRJDD{y#F`_Dgb-n`j6 zv8664>{>x@&&ezLk*>}&H78DHiZUjIEj`zp_SQXRMPK6O;ai)Q^F6r>orWswI zN;I{|6Si+@+xUL!wz!;geN}-x=kNY1K%dwZIXxleFYA6i^YGJ$`op*GcdX#j4H8Y} z&(0Z7^Ys4uABcg-e>4r$0U z2`*52P_uGS4qIQJavxm3@fZ~6{ms^Q1>e`sgGoILWb2YL&V{XCzrogd9$0Pgjo-2C z+_AY?^zD%!inniHRd^(Y zs2wK#U6`~Za)XM}zBswMFJweIaU+;N)^58$noAmNo;kAFX>DRS%LkY4X`S-G3`GzJ z50&Q(HrwP+P+F&;e6r3yX~)OXBdl2cbsIz4Q9Ar7uOKht578=^l+!vhK62@=8k5V1 zW|;>i#_`9klFcx;bSnBsA37%?ntx`s`Zo^5=;p6__vpfWr%50*l@W)bt5axzQwoJ@X zS2(!2`&rPe4pqWUi>@*=gX#-1$cRs*YRm2aq$5^|u zs>~zE&!^)Op19wXeql8k2@V+$Q1i?=$2uUhf ztstyL=6hmBN5d6C1FC%hAtGTdg78NNk~AtQ0X0!yvJ#dY*Cw(rXz8#Fpwn z2&2cSh1!W?Un|#S*W&$X}m@O zM3NnBJc0OtC}%VP>R(_6G|s~!=opI!VH#gE6Q>g1Ch7vX%jy`1-@-J3W*WXrcpIxr zA(hoJ0k<$WwR|*Sn9ssO`UemaQkEji3R}~8I zx^;b}DLud&dM`T*`{5xA7DlYFq@1;A_~h!6@)@j zK6JR?aWING!E&~kr9KbmnSBhkq^z(pK119lD8l;@OPveFunQho&{o(C?}j*fK;dLljbgq=VET zY7ui<(m`^Ylo3=glA;oj8H-wiWatw}Vo42)TEKK?jU*CMMpAQ- zoKcmykQ$QmLkcEC!XssD^$7dLTLyRs-iDn?j9k`iZ*AVyMw) zicNskLdaQ-YGyiJVCW{Gjd3V)M4ym#lcg+$kQo)iF>rx*US))Yz=VPugQd*0_;BMQ z@+fivV_f#4rmr%SIqFuPMMNy2o6Kx-CioiVyJBbRcOX8QSmU|^hX5vL<_1F5WQND% zBSbnOMkPNxSke|^kq28Ug~cFcjbKvMWU7|YJ1F?YdOli?p06nev+>N@Pvbd@U^Qbh}0Q>vMQDa(^sA& zsx3$JUjkyrt6uq=3cJrV)xqXI0DZ0Ma**J%2Cm|Kzvp4aKB-<@vJNto%>pf0t3h(@=$*gVF%b~*{|1Y@W zoa|V20yi7VcYGc3eF(Ldqi~g4LL$-mjl98yyEpiQL4KKK^j4$AX2K!cH$Noq?jrDg8Zj^l)0l*_T*@bbopFrGU`08suDinKnN&_ zkA~*L5b)zSnp70bLnL2n;@3@5mQ*%9<4;6?vAZ5Yd}VieqVW-9zXT{srDGD#np9-` z4e_asZ}&6^AIukL*U-grDgWVhIt#ZFbsJT$ep8Pa;xeMWK=sP5_lN{4jvw|-dR~P7 z4%cWGD=K9?a=yv3-;3A3i%Vn$w*~{ zMrJtjsr_Hj6cnwG$r#x^8(-&VJ~qTJZT(lUPs61I;A9HOaP@CczuIwO+ zb)ty(71oEa95u)D!bHUHc|;o_wu9OhhwN1@JxTF?hE$orl$SD?*Mi+kJsKW{4d`t> z^WQ(Y>F3y!bn8^o97EbHBFC4dcIAi-QU+fOVV)=aWKo_Q|0Sg6Ne1i#4|H-4W5Mv9 zWMMMSPMUz4%yym7D%&>I`NFc~qPG(wa=TbXPQ2+p9?P%ZpT$w0>UwX#p)1N}S2dYE zyh%Vg5_kJ1MzOzLFUcN$zYMwy#3yH>bNaO>0uS#bH@8~e8PwZeIB&9$FhA&NT{l>s zn*QkOQM}!vuIA($BGgemaJsR3r2UT6iyzfK{5}$6^5qMyM1PQ6x>N0!v}T}H6ktT& z+r2@r&B`|+GpdzLok7v*y`JB{(%{Jnh(?C96|XgFW1P0@Wld_48^&6*_B&x_Kwz?#G1eS?qbz3r_o$~E_+LI`bc`8DJC;ZI4y p8f}eUplt2l`+vVPcV7(GbMaG;#*Wpw8<}@6%n6(yaNb{*{9oB{Ev^6n literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/widget_success.xml b/app/src/main/res/drawable/widget_success.xml new file mode 100644 index 0000000..b7a91d2 --- /dev/null +++ b/app/src/main/res/drawable/widget_success.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/zoomin.xml b/app/src/main/res/drawable/zoomin.xml new file mode 100644 index 0000000..ef43a8a --- /dev/null +++ b/app/src/main/res/drawable/zoomin.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/zoomout.xml b/app/src/main/res/drawable/zoomout.xml new file mode 100644 index 0000000..caf4b44 --- /dev/null +++ b/app/src/main/res/drawable/zoomout.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/dialog_color_picker.xml b/app/src/main/res/layout-land/dialog_color_picker.xml new file mode 100644 index 0000000..a49ca36 --- /dev/null +++ b/app/src/main/res/layout-land/dialog_color_picker.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_custom_rules.xml b/app/src/main/res/layout/activity_custom_rules.xml new file mode 100644 index 0000000..aebff70 --- /dev/null +++ b/app/src/main/res/layout/activity_custom_rules.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_prefs.xml b/app/src/main/res/layout/activity_prefs.xml new file mode 100644 index 0000000..ce8039b --- /dev/null +++ b/app/src/main/res/layout/activity_prefs.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_detail.xml b/app/src/main/res/layout/app_detail.xml new file mode 100644 index 0000000..6c92d4b --- /dev/null +++ b/app/src/main/res/layout/app_detail.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +