diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..bc0e016
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+EZHZ
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
index 904be2a..b967a5a 100644
--- a/.idea/deploymentTargetDropDown.xml
+++ b/.idea/deploymentTargetDropDown.xml
@@ -12,6 +12,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..fd64720
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 88ee048..80cbba5 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 76ed4b2..2a367c8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -35,6 +35,7 @@ android {
}
buildFeatures {
viewBinding = true
+ dataBinding = true
}
}
@@ -50,6 +51,13 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
implementation("androidx.preference:preference:1.2.0")
implementation("androidx.preference:preference-ktx:1.2.0")
+ implementation("com.github.topjohnwu.libsu:core:5.2.0")
+ implementation("com.github.topjohnwu.libsu:service:5.2.0")
+ implementation("com.github.topjohnwu.libsu:nio:5.2.0")
+ implementation("com.github.ixuea:android-downloader:3.0.1")
+ implementation("com.squareup.okhttp3:okhttp:4.11.0")
+ implementation("com.google.code.gson:gson:2.10.1")
+ implementation("org.tukaani:xz:1.9")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 65b1c31..8f2b95f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,8 +1,13 @@
-
+
+
+
-
+ android:name=".SplashActivity"
+ android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"
+ android:exported="true">
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/armeabi-v7a/gost b/app/src/main/assets/armeabi-v7a/gost
new file mode 100755
index 0000000..ce61745
Binary files /dev/null and b/app/src/main/assets/armeabi-v7a/gost differ
diff --git a/app/src/main/assets/x86/gost b/app/src/main/assets/x86/gost
new file mode 100755
index 0000000..cec0624
Binary files /dev/null and b/app/src/main/assets/x86/gost differ
diff --git a/app/src/main/java/cc/ggez/ezhz/MainActivity.kt b/app/src/main/java/cc/ggez/ezhz/MainActivity.kt
index 98dc93f..2241624 100644
--- a/app/src/main/java/cc/ggez/ezhz/MainActivity.kt
+++ b/app/src/main/java/cc/ggez/ezhz/MainActivity.kt
@@ -1,18 +1,21 @@
package cc.ggez.ezhz
+import android.content.Intent
import android.os.Bundle
-import com.google.android.material.bottomnavigation.BottomNavigationView
+import android.util.Log
import androidx.appcompat.app.AppCompatActivity
-import androidx.navigation.findNavController
+import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import cc.ggez.ezhz.databinding.ActivityMainBinding
+import com.google.android.material.bottomnavigation.BottomNavigationView
class MainActivity : AppCompatActivity() {
-
+ val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding
+ private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -20,9 +23,19 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+ val menu = intent?.getStringExtra("menu")
+ Log.d(TAG, "onCreate: ${menu}")
+ if (menu.equals("proxy")) {
+ navController.navigate(R.id.navigation_frida)
+ } else if (menu.equals("shared_pref")) {
+ navController.navigate(R.id.navigation_shared_pref)
+ } else if (menu.equals("frida")) {
+ navController.navigate(R.id.navigation_proxy)
+ }
+
val navView: BottomNavigationView = binding.navView
- val navController = binding.navHostFragmentActivityMain.getFragment().navController
+ navController = binding.navHostFragmentActivityMain.getFragment().navController
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
@@ -32,5 +45,20 @@ class MainActivity : AppCompatActivity() {
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
+
+
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ val menu = intent?.getStringExtra("menu")
+ Log.d(TAG, "onActivityReenter: ${intent.toString()}")
+ if (menu.equals("proxy")) {
+ navController.navigate(R.id.navigation_frida)
+ } else if (menu.equals("shared_pref")) {
+ navController.navigate(R.id.navigation_shared_pref)
+ } else if (menu.equals("frida")) {
+ navController.navigate(R.id.navigation_proxy)
+ }
+ super.onNewIntent(intent)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/SplashActivity.kt b/app/src/main/java/cc/ggez/ezhz/SplashActivity.kt
new file mode 100644
index 0000000..3b5bddf
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/SplashActivity.kt
@@ -0,0 +1,116 @@
+package cc.ggez.ezhz
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import com.topjohnwu.superuser.Shell
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+
+class SplashActivity : AppCompatActivity() {
+ companion object {
+ init {
+ Shell.enableVerboseLogging = true
+ }
+ }
+
+ private lateinit var requestPermissionLauncher: ActivityResultLauncher
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (it) {
+ startMain()
+ } else {
+ val builder: AlertDialog.Builder = AlertDialog.Builder(this)
+ builder.setTitle("Notification Permission Required")
+ builder.setMessage("For foreground service, notification permission is required.")
+ builder.setPositiveButton("OK") { _, _ ->
+ startMain()
+ }
+
+ val dialog: AlertDialog = builder.create()
+ dialog.show()
+ }
+ }
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS,
+ ) != PackageManager.PERMISSION_GRANTED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+ ) {
+ requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ } else {
+ startMain()
+ }
+
+ }
+
+ private fun startMain() {
+ Shell.getShell { shell ->
+ if (!shell.isRoot) {
+ val builder: AlertDialog.Builder = AlertDialog.Builder(this)
+ builder.setTitle("Root Required")
+ builder.setMessage("Please root your device and grant su permission first.")
+ builder.setPositiveButton("Exit") { _, _ ->
+ finish()
+ }
+
+ val dialog: AlertDialog = builder.create()
+ dialog.show()
+ return@getShell
+ }
+
+ copyAssets()
+
+ val intent = Intent(this, MainActivity::class.java)
+ startActivity(intent)
+ finish()
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun copyFile(`in`: InputStream, out: OutputStream) {
+ val buffer = ByteArray(1024)
+ var read: Int
+ while (`in`.read(buffer).also { read = it } != -1) {
+ out.write(buffer, 0, read)
+ }
+ }
+
+ private fun copyAssets() {
+ val assetManager = assets
+ var files: Array? = null
+ val abi = Build.SUPPORTED_ABIS[0]
+ val arch = if (abi.matches("armeabi-v7a|arm64-v8a".toRegex())) "armeabi-v7a" else "x86"
+
+ files = assetManager.list(arch)
+
+ if (files != null) {
+ for (file in files) {
+ try {
+ val inFile = assetManager.open("${arch}/$file")
+ val outFile = FileOutputStream("${filesDir.absolutePath}/${file}")
+ copyFile(inFile, outFile)
+ inFile.close()
+ outFile.flush()
+ outFile.close()
+ } catch (e: IOException) {
+ Log.e("tag", "Failed to copy asset file: $file", e)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppAdapter.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppAdapter.kt
new file mode 100644
index 0000000..7688908
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppAdapter.kt
@@ -0,0 +1,107 @@
+package cc.ggez.ezhz.core.appselect
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.Filter
+import androidx.recyclerview.widget.RecyclerView
+import cc.ggez.ezhz.databinding.ItemAppBinding
+
+class AppAdapter : RecyclerView.Adapter() {
+
+ private var isSystemAppShown = false
+ val rawAppList = ArrayList()
+ var appList = ArrayList()
+
+
+ // create an inner class with name ViewHolder
+ // It takes a view argument, in which pass the generated class of single_item.xml
+ // ie SingleItemBinding and in the RecyclerView.ViewHolder(binding.root) pass it like this
+ inner class ViewHolder(val binding: ItemAppBinding) : RecyclerView.ViewHolder(binding.root)
+
+ // inside the onCreateViewHolder inflate the view of SingleItemBinding
+ // and return new ViewHolder object containing this layout
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding = ItemAppBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+
+ return ViewHolder(binding)
+ }
+
+ // bind the items with each item
+ // of the list languageList
+ // which than will be
+ // shown in recycler view
+ // to keep it simple we are
+ // not setting any image data to view
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ with(holder){
+ with(appList[position]){
+ binding.tvAppName.text = this.name
+ binding.tvPackageName.text = this.packageName
+ binding.ivAppIcon.setImageDrawable(this.icon)
+ binding.cardApp.isChecked = this.proxyed
+ binding.cardApp.setOnClickListener {
+ binding.cardApp.isChecked = !binding.cardApp.isChecked
+ this.proxyed = binding.cardApp.isChecked
+ }
+ }
+ }
+ }
+
+ // return the size of languageList
+ override fun getItemCount(): Int {
+ return appList.size
+ }
+
+ fun addAll(appList: ArrayList) {
+ rawAppList.clear()
+ rawAppList.addAll(appList)
+ this.appList.clear()
+ this.appList.addAll(getAppListFromRaw())
+ }
+
+ fun getFilter(): Filter {
+ return cityFilter
+ }
+
+ fun getSelectedApp(): ArrayList {
+ return ArrayList(appList.filter { it.proxyed })
+ }
+
+ fun setIsSystemAppShown(isSystemAppShown: Boolean) {
+ this.isSystemAppShown = isSystemAppShown
+ appList.clear()
+ appList.addAll(getAppListFromRaw())
+ notifyDataSetChanged()
+ }
+
+ fun getAppListFromRaw(): ArrayList {
+ return ArrayList(rawAppList.filter { !it.system || isSystemAppShown || it.proxyed })
+ }
+
+ private val cityFilter = object : Filter() {
+ override fun performFiltering(constraint: CharSequence?): FilterResults {
+ val filteredAppList: ArrayList = ArrayList()
+ if (constraint.isNullOrEmpty()) {
+ getAppListFromRaw().let { filteredAppList.addAll(it) }
+ } else {
+ val query = constraint.toString().trim().lowercase()
+ getAppListFromRaw().forEach {
+ if (it.name.lowercase().contains(query) || it.packageName.lowercase().contains(query) || it.proxyed) {
+ filteredAppList.add(it)
+ }
+ }
+ }
+ val results = FilterResults()
+ results.values = filteredAppList
+ return results
+ }
+
+ override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
+ if (results?.values is ArrayList<*>) {
+ appList.clear()
+ appList.addAll(results.values as ArrayList)
+ notifyDataSetChanged()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppObject.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppObject.kt
new file mode 100644
index 0000000..bf5f2a2
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppObject.kt
@@ -0,0 +1,15 @@
+package cc.ggez.ezhz.core.appselect
+
+import android.graphics.drawable.Drawable
+
+data class AppObject(
+ val name: String,
+ val packageName: String,
+ val icon: Drawable,
+ val enabled: Boolean,
+ val uid: Int,
+ val username: String?,
+ val procname: String,
+ var proxyed: Boolean,
+ val system: Boolean
+)
diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectActivity.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectActivity.kt
new file mode 100644
index 0000000..f77e6f8
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectActivity.kt
@@ -0,0 +1,106 @@
+package cc.ggez.ezhz.core.appselect
+
+import android.content.Intent
+import android.opengl.Visibility
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import cc.ggez.ezhz.R
+import cc.ggez.ezhz.databinding.AppSelectActivityBinding
+import cc.ggez.ezhz.core.appselect.AppSelectUtil.Companion.getApps
+import kotlin.concurrent.thread
+
+
+class AppSelectActivity : AppCompatActivity() {
+ private lateinit var binding: AppSelectActivityBinding
+ private lateinit var appAdapter: AppAdapter
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val selectedApps = intent.getStringArrayListExtra("selectedApps") ?: ArrayList()
+ selectedApps.sort()
+
+ binding = AppSelectActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setLoading(true)
+
+ val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this)
+
+ binding.rvAppList.layoutManager = layoutManager
+
+ appAdapter = AppAdapter()
+ binding.rvAppList.adapter = appAdapter
+
+ binding.sbAppSearch.setOnQueryTextListener(object: SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ appAdapter.getFilter().filter(query)
+ return false
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ appAdapter.getFilter().filter(newText)
+ return false
+ }
+ })
+
+ binding.tbAppType.setOnCheckedChangeListener { _, isChecked ->
+ appAdapter.setIsSystemAppShown(isChecked)
+ }
+
+ thread {
+ val appList = getApps(baseContext)
+
+ appList.forEach {
+ if (selectedApps.binarySearch("${it.packageName}:${it.uid}") >= 0) {
+ it.proxyed = true
+ }
+ }
+ appAdapter.addAll(appList.sortedBy { it.proxyed }.reversed() as ArrayList)
+ runOnUiThread {
+ appAdapter.notifyDataSetChanged()
+ setLoading(false)
+ }
+ }
+ }
+
+ private fun setLoading(loading: Boolean) {
+ if (loading) {
+ binding.pbLoading.visibility = View.VISIBLE
+ binding.rvAppList.visibility = View.GONE
+ binding.sbAppSearch.visibility = View.GONE
+ binding.tbAppType.visibility = View.GONE
+ } else {
+ binding.pbLoading.visibility = View.GONE
+ binding.rvAppList.visibility = View.VISIBLE
+ binding.sbAppSearch.visibility = View.VISIBLE
+ binding.tbAppType.visibility = View.VISIBLE
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val inflater: MenuInflater = menuInflater
+ inflater.inflate(R.menu.manu_app_select, menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ // Handle item selection
+ return when (item.itemId) {
+ R.id.action_save -> {
+ setResult(RESULT_OK, Intent().apply {
+ putStringArrayListExtra("selectedApps", appAdapter.getSelectedApp().map { "${it.packageName}:${it.uid}" } as ArrayList)
+ })
+ finish()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectUtil.kt b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectUtil.kt
new file mode 100644
index 0000000..da6b999
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/core/appselect/AppSelectUtil.kt
@@ -0,0 +1,34 @@
+package cc.ggez.ezhz.core.appselect
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.Build
+
+
+class AppSelectUtil {
+ companion object {
+ fun getApps(context: Context): ArrayList {
+ val pm = context.packageManager
+ val appInfos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ pm.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L))
+ } else {
+ pm.getInstalledApplications(0)
+ }
+
+ return ArrayList(appInfos.map {
+ AppObject(
+ name = pm.getApplicationLabel(it).toString(),
+ packageName = it.packageName,
+ icon = pm.getApplicationIcon(it),
+ enabled = it.enabled,
+ system = (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0,
+ uid = it.uid,
+ username = pm.getNameForUid(it.uid),
+ procname = it.processName,
+ proxyed = false
+ )
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaAdapter.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaAdapter.kt
new file mode 100644
index 0000000..16bc636
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaAdapter.kt
@@ -0,0 +1,70 @@
+package cc.ggez.ezhz.module.frida
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import androidx.annotation.NonNull
+import androidx.recyclerview.widget.RecyclerView
+import android.view.ViewGroup
+import cc.ggez.ezhz.databinding.ItemFridaBinding
+import cc.ggez.ezhz.module.frida.model.FridaItem
+import cc.ggez.ezhz.module.frida.model.GithubTag
+
+class FridaAdapter: RecyclerView.Adapter() {
+ private var fridaItems = mutableListOf()
+ private lateinit var onExecuteCallback: ((FridaItem, Int) -> Unit)
+ private lateinit var onInstallCallback: ((FridaItem, Int) -> Unit)
+ private var globalDisabled = false
+ @SuppressLint("NotifyDataSetChanged")
+ fun setFridaList(fridaItems: List) {
+ this.fridaItems = fridaItems.toMutableList()
+ notifyDataSetChanged()
+ }
+
+ fun setExecuteListener(callback: (FridaItem, Int) -> Unit) {
+ onExecuteCallback = callback
+ }
+
+ fun setInstallListener(callback: (FridaItem, Int) -> Unit) {
+ onInstallCallback = callback
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun disableAll() {
+ if (globalDisabled) return
+ globalDisabled = true
+ notifyDataSetChanged()
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun enableAll() {
+ if(!globalDisabled) return
+ globalDisabled = false
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FridaViewHolder =
+ FridaViewHolder(ItemFridaBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+
+ override fun onBindViewHolder(holder: FridaViewHolder, position: Int) {
+ holder.apply {
+ bind(fridaItems[position], position, onExecuteCallback, onInstallCallback, globalDisabled)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return fridaItems.size
+ }
+}
+class FridaViewHolder(private val binding: ItemFridaBinding) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(item: FridaItem, position: Int, onExecuteCallback: ((FridaItem, Int) -> Unit), onInstallCallback: ((FridaItem, Int) -> Unit), globalDisabled: Boolean) {
+ binding.frida = item
+ binding.btnExecute.setOnClickListener {
+ onExecuteCallback(item, position)
+ }
+ binding.btnInstall.setOnClickListener {
+ onInstallCallback(item, position)
+ }
+ binding.globalDisabled = globalDisabled
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaFragment.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaFragment.kt
new file mode 100644
index 0000000..fd83e6c
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaFragment.kt
@@ -0,0 +1,104 @@
+package cc.ggez.ezhz.module.frida
+
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import cc.ggez.ezhz.R
+import cc.ggez.ezhz.databinding.DialogDownloadBinding
+import cc.ggez.ezhz.databinding.FragmentFridaBinding
+import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.checkFridaServerProcessTag
+
+
+class FridaFragment : Fragment() {
+
+ private var _binding: FragmentFridaBinding? = null
+
+ // This property is only valid between onCreateView and
+ // onDestroyView.
+ private val binding get() = _binding!!
+ private val fridaAdapter = FridaAdapter()
+ private lateinit var dialogDownloadBinding: DialogDownloadBinding
+ private lateinit var dialog: AlertDialog
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val fridaViewModel =
+ ViewModelProvider(this)[FridaViewModel::class.java]
+
+ _binding = FragmentFridaBinding.inflate(inflater, container, false)
+ val root: View = binding.root
+
+ dialogDownloadBinding = DialogDownloadBinding.inflate(LayoutInflater.from(context))
+ val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
+ builder.setCancelable(false)
+ builder.setView(dialogDownloadBinding.root)
+ dialog = builder.create()
+
+
+ binding.rvFrida.adapter = fridaAdapter
+
+ fridaAdapter.setInstallListener { fridaItem, position ->
+ if (!fridaItem.isInstallable) return@setInstallListener
+
+ if (fridaItem.isInstalled) {
+ fridaViewModel.uninstallFridaServer(fridaItem)
+ } else {
+ fridaViewModel.installFridaServer(fridaItem)
+ }
+ }
+
+ fridaAdapter.setExecuteListener { fridaItem, position ->
+ if (!fridaItem.isExecutable) return@setExecuteListener
+
+ if (fridaItem.isExecuted) {
+ fridaViewModel.killFridaServer(fridaItem)
+ } else {
+ val tag = checkFridaServerProcessTag()
+ if (tag.isNotEmpty()) {
+ Toast.makeText(requireContext(), "Frida Server v${tag} has already been executed.", Toast.LENGTH_LONG).show()
+ return@setExecuteListener
+ }
+
+ fridaViewModel.executeFridaServer(fridaItem)
+ }
+ }
+
+ fridaViewModel.fridaItemList.observe(viewLifecycleOwner) { fridaItems ->
+ fridaAdapter.setFridaList(fridaItems)
+ }
+
+ fridaViewModel.errorMessage.observe(viewLifecycleOwner, {
+ })
+
+ fridaViewModel.installProgress.observe(viewLifecycleOwner) {
+ if (it == -1) {
+ fridaAdapter.enableAll()
+ dialog.dismiss()
+ dialogDownloadBinding.progress = 0
+ } else {
+ fridaAdapter.disableAll()
+ dialog.show()
+ dialogDownloadBinding.progress = it
+ }
+ }
+
+ fridaViewModel.getAllFridaItems(true)
+
+ return root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaService.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaService.kt
new file mode 100644
index 0000000..bfe7d22
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaService.kt
@@ -0,0 +1,127 @@
+package cc.ggez.ezhz.module.frida
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.media.AudioManager
+import android.os.Build
+import android.os.IBinder
+import android.telephony.mbms.StreamingService
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import cc.ggez.ezhz.MainActivity
+import cc.ggez.ezhz.R
+import cc.ggez.ezhz.module.frida.helper.FridaHelper
+import java.io.File
+
+
+class FridaService : Service() {
+ val TAG = "FridaService"
+ lateinit var fridaServerDir: String
+ private val notificationManager: NotificationManager by lazy {
+ getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ }
+ private lateinit var pendIntent: PendingIntent
+ private lateinit var pStopSelf: PendingIntent
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onCreate() {
+ Log.d(TAG, "Service Frida Create");
+ super.onCreate()
+ fridaServerDir = "${filesDir.absolutePath}/server"
+ createNotificationChannel()
+ val intent = Intent(this, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+ intent.putExtra("menu", "frida")
+ pendIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ } else {
+ PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+ val stopIntent = Intent(this, FridaService::class.java)
+ stopIntent.action = "STOP"
+ pStopSelf = PendingIntent.getForegroundService(this, 0, stopIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ notifyAlert(
+ getString(R.string.app_name) + " | Frida Module",
+ getString(R.string.service_running)
+ )
+ }
+
+ private fun handlerCommand(tag: String): Boolean {
+ if (File("$fridaServerDir/frida-server-$tag").exists()) {
+ FridaHelper.startFridaServer(fridaServerDir, tag)
+ return true
+ }
+ return false
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if(intent?.action.equals("STOP")) {
+ stopSelf()
+ return super.onStartCommand(intent, flags, startId)
+ }
+ val tag = intent?.getStringExtra("tag")
+ Thread {
+ if (tag == null || !handlerCommand(tag)) {
+ stopSelf()
+ }
+ }.start()
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onDestroy() {
+ FridaHelper.stopFridaServer()
+ notificationManager.cancelAll();
+ stopForeground(STOP_FOREGROUND_DETACH);
+ super.onDestroy()
+ }
+
+ private fun createNotificationChannel() {
+ val name: CharSequence = "EZHZ Frida Service"
+ val description = "EZHZ Frida Background Service"
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel = NotificationChannel("EZHZ Frida Service", name, importance)
+ channel.description = description
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun initSoundVibrateLights(builder: NotificationCompat.Builder) {
+ val audioManager = this.getSystemService(AUDIO_SERVICE) as AudioManager
+ if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
+ builder.setSound(null)
+ }
+
+ builder.setVibrate(longArrayOf(0, 1000, 500, 1000, 500, 1000))
+ }
+
+ private fun notifyAlert(title: String, info: String) {
+ val notiTitle = "${getString(R.string.app_name)} | Frida Module"
+ val builder = NotificationCompat.Builder(this, "Service")
+ initSoundVibrateLights(builder)
+ builder.setAutoCancel(false)
+ builder.setTicker(notiTitle)
+ builder.setContentTitle(title)
+ builder.setContentText(info)
+ builder.setSmallIcon(R.drawable.ic_debugging)
+ builder.setContentIntent(pendIntent)
+ builder.addAction(
+ android.R.drawable.ic_lock_power_off,
+ getString(R.string.service_stop),
+ pStopSelf)
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ builder.setOngoing(true)
+ builder.setChannelId("EZHZ Frida Service")
+
+ startForeground(2, builder.build())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/FridaViewModel.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaViewModel.kt
new file mode 100644
index 0000000..8390bb5
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/FridaViewModel.kt
@@ -0,0 +1,256 @@
+package cc.ggez.ezhz.module.frida
+
+import android.app.Application
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MutableLiveData
+import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.checkFridaServerProcessTag
+import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.getDownloadedFridaTags
+import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.removeFridaServer
+import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.startFridaServer
+import cc.ggez.ezhz.module.frida.helper.FridaHelper.Companion.stopFridaServer
+import cc.ggez.ezhz.module.frida.helper.GithubHelper
+import cc.ggez.ezhz.module.frida.model.FridaItem
+import cc.ggez.ezhz.module.frida.model.FridaItemState
+import cc.ggez.ezhz.module.frida.model.GithubRelease
+import cc.ggez.ezhz.module.frida.model.GithubTag
+import cc.ggez.ezhz.module.proxy.ProxyService
+import com.ixuea.android.downloader.DownloadService
+import com.ixuea.android.downloader.callback.DownloadListener
+import com.ixuea.android.downloader.domain.DownloadInfo
+import com.ixuea.android.downloader.exception.DownloadException
+import org.tukaani.xz.XZInputStream
+import java.io.BufferedInputStream
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+
+
+class FridaViewModel(application: Application) : AndroidViewModel(application) {
+
+ val TAG = "FridaViewModel"
+
+ val fridaItemList = MutableLiveData>()
+ val installProgress = MutableLiveData(-1)
+ val errorMessage = MutableLiveData()
+ val cacheDir: File = getApplication().cacheDir
+ var fridaServerDir = "${getApplication().filesDir.absolutePath}/server"
+ val downloadManager = DownloadService.getDownloadManager(getApplication().applicationContext);
+
+ private fun checkCacheTags(): Boolean {
+ val file = File(cacheDir, "frida_tags.json")
+ return file.exists()
+ }
+
+ private fun getCacheTags(): String {
+ val file = File(cacheDir, "frida_tags.json")
+ return file.readText()
+ }
+
+ private fun cacheTags(tags: String) {
+ val file = File(cacheDir, "frida_tags.json")
+ file.writeText(tags)
+ }
+
+ private fun fridaItemsFromTags(tags: List): List {
+ val installedTags = getDownloadedFridaTags(fridaServerDir)
+ val executedTag = checkFridaServerProcessTag()
+
+ Log.d("installedTags", installedTags.toString())
+
+ val fridaItems = tags.map {
+ val state = if (executedTag == it.name) {
+ FridaItemState.EXECUTING
+ }
+ else if (installedTags.contains(it.name)) {
+ FridaItemState.INSTALLED
+ } else {
+ FridaItemState.NOT_INSTALL
+ }
+
+ FridaItem(it, state)
+ }
+
+ return fridaItems.sortedWith(Comparator{ a, b ->
+ if (a.state < b.state) {
+ return@Comparator 1
+ }
+ else if (a.state > b.state) {
+ return@Comparator -1
+ }
+
+ return@Comparator b.tag.name.compareTo(a.tag.name)
+ })
+ }
+
+ fun getAllFridaItems(forceReload: Boolean = false) {
+ if (forceReload && checkCacheTags()) {
+ val tags = GithubHelper.translateTags(getCacheTags())
+ fridaItemList.postValue(fridaItemsFromTags(tags))
+ } else {
+ GithubHelper.fetchFridaTags("frida", "frida", 0, object: GithubHelper.GithubTagsCallback {
+ override fun onSuccess(resJson: String) {
+ cacheTags(resJson)
+ val tags = GithubHelper.translateTags(getCacheTags())
+ fridaItemList.postValue(fridaItemsFromTags(tags))
+ }
+
+ override fun onFailure(e: IOException) {
+ errorMessage.postValue(e.message)
+ }
+ })
+ }
+ }
+
+ fun executeFridaServer(fridaItem: FridaItem) {
+ try {
+ val it = Intent(getApplication(), FridaService::class.java)
+ val bundle = Bundle()
+ bundle.putString("tag", fridaItem.tag.name)
+ it.putExtras(bundle)
+ getApplication().startForegroundService(it)
+ } catch (e: Exception) {
+ Log.e(TAG, "serviceStart: ${e.message}")
+ }
+
+ val fridaItems = fridaItemList.value!!
+ fridaItems.map { item ->
+ if (item.tag.name == fridaItem.tag.name) {
+ item.state = FridaItemState.EXECUTING
+ }
+ }
+ fridaItemList.postValue(fridaItems.toMutableList())
+ }
+
+ fun killFridaServer(fridaItem: FridaItem) {
+ try {
+ getApplication().stopService(Intent(getApplication(), FridaService::class.java))
+ stopFridaServer()
+ } catch (e: Exception) {
+ Log.e(TAG, "serviceStop: ${e.message}")
+ }
+ val fridaItems = fridaItemList.value!!
+ fridaItems.map { item ->
+ if (item.tag.name == fridaItem.tag.name) {
+ item.state = FridaItemState.INSTALLED
+ }
+ }
+ fridaItemList.postValue(fridaItems.toMutableList())
+ }
+
+ fun installFridaServer(fridaItem: FridaItem) {
+ installProgress.postValue(0)
+ Log.d(TAG, "[+] Install Start ${fridaItem.tag.name}")
+ GithubHelper.fetchFridaRelease("frida", "frida", fridaItem.tag.name, object: GithubHelper.GithubReleaseCallback {
+ override fun onSuccess(resJson: String) {
+ val release = GithubHelper.translateRelease(resJson)
+ val downloadUrl = GithubHelper.downloadUrlFromRelease(release)
+ Log.d(TAG, "[+] Download Start ${downloadUrl}")
+ val downloadInfo = DownloadInfo.Builder().setUrl(downloadUrl)
+ .setPath("${cacheDir.absolutePath}/frida-server-${fridaItem.tag.name}.xz")
+ .build()
+
+ downloadInfo.downloadListener = object : DownloadListener {
+ override fun onStart() {
+ installProgress.postValue(0)
+ }
+
+ override fun onWaited() {
+ }
+
+ override fun onPaused() {
+ }
+
+ override fun onDownloading(progress: Long, size: Long) {
+ installProgress.postValue((progress * 100 / size).toInt())
+ }
+
+ override fun onRemoved() {
+ installProgress.postValue(-1)
+ }
+
+ override fun onDownloadSuccess() {
+ installProgress.postValue(-1)
+ Log.d(TAG, "[+] Download Finish")
+ val fridaServerDir = File(fridaServerDir)
+ if (!fridaServerDir.exists()) {
+ fridaServerDir.mkdir()
+ }
+
+ Log.d(TAG, "[+] Uncompressing XZ")
+ try {
+ val fin =
+ FileInputStream(
+ File(
+ cacheDir,
+ "frida-server-${fridaItem.tag.name}.xz"
+ )
+ )
+ val inXZ = BufferedInputStream(fin)
+ val outXZ =
+ FileOutputStream(
+ File(
+ fridaServerDir,
+ "frida-server-${fridaItem.tag.name}"
+ )
+ )
+
+ val xzIn = XZInputStream(inXZ)
+ val buffer = ByteArray(8192)
+
+ var n = 0
+ while (-1 != xzIn.read(buffer).also { n = it }) {
+ outXZ.write(buffer, 0, n)
+ }
+ xzIn.close()
+ fin.close()
+ outXZ.close()
+ val fridaItems = fridaItemList.value!!
+ fridaItems.map { item ->
+ if (item.tag.name == fridaItem.tag.name) {
+ item.state = FridaItemState.INSTALLED
+ }
+ }
+ fridaItemList.postValue(fridaItems.toMutableList())
+ } catch (e: IOException) {
+ Log.e(TAG, "Uncompress XZ Error: ${e.message}")
+ }
+
+ val cacheXZ = File(cacheDir, "frida-server-${fridaItem.tag.name}.xz")
+ if (cacheXZ.exists()) {
+ cacheXZ.delete()
+ }
+ }
+
+ override fun onDownloadFailed(e: DownloadException?) {
+ installProgress.postValue(-1)
+ errorMessage.postValue("Frida server download failed: ${e?.message}")
+ }
+
+ }
+
+ downloadManager.download(downloadInfo)
+ }
+
+ override fun onFailure(e: IOException) {
+ Log.d(TAG, e.message.toString())
+ errorMessage.postValue(e.message)
+ }
+ })
+ }
+
+ fun uninstallFridaServer(fridaItem: FridaItem) {
+ removeFridaServer(fridaServerDir, fridaItem.tag.name)
+
+ val fridaItems = fridaItemList.value!!
+ fridaItems.map { item ->
+ if (item.tag.name == fridaItem.tag.name) {
+ item.state = FridaItemState.NOT_INSTALL
+ }
+ }
+ fridaItemList.postValue(fridaItems.toMutableList())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/helper/CommonHelper.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/CommonHelper.kt
new file mode 100644
index 0000000..18f7f36
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/CommonHelper.kt
@@ -0,0 +1,21 @@
+package cc.ggez.ezhz.module.frida.helper
+
+import com.topjohnwu.superuser.Shell
+import java.io.File
+
+class CommonHelper {
+
+ companion object {
+ fun getArchType(): String {
+ val archType = Shell.cmd("getprop ro.product.cpu.abi").exec().out[0]
+ return when (archType) {
+ "x86" -> "x86"
+ "arm64-v8a" -> "arm64"
+ "x86_64" -> "x86_64"
+ "armeabi-v7a" -> "arm"
+ "armeabi" -> "arm"
+ else -> "arm64"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/helper/FridaHelper.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/FridaHelper.kt
new file mode 100644
index 0000000..8ee2a3c
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/FridaHelper.kt
@@ -0,0 +1,69 @@
+package cc.ggez.ezhz.module.frida.helper
+
+import android.util.Log
+import cc.ggez.ezhz.module.frida.model.GithubRelease
+import cc.ggez.ezhz.module.frida.model.GithubTag
+import com.google.gson.Gson
+import com.topjohnwu.superuser.Shell
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+class FridaHelper {
+ companion object {
+ val archType: String = CommonHelper.getArchType()
+
+ fun checkFridaServerProcessTag(): String {
+ val stdout: List = ArrayList()
+ Shell.cmd("ps -A | grep -v grep | grep ggez-server | awk '{print \$9}'").to(stdout).exec()
+ if (stdout.isNotEmpty()) {
+ return stdout[0].replace( "ggez-server-", "")
+ }
+ return ""
+ }
+
+ fun checkFridaServerProcessId(): String {
+ val stdout: List = ArrayList()
+ Shell.cmd("ps -A | grep -v grep | grep ggez-server | awk '{print \$2}'").to(stdout).exec()
+ if (stdout.isNotEmpty()) {
+ return stdout[0].replace( "ggez-server-", "")
+ }
+ return ""
+ }
+
+ fun startFridaServer(serverPath: String, tag: String) {
+ Shell.cmd("cp ${serverPath}/frida-server-${tag} /data/local/tmp/ggez-server-${tag}",
+ "chmod +x /data/local/tmp/ggez-server-${tag}",
+ "/data/local/tmp/ggez-server-${tag} &"
+ ).submit()
+ }
+
+ fun stopFridaServer() {
+ val pid = checkFridaServerProcessId()
+ if (pid.isNotEmpty()) Shell.cmd("kill -9 ${pid}").exec()
+ }
+
+ fun getDownloadedFridaTags(path: String): List {
+ val serverDir = File(path)
+ val files = serverDir.listFiles()
+ return files?.map { it.name.replace("frida-server-", "") } ?: emptyList()
+ }
+
+ fun removeFridaServer(path: String, tag: String) {
+ val serverDir = File(path)
+ val files = serverDir.listFiles()
+ files?.forEach {
+ if (it.name.contains(tag)) {
+ it.delete()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/helper/GithubHelper.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/GithubHelper.kt
new file mode 100644
index 0000000..e99f564
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/helper/GithubHelper.kt
@@ -0,0 +1,113 @@
+package cc.ggez.ezhz.module.frida.helper
+
+import android.util.Log
+import cc.ggez.ezhz.module.frida.model.GithubRelease
+import cc.ggez.ezhz.module.frida.model.GithubTag
+import com.google.gson.Gson
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+class GithubHelper {
+ interface GithubTagsCallback {
+ fun onSuccess(resJson: String)
+ fun onFailure(e: IOException)
+ }
+
+ interface GithubReleaseCallback {
+ fun onSuccess(resJson: String)
+ fun onFailure(e: IOException)
+ }
+ companion object {
+
+ const val TAG = "FridaGithubHelper"
+ const val fridaGithubEndpoint = "https://api.github.com/repos"
+ val archType = CommonHelper.getArchType()
+
+ private val client = OkHttpClient.Builder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .build()
+
+ fun fetchFridaTags(owner: String, repo: String, page: Int, callback: GithubTagsCallback) {
+ val urlBuilder: HttpUrl.Builder =
+ ("$fridaGithubEndpoint/$owner/$repo/tags").toHttpUrl().newBuilder()
+
+ urlBuilder.addQueryParameter("per_page", "100")
+ urlBuilder.addQueryParameter("page", page.toString())
+ val url = urlBuilder.build().toString()
+
+ val request = Request.Builder()
+ .url(url)
+ .build()
+
+ val call = client.newCall(request)
+ call.enqueue(object : Callback {
+
+ override fun onFailure(call: Call, e: IOException) {
+ Log.e(TAG, "${e.message}")
+ callback.onFailure(e)
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ val resJson = response.body!!.string()
+ callback.onSuccess(resJson)
+ }
+ })
+ }
+
+ fun fetchFridaRelease(owner: String, repo: String, tag: String, callback: GithubReleaseCallback) {
+ val urlBuilder: HttpUrl.Builder =
+ ("$fridaGithubEndpoint/$owner/$repo/releases/tags/$tag").toHttpUrl().newBuilder()
+
+ val url = urlBuilder.build().toString()
+ Log.d(TAG, url)
+
+ val request: Request = Request.Builder()
+ .url(url)
+ .build()
+
+ val call = client.newCall(request)
+ call.enqueue(object : Callback {
+
+ override fun onFailure(call: Call, e: IOException) {
+ Log.e(TAG, "${e.message}")
+ callback.onFailure(e)
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ val resJson = response.body!!.string()
+ callback.onSuccess(resJson)
+ }
+ })
+ }
+
+ fun translateTags(resJson: String): List {
+ val gson = Gson()
+ val tags = gson.fromJson(resJson, Array::class.java)
+ return ArrayList(tags.toList())
+ }
+
+ fun translateRelease(resJson: String): GithubRelease {
+ val gson = Gson()
+ return gson.fromJson(resJson, GithubRelease::class.java)
+ }
+
+ fun downloadUrlFromRelease(release: GithubRelease): String {
+ val assets = release.assets
+ for (asset in assets) {
+ if (asset.name.contains("-$archType") && asset.name.contains("frida-server")) {
+ return asset.browser_download_url
+ }
+ }
+ return ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/FridaItem.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/FridaItem.kt
new file mode 100644
index 0000000..2ee5e4b
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/FridaItem.kt
@@ -0,0 +1,27 @@
+package cc.ggez.ezhz.module.frida.model
+
+enum class FridaItemState(s: Int) {
+ NOT_INSTALL(0), INSTALLED(1), EXECUTING(2)
+}
+data class FridaItem(
+ val tag: GithubTag,
+ var state: FridaItemState = FridaItemState.NOT_INSTALL,
+) {
+ val isExecuted: Boolean
+ get(): Boolean {
+ return state == FridaItemState.EXECUTING
+ }
+ val isExecutable: Boolean
+ get(): Boolean {
+ return state == FridaItemState.INSTALLED || state == FridaItemState.EXECUTING
+ }
+ val isInstalled: Boolean
+ get(): Boolean {
+ return state == FridaItemState.EXECUTING || state == FridaItemState.INSTALLED
+ }
+
+ val isInstallable: Boolean
+ get(): Boolean {
+ return state != FridaItemState.EXECUTING
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubAsset.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubAsset.kt
new file mode 100644
index 0000000..ad50c2c
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubAsset.kt
@@ -0,0 +1,14 @@
+package cc.ggez.ezhz.module.frida.model
+
+data class GithubAsset(
+ val url: String,
+ val browser_download_url: String,
+ val id: Int,
+ val node_id: String,
+ val name: String,
+ val label: String,
+ val state: String,
+ val created_at: String,
+ val updated_at: String,
+ val uploader: GithubUser
+)
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubCommit.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubCommit.kt
new file mode 100644
index 0000000..105a655
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubCommit.kt
@@ -0,0 +1,6 @@
+package cc.ggez.ezhz.module.frida.model
+
+data class GithubCommit(
+ val sha: String,
+ val url: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubRelease.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubRelease.kt
new file mode 100644
index 0000000..6a88636
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubRelease.kt
@@ -0,0 +1,23 @@
+package cc.ggez.ezhz.module.frida.model
+
+data class GithubRelease(
+ val url: String,
+ val html_url: String,
+ val assets_url: String,
+ val upload_url: String,
+ val tarball_url: String,
+ val zipball_url: String,
+ val discussion_url: String,
+ val id: Int,
+ val node_id: String,
+ val tag_name: String,
+ val target_commitish: String,
+ val name: String,
+ val body: String,
+ val draft: Boolean,
+ val prerelease: Boolean,
+ val created_at: String,
+ val published_at: String,
+ val author: GithubUser,
+ val assets: List,
+)
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubTag.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubTag.kt
new file mode 100644
index 0000000..79ec7ae
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubTag.kt
@@ -0,0 +1,9 @@
+package cc.ggez.ezhz.module.frida.model
+
+data class GithubTag(
+ val name: String,
+ val commit: GithubCommit,
+ val zipball_url: String,
+ val tarball_url: String,
+ val node_id: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubUser.kt b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubUser.kt
new file mode 100644
index 0000000..73f017b
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/frida/model/GithubUser.kt
@@ -0,0 +1,22 @@
+package cc.ggez.ezhz.module.frida.model
+
+data class GithubUser(
+ val login: String,
+ val id: Int,
+ val node_id: String,
+ val avatar_url: String,
+ val gravatar_id: String,
+ val url: String,
+ val html_url: String,
+ val followers_url: String,
+ val following_url: String,
+ val gists_url: String,
+ val starred_url: String,
+ val subscriptions_url: String,
+ val organizations_url: String,
+ val repos_url: String,
+ val events_url: String,
+ val received_events_url: String,
+ val type: String,
+ val site_admin: Boolean,
+)
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyFragment.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyFragment.kt
new file mode 100644
index 0000000..b7ad1af
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyFragment.kt
@@ -0,0 +1,193 @@
+package cc.ggez.ezhz.module.proxy
+
+import android.app.Activity
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.preference.CheckBoxPreference
+import androidx.preference.EditTextPreference
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.SwitchPreferenceCompat
+import cc.ggez.ezhz.R
+import cc.ggez.ezhz.core.appselect.AppSelectActivity
+
+
+class ProxyFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
+
+ val TAG = "ProxyFragment"
+
+ private lateinit var isProxyRunningSwitch: SwitchPreferenceCompat
+
+ private lateinit var isProxyAutoCheck: CheckBoxPreference
+ private lateinit var proxyAutoUrlText: EditTextPreference
+ private lateinit var proxyHostText: EditTextPreference
+ private lateinit var proxyPortText: EditTextPreference
+ private lateinit var proxyTypeList: ListPreference
+ private lateinit var dnsText: EditTextPreference
+
+ private lateinit var isAuthCheck: CheckBoxPreference
+ private lateinit var authUsernameText: EditTextPreference
+ private lateinit var authPasswordText: EditTextPreference
+
+ private lateinit var isTargetGlobalCheck: CheckBoxPreference
+ private lateinit var targetApps: Preference
+ private lateinit var isTargetBypassModeCheck: CheckBoxPreference
+
+ val appSelectActivityLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result: ActivityResult? ->
+ if (result?.resultCode == Activity.RESULT_OK) {
+ val data = result.data
+ val selectedApps = data?.getStringArrayListExtra("selectedApps")
+ val targetAppsEdit = targetApps!!.sharedPreferences?.edit()!!
+ targetAppsEdit.putStringSet("target_apps", selectedApps?.toSet())
+ targetAppsEdit.apply()
+ targetApps.summaryProvider = targetApps.summaryProvider
+ }
+ }
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.setting_proxy, rootKey)
+
+ isProxyRunningSwitch = findPreference("proxy_running")!!
+
+ isProxyAutoCheck = findPreference("proxy_auto")!!
+ proxyAutoUrlText = findPreference("proxy_pac_url")!!
+ proxyHostText = findPreference("proxy_host")!!
+ proxyPortText = findPreference("proxy_port")!!
+ proxyTypeList = findPreference("proxy_type")!!
+ dnsText = findPreference("proxy_dns")!!
+
+ isAuthCheck = findPreference("auth_enable")!!
+ authUsernameText = findPreference("auth_username")!!
+ authPasswordText = findPreference("auth_password")!!
+
+ isTargetGlobalCheck = findPreference("target_global")!!
+ targetApps = findPreference("target_apps")!!
+ isTargetBypassModeCheck = findPreference("target_bypass_mode")!!
+
+ // App select
+ targetApps.setOnPreferenceClickListener {
+ val intent = Intent(context, AppSelectActivity::class.java)
+ val selectedApps = it.sharedPreferences!!.getStringSet("target_apps", setOf())
+ intent.putExtra("selectedApps", ArrayList(selectedApps!!))
+ appSelectActivityLauncher.launch(intent)
+ true
+ }
+
+ targetApps.summaryProvider = Preference.SummaryProvider { preference ->
+ getTargetAppsSummary(preference)
+ }
+
+ val isProxyAuto = findPreference("proxy_auto")!!.sharedPreferences!!.getBoolean("proxy_auto", false)
+ controlProxyAuto(isProxyAuto)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ preferenceScreen.sharedPreferences
+ ?.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ override fun onPause() {
+ super.onPause()
+ preferenceScreen.sharedPreferences
+ ?.unregisterOnSharedPreferenceChangeListener(this)
+ }
+
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ when (key) {
+ "proxy_running" -> {
+ val isProxyRunning = sharedPreferences!!.getBoolean("proxy_running", false)
+ if (isProxyRunning) {
+ disableAll();
+ if (!ProxySingleton.isConnecting) serviceStart()
+ } else {
+ enableAll();
+ if (!ProxySingleton.isConnecting) serviceStop()
+ }
+ }
+ "proxy_auto" -> {
+ val isProxyAuto = sharedPreferences!!.getBoolean("proxy_auto", false)
+ controlProxyAuto(isProxyAuto)
+ }
+ }
+ }
+
+ private fun getTargetAppsSummary(preference: Preference): String {
+ val selectedApps = preference.sharedPreferences?.getStringSet("target_apps", setOf())
+ val selectedSize = selectedApps?.size ?: 0
+ return if (selectedSize > 0)
+ "You have already selected $selectedSize app"
+ else
+ "Not select any app yet"
+ }
+
+ private fun controlProxyAuto(isProxyAuto: Boolean) {
+ proxyAutoUrlText.isEnabled = isProxyAuto
+ proxyHostText.isEnabled = !isProxyAuto
+ proxyPortText.isEnabled = !isProxyAuto
+ }
+
+ private fun disableAll() {
+ isProxyAutoCheck.isEnabled = false
+ proxyAutoUrlText.isEnabled = false
+ proxyHostText.isEnabled = false
+ proxyPortText.isEnabled = false
+ proxyTypeList.isEnabled = false
+ dnsText.isEnabled = false
+
+ isAuthCheck.isEnabled = false
+ authUsernameText.isEnabled = false
+ authPasswordText.isEnabled = false
+
+ isTargetGlobalCheck.isEnabled = false
+ targetApps.isEnabled = false
+ isTargetBypassModeCheck.isEnabled = false
+ }
+
+ private fun enableAll() {
+ isProxyAutoCheck.isEnabled = false
+ proxyAutoUrlText.isEnabled = false
+ proxyHostText.isEnabled = true
+ proxyPortText.isEnabled = true
+ proxyTypeList.isEnabled = true
+ dnsText.isEnabled = true
+
+ isAuthCheck.isEnabled = true
+ authUsernameText.isEnabled = true
+ authPasswordText.isEnabled = true
+
+ isTargetGlobalCheck.isEnabled = true
+ targetApps.isEnabled = true
+ isTargetBypassModeCheck.isEnabled = true
+
+ }
+
+ private fun serviceStart() {
+ if (ProxyService.isServiceStarted()) return
+ val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
+ val proxyProfile = ProxyProfile.fromSharedPref(settings)
+ try {
+ val it = Intent(requireActivity(), ProxyService::class.java)
+ it.putExtras(proxyProfile.toBundle())
+ requireActivity().startForegroundService(it)
+ } catch (e: Exception) {
+ Log.e(TAG, "serviceStart: ${e.message}")
+ }
+ }
+
+ private fun serviceStop() {
+ if (!ProxyService.isServiceStarted()) return
+ try {
+ requireActivity().stopService(Intent(requireActivity(), ProxyService::class.java))
+ } catch (e: Exception) {
+ Log.e(TAG, "serviceStop: ${e.message}")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyProfile.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyProfile.kt
new file mode 100644
index 0000000..4e00667
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyProfile.kt
@@ -0,0 +1,217 @@
+package cc.ggez.ezhz.module.proxy
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import org.json.JSONObject
+import java.io.Serializable
+import java.net.InetAddress
+import java.util.Vector
+import java.util.regex.Pattern
+
+
+class ProxyProfile : Serializable {
+ var isProxyRunning: Boolean = false
+
+ var isProxyAuto: Boolean = false
+ var pacUrl: String
+ var host: String
+ var port: Int = 0
+ var proxyType: String
+ var dns: String
+
+ var isAuth: Boolean = false
+ var username: String
+ var password: String
+
+ var isTargetGlobal: Boolean = true
+ var targetApps: ArrayList
+ var isTargetBypassMode: Boolean = false
+
+ companion object {
+ fun fromSharedPref(settings: SharedPreferences): ProxyProfile {
+ val proxyProfile = ProxyProfile()
+
+ proxyProfile.isProxyRunning = settings.getBoolean("proxy_running", false)
+
+ proxyProfile.isProxyAuto = settings.getBoolean("proxy_auto", false)
+ proxyProfile.pacUrl = settings.getString("proxy_pac_url", "").toString()
+ proxyProfile.host = settings.getString("proxy_host", "127.0.0.1").toString()
+ proxyProfile.port = settings.getString("proxy_port", "1337").toString().toInt()
+ proxyProfile.proxyType = settings.getString("proxy_type", "http").toString()
+ proxyProfile.dns = settings.getString("proxy_dns", "1.1.1.1").toString()
+ proxyProfile.isAuth = settings.getBoolean("auth_enable", false)
+ proxyProfile.username = settings.getString("auth_username", "").toString()
+ proxyProfile.password = settings.getString("auth_password", "").toString()
+
+ proxyProfile.isTargetGlobal = settings.getBoolean("target_global", false)
+ proxyProfile.targetApps = ArrayList(settings.getStringSet("target_apps", setOf())?: setOf())
+ proxyProfile.isTargetBypassMode = settings.getBoolean("target_bypass_mode", false)
+
+ return proxyProfile
+ }
+
+ fun fromBundle(bundle: Bundle): ProxyProfile {
+ val proxyProfile = ProxyProfile()
+
+ proxyProfile.isProxyRunning = bundle.getBoolean("proxy_running", false)
+
+ proxyProfile.isProxyAuto = bundle.getBoolean("proxy_auto", false)
+ proxyProfile.pacUrl = bundle.getString("proxy_pac_url", "").toString()
+ proxyProfile.host = bundle.getString("proxy_host", "127.0.0.1").toString()
+ proxyProfile.port = bundle.getInt("proxy_port", 1337)
+ proxyProfile.proxyType = bundle.getString("proxy_type", "http").toString()
+ proxyProfile.dns = bundle.getString("proxy_dns", "1.1.1.1").toString()
+
+ proxyProfile.isAuth = bundle.getBoolean("auth_enable", false)
+ proxyProfile.username = bundle.getString("auth_username", "").toString()
+ proxyProfile.password = bundle.getString("auth_password", "").toString()
+
+ proxyProfile.isTargetGlobal = bundle.getBoolean("target_global", false)
+ proxyProfile.targetApps = bundle.getStringArrayList("target_apps") ?: ArrayList()
+ proxyProfile.isTargetBypassMode = bundle.getBoolean("target_bypass_mode", false)
+
+ return proxyProfile
+ }
+
+ fun validateAddr(ia: String?): String? {
+ val valid1: Boolean = Pattern.matches(
+ "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}/[0-9]{1,2}",
+ ia
+ )
+ val valid2: Boolean = Pattern.matches(
+ "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}", ia
+ )
+ return if (valid1 || valid2) {
+ ia
+ } else {
+ var addrString: String? = null
+ try {
+ val addr = InetAddress.getByName(ia)
+ addrString = addr.hostAddress
+ } catch (ignore: Exception) {
+ }
+ if (addrString != null) {
+ val valid3: Boolean = Pattern.matches(
+ "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}",
+ addrString
+ )
+ if (!valid3) addrString = null
+ }
+ addrString
+ }
+ }
+
+ fun decodeAddrs(addrs: String): Array {
+ val list = addrs.split("\\|".toRegex()).dropLastWhile { it.isEmpty() }
+ .toTypedArray()
+ val ret = Vector()
+ for (addr in list) {
+ val ta = validateAddr(addr)
+ if (ta != null) ret.add(ta)
+ }
+ return ret.toTypedArray()
+ }
+
+ fun encodeAddrs(addrs: Array): String {
+ if (addrs.size == 0) return ""
+ val sb = StringBuilder()
+ for (addr in addrs) {
+ val ta = validateAddr(addr)
+ if (ta != null) sb.append(ta).append("|")
+ }
+ return sb.substring(0, sb.length - 1)
+ }
+ }
+
+ init {
+ isProxyRunning = false
+
+ isProxyAuto = false
+ pacUrl = ""
+ host = "127.0.0.1"
+ port = 1337
+ proxyType = "http"
+ dns = "1.1.1.1"
+
+ isAuth = false
+ username = ""
+ password = ""
+
+ isTargetGlobal = true
+ targetApps = ArrayList()
+ isTargetBypassMode = false
+ }
+
+ fun toBundle(): Bundle {
+ val bundle = Bundle()
+
+ bundle.putBoolean("proxy_running", isProxyRunning)
+
+ bundle.putBoolean("proxy_auto", isProxyAuto)
+ bundle.putString("proxy_pac_url", pacUrl)
+ bundle.putString("proxy_host", host)
+ bundle.putInt("proxy_port", port)
+ bundle.putString("proxy_type", proxyType)
+ bundle.putString("proxy_dns", dns)
+
+ bundle.putBoolean("auth_enable", isAuth)
+ bundle.putString("auth_username", username)
+ bundle.putString("auth_password", password)
+
+ bundle.putBoolean("target_global", isTargetGlobal)
+ bundle.putStringArrayList("target_apps", targetApps)
+ bundle.putBoolean("target_bypass_mode", isTargetBypassMode)
+
+ return bundle
+ }
+
+ fun toSharedPref(settings: SharedPreferences) {
+ val ed = settings.edit()
+
+ ed.putBoolean("proxy_running", isProxyRunning)
+
+ ed.putBoolean("proxy_auto", isProxyAuto)
+ ed.putString("proxy_pac_url", pacUrl)
+ ed.putString("proxy_host", host)
+ ed.putString("proxy_port", port.toString())
+ ed.putString("proxy_type", proxyType)
+ ed.putString("proxy_dns", dns)
+
+ ed.putBoolean("auth_enable", isAuth)
+ ed.putString("auth_username", username)
+ ed.putString("auth_password", password)
+
+ ed.putBoolean("target_global", isTargetGlobal)
+ ed.putBoolean("target_bypass_mode", isTargetBypassMode)
+ ed.putStringSet("target_apps", targetApps.toSet())
+ ed.apply()
+ }
+
+ override fun toString(): String {
+ return toJson().toString()
+ }
+
+ fun toJson(): JSONObject {
+ val obj = JSONObject()
+
+ obj.put("proxy_running", isProxyRunning)
+
+ obj.put("proxy_auto", isProxyAuto)
+ obj.put("proxy_pac_url", pacUrl)
+ obj.put("proxy_host", host)
+ obj.put("proxy_port", port)
+ obj.put("proxy_type", proxyType)
+ obj.put("proxy_dns", dns)
+
+ obj.put("auth_enable", isAuth)
+ obj.put("auth_username", username)
+ obj.put("auth_password", password)
+
+ obj.put("target_global", isTargetGlobal)
+ obj.put("target_apps", targetApps)
+ obj.put("target_bypass_mode", isTargetBypassMode)
+
+ return obj
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyService.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyService.kt
new file mode 100644
index 0000000..18a5bee
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyService.kt
@@ -0,0 +1,325 @@
+package cc.ggez.ezhz.module.proxy
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.SharedPreferences.Editor
+import android.media.AudioManager
+import android.os.Build
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.util.Log
+import android.widget.Toast
+import androidx.core.app.NotificationCompat
+import androidx.preference.PreferenceManager
+import cc.ggez.ezhz.MainActivity
+import cc.ggez.ezhz.R
+import cc.ggez.ezhz.module.frida.FridaService
+import com.topjohnwu.superuser.Shell
+import java.lang.ref.WeakReference
+
+class ProxyService : Service() {
+
+ companion object {
+ private const val TAG = "ProxyService"
+
+ private const val MSG_CONNECT_START = 0
+ private const val MSG_CONNECT_FINISH = 1
+ private const val MSG_CONNECT_SUCCESS = 2
+ private const val MSG_CONNECT_FAIL = 3
+ private const val MSG_CONNECT_PAC_ERROR = 4
+ private const val MSG_CONNECT_RESOLVE_ERROR = 5
+
+ const val CMD_IPTABLES_RETURN = "iptables -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN\n"
+
+ const val CMD_IPTABLES_REDIRECT_ADD_HTTP =
+ ("iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to 8123\n"
+ + "iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to 8123\n"
+ + "iptables -t nat -A OUTPUT -p tcp --dport 8443 -j REDIRECT --to 8123\n"
+ + "iptables -t nat -A OUTPUT -p tcp --dport 5228 -j REDIRECT --to 8123\n"
+ + "iptables -t nat -A OUTPUT -p udp --dport 443 -j REDIRECT --to 8124\n"
+ + "iptables -t nat -A OUTPUT -p udp --dport 8443 -j REDIRECT --to 8124\n"
+ + "iptables -t nat -A OUTPUT -p udp --dport 5228 -j REDIRECT --to 8124\n")
+
+ const val CMD_IPTABLES_DNAT_ADD_HTTP =
+ ("iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:8123\n"
+ + "iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 127.0.0.1:8123\n"
+ + "iptables -t nat -A OUTPUT -p tcp --dport 8443 -j DNAT --to-destination 127.0.0.1:8123\n"
+ + "iptables -t nat -A OUTPUT -p tcp --dport 5228 -j DNAT --to-destination 127.0.0.1:8123\n"
+ + "iptables -t nat -A OUTPUT -p udp --dport 443 -j DNAT --to-destination 127.0.0.1:8124\n"
+ + "iptables -t nat -A OUTPUT -p udp --dport 8443 -j DNAT --to-destination 127.0.0.1:8124\n"
+ + "iptables -t nat -A OUTPUT -p udp --dport 5228 -j DNAT --to-destination 127.0.0.1:8124\n")
+
+ const val CMD_IPTABLES_REDIRECT_ADD_SOCKS =
+ "iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to 8123\n"
+
+ const val CMD_IPTABLES_DNAT_ADD_SOCKS =
+ "iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:8123\n"
+
+ private var sRunningInstance: WeakReference? = null
+ fun isServiceStarted(): Boolean {
+ val isServiceStarted: Boolean
+ if (sRunningInstance == null) {
+ isServiceStarted = false
+ } else if (sRunningInstance!!.get() == null) {
+ isServiceStarted = false
+ sRunningInstance = null
+ } else {
+ isServiceStarted = true
+ }
+ return isServiceStarted
+ }
+ }
+
+ lateinit var basePath: String
+ private val notificationManager: NotificationManager by lazy {
+ getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ }
+ private lateinit var settings: SharedPreferences
+ private lateinit var pendIntent: PendingIntent
+ private lateinit var pStopSelf: PendingIntent
+ private lateinit var proxyProfile: ProxyProfile
+
+ val handler = Handler(Looper.getMainLooper()) { msg ->
+ val ed = settings.edit()
+ when (msg.what) {
+ MSG_CONNECT_START -> {
+ ed.putBoolean("is_connecting", true)
+ ProxySingleton.isConnecting = true
+ }
+
+ MSG_CONNECT_FINISH -> {
+ ed.putBoolean("is_connecting", false)
+ ProxySingleton.isConnecting = false
+ }
+
+ MSG_CONNECT_SUCCESS -> ed.putBoolean("proxy_running", true)
+ MSG_CONNECT_FAIL -> ed.putBoolean("proxy_running", false)
+ MSG_CONNECT_PAC_ERROR -> Toast.makeText(
+ this@ProxyService,
+ R.string.msg_pac_error,
+ Toast.LENGTH_SHORT
+ ).show()
+
+ MSG_CONNECT_RESOLVE_ERROR -> Toast.makeText(
+ this@ProxyService, R.string.msg_resolve_error,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ ed.apply()
+ true
+ }
+
+ private fun markServiceStarted() {
+ sRunningInstance = WeakReference(this)
+ }
+
+ private fun markServiceStopped() {
+ sRunningInstance = null
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onCreate() {
+ Log.d(TAG, "Service Proxy Create");
+ super.onCreate()
+ basePath = filesDir.absolutePath;
+ settings = PreferenceManager.getDefaultSharedPreferences(this)
+
+ createNotificationChannel()
+
+ val intent = Intent(this, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+ intent.putExtra("menu", "proxy")
+ pendIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
+ } else {
+ PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+ val stopIntent = Intent(this, ProxyService::class.java)
+ stopIntent.action = "STOP"
+ pStopSelf = PendingIntent.getForegroundService(this, 0, stopIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ notifyAlert(
+ getString(R.string.app_name) + " | Proxy Module",
+ getString(R.string.service_running)
+ )
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d("xxx", intent?.action.toString())
+ if(intent?.action.equals("STOP")) {
+ stopSelf()
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ if (intent?.extras == null) {
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ Log.d(TAG, "Service Start");
+
+ proxyProfile = ProxyProfile.fromBundle(intent.extras!!)
+
+ Thread {
+ handler.sendEmptyMessage(MSG_CONNECT_START)
+ if (handleCommand()) {
+ // Connection and forward successful
+ handler.sendEmptyMessage(MSG_CONNECT_SUCCESS)
+ } else {
+ // Connection or forward unsuccessful
+ stopSelf()
+ handler.sendEmptyMessage(MSG_CONNECT_FAIL)
+ }
+ handler.sendEmptyMessage(MSG_CONNECT_FINISH)
+ }.start()
+
+ markServiceStarted()
+
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "Service Stop");
+ ProxySingleton.isConnecting = true
+ notificationManager.cancelAll();
+ stopForeground(STOP_FOREGROUND_DETACH);
+ super.onDestroy()
+
+ onDisconnect()
+
+ val ed: Editor = settings.edit()
+ ed.putBoolean("proxy_running", false)
+ ed.apply()
+
+ markServiceStopped()
+ ProxySingleton.isConnecting = false
+ }
+
+ private fun onDisconnect() {
+ val sb = StringBuilder()
+ sb.append("iptables -t nat -F OUTPUT\n")
+ sb.append("kill -9 `cat ${basePath}/gost_tcp.pid`\n")
+ sb.append("kill -9 `cat ${basePath}/gost_udp.pid`\n")
+ sb.append("kill -9 `cat ${basePath}/gost_dns.pid`\n")
+ Shell.cmd(sb.toString()).exec()
+ }
+
+ private fun handleCommand(): Boolean {
+ Shell.cmd("chmod +x $basePath/gost").exec()
+
+ try {
+ val u: String = ProxySingleton.preserve(proxyProfile.username)
+ val p: String = ProxySingleton.preserve(proxyProfile.password)
+ val srcTcp = "-L=red://127.0.0.1:8123?sniffing=true"
+ val srcUdp = "-L=redu://127.0.0.1:8124?ttl=30s"
+ val srcDns = "-L=dns://:53/${proxyProfile.dns}"
+ var auth = ""
+ if (u.isNotEmpty() && p.isNotEmpty()) {
+ auth = "$u:$p"
+ }
+ val dstTcp = "-F=${proxyProfile.proxyType}://$auth@${proxyProfile.host}:${proxyProfile.port}"
+ val dstUdp = "-F=relay://${proxyProfile.host}:${proxyProfile.port}"
+
+ // Start gost tcp here
+ Shell.cmd("$basePath/gost $srcTcp $dstTcp &> $basePath/gost_tcp.log &\n echo $! > $basePath/gost_tcp.pid").exec()
+ Shell.cmd("$basePath/gost $srcUdp $dstUdp &> $basePath/gost_udp.log &\n echo $! > $basePath/gost_udp.pid").exec()
+ Shell.cmd("$basePath/gost $srcDns &> $basePath/gost_dns.log &\n echo $! > $basePath/gost_dns.pid").exec()
+
+ val cmd = StringBuilder()
+ cmd.append(CMD_IPTABLES_RETURN.replace("0.0.0.0", proxyProfile.host))
+
+ var redirectCmd = CMD_IPTABLES_REDIRECT_ADD_HTTP
+ var dnatCmd = CMD_IPTABLES_DNAT_ADD_HTTP
+
+ if (proxyProfile.proxyType.equals("socks4") || proxyProfile.proxyType.equals("socks5")) {
+ redirectCmd = CMD_IPTABLES_REDIRECT_ADD_SOCKS
+ dnatCmd = CMD_IPTABLES_DNAT_ADD_SOCKS
+ }
+
+ if (proxyProfile.isTargetGlobal) {
+ cmd.append(if (ProxySingleton.isRedirectSupport == 1) redirectCmd else dnatCmd)
+ }
+ else if (proxyProfile.isTargetBypassMode) {
+ // for host specified apps
+ for (app in proxyProfile.targetApps) {
+ val appData = app.split(":")
+ cmd.append(
+ CMD_IPTABLES_RETURN.replace("-d 0.0.0.0", "").replace(
+ "-t nat",
+ "-t nat -m owner --uid-owner ${appData[1]}"
+ )
+ )
+ }
+
+ cmd.append(if (ProxySingleton.isRedirectSupport == 1) redirectCmd else dnatCmd)
+ } else {
+ for (app in proxyProfile.targetApps) {
+ val appData = app.split(":")
+ cmd.append(
+ (if (ProxySingleton.isRedirectSupport == 1) redirectCmd else dnatCmd).replace(
+ "-t nat",
+ "-t nat -m owner --uid-owner ${appData[1]}"
+ )
+ )
+ }
+ }
+
+ Shell.cmd(cmd.toString()).exec()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error setting up port forward during connect", e)
+ }
+
+ return true
+ }
+
+ private fun createNotificationChannel() {
+ val name: CharSequence = "EZHZ Proxy Service"
+ val description = "EZHZ Proxy Background Service"
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel = NotificationChannel("EZHZ Proxy Service", name, importance)
+ channel.description = description
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun initSoundVibrateLights(builder: NotificationCompat.Builder) {
+ val audioManager = this.getSystemService(AUDIO_SERVICE) as AudioManager
+ if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
+ builder.setSound(null)
+ }
+
+ builder.setVibrate(longArrayOf(0, 1000, 500, 1000, 500, 1000))
+ }
+
+ private fun notifyAlert(title: String, info: String) {
+ val notiTitle = "${getString(R.string.app_name)} | Proxy Module"
+ val builder = NotificationCompat.Builder(this, "Service")
+ initSoundVibrateLights(builder)
+ builder.setAutoCancel(false)
+ builder.setTicker(notiTitle)
+ builder.setContentTitle(title)
+ builder.setContentText(info)
+ builder.setSmallIcon(R.drawable.ic_proxy)
+ builder.setContentIntent(pendIntent)
+ builder.addAction(
+ android.R.drawable.ic_lock_power_off,
+ getString(R.string.service_stop),
+ pStopSelf)
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ builder.setOngoing(true)
+ builder.setChannelId("EZHZ Proxy Service")
+
+ startForeground(1, builder.build())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxySingleton.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxySingleton.kt
new file mode 100644
index 0000000..ece07ab
--- /dev/null
+++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxySingleton.kt
@@ -0,0 +1,39 @@
+package cc.ggez.ezhz.module.proxy
+
+import com.topjohnwu.superuser.Shell
+
+
+class ProxySingleton {
+ companion object {
+ var isConnecting = false
+ var isRedirectSupport = -1
+ get() {
+ if (field == -1) initHasRedirectSupported()
+ return field
+ }
+
+ fun initHasRedirectSupported() {
+ val sb = StringBuilder()
+ val command = "iptables -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"
+ val result = Shell.cmd(command).exec()
+ val lines = result.out + result.err
+ isRedirectSupport = 1
+
+ // flush the check command
+ Shell.cmd(command.replace("-A", "-D"))
+
+ if (lines.contains("No chain/target/match")) {
+ isRedirectSupport = 0
+ }
+ }
+
+ fun preserve(s: String): String {
+ val sb = java.lang.StringBuilder()
+ for (element in s) {
+ if (element == '\\' || element == '$' || element == '`' || element == '"') sb.append('\\')
+ sb.append(element)
+ }
+ return sb.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyViewModel.kt b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyViewModel.kt
similarity index 89%
rename from app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyViewModel.kt
rename to app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyViewModel.kt
index 08fdd2a..2cc595e 100644
--- a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyViewModel.kt
+++ b/app/src/main/java/cc/ggez/ezhz/module/proxy/ProxyViewModel.kt
@@ -1,4 +1,4 @@
-package cc.ggez.ezhz.ui.proxy
+package cc.ggez.ezhz.module.proxy
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefFragment.kt b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefFragment.kt
similarity index 96%
rename from app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefFragment.kt
rename to app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefFragment.kt
index 4670f00..0241a1e 100644
--- a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefFragment.kt
+++ b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefFragment.kt
@@ -1,4 +1,4 @@
-package cc.ggez.ezhz.ui.sharedpref
+package cc.ggez.ezhz.module.sharedpref
import android.os.Bundle
import android.view.LayoutInflater
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefViewModel.kt b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefViewModel.kt
similarity index 74%
rename from app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefViewModel.kt
rename to app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefViewModel.kt
index de9f48e..33e74b5 100644
--- a/app/src/main/java/cc/ggez/ezhz/ui/sharedpref/SharedPrefViewModel.kt
+++ b/app/src/main/java/cc/ggez/ezhz/module/sharedpref/SharedPrefViewModel.kt
@@ -1,4 +1,4 @@
-package cc.ggez.ezhz.ui.sharedpref
+package cc.ggez.ezhz.module.sharedpref
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel
class SharedPrefViewModel : ViewModel() {
private val _text = MutableLiveData().apply {
- value = "This is shared pref Fragment"
+ value = "SharedPref Modifier Coming Soon"
}
val text: LiveData = _text
}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectActivity.kt b/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectActivity.kt
deleted file mode 100644
index cb9e3fe..0000000
--- a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectActivity.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package cc.ggez.ezhz.ui.appselect
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.preference.PreferenceFragmentCompat
-import cc.ggez.ezhz.R
-
-class AppSelectActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.settings_activity)
- if (savedInstanceState == null) {
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.settings, SettingsFragment())
- .commit()
- }
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- }
-
- class SettingsFragment : PreferenceFragmentCompat() {
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.root_preferences, rootKey)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectService.kt b/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectService.kt
deleted file mode 100644
index 18aaa36..0000000
--- a/app/src/main/java/cc/ggez/ezhz/ui/appselect/AppSelectService.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package cc.ggez.ezhz.ui.appselect
-
-import android.content.Context
-import android.content.SharedPreferences
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.os.Build
-import java.util.Arrays
-import java.util.StringTokenizer
-import java.util.Vector
-
-
-class AppSelectService {
- companion object {
- fun getApps(context: Context) {
-
- val pm = context.packageManager
- val appInfos = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- pm.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L))
- } else {
- pm.getInstalledApplications(0)
- }
-
- val itAppInfo: Iterator = lAppInfo.iterator()
- var aInfo: ApplicationInfo
- while (itAppInfo.hasNext()) {
- aInfo = itAppInfo.next()
-
- // ignore system apps
- if (aInfo.uid < 10000) continue
- if (aInfo.processName == null) continue
- if (pMgr.getApplicationLabel(aInfo) == null || pMgr.getApplicationLabel(aInfo)
- .toString() == ""
- ) continue
- if (pMgr.getApplicationIcon(aInfo) == null) continue
- val tApp = ProxyedApp()
- tApp.setEnabled(aInfo.enabled)
- tApp.setUid(aInfo.uid)
- tApp.setUsername(pMgr.getNameForUid(tApp.getUid()))
- tApp.setProcname(aInfo.processName)
- tApp.setName(pMgr.getApplicationLabel(aInfo).toString())
-
- // check if this application is allowed
- tApp.setProxyed(Arrays.binarySearch(tordApps, tApp.getUsername()) >= 0)
- vectorApps.add(tApp)
- }
- apps = arrayOfNulls(vectorApps.size)
- vectorApps.toArray(apps)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaFragment.kt b/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaFragment.kt
deleted file mode 100644
index d8a88a8..0000000
--- a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaFragment.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package cc.ggez.ezhz.ui.frida
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
-import cc.ggez.ezhz.databinding.FragmentFridaBinding
-
-class FridaFragment : Fragment() {
-
- private var _binding: FragmentFridaBinding? = null
-
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- val fridaViewModel =
- ViewModelProvider(this)[FridaViewModel::class.java]
-
- _binding = FragmentFridaBinding.inflate(inflater, container, false)
- val root: View = binding.root
-
- val textView: TextView = binding.textDashboard
- fridaViewModel.text.observe(viewLifecycleOwner) {
- textView.text = it
- }
- return root
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaViewModel.kt b/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaViewModel.kt
deleted file mode 100644
index da761c3..0000000
--- a/app/src/main/java/cc/ggez/ezhz/ui/frida/FridaViewModel.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package cc.ggez.ezhz.ui.frida
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-
-class FridaViewModel : ViewModel() {
-
- private val _text = MutableLiveData().apply {
- value = "This is frida Fragment"
- }
- val text: LiveData = _text
-}
\ No newline at end of file
diff --git a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyFragment.kt b/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyFragment.kt
deleted file mode 100644
index 50fd2f4..0000000
--- a/app/src/main/java/cc/ggez/ezhz/ui/proxy/ProxyFragment.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package cc.ggez.ezhz.ui.proxy
-
-import android.os.Bundle
-import androidx.preference.PreferenceFragmentCompat
-import cc.ggez.ezhz.R
-
-class ProxyFragment : PreferenceFragmentCompat() {
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.setting_proxy, rootKey)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_correct.xml b/app/src/main/res/drawable/ic_correct.xml
new file mode 100644
index 0000000..a2cbc93
--- /dev/null
+++ b/app/src/main/res/drawable/ic_correct.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_proxy.xml b/app/src/main/res/drawable/ic_proxy.xml
index d649c1c..44b90bc 100644
--- a/app/src/main/res/drawable/ic_proxy.xml
+++ b/app/src/main/res/drawable/ic_proxy.xml
@@ -1,60 +1,27 @@
+ android:width="682dp"
+ android:height="682dp"
+ android:viewportWidth="682.67"
+ android:viewportHeight="682">
+ android:fillColor="#FF000000"
+ android:pathData="m442.14,225.06h120.55c-7.84,-93.6 -73.88,-170.9 -161.71,-195.51 25.25,45.65 39.17,119.75 41.16,195.51zM442.14,225.06"/>
+ android:fillColor="#FF000000"
+ android:pathData="m404.63,225.06c-1.48,-55.3 -9.5,-106.49 -22.94,-145.75 -13.73,-40.05 -30.33,-57.98 -40.69,-57.98s-26.96,17.93 -40.69,57.98c-13.44,39.26 -21.46,90.45 -22.94,145.75zM404.63,225.06"/>
+ android:fillColor="#FF000000"
+ android:pathData="m281.02,29.55c-87.84,24.61 -153.87,101.91 -161.71,195.51h120.55c1.99,-75.76 15.91,-149.86 41.16,-195.51zM281.02,29.55"/>
+ android:fillColor="#FF000000"
+ android:pathData="m562.69,262.56h-120.55c-1.99,75.76 -15.91,149.86 -41.16,195.51 87.84,-24.61 153.87,-101.91 161.71,-195.51zM562.69,262.56"/>
+ android:fillColor="#FF000000"
+ android:pathData="m381.69,408.3c13.44,-39.26 21.46,-90.45 22.94,-145.75h-127.25c1.48,55.3 9.5,106.48 22.94,145.75 13.73,40.05 30.33,57.98 40.69,57.98s26.96,-17.93 40.69,-57.98zM381.69,408.3"/>
+ android:fillColor="#FF000000"
+ android:pathData="m399.96,580.66c-6.08,-19.06 -21.15,-34.14 -40.21,-40.21v-37.34c-6.2,0.45 -12.45,0.68 -18.75,0.68s-12.55,-0.23 -18.75,-0.68v37.34c-19.06,6.07 -34.14,21.15 -40.21,40.21h-261.04v37.5h261.04c7.95,24.98 31.39,43.13 58.96,43.13s51.01,-18.15 58.96,-43.13h261.04v-37.5zM399.96,580.66"/>
-
-
-
-
-
-
-
-
-
-
-
+ android:fillColor="#FF000000"
+ android:pathData="m119.31,262.56c7.84,93.6 73.88,170.9 161.71,195.51 -25.25,-45.65 -39.17,-119.75 -41.16,-195.51zM119.31,262.56"/>
diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..5825a84
--- /dev/null
+++ b/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_select_activity.xml b/app/src/main/res/layout/app_select_activity.xml
new file mode 100644
index 0000000..c2e80cf
--- /dev/null
+++ b/app/src/main/res/layout/app_select_activity.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_download.xml b/app/src/main/res/layout/dialog_download.xml
new file mode 100644
index 0000000..19e6f43
--- /dev/null
+++ b/app/src/main/res/layout/dialog_download.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_frida.xml b/app/src/main/res/layout/fragment_frida.xml
index a4edeac..4312611 100644
--- a/app/src/main/res/layout/fragment_frida.xml
+++ b/app/src/main/res/layout/fragment_frida.xml
@@ -4,18 +4,16 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.frida.FridaFragment">
+ tools:context=".module.frida.FridaFragment">
-
+ android:layout_height="match_parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:itemCount="5"
+ tools:listitem="@layout/item_frida"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_constraintStart_toStartOf="parent"/>
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_shared_pref.xml b/app/src/main/res/layout/fragment_shared_pref.xml
index 393a7a6..71fe5b9 100644
--- a/app/src/main/res/layout/fragment_shared_pref.xml
+++ b/app/src/main/res/layout/fragment_shared_pref.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.sharedpref.SharedPrefFragment">
+ tools:context=".module.sharedpref.SharedPrefFragment">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_frida.xml b/app/src/main/res/layout/item_frida.xml
new file mode 100644
index 0000000..acf8671
--- /dev/null
+++ b/app/src/main/res/layout/item_frida.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
deleted file mode 100644
index de6591a..0000000
--- a/app/src/main/res/layout/settings_activity.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/manu_app_select.xml b/app/src/main/res/menu/manu_app_select.xml
new file mode 100644
index 0000000..34a61f6
--- /dev/null
+++ b/app/src/main/res/menu/manu_app_select.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/manu_frida_select.xml b/app/src/main/res/menu/manu_frida_select.xml
new file mode 100644
index 0000000..f13c551
--- /dev/null
+++ b/app/src/main/res/menu/manu_frida_select.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
index 7198239..c962d98 100644
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -7,18 +7,18 @@
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 0a25a0d..2697114 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,10 +1,11 @@
- #7FAEF7
- #427BD2
- #000732
- #d24287
- #932e5f
+ #FF7FAEF7
+ #FF427BD2
+ #FF000732
+ #FFd24287
+ #FF932e5f
+ #FF666666
#FF000000
#FFFFFFFF
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1a6282e..542a739 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3,7 +3,7 @@
Proxy
Frida
Shared Preferences
- AppSelectActivity
+ App Select Activity
Messages
@@ -19,4 +19,22 @@
Automatically download attachments for incoming emails
Only download attachments when manually requested
+ App Name
+ package.name
+ App Icon
+ User
+ System
+ Save
+ Reload
+ Cannot connect to proxy by using PAC URL
+ Cannot resolve proxy hostname
+ The service is running
+ Execute
+ Kill
+ Install
+ Uninstall
+ Frida Server v%s
+ Download Frida Server
+ This may take a moment.
+ Stop Service
\ No newline at end of file
diff --git a/app/src/main/res/xml/setting_proxy.xml b/app/src/main/res/xml/setting_proxy.xml
index a6f6e29..a7b2138 100644
--- a/app/src/main/res/xml/setting_proxy.xml
+++ b/app/src/main/res/xml/setting_proxy.xml
@@ -1,15 +1,13 @@
-
-
-
+ app:enabled="false"
+ app:title="Auto Proxy with PAC" />
@@ -70,12 +80,16 @@
app:key="auth_username"
app:summary="Proxy's username"
app:iconSpaceReserved="false"
+ app:useSimpleSummaryProvider="true"
+ app:dependency="auth_enable"
app:title="Username" />
@@ -89,19 +103,25 @@
app:key="target_global"
app:iconSpaceReserved="false"
app:summary="Enable the global proxy"
+ app:disableDependentsState="true"
+ app:defaultValue="true"
app:title="Global Proxy" />
+
+ app:dependency="target_global"
+ app:title="Apps Proxy">
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d995ed2..99013f5 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,6 +3,7 @@ pluginManagement {
google()
mavenCentral()
gradlePluginPortal()
+
}
}
dependencyResolutionManagement {
@@ -10,6 +11,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven("https://jitpack.io")
}
}