├── .github
├── taamarinbot.py
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── release.keystore
└── src
│ ├── androidTest
│ └── java
│ │ └── xyz
│ │ └── chz
│ │ └── bfm
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── xyz
│ │ │ └── chz
│ │ │ └── bfm
│ │ │ ├── BFRApp.kt
│ │ │ ├── adapter
│ │ │ └── AppListAdapter.kt
│ │ │ ├── data
│ │ │ ├── AppInfo.kt
│ │ │ └── AppManager.kt
│ │ │ ├── dialog
│ │ │ ├── IMakeDialog.kt
│ │ │ ├── ISettingDialog.kt
│ │ │ ├── MakeDialog.kt
│ │ │ ├── MaterialDialogFragment.kt
│ │ │ └── SettingDialog.kt
│ │ │ ├── enm
│ │ │ └── StatusConnection.kt
│ │ │ ├── service
│ │ │ └── TileBox.kt
│ │ │ ├── ui
│ │ │ ├── MainActivity.kt
│ │ │ ├── base
│ │ │ │ └── BaseActivity.kt
│ │ │ ├── converter
│ │ │ │ ├── ConfigManager.kt
│ │ │ │ ├── ConverterActivity.kt
│ │ │ │ └── config
│ │ │ │ │ ├── ClashData.kt
│ │ │ │ │ ├── ConfigType.kt
│ │ │ │ │ ├── ConfigUtil.kt
│ │ │ │ │ └── SingBoxData.kt
│ │ │ ├── core
│ │ │ │ ├── CoreActivity.kt
│ │ │ │ └── util
│ │ │ │ │ ├── CoreUtil.kt
│ │ │ │ │ ├── DownloaderCore.kt
│ │ │ │ │ └── IDownloadCore.kt
│ │ │ ├── fragment
│ │ │ │ ├── AppListFragment.kt
│ │ │ │ ├── ConfigHelperFragment.kt
│ │ │ │ ├── DashboardFragment.kt
│ │ │ │ └── MainFragment.kt
│ │ │ └── model
│ │ │ │ └── MainViewModel.kt
│ │ │ └── util
│ │ │ ├── Constant.kt
│ │ │ ├── OkHttpHelper.kt
│ │ │ ├── Util.kt
│ │ │ ├── ViewUtils.kt
│ │ │ ├── command
│ │ │ ├── CoreCmd.kt
│ │ │ ├── SettingCmd.kt
│ │ │ └── TermCmd.kt
│ │ │ ├── modul
│ │ │ └── ModuleManager.kt
│ │ │ └── terminal
│ │ │ └── TerminalHelper.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_app.xml
│ │ ├── ic_commit.xml
│ │ ├── ic_config.xml
│ │ ├── ic_converter.xml
│ │ ├── ic_dashboard.xml
│ │ ├── ic_disabled.xml
│ │ ├── ic_done.xml
│ │ ├── ic_download.xml
│ │ ├── ic_enabled.xml
│ │ ├── ic_error.xml
│ │ ├── ic_home.xml
│ │ ├── ic_info.xml
│ │ ├── ic_launcher.png
│ │ ├── ic_loading.xml
│ │ ├── ic_log.xml
│ │ ├── ic_modul.xml
│ │ ├── ic_save.xml
│ │ ├── ic_search.xml
│ │ ├── ic_select_all.xml
│ │ └── ic_setting.xml
│ │ ├── layout
│ │ ├── activity_converter.xml
│ │ ├── activity_core.xml
│ │ ├── activity_main.xml
│ │ ├── custom_dialog.xml
│ │ ├── fragment_app_list.xml
│ │ ├── fragment_config_helper.xml
│ │ ├── fragment_dashboard.xml
│ │ ├── fragment_main.xml
│ │ ├── item_applist.xml
│ │ └── setting_dialog.xml
│ │ ├── menu
│ │ └── bottom_nav_menu.xml
│ │ ├── navigation
│ │ └── nav_main.xml
│ │ ├── raw
│ │ ├── clashtemplate
│ │ └── singboxtemplate
│ │ ├── values-night
│ │ ├── colors.xml
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── network_security_config.xml
│ └── test
│ └── java
│ └── xyz
│ └── chz
│ └── bfm
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── ke
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── de
│ │ └── markusressel
│ │ └── kodeeditor
│ │ └── library
│ │ ├── extensions
│ │ └── Extensions.kt
│ │ └── view
│ │ ├── CodeEditText.kt
│ │ ├── CodeEditorLayout.kt
│ │ ├── CodeEditorView.kt
│ │ ├── CodeTextView.kt
│ │ └── SelectionChangedListener.kt
│ └── res
│ ├── drawable
│ ├── ic_launcher_background.xml
│ └── ic_launcher_foreground.xml
│ ├── layout
│ ├── layout_code_editor__main_layout.xml
│ ├── view_code_editor__divider.xml
│ ├── view_code_editor__inner_layout.xml
│ ├── view_code_editor__linenumbers.xml
│ └── view_code_editor__minimap.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values-night
│ └── themes.xml
│ └── values
│ ├── attributes.xml
│ ├── colors.xml
│ ├── dimens.xml
│ ├── library_kodeeditor_strings.xml
│ ├── strings.xml
│ └── themes.xml
└── settings.gradle
/.github/taamarinbot.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | import sys
4 | from telethon import TelegramClient
5 | from telethon.tl.functions.messages import GetMessagesRequest
6 |
7 | API_ID = os.environ.get("API_ID")
8 | API_HASH = os.environ.get("API_HASH")
9 |
10 | CHAT_ID = int(os.environ.get("CHAT_ID"))
11 | MESSAGE_THREAD_ID = int(os.environ.get("MESSAGE_THREAD_ID"))
12 | BOT_TOKEN = os.environ.get("BOT_TOKEN")
13 | VERSION = os.environ.get("VERSION")
14 | COMMIT = os.environ.get("COMMIT")
15 | # TAGS = os.environ.get("TAGS")
16 | MSG_TEMPLATE = """
17 | {version}
18 |
19 | Commit:
20 | {commit}
21 |
22 | [source](https://github.com/taamarin/box.manager)
23 | [module](https://github.com/taamarin/box_for_magisk)
24 |
25 | #apk #manager #BFR #root
26 | """.strip()
27 |
28 | def get_caption():
29 | msg = MSG_TEMPLATE.format(
30 | version=VERSION,
31 | # tags=TAGS,
32 | commit=COMMIT
33 | )
34 | if len(msg) > 1024:
35 | return COMMIT
36 | return msg
37 |
38 | def check_environ():
39 | if BOT_TOKEN is None:
40 | print("[-] Invalid BOT_TOKEN")
41 | exit(1)
42 | if CHAT_ID is None:
43 | print("[-] Invalid CHAT_ID")
44 | exit(1)
45 | if MESSAGE_THREAD_ID is None:
46 | print("[-] Invalid MESSAGE_THREAD_ID")
47 | exit(1)
48 | if VERSION is None:
49 | print("[-] Invalid VERSION")
50 | exit(1)
51 | if COMMIT is None:
52 | print("[-] Invalid COMMIT")
53 | exit(1)
54 | # if TAGS is None:
55 | # print("[-] Invalid TAGS")
56 | # exit(1)
57 |
58 | async def main():
59 | print("[+] Uploading to telegram")
60 | check_environ()
61 | files = sys.argv[1:]
62 | print("[+] Files:", files)
63 | if len(files) <= 0:
64 | print("[-] No files to upload")
65 | exit(1)
66 | print("[+] Logging in Telegram with bot")
67 | script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
68 | session_dir = os.path.join(script_dir, "bot.session")
69 | async with await TelegramClient(session=session_dir, api_id=API_ID, api_hash=API_HASH).start(bot_token=BOT_TOKEN) as bot:
70 | caption = [""] * len(files)
71 | caption[-1] = get_caption()
72 | print("[+] Caption: ")
73 | print("---")
74 | print(caption)
75 | print("---")
76 | print("[+] Sending")
77 | sent_messages = await bot.send_file(entity=CHAT_ID, file=files, caption=caption, reply_to=MESSAGE_THREAD_ID, parse_mode="markdown")
78 |
79 | # Pin the last sent message
80 | if sent_messages:
81 | last_message_id = sent_messages[-1].id
82 | await bot.pin_message(entity=CHAT_ID, message=last_message_id)
83 |
84 | print("[+] Done!")
85 |
86 | if __name__ == "__main__":
87 | try:
88 | asyncio.run(main())
89 | except Exception as e:
90 | print(f"[-] An error occurred: {e}")
91 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build android apps
2 | on:
3 | workflow_dispatch:
4 | push:
5 | paths-ignore:
6 | - "docs/**"
7 | - "README.md"
8 | - ".github/ISSUE_TEMPLATE/**"
9 | branches:
10 | - main
11 | jobs:
12 | build:
13 | permissions: write-all
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - uses: actions/setup-node@v3
21 | with:
22 | node-version: '16.x'
23 |
24 | - uses: actions/setup-java@v3
25 | with:
26 | java-version: '17'
27 | distribution: 'zulu'
28 |
29 | - name: update version
30 | run: |
31 | sed -i "s|versionCode .*|versionCode $(date +%Y%m%d)|g" app/build.gradle
32 | sed -i "s|versionName .*|versionName \"1.14-$(git rev-parse --short HEAD)\"|g" app/build.gradle
33 |
34 | - name: Build with Gradle
35 | run: |
36 | chmod +x gradlew
37 | ./gradlew --no-daemon --no-configuration-cache -q app:assembleRelease
38 |
39 | - name: checking release version
40 | id: version
41 | run: |
42 | echo ::set-output name=release_version::$(cat app/build.gradle | grep -o "versionName \"[0-9.]*-[a-z,0-9]*\"" | grep -o "[0-9.]*-[a-z,0-9]*")
43 |
44 | - name: rename apks
45 | run: |
46 | APK="Box4Root-Manager_v${{ steps.version.outputs.release_version }}.apk"
47 | mv -f ./app/build/outputs/apk/release/*.apk ./app/build/outputs/apk/release/$APK
48 |
49 | - name: upload artifact
50 | uses: actions/upload-artifact@v3
51 | if: ${{ success() }}
52 | with:
53 | name: b4r_manager
54 | path: ./app/build/outputs/apk/release/*.apk
55 |
56 | - uses: andreaswilli/delete-release-assets-action@v2.0.0
57 | with:
58 | github_token: ${{ secrets.GITHUB_TOKEN }}
59 | tag: Prerelease
60 | deleteOnlyFromDrafts: false
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 |
64 | - uses: richardsimko/update-tag@v1.0.7
65 | with:
66 | tag_name: Prerelease
67 | env:
68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69 |
70 | - uses: softprops/action-gh-release@v1
71 | with:
72 | tag_name: Prerelease
73 | files: ${{ github.workspace }}/app/build/outputs/apk/release/*.apk
74 | draft: false
75 | prerelease: true
76 | generate_release_notes: true
77 |
78 | - name: upload to telegram
79 | if: ${{ success() }}
80 | env:
81 | CHAT_ID: "-1001597117128"
82 | MESSAGE_THREAD_ID: "282263"
83 | API_ID: ${{ secrets.API_ID }}
84 | API_HASH: ${{ secrets.API_HASH }}
85 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
86 | run: |
87 | if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
88 | export VERSION="v${{ steps.version.outputs.release_version }}"
89 | export COMMIT=$(git log --oneline -n 10 --no-decorate | sed 's/^[0-9a-f]* //' | sed 's/^/— /')
90 | FILE=$(find app/build/outputs/apk/release -name "*.apk")
91 | pip3 install telethon==1.31.1
92 | python3 $GITHUB_WORKSPACE/.github/taamarinbot.py $FILE
93 | fi
94 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | .idea
17 | .gitmessage
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BFR Manager
2 |
3 |
4 |
BOX
5 |
6 | BFR Manager
7 |
8 |
9 |
10 | []()
11 | [](https://github.com/taamarin/box/actions/workflows/debug.yml)
12 | [](https://t.me/nothing_taamarin)
13 | [](https://t.me/taamarin)
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /libs
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-parcelize'
5 | id 'kotlin-kapt'
6 | id 'dagger.hilt.android.plugin'
7 | id 'androidx.navigation.safeargs'
8 | }
9 |
10 | android {
11 | namespace 'xyz.chz.bfm'
12 | compileSdk 33
13 |
14 | defaultConfig {
15 | applicationId "xyz.chz.bfm"
16 | minSdk 24
17 | targetSdk 33
18 | versionCode 20241205
19 | versionName "1.14"
20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
21 | vectorDrawables.useSupportLibrary = true
22 | }
23 |
24 | signingConfigs {
25 | release {
26 | storeFile file('release.keystore')
27 | storePassword 'qwerty'
28 | keyAlias 'key.alias'
29 | keyPassword 'qwerty'
30 | }
31 | }
32 |
33 | buildTypes {
34 | release {
35 | signingConfig signingConfigs.release
36 | minifyEnabled true
37 | shrinkResources true
38 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
39 | }
40 | }
41 |
42 | compileOptions {
43 | sourceCompatibility JavaVersion.VERSION_1_8
44 | targetCompatibility JavaVersion.VERSION_1_8
45 | }
46 | kotlinOptions {
47 | jvmTarget = '1.8'
48 | }
49 |
50 | buildFeatures {
51 | viewBinding true
52 | }
53 | }
54 |
55 | dependencies {
56 | implementation project(':ke')
57 | implementation 'androidx.core:core-ktx:1.9.0'
58 | implementation 'androidx.appcompat:appcompat:1.6.1'
59 | implementation 'com.google.android.material:material:1.9.0'
60 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
61 | testImplementation 'junit:junit:4.13.2'
62 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
63 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
64 | implementation 'com.google.android.material:material:1.9.0'
65 | implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
66 | implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
67 | implementation 'com.google.dagger:hilt-android:2.43.2'
68 | kapt 'com.google.dagger:hilt-compiler:2.43.2'
69 | implementation 'io.reactivex:rxjava:1.3.8'
70 | implementation 'io.reactivex:rxandroid:1.2.1'
71 | implementation 'androidx.preference:preference-ktx:1.2.1'
72 | implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.1'
73 | implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.1'
74 | }
75 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class xyz.chz.bfm.data.** { *; }
2 | -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
3 | long producerIndex;
4 | long consumerIndex;
5 | }
--------------------------------------------------------------------------------
/app/release.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/app/release.keystore
--------------------------------------------------------------------------------
/app/src/androidTest/java/xyz/chz/bfm/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.*
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("xyz.chz.bfm", appContext.packageName)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
23 |
26 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/BFRApp.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm
2 |
3 | import android.app.Application
4 | import android.os.StrictMode
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | class BFRApp : Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 | val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
12 | StrictMode.setThreadPolicy(policy)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/adapter/AppListAdapter.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.adapter
2 |
3 | import android.app.Activity
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.recyclerview.widget.RecyclerView
8 | import xyz.chz.bfm.R
9 | import xyz.chz.bfm.data.AppInfo
10 | import xyz.chz.bfm.databinding.ItemApplistBinding
11 |
12 | class AppListAdapter(
13 | val activity: Activity,
14 | val apps: List,
15 | blacklist: MutableSet?
16 | ) :
17 | RecyclerView.Adapter() {
18 |
19 | companion object {
20 | private const val VIEW_TYPE_HEADER = 0
21 | private const val VIEW_TYPE_ITEM = 1
22 | }
23 |
24 | val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist)
25 |
26 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
27 | if (holder is AppViewHolder) {
28 | val appInfo = apps[position - 1]
29 | holder.bind(appInfo)
30 | }
31 | }
32 |
33 | override fun getItemCount() = apps.size + 1
34 |
35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
36 | val ctx = parent.context
37 | return when (viewType) {
38 | VIEW_TYPE_HEADER -> {
39 | val view = View(ctx)
40 | view.layoutParams = ViewGroup.LayoutParams(
41 | ViewGroup.LayoutParams.MATCH_PARENT,
42 | ctx.resources.getDimensionPixelSize(R.dimen.list_header_height) * 0
43 | )
44 | BaseViewHolder(view)
45 | }
46 |
47 | else -> AppViewHolder(
48 | ItemApplistBinding.inflate(
49 | LayoutInflater.from(ctx),
50 | parent,
51 | false
52 | )
53 | )
54 | }
55 | }
56 |
57 | override fun getItemViewType(position: Int) =
58 | if (position == 0) VIEW_TYPE_HEADER else VIEW_TYPE_ITEM
59 |
60 | open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
61 |
62 | inner class AppViewHolder(private val item: ItemApplistBinding) :
63 | BaseViewHolder(item.root),
64 | View.OnClickListener {
65 | private lateinit var appInfo: AppInfo
66 | private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
67 |
68 | fun bind(appInfo: AppInfo) {
69 | this.appInfo = appInfo
70 | with(item) {
71 | icon.setImageDrawable(appInfo.appIcon)
72 | checkBox.isChecked = inBlacklist
73 | item.packageName.text = appInfo.packageName
74 | if (appInfo.isSystemApp) {
75 | item.name.text = String.format("** %1s", appInfo.appName)
76 | } else {
77 | item.name.text = appInfo.appName
78 | }
79 | }
80 | itemView.setOnClickListener(this)
81 | }
82 |
83 | override fun onClick(v: View?) {
84 | if (inBlacklist) {
85 | blacklist.remove(appInfo.packageName)
86 | item.checkBox.isChecked = false
87 | } else {
88 | blacklist.add(appInfo.packageName)
89 | item.checkBox.isChecked = true
90 | }
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/data/AppInfo.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.data
2 |
3 | import android.graphics.drawable.Drawable
4 |
5 | data class AppInfo(
6 | val appName: String,
7 | val packageName: String,
8 | val appIcon: Drawable,
9 | val isSystemApp: Boolean,
10 | var isSelected: Int
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/data/AppManager.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.data
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.pm.ApplicationInfo
6 | import android.content.pm.PackageInfo
7 | import android.content.pm.PackageManager
8 | import rx.Observable
9 |
10 | object AppManager {
11 | fun loadNetworkAppList(ctx: Context): ArrayList {
12 | val packageManager = ctx.packageManager
13 | val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
14 | val apps = ArrayList()
15 |
16 | for (pkg in packages) {
17 | if (!pkg.hasInternetPermission && pkg.packageName != "android") continue
18 |
19 | val applicationInfo = pkg.applicationInfo
20 |
21 | val appName = applicationInfo.loadLabel(packageManager).toString()
22 | val appIcon = applicationInfo.loadIcon(packageManager)
23 | val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
24 |
25 | val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
26 | apps.add(appInfo)
27 | }
28 | return apps
29 | }
30 |
31 | fun rxLoadNetworkAppList(ctx: Context): Observable> =
32 | Observable.unsafeCreate {
33 | it.onNext(loadNetworkAppList(ctx))
34 | }
35 |
36 |
37 | val PackageInfo.hasInternetPermission: Boolean
38 | get() {
39 | val permissions = requestedPermissions
40 | return permissions?.any { it == Manifest.permission.INTERNET } ?: false
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/dialog/IMakeDialog.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.dialog
2 |
3 | import androidx.fragment.app.DialogFragment
4 |
5 | interface IMakeDialog {
6 | fun onDialogPositiveButton(dialog: DialogFragment)
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/dialog/ISettingDialog.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.dialog
2 |
3 | import androidx.fragment.app.DialogFragment
4 |
5 | interface ISettingDialog {
6 | fun onLoading(dialog: DialogFragment)
7 | fun onAbout(dialog: DialogFragment)
8 | fun onUpdate(dialog: DialogFragment)
9 | fun onCheckIP(dialog: DialogFragment)
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/dialog/MakeDialog.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.dialog
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.Fragment
9 | import xyz.chz.bfm.databinding.CustomDialogBinding
10 |
11 | class MakeDialog(
12 | private val strTitle: String? = "",
13 | private val strMsg: String? = "",
14 | private val isClick: Boolean? = false,
15 | private val canCancel: Boolean? = true
16 | ) : MaterialDialogFragment() {
17 | private lateinit var binding: CustomDialogBinding
18 |
19 | lateinit var listener: IMakeDialog
20 |
21 | override fun onCreateView(
22 | inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
23 | ): View {
24 | binding = CustomDialogBinding.inflate(layoutInflater, container, false)
25 | return binding.root
26 | }
27 |
28 | @Deprecated("Deprecated in Java")
29 | override fun onAttachFragment(childFragment: Fragment) {
30 | super.onAttachFragment(childFragment)
31 | try {
32 | listener = context as IMakeDialog
33 | } catch (e: ClassCastException) {
34 | throw ClassCastException("not cast")
35 | }
36 | }
37 |
38 | override fun onAttach(context: Context) {
39 | super.onAttach(context)
40 | try {
41 | listener = context as IMakeDialog
42 | } catch (e: ClassCastException) {
43 | throw ClassCastException("not cast")
44 | }
45 | }
46 |
47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
48 | super.onViewCreated(view, savedInstanceState)
49 | binding.apply {
50 | isCancelable = canCancel!!
51 | tvTitle.text = strTitle
52 | tvMessage.text = strMsg
53 | btnOk.setOnClickListener {
54 | if (isClick!!) listener.onDialogPositiveButton(this@MakeDialog)
55 | this@MakeDialog.dismiss()
56 | }
57 | }
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/dialog/MaterialDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.dialog
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Dialog
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import androidx.fragment.app.DialogFragment
9 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
10 | import xyz.chz.bfm.R
11 |
12 | open class MaterialDialogFragment : DialogFragment() {
13 |
14 | private var dialogView: View? = null
15 |
16 | @SuppressLint("UseGetLayoutInflater")
17 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
18 | return MaterialAlertDialogBuilder(
19 | requireContext(),
20 | R.style.ThemeOverlay_MaterialComponents_MaterialAlertDialog_background
21 | ).apply {
22 | dialogView =
23 | onCreateView(LayoutInflater.from(requireContext()), null, savedInstanceState)
24 |
25 | dialogView?.let { onViewCreated(it, savedInstanceState) }
26 | setView(dialogView)
27 | }.create()
28 | }
29 |
30 | override fun getView(): View? {
31 | return dialogView
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/enm/StatusConnection.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.enm
2 |
3 | enum class StatusConnection(val str: String) {
4 | Enabled("Enabled"),
5 | Error("Error"),
6 | Disabled("Disabled"),
7 | Loading("Loading"),
8 | Unknown("Unknown")
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/service/TileBox.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.service
2 |
3 | import android.service.quicksettings.Tile
4 | import android.service.quicksettings.TileService
5 | import xyz.chz.bfm.util.Util
6 | import xyz.chz.bfm.util.command.TermCmd
7 |
8 | class TileBox : TileService() {
9 | override fun onClick() {
10 | super.onClick()
11 | when (qsTile.state) {
12 |
13 | Tile.STATE_INACTIVE -> {
14 | qsTile.state = Tile.STATE_ACTIVE
15 | qsTile.updateTile()
16 | TermCmd.start {
17 | Util.isProxyed = if (it) {
18 | qsTile.state = Tile.STATE_ACTIVE
19 | qsTile.updateTile()
20 | true
21 | } else {
22 | qsTile.state = Tile.STATE_INACTIVE
23 | qsTile.updateTile()
24 | false
25 | }
26 | }
27 | }
28 |
29 | else -> {
30 | qsTile.state = Tile.STATE_INACTIVE
31 | qsTile.updateTile()
32 | TermCmd.stop {
33 | Util.isProxyed = if (it) {
34 | qsTile.state = Tile.STATE_INACTIVE
35 | qsTile.updateTile()
36 | false
37 | } else {
38 | qsTile.state = Tile.STATE_ACTIVE
39 | qsTile.updateTile()
40 | true
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | override fun onStartListening() {
48 | super.onStartListening()
49 | if (Util.isProxyed) qsTile.state = Tile.STATE_ACTIVE else qsTile.state = Tile.STATE_INACTIVE
50 | qsTile.updateTile()
51 | }
52 |
53 | override fun onStopListening() {
54 | super.onStopListening()
55 | qsTile.updateTile()
56 | }
57 |
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.view.WindowManager
6 | import androidx.navigation.findNavController
7 | import androidx.navigation.ui.setupWithNavController
8 | import com.google.android.material.bottomnavigation.BottomNavigationView
9 | import dagger.hilt.android.AndroidEntryPoint
10 | import xyz.chz.bfm.R
11 | import xyz.chz.bfm.databinding.ActivityMainBinding
12 | import xyz.chz.bfm.ui.base.BaseActivity
13 |
14 | @AndroidEntryPoint
15 | class MainActivity : BaseActivity() {
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | val binding = ActivityMainBinding.inflate(layoutInflater)
20 | setContentView(binding.root)
21 |
22 | val navView: BottomNavigationView = binding.navBottom
23 | val navController = findNavController(R.id.nav_host_fragment_container)
24 | navView.setupWithNavController(navController)
25 |
26 | navController.addOnDestinationChangedListener { _, dest, _ ->
27 | if (dest.id == R.id.nav_dashboard) this.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
28 | if (dest.id == R.id.configHelperFragment) navView.visibility =
29 | View.GONE else navView.visibility = View.VISIBLE
30 | }
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.base
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.fragment.app.DialogFragment
8 | import dagger.hilt.android.AndroidEntryPoint
9 | import xyz.chz.bfm.R
10 | import xyz.chz.bfm.dialog.IMakeDialog
11 | import xyz.chz.bfm.dialog.MakeDialog
12 | import xyz.chz.bfm.enm.StatusConnection
13 | import xyz.chz.bfm.util.modul.ModuleManager
14 |
15 | @AndroidEntryPoint
16 | open class BaseActivity : AppCompatActivity(), IMakeDialog {
17 | private var state: Int? = 0
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | if (ModuleManager.moduleVersionCode == "") {
22 | val df = MakeDialog(
23 | StatusConnection.Error.str,
24 | getString(R.string.module_not_found),
25 | true,
26 | false
27 | )
28 | df.listener = this
29 | df.show(supportFragmentManager, "")
30 | } else {
31 | if (ModuleManager.moduleVersionCode.toInt() < getString(R.string.min_module_vrsion).toInt()) {
32 | val df = MakeDialog(
33 | StatusConnection.Loading.str,
34 | String.format(
35 | getString(R.string.update_module),
36 | ModuleManager.moduleVersionCode.toInt()
37 | ),
38 | true,
39 | false
40 | )
41 | df.listener = this
42 | df.show(supportFragmentManager, "")
43 | state = 1
44 | }
45 | }
46 | }
47 |
48 | override fun onDialogPositiveButton(dialog: DialogFragment) {
49 | when (state) {
50 | 1 -> {
51 | val intent =
52 | Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.release_repo_url)))
53 | startActivity(intent)
54 | finish()
55 | }
56 |
57 | else -> {
58 | val intent =
59 | Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.module_repo_url)))
60 | startActivity(intent)
61 | finish()
62 | }
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/converter/ConfigManager.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.converter
2 |
3 | import org.json.JSONObject
4 | import xyz.chz.bfm.ui.converter.config.ClashData
5 | import xyz.chz.bfm.ui.converter.config.ConfigType
6 | import xyz.chz.bfm.ui.converter.config.ConfigUtil
7 | import xyz.chz.bfm.ui.converter.config.SingBoxData
8 | import java.util.regex.Pattern
9 |
10 | object ConfigManager {
11 |
12 | fun importConfig(config: String, isClash: Boolean, useIndent: Boolean = false): String {
13 | try {
14 | if (config.startsWith(ConfigType.VMESS.scheme)) {
15 | var result = config.replace(ConfigType.VMESS.scheme, "")
16 | result = ConfigUtil.decode(result)
17 | if (result.isEmpty()) {
18 | return "failed decode vms"
19 | }
20 | return if (isClash) ClashData(result, useIndent).newVmessConfig() else SingBoxData(
21 | result
22 | ).vmessSing()
23 | } else if (config.startsWith(ConfigType.VLESS.scheme)) {
24 | return if (isClash) ClashData(config, useIndent).newVlessConfig() else SingBoxData(
25 | config
26 | ).vlessSing()
27 | } else if (config.startsWith(ConfigType.TROJAN.scheme) || config.startsWith(ConfigType.TROJANGO.scheme)) {
28 | return if (isClash) ClashData(config, useIndent).newTrojanConfig() else SingBoxData(
29 | config
30 | ).trojanSing()
31 | } else {
32 | return ""
33 | }
34 | } catch (_: Exception) {
35 | }
36 | return ""
37 | }
38 |
39 | fun fullClashSimple(config: String, strRaw: String): String {
40 | val name = "match"
41 | val p = Pattern.compile("- name:(.*)")
42 | val m = p.matcher(config)
43 | val sb = StringBuilder()
44 | while (m.find()) {
45 | sb.appendLine(" - ${m.group(1)?.trim()}")
46 | }
47 | return String.format(
48 | strRaw,
49 | config,
50 | ClashData().proxyGroupBuilder(name, sb.toString()),
51 | name
52 | )
53 | }
54 |
55 | fun fullSingSimple(config: String, strRaw: String): String {
56 | val s = ArrayList()
57 | val p = Pattern.compile("\"tag\":(.*)")
58 | val m = p.matcher(config)
59 | val sb = StringBuilder()
60 | while (m.find()) {
61 | s.add(m.group(1)?.trim()!!.replace("\"|,", ""))
62 | }
63 | sb.appendLine(SingBoxData().buildHeaderSlector(s))
64 | sb.appendLine(SingBoxData().buildHeaderBestUrl(s))
65 | sb.append(config)
66 | sb.appendLine(SingBoxData().buildFooter())
67 | val strResult = String.format(strRaw, sb.toString())
68 | return JSONObject(strResult).toString(2).replace("\\", "").replace("null,", "")
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/converter/ConverterActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.converter
2 |
3 | import android.os.Bundle
4 | import androidx.annotation.RawRes
5 | import androidx.appcompat.app.AppCompatActivity
6 | import dagger.hilt.android.AndroidEntryPoint
7 | import xyz.chz.bfm.R
8 | import xyz.chz.bfm.databinding.ActivityConverterBinding
9 | import xyz.chz.bfm.util.copyToClipboard
10 | import xyz.chz.bfm.util.isValidCheck
11 | import xyz.chz.bfm.util.removeEmptyLines
12 | import xyz.chz.bfm.util.toast
13 |
14 | @AndroidEntryPoint
15 | class ConverterActivity : AppCompatActivity() {
16 | private lateinit var binding: ActivityConverterBinding
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | binding = ActivityConverterBinding.inflate(layoutInflater)
21 | setContentView(binding.root)
22 | setupConfig()
23 | }
24 |
25 | private fun setupConfig() = with(binding) {
26 | var isFull = false
27 | var isSing = false
28 | fullClashConfig.setOnCheckedChangeListener { _, b ->
29 | isFull = b
30 | isSing = false
31 | fullSingConfig.isChecked = false
32 | }
33 | fullSingConfig.setOnCheckedChangeListener { _, b ->
34 | if (b) {
35 | isSing = true
36 | isFull = false
37 | fullClashConfig.isChecked = false
38 | }
39 | }
40 | btnConvert.setOnClickListener {
41 | if (textInput.isValidCheck()) {
42 | val str = textInput.removeEmptyLines()
43 | val spltStr = str.split("\n")
44 | val result = StringBuilder()
45 | for (x in spltStr) {
46 | result.appendLine(
47 | if (isSing) ConfigManager.importConfig(
48 | x,
49 | false
50 | ) + "," else ConfigManager.importConfig(x, true)
51 | )
52 | }
53 | if (isFull)
54 | tvResult.text =
55 | ConfigManager.fullClashSimple(
56 | result.toString(),
57 | readRawFile(R.raw.clashtemplate)
58 | )
59 | else
60 | tvResult.text = if (isSing) ConfigManager.fullSingSimple(
61 | result.toString(),
62 | readRawFile(R.raw.singboxtemplate)
63 | ) else result
64 | tvResult.apply {
65 | setOnClickListener {
66 | toast("Copied to Clipboard!", this@ConverterActivity)
67 | copyToClipboard(this@ConverterActivity)
68 | }
69 | }
70 | toast("Click result for copy config", this@ConverterActivity)
71 | } else toast("???????????", this@ConverterActivity)
72 | }
73 | }
74 |
75 |
76 | private fun readRawFile(@RawRes id: Int): String {
77 | return resources.openRawResource(id).bufferedReader().readText()
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/converter/config/ConfigType.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.converter.config
2 |
3 | enum class ConfigType(val scheme: String) {
4 | VMESS("vmess://"),
5 | VLESS("vless://"),
6 | TROJAN("trojan://"),
7 | TROJANGO("trojan-go://")
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/converter/config/ConfigUtil.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.converter.config
2 |
3 | import android.net.Uri
4 | import android.util.Base64
5 |
6 | object ConfigUtil {
7 |
8 | fun safeDecodeURLB64(url: String): String {
9 | val split = url.split("://").toTypedArray()
10 | val schema = split[0]
11 | val result = split[1].substring(5)
12 | .replace(' ', '-')
13 | .replace('/', '_')
14 | .replace('+', '-')
15 | return "$schema://$result"
16 | }
17 |
18 | fun getQueryParams(uri: Uri, param: String): String? {
19 | for (p in uri.queryParameterNames) {
20 | if (param.equals(p, ignoreCase = true))
21 | return uri.getQueryParameter(p)
22 | }
23 | return null
24 | }
25 |
26 | fun decode(text: String): String {
27 | tryDecodeBase64(text)?.let { return it }
28 | if (text.endsWith('=')) {
29 | // try again for some loosely formatted base64
30 | tryDecodeBase64(text.trimEnd('='))?.let { return it }
31 | }
32 | return ""
33 | }
34 |
35 | private fun tryDecodeBase64(text: String): String? {
36 | try {
37 | return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
38 | } catch (e: Exception) {
39 | e.printStackTrace()
40 | }
41 | try {
42 | return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE))
43 | .toString(charset("UTF-8"))
44 | } catch (e: Exception) {
45 | e.printStackTrace()
46 | }
47 | return null
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/core/CoreActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.core
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.core.view.isVisible
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import okhttp3.Call
9 | import okhttp3.Callback
10 | import okhttp3.Response
11 | import org.json.JSONArray
12 | import org.json.JSONException
13 | import org.json.JSONObject
14 | import xyz.chz.bfm.databinding.ActivityCoreBinding
15 | import xyz.chz.bfm.ui.core.util.CoreUtil
16 | import xyz.chz.bfm.ui.core.util.DownloaderCore
17 | import xyz.chz.bfm.ui.core.util.IDownloadCore
18 | import xyz.chz.bfm.util.META_DOWNLOAD
19 | import xyz.chz.bfm.util.META_REPO
20 | import xyz.chz.bfm.util.OkHttpHelper
21 | import xyz.chz.bfm.util.SING_DOWNLOAD
22 | import xyz.chz.bfm.util.SING_REPO
23 | import xyz.chz.bfm.util.command.CoreCmd
24 | import xyz.chz.bfm.util.toast
25 | import xyz.chz.bfm.util.urlText
26 | import java.io.IOException
27 |
28 | @AndroidEntryPoint
29 | @SuppressLint("SetTextI18n")
30 | class CoreActivity : AppCompatActivity() {
31 | private lateinit var binding: ActivityCoreBinding
32 | private lateinit var dCore: DownloaderCore
33 |
34 | private var urlClash = ""
35 | private var urlSing = ""
36 | private var tagNameClash = ""
37 | private var tagNameSing = ""
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 | binding = ActivityCoreBinding.inflate(layoutInflater)
42 | setContentView(binding.root)
43 | dCore = DownloaderCore(this)
44 | checkClash()
45 | buttonUp()
46 | }
47 |
48 | private fun checkClash() = with(binding) {
49 | tvClash.text = "Checking for mihomo"
50 | prgClash.isVisible = true
51 | OkHttpHelper().reqGithub(META_REPO, object : Callback {
52 | override fun onFailure(call: Call, e: IOException) {
53 | runOnUiThread {
54 | tvClash.text = "Failed to get repo clash, check ur internet connection"
55 | prgClash.isVisible = false
56 | }
57 | }
58 |
59 | override fun onResponse(call: Call, response: Response) {
60 | val respom = response.body!!.string()
61 | runOnUiThread {
62 | urlClash = parseClash(respom)
63 | if (parseVersionClashMeta() != CoreCmd.checkVerClashMeta) {
64 | tvClash.text = "Update available mihomo"
65 | btnClash.isVisible = true
66 | } else {
67 | tvClash.text = "mihomo has Latest Version"
68 | imgDoneClash.isVisible = true
69 | btnClash.isVisible = false
70 | }
71 | prgClash.isVisible = false
72 | }
73 | }
74 | })
75 | }
76 |
77 | private fun parseClash(str: String): String {
78 | return try {
79 | val jo = JSONObject(str)
80 | tagNameClash = jo.getString("tag_name")
81 | if (CoreUtil.getAbis().contains("arm64")) {
82 | "$META_DOWNLOAD/$tagNameClash/mihomo-android-arm64-v8-$tagNameClash.gz"
83 | } else {
84 | "$META_DOWNLOAD/$tagNameClash/mihomo-android-armv7-$tagNameClash.gz"
85 | }
86 | } catch (e: JSONException) {
87 | e.message!!.toString()
88 | }
89 | }
90 |
91 | private fun parseVersionClashMeta(): String {
92 | return try {
93 | "$META_DOWNLOAD/$tagNameClash/version.txt".urlText()
94 | } catch (e: Exception) {
95 | e.message.toString()
96 | }
97 | }
98 |
99 | private fun buttonUp() = with(binding) {
100 | btnClash.apply {
101 | setOnClickListener {
102 | dCore.downloadFile(
103 | urlClash,
104 | "clash_meta.gz",
105 | "/data/adb/box/bin/xclash/mihomo",
106 | object : IDownloadCore {
107 | override fun onDownloadingStart() {
108 | prgHrzClash.isVisible = true
109 | btnClash.isVisible = false
110 | }
111 |
112 | override fun onDownloadingProgress(progress: Int) {
113 | prgHrzClash.progress = progress
114 | }
115 |
116 | override fun onDownloadingComplete() {
117 | btnClash.isVisible = false
118 | prgHrzClash.isVisible = false
119 | imgDoneClash.isVisible = true
120 | tvClash.text = "mihomo has Latest Version"
121 | }
122 |
123 | override fun onDownloadingFailed(e: Exception?) {
124 | toast("failed downloading clash", this@CoreActivity)
125 | btnClash.isVisible = true
126 | prgHrzClash.progress = 0
127 | prgHrzClash.isVisible = false
128 | }
129 | })
130 | }
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/core/util/CoreUtil.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.core.util
2 |
3 | import xyz.chz.bfm.util.terminal.TerminalHelper
4 | import java.io.FileInputStream
5 | import java.io.FileOutputStream
6 | import java.io.IOException
7 | import java.util.zip.GZIPInputStream
8 |
9 | object CoreUtil {
10 |
11 | fun gZipExtractor(gzipIn: String, gzipOut: String) {
12 | try {
13 | val fis = FileInputStream(gzipIn)
14 | val gis = GZIPInputStream(fis)
15 | val fos = FileOutputStream(gzipOut)
16 | val buff = ByteArray(1024)
17 | var len: Int
18 | while (gis.read(buff).also { len = it } != -1) {
19 | fos.write(buff, 0, len)
20 | }
21 | fos.close()
22 | gis.close()
23 | } catch (e: IOException) {
24 | e.printStackTrace()
25 | }
26 | }
27 |
28 | fun tarExtractUsingRoot(tarIn: String, tarOut: String) {
29 | TerminalHelper.execRootCmdSilent("tar xfz $tarIn -C $tarOut && mv -f $tarOut/sing-box-v* $tarOut/sing-box && chmod 755 $tarOut/sing-box && chown root:net_admin $tarOut/sing-box")
30 | }
31 |
32 | fun getAbis(): String {
33 | for (abis in android.os.Build.SUPPORTED_ABIS) {
34 | when (abis) {
35 | "arm64-v8a" -> return "arm64"
36 | "armeabi-v7a" -> return "armv7"
37 | }
38 | }
39 | return throw RuntimeException("unable get abis")
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/core/util/DownloaderCore.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.core.util
2 |
3 | import android.content.Context
4 | import android.os.Handler
5 | import android.os.Looper
6 | import xyz.chz.bfm.util.command.CoreCmd
7 | import java.io.BufferedInputStream
8 | import java.io.File
9 | import java.io.FileOutputStream
10 | import java.io.InputStream
11 | import java.net.URL
12 | import java.util.concurrent.ExecutorService
13 | import java.util.concurrent.Executors
14 |
15 | open class DownloaderCore(private val ctx: Context) {
16 | private var exFile: File? = null
17 |
18 | fun downloadFile(
19 | stringUrl: String,
20 | nameFile: String,
21 | path: String,
22 | callback: IDownloadCore
23 | ) {
24 | val executor: ExecutorService = Executors.newSingleThreadExecutor()
25 | val handler = Handler(Looper.getMainLooper())
26 | executor.execute {
27 | handler.post { callback.onDownloadingStart() }
28 | try {
29 | val url = URL(stringUrl)
30 | val conection = url.openConnection()
31 | conection.connect()
32 | val lenghtOfFile = conection.contentLength
33 | val input: InputStream = BufferedInputStream(url.openStream(), 8192)
34 | exFile = File(ctx.getExternalFilesDir(null), nameFile)
35 | if (exFile!!.exists()) exFile!!.delete()
36 | val output = FileOutputStream(exFile)
37 | val data = ByteArray(1024)
38 | var total: Long = 0
39 | while (true) {
40 | val read = input.read(data)
41 | if (read == -1) {
42 | break
43 | }
44 | output.write(data, 0, read)
45 | val j2 = total + read.toLong()
46 | if (lenghtOfFile > 0) {
47 | val progress = ((100 * j2 / lenghtOfFile.toLong()).toInt())
48 | callback.onDownloadingProgress(progress)
49 | }
50 | total = j2
51 | }
52 | output.flush()
53 | output.close()
54 | input.close()
55 | handler.post { callback.onDownloadingComplete() }
56 | if (nameFile.contains("clash")) {
57 | val x = exFile.toString().replace(".gz", "")
58 | CoreUtil.gZipExtractor(
59 | exFile.toString(),
60 | x
61 | )
62 | CoreCmd.moveResult(x, path)
63 | } else {
64 | CoreUtil.tarExtractUsingRoot(exFile.toString(), path)
65 | }
66 | exFile!!.delete()
67 | } catch (e: Exception) {
68 | handler.post {
69 | exFile!!.delete()
70 | callback.onDownloadingFailed(e)
71 | }
72 | }
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/core/util/IDownloadCore.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.core.util
2 |
3 | interface IDownloadCore {
4 | fun onDownloadingStart()
5 | fun onDownloadingProgress(progress: Int)
6 | fun onDownloadingComplete()
7 | fun onDownloadingFailed(e: Exception?)
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/fragment/AppListFragment.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.fragment
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.core.widget.doOnTextChanged
9 | import androidx.fragment.app.Fragment
10 | import androidx.preference.PreferenceManager
11 | import androidx.recyclerview.widget.DividerItemDecoration
12 | import androidx.recyclerview.widget.LinearLayoutManager
13 | import dagger.hilt.android.AndroidEntryPoint
14 | import rx.android.schedulers.AndroidSchedulers
15 | import rx.schedulers.Schedulers
16 | import xyz.chz.bfm.R
17 | import xyz.chz.bfm.adapter.AppListAdapter
18 | import xyz.chz.bfm.data.AppInfo
19 | import xyz.chz.bfm.data.AppManager
20 | import xyz.chz.bfm.databinding.FragmentAppListBinding
21 | import xyz.chz.bfm.util.Util
22 | import xyz.chz.bfm.util.command.TermCmd
23 | import xyz.chz.bfm.util.setMyFab
24 | import xyz.chz.bfm.util.showKeyboard
25 | import java.text.Collator
26 |
27 | @AndroidEntryPoint
28 | class AppListFragment : Fragment() {
29 |
30 | private lateinit var binding: FragmentAppListBinding
31 |
32 | private var adapter: AppListAdapter? = null
33 | private var appsAll: List? = null
34 | private val defaultsSharedPreferences by lazy {
35 | PreferenceManager.getDefaultSharedPreferences(
36 | requireActivity()
37 | )
38 | }
39 |
40 | override fun onCreateView(
41 | inflater: LayoutInflater, container: ViewGroup?,
42 | savedInstanceState: Bundle?
43 | ): View {
44 | binding = FragmentAppListBinding.inflate(layoutInflater, container, false)
45 | return binding.root
46 | }
47 |
48 |
49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
50 | super.onViewCreated(view, savedInstanceState)
51 | binding.apply {
52 | val dividerItemDecoration =
53 | DividerItemDecoration(requireActivity(), LinearLayoutManager.VERTICAL)
54 | rvApps.addItemDecoration(dividerItemDecoration)
55 |
56 | val applist = TermCmd.appidList
57 |
58 | AppManager.rxLoadNetworkAppList(requireActivity())
59 | .subscribeOn(Schedulers.io())
60 | .map {
61 | if (applist != null) {
62 | it.forEach { one ->
63 | if ((applist.contains(one.packageName))) {
64 | one.isSelected = 1
65 | } else {
66 | one.isSelected = 0
67 | }
68 | }
69 | val comparator = Comparator { p1, p2 ->
70 | when {
71 | p1.isSelected > p2.isSelected -> -1
72 | p1.isSelected == p2.isSelected -> 0
73 | else -> 1
74 | }
75 | }
76 | it.sortedWith(comparator)
77 | } else {
78 | val comparator = object : Comparator {
79 | val collator = Collator.getInstance()
80 | override fun compare(o1: AppInfo, o2: AppInfo) =
81 | collator.compare(o1.appName, o2.appName)
82 | }
83 | it.sortedWith(comparator)
84 | }
85 | }
86 | .observeOn(AndroidSchedulers.mainThread())
87 | .subscribe {
88 | appsAll = it
89 | adapter = AppListAdapter(requireActivity(), it, applist)
90 | rvApps.adapter = adapter
91 | prgWaiting.visibility = View.GONE
92 | }
93 |
94 | with(fbSave) {
95 | setOnClickListener {
96 | setMyFab("#888F96", R.drawable.ic_commit)
97 | if (Util.isProxyed) {
98 | TermCmd.renewBox {
99 | Util.runOnUiThread {
100 | if (it) {
101 | setMyFab("#6fa251", R.drawable.ic_done)
102 | } else {
103 | setMyFab("#EC7474", R.drawable.ic_error)
104 | }
105 | }
106 | }
107 | }
108 | adapter?.let {
109 | TermCmd.setAppidList(it.blacklist)
110 | }
111 | }
112 | }
113 | }
114 | setupSearchApp()
115 | setupSelect()
116 | }
117 |
118 | @SuppressLint("NotifyDataSetChanged")
119 | private fun setupSelect() = with(binding) {
120 | selectAll.setOnClickListener {
121 | adapter?.let {
122 | if (it.blacklist.containsAll(it.apps.map { it.packageName })) {
123 | it.apps.forEach {
124 | adapter?.blacklist!!.remove(it.packageName)
125 | }
126 | } else {
127 | it.apps.forEach {
128 | adapter?.blacklist!!.add(it.packageName)
129 |
130 | }
131 | }
132 | it.notifyDataSetChanged()
133 | }
134 | }
135 | }
136 |
137 | private fun setupSearchApp() = with(binding) {
138 | searchBtn.setOnClickListener {
139 | tvInfoApps.visibility = View.GONE
140 | edSearch.visibility = View.VISIBLE
141 | edSearch.doOnTextChanged { text, _, _, _ ->
142 | search(text.toString())
143 | }
144 | edSearch.showKeyboard()
145 | }
146 | }
147 |
148 | @SuppressLint("NotifyDataSetChanged")
149 | private fun search(str: String) {
150 | val apps = ArrayList()
151 |
152 | val key = str.uppercase()
153 | if (key.isNotEmpty()) {
154 | appsAll?.forEach {
155 | if (it.appName.uppercase().indexOf(key) >= 0
156 | || it.packageName.uppercase().indexOf(key) >= 0
157 | ) {
158 | apps.add(it)
159 | }
160 | }
161 | } else {
162 | appsAll?.forEach {
163 | apps.add(it)
164 | }
165 | }
166 |
167 | adapter = AppListAdapter(requireActivity(), apps, adapter?.blacklist)
168 | binding.rvApps.adapter = adapter
169 | adapter?.notifyDataSetChanged()
170 | }
171 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/fragment/ConfigHelperFragment.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.fragment
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.appcompat.app.AlertDialog
9 | import androidx.fragment.app.Fragment
10 | import xyz.chz.bfm.R
11 | import xyz.chz.bfm.databinding.FragmentConfigHelperBinding
12 | import xyz.chz.bfm.ui.converter.ConverterActivity
13 | import xyz.chz.bfm.util.Util
14 | import xyz.chz.bfm.util.command.TermCmd
15 | import xyz.chz.bfm.util.terminal.TerminalHelper.execRootCmd
16 | import xyz.chz.bfm.util.toast
17 |
18 |
19 | class ConfigHelperFragment : Fragment() {
20 |
21 | private lateinit var binding: FragmentConfigHelperBinding
22 | private var statePath: String? = null
23 |
24 | override fun onCreateView(
25 | inflater: LayoutInflater, container: ViewGroup?,
26 | savedInstanceState: Bundle?
27 | ): View? {
28 | binding = FragmentConfigHelperBinding.inflate(layoutInflater, container, false)
29 | return binding.root
30 | }
31 |
32 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
33 | super.onViewCreated(view, savedInstanceState)
34 | setupEditor()
35 | initEditor()
36 | editorApply()
37 | }
38 |
39 | private fun setupEditor() = with(binding.myEditor) {
40 | languageRuleBook = null
41 | lineNumberGenerator = { l ->
42 | (1..l).map { " $it " }
43 | }
44 | editable = true
45 | showDivider = false
46 | showMinimap = false
47 | }
48 |
49 | private fun initEditor() = with(binding) {
50 | dialogConfig()
51 | }
52 |
53 | private fun editorApply() = with(binding) {
54 | fbSave.apply {
55 | setOnClickListener {
56 | dialogSave()
57 | }
58 | }
59 |
60 | fbConverter.apply {
61 | setOnClickListener {
62 | val intent = Intent(context, ConverterActivity::class.java)
63 | startActivity(intent)
64 | }
65 | }
66 | }
67 |
68 | private fun dialogConfig() {
69 | val builder = AlertDialog.Builder(
70 | requireActivity(),
71 | R.style.ThemeOverlay_MaterialComponents_MaterialAlertDialog_background
72 | )
73 | builder.setCancelable(false)
74 | val items = TermCmd.proxyProviderPath.toTypedArray()
75 | builder.setItems(items) { _, w ->
76 | val configSlected = execRootCmd("cat ${items[w]}")
77 | if (configSlected.isNotEmpty()) {
78 | binding.myEditor.text = configSlected
79 | } else {
80 | toast("Empty file", requireActivity())
81 | }
82 | statePath = items[w]
83 |
84 | }
85 | val dialog = builder.create()
86 | dialog.show()
87 | }
88 |
89 | private fun dialogSave() {
90 | val builder = AlertDialog.Builder(
91 | requireActivity(),
92 | R.style.ThemeOverlay_MaterialComponents_MaterialAlertDialog_background
93 | )
94 | builder.setTitle("Save")
95 | builder.setMessage("Do you want save config now ?")
96 | builder.setPositiveButton("yes") { _, _ ->
97 | val input = binding.myEditor.text
98 | TermCmd.saveConfig(requireActivity(), input, statePath!!) {
99 | Util.runOnUiThread {
100 | if (it)
101 | toast(requireActivity().getString(R.string.success), requireActivity())
102 | else
103 | toast(requireActivity().getString(R.string.failed), requireActivity())
104 | }
105 | }
106 | }
107 | builder.setNegativeButton("cancel") { d, _ ->
108 | d.dismiss()
109 | }
110 | val dialog = builder.create()
111 | dialog.show()
112 | }
113 |
114 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/fragment/DashboardFragment.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.fragment
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.webkit.WebSettings
9 | import android.webkit.WebViewClient
10 | import androidx.fragment.app.DialogFragment
11 | import androidx.fragment.app.Fragment
12 | import dagger.hilt.android.AndroidEntryPoint
13 | import xyz.chz.bfm.databinding.FragmentDashboardBinding
14 | import xyz.chz.bfm.dialog.IMakeDialog
15 | import xyz.chz.bfm.dialog.MakeDialog
16 | import xyz.chz.bfm.util.Util
17 | import xyz.chz.bfm.util.command.SettingCmd
18 | import xyz.chz.bfm.util.command.TermCmd
19 |
20 | @AndroidEntryPoint
21 | class DashboardFragment : Fragment(), IMakeDialog {
22 |
23 | private lateinit var binding: FragmentDashboardBinding
24 | override fun onCreateView(
25 | inflater: LayoutInflater, container: ViewGroup?,
26 | savedInstanceState: Bundle?
27 | ): View {
28 | binding = FragmentDashboardBinding.inflate(layoutInflater, container, false)
29 | return binding.root
30 | }
31 |
32 |
33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
34 | super.onViewCreated(view, savedInstanceState)
35 | if (Util.isClashOrSing) {
36 | showRes()
37 | } else {
38 | MakeDialog(
39 | "Wowo :( ",
40 | "Your core is not clash or sing-box so dashboard not available",
41 | false,
42 | false
43 | ).show(requireActivity().supportFragmentManager, "")
44 | }
45 | }
46 |
47 | @SuppressLint("SetJavaScriptEnabled")
48 | private fun showRes() = with(binding) {
49 | val linkDB: String = if (SettingCmd.core == "clash") {
50 | TermCmd.linkDBClash
51 | } else {
52 | TermCmd.linkDBSing
53 | }
54 | dbWebview.loadUrl("${linkDB}/ui/#/proxies")
55 |
56 | with(dbWebview.settings) {
57 | domStorageEnabled = true
58 | databaseEnabled = true
59 | allowContentAccess = true
60 | javaScriptEnabled = true
61 | cacheMode = WebSettings.LOAD_NO_CACHE
62 | dbWebview.webViewClient = WebViewClient()
63 | }
64 | }
65 |
66 | override fun onDialogPositiveButton(dialog: DialogFragment) {
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/ui/model/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.ui.model
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.Job
10 | import kotlinx.coroutines.delay
11 | import kotlinx.coroutines.isActive
12 | import kotlinx.coroutines.launch
13 | import okhttp3.Call
14 | import okhttp3.Callback
15 | import okhttp3.Response
16 | import xyz.chz.bfm.util.OkHttpHelper
17 | import xyz.chz.bfm.util.command.TermCmd
18 | import xyz.chz.bfm.util.myReplacer
19 | import javax.inject.Inject
20 |
21 | @HiltViewModel
22 | class MainViewModel @Inject constructor() : ViewModel() {
23 |
24 | private val _log = MutableLiveData()
25 | val log: LiveData get() = _log
26 |
27 | private val _theresult = MutableLiveData()
28 | val theresult: LiveData = _theresult
29 |
30 | fun dataLog(): Job {
31 | return CoroutineScope(Dispatchers.IO).launch {
32 | while (isActive) {
33 | _log.postValue(
34 | TermCmd.readLog().myReplacer(
35 | "\\[Info\\]".toRegex() to "[Info] ",
36 | "\\[Debug\\]".toRegex() to "[Debug] ",
37 | "\\[Error\\]".toRegex() to "[Error] ",
38 | "\\[Warning\\]".toRegex() to "[Warning] ",
39 | "\n".toRegex() to "
"
40 | )
41 | )
42 | delay(1500)
43 | }
44 | }
45 | }
46 |
47 | fun getTextFromUrl(url: String) {
48 | OkHttpHelper().getRawTextFromURL(url, object : Callback {
49 | override fun onFailure(call: Call, e: java.io.IOException) {
50 | _theresult.value = e.message!!
51 | }
52 |
53 | override fun onResponse(call: Call, response: Response) {
54 | _theresult.postValue(response.body!!.string())
55 | }
56 | })
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/Constant.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util
2 |
3 | // bcs yq yaml cant get emojis
4 | // dont make this to regex
5 | const val EXTRACTOR =
6 | "sed 's/\\[//g' | sed 's/\\]//g' | sed 's/\"//g' | sed 's/,//g' | sed 's/ //g' | awk 'NF'"
7 | const val QUOTES = "sed 's/\"//g'"
8 | const val META_REPO = "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest"
9 | const val META_DOWNLOAD = "https://github.com/MetaCubeX/mihomo/releases/download"
10 | const val SING_REPO = "https://api.github.com/repos/shioeri/sing-box/releases"
11 | const val SING_DOWNLOAD = "https://github.com/shioeri/sing-box/releases/download"
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/OkHttpHelper.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util
2 |
3 | import okhttp3.Call
4 | import okhttp3.Callback
5 | import okhttp3.OkHttpClient
6 | import okhttp3.Request
7 |
8 | class OkHttpHelper {
9 | fun getRawTextFromURL(url: String, callback: Callback): Call {
10 | val client = OkHttpClient()
11 | val request = Request.Builder()
12 | .url(url)
13 | .build()
14 |
15 | val call = client.newCall(request)
16 | call.enqueue(callback)
17 | return call
18 | }
19 |
20 | fun reqGithub(url: String, callback: Callback): Call {
21 | val client = OkHttpClient()
22 | val request = Request.Builder()
23 | .url(url)
24 | .addHeader("Accept", "application/vnd.github+json")
25 | .addHeader("X-GitHub-Api-Version", "2022-11-28")
26 | .build()
27 |
28 | val call = client.newCall(request)
29 | call.enqueue(callback)
30 | return call
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/Util.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import org.json.JSONArray
6 | import org.json.JSONObject
7 | import xyz.chz.bfm.util.command.SettingCmd
8 | import xyz.chz.bfm.util.command.TermCmd
9 | import java.util.ArrayDeque
10 | import java.util.Deque
11 |
12 | object Util {
13 | var isProxyed = TermCmd.isProxying()
14 | private val handler = Handler(Looper.getMainLooper())
15 |
16 | fun runOnUiThread(action: () -> Unit) {
17 | if (Looper.myLooper() != Looper.getMainLooper()) {
18 | handler.post(action)
19 | } else {
20 | action.invoke()
21 | }
22 | }
23 |
24 | val isClashOrSing: Boolean
25 | get() {
26 | if (SettingCmd.core.contains("clash") or SettingCmd.core.contains("sing-box"))
27 | return true
28 | return false
29 | }
30 |
31 | fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
32 | return JsonObjectBuilder().json(build)
33 | }
34 |
35 | class JsonObjectBuilder {
36 | private val deque: Deque = ArrayDeque()
37 |
38 | fun json(build: JsonObjectBuilder.() -> Unit): JSONObject {
39 | deque.push(JSONObject())
40 | this.build()
41 | return deque.pop()
42 | }
43 |
44 | infix fun String.to(value: T) {
45 | val wrapped = when (value) {
46 | is Function0<*> -> json { value.invoke() }
47 | is Array<*> -> JSONArray().apply { value.forEach { put(it) } }
48 | else -> value
49 | }
50 |
51 | deque.peek().put(this, wrapped)
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/ViewUtils.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.ClipData
6 | import android.content.ClipboardManager
7 | import android.content.Context
8 | import android.content.res.ColorStateList
9 | import android.graphics.Color
10 | import android.os.Build
11 | import android.text.Html
12 | import android.text.Spanned
13 | import android.util.TypedValue
14 | import android.view.inputmethod.InputMethodManager
15 | import android.widget.EditText
16 | import android.widget.ImageView
17 | import android.widget.TextView
18 | import android.widget.Toast
19 | import com.google.android.material.card.MaterialCardView
20 | import com.google.android.material.floatingactionbutton.FloatingActionButton
21 | import xyz.chz.bfm.ui.converter.config.ConfigType
22 | import xyz.chz.bfm.util.modul.ModuleManager
23 | import java.net.HttpURLConnection
24 | import java.net.URL
25 |
26 |
27 | fun MaterialCardView.setColorBackground(str: String) {
28 | this.setCardBackgroundColor(Color.parseColor(str))
29 | this.radius =
30 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, context.resources.displayMetrics)
31 | }
32 |
33 | fun ImageView.setImage(res: Int) {
34 | this.setImageResource(res)
35 | }
36 |
37 | fun TextView.moduleVer() {
38 | this.text = ModuleManager.moduleVersion
39 | }
40 |
41 | fun FloatingActionButton.setMyFab(color: String, res: Int) {
42 | this.backgroundTintList = ColorStateList.valueOf(Color.parseColor(color))
43 | this.setImageResource(res)
44 | }
45 |
46 | fun toast(str: String, ctx: Context) {
47 | Toast.makeText(ctx, str, Toast.LENGTH_SHORT).show()
48 | }
49 |
50 | fun String.myReplacer(vararg replacements: Pair): String =
51 | replacements.fold(this) { acc, (old, new) -> acc.replace(old, new) }
52 |
53 | @SuppressLint("ObsoleteSdkInt")
54 | fun setTextHtml(text: String): Spanned {
55 | val res: Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
56 | Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
57 | } else {
58 | Html.fromHtml(text)
59 | }
60 | return res
61 | }
62 |
63 | fun EditText.hideKeyboard(): Boolean {
64 | return (context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager)
65 | .hideSoftInputFromWindow(windowToken, 0)
66 | }
67 |
68 | fun EditText.showKeyboard(): Boolean {
69 | return (context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager)
70 | .showSoftInput(this, 0)
71 | }
72 |
73 | fun EditText.removeEmptyLines(): String {
74 | //val regex = Regex()
75 | return this.text.replace("(?m)^[ \t]*\r?\n".toRegex(), "")
76 | }
77 |
78 | fun TextView.copyToClipboard(context: Context) {
79 | (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
80 | .setPrimaryClip(ClipData.newPlainText("copied", this.text.toString()))
81 | }
82 |
83 | fun EditText.isValidCheck(): Boolean {
84 | if (this.text.toString().isNotEmpty() || this.text.toString()
85 | .startsWith(ConfigType.VMESS.scheme) || this.text.toString().startsWith(
86 | ConfigType.VLESS.scheme
87 | ) || this.text.toString().startsWith(ConfigType.TROJAN.scheme) || this.text.toString()
88 | .startsWith(ConfigType.TROJANGO.scheme)
89 | ) {
90 | return true
91 | }
92 | return false
93 | }
94 |
95 | fun String.urlText(): String {
96 | return URL(this).run {
97 | openConnection().run {
98 | this as HttpURLConnection
99 | inputStream.bufferedReader().readText().trim()
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/command/CoreCmd.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util.command
2 |
3 | import xyz.chz.bfm.util.terminal.TerminalHelper.execRootCmd
4 | import xyz.chz.bfm.util.terminal.TerminalHelper.execRootCmdSilent
5 |
6 | object CoreCmd {
7 | private const val path = "/data/adb/box"
8 |
9 | val checkVerSing: String
10 | get() {
11 | val cmd = "$path/bin/sing-box version | awk '{print $3}' | awk '{print $1; exit}'"
12 | return execRootCmd(cmd)
13 | }
14 |
15 | val checkVerClashMeta: String
16 | get() {
17 | val cmd = "$path/bin/xclash/mihomo -v | awk '{print $3}' | awk '{print $1; exit}'"
18 | return execRootCmd(cmd)
19 | }
20 |
21 | fun moveResult(pathOut: String, pathDir: String) {
22 | val cmd = "mv -f $pathOut $pathDir && chmod 755 $pathDir && chown root:net_admin $pathDir"
23 | execRootCmdSilent(cmd)
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/command/SettingCmd.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util.command
2 |
3 | import xyz.chz.bfm.util.terminal.TerminalHelper.execRootCmd
4 |
5 | object SettingCmd {
6 |
7 | val networkMode: String
8 | get() = execRootCmd("grep 'network_mode=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
9 |
10 | fun setNetworkMode(mode: String): String {
11 | return execRootCmd("sed -i 's/network_mode=.*/network_mode=\"$mode\"/;' /data/adb/box/settings.ini")
12 | }
13 |
14 | val proxyMode: String
15 | get() = execRootCmd("sed -n 's/^mode:\\([^ ]*\\).*/\\1/p' /data/adb/box/package.list.cfg")
16 |
17 | fun setProxyMode(mode: String): String {
18 | return execRootCmd("sed -i 's/^mode:[^ ]*/mode:$mode/' /data/adb/box/package.list.cfg")
19 | }
20 |
21 | val cron: String
22 | get() = execRootCmd("grep 'run_crontab=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
23 |
24 | fun setCron(mode: String): String {
25 | return execRootCmd("sed -i 's/run_crontab=.*/run_crontab=\"$mode\"/;' /data/adb/box/settings.ini")
26 | }
27 |
28 | val geo: String
29 | get() = execRootCmd("grep 'update_geo=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
30 |
31 | fun setGeo(mode: String): String {
32 | return execRootCmd("sed -i 's/update_geo=.*/update_geo=\"$mode\"/;' /data/adb/box/settings.ini")
33 | }
34 |
35 | val memcg: String
36 | get() = execRootCmd("grep 'cgroup_memcg=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
37 |
38 | fun setMemcg(mode: String): String {
39 | return execRootCmd("sed -i 's/cgroup_memcg=.*/cgroup_memcg=\"$mode\"/;' /data/adb/box/settings.ini")
40 | }
41 |
42 | val blkio: String
43 | get() = execRootCmd("grep 'cgroup_blkio=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
44 |
45 | fun setBlkio(mode: String): String {
46 | return execRootCmd("sed -i 's/cgroup_blkio=.*/cgroup_blkio=\"$mode\"/;' /data/adb/box/settings.ini")
47 | }
48 |
49 | val cpuset: String
50 | get() = execRootCmd("grep 'cgroup_cpuset=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
51 |
52 | fun setCpuset(mode: String): String {
53 | return execRootCmd("sed -i 's/cgroup_cpuset=.*/cgroup_cpuset=\"$mode\"/;' /data/adb/box/settings.ini")
54 | }
55 |
56 | val subs: String
57 | get() = execRootCmd("grep 'update_subscription=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
58 |
59 | fun setSubs(mode: String): String {
60 | return execRootCmd("sed -i 's/update_subscription=.*/update_subscription=\"$mode\"/;' /data/adb/box/settings.ini")
61 | }
62 |
63 | val redirHost: Boolean
64 | get() = "redir-host" == execRootCmd("grep 'enhanced-mode:' /data/adb/box/clash/config.yaml | awk '{print $2}'")
65 |
66 | fun setRedirHost(mode: String): String {
67 | return execRootCmd("sed -i 's/enhanced-mode:.*/enhanced-mode: $mode/;' /data/adb/box/clash/config.yaml")
68 | }
69 |
70 | val quic: Boolean
71 | get() = "enable" == execRootCmd("grep 'quic=' /data/adb/box/scripts/box.iptables | sed 's/^.*=//' | sed 's/\"//g'")
72 |
73 | fun setQuic(mode: String): String {
74 | return execRootCmd("sed -i 's/quic=.*/quic=\"$mode\"/;' /data/adb/box/scripts/box.iptables")
75 | }
76 |
77 | val unified: Boolean
78 | get() = "true" == execRootCmd("grep 'unified-delay:' /data/adb/box/clash/config.yaml | awk '{print $2}'")
79 |
80 | fun setUnified(mode: String): String {
81 | return execRootCmd("sed -i 's/unified-delay:.*/unified-delay: $mode/;' /data/adb/box/clash/config.yaml")
82 | }
83 |
84 | val geodata: Boolean
85 | get() = "true" == execRootCmd("grep 'geodata-mode:' /data/adb/box/clash/config.yaml | awk '{print $2}'")
86 |
87 | fun setGeodata(mode: String): String {
88 | return execRootCmd("sed -i 's/geodata-mode:.*/geodata-mode: $mode/;' /data/adb/box/clash/config.yaml")
89 | }
90 |
91 | val tcpCon: Boolean
92 | get() = "true" == execRootCmd("grep 'tcp-concurrent:' /data/adb/box/clash/config.yaml | awk '{print $2}'")
93 |
94 | fun setTcpCon(mode: String): String {
95 | return execRootCmd("sed -i 's/tcp-concurrent:.*/tcp-concurrent: $mode/;' /data/adb/box/clash/config.yaml")
96 | }
97 |
98 | val sniffer: Boolean
99 | get() = "true" == execRootCmd("grep -C 1 'sniffer:' /data/adb/box/clash/config.yaml | grep 'enable:' | awk '{print $2}'")
100 |
101 | fun setSniffer(mode: String): String {
102 | return execRootCmd("sed -i '/^sniffer:/{n;s/enable:.*/enable: $mode/;}' /data/adb/box/clash/config.yaml")
103 | }
104 |
105 | val ipv6: Boolean
106 | get() = "true" == execRootCmd("grep 'ipv6=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
107 |
108 | fun setIpv6(mode: String): String {
109 | return execRootCmd("sed -i 's/ipv6=.*/ipv6=\"$mode\"/;' /data/adb/box/settings.ini")
110 | }
111 |
112 | val findProc: String
113 | get() = execRootCmd("grep 'find-process-mode:' /data/adb/box/clash/config.yaml | awk '{print $2}'")
114 |
115 | fun setFindProc(mode: String): String {
116 | return execRootCmd("sed -i 's/find-process-mode:.*/find-process-mode: $mode/;' /data/adb/box/clash/config.yaml")
117 | }
118 |
119 | val findConf: String
120 | get() = execRootCmd("grep 'name_clash_config=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
121 |
122 | fun setFindConf(mode: String): String {
123 | return execRootCmd("sed -i 's/name_clash_config=.*/name_clash_config=\"$mode\"/;' /data/adb/box/settings.ini")
124 | }
125 |
126 | val clashType: String
127 | get() = execRootCmd("grep 'xclash_option=' /data/adb/box/settings.ini | sed 's/^.*=//' | sed 's/\"//g'")
128 |
129 | fun setClashType(mode: String): String {
130 | return execRootCmd("sed -i 's/xclash_option=.*/xclash_option=\"$mode\"/;' /data/adb/box/settings.ini")
131 | }
132 |
133 | val core: String
134 | get() = execRootCmd("grep 'bin_name=' /data/adb/box/settings.ini | sed 's/^.*=//g' | sed 's/\"//g'")
135 |
136 | // fun setCore(x: String): Boolean {
137 | // return execRootCmdSilent("sed -i 's/bin_name=.*/bin_name=$x/;' /data/adb/box/settings.ini") != -1
138 | // }
139 |
140 | var setCore: String = ""
141 | set(value) {
142 | field = value
143 | execRootCmd("sed -i 's/bin_name=.*/bin_name=$field/;' /data/adb/box/settings.ini")
144 | }
145 |
146 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/command/TermCmd.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util.command
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import xyz.chz.bfm.util.EXTRACTOR
6 | import xyz.chz.bfm.util.QUOTES
7 | import xyz.chz.bfm.util.terminal.TerminalHelper.execRootCmd
8 | import xyz.chz.bfm.util.terminal.TerminalHelper.execRootCmdSilent
9 | import xyz.chz.bfm.util.terminal.TerminalHelper.execRootCmdVoid
10 | import java.io.File
11 | import java.io.FileOutputStream
12 | import kotlin.concurrent.thread
13 |
14 | object TermCmd {
15 | private const val path = "/data/adb/box"
16 | private const val yq = "${path}/bin/yq"
17 |
18 | fun isProxying(): Boolean {
19 | return execRootCmdSilent("if [ -f ${path}/run/box.pid ] ; then exit 1;fi") == 1
20 | }
21 |
22 | fun renewBox(callback: (Boolean) -> Unit) {
23 | thread {
24 | val cmd = "${path}/scripts/box.iptables renew"
25 | execRootCmdVoid(cmd, callback)
26 | }
27 | }
28 |
29 | fun start(callback: (Boolean) -> Unit) {
30 | thread {
31 | val cmd = "${path}/scripts/box.service start && ${path}/scripts/box.iptables enable"
32 | execRootCmdVoid(cmd, callback)
33 | }
34 | }
35 |
36 | fun stop(callback: (Boolean) -> Unit) {
37 | thread {
38 | val cmd = "${path}/scripts/box.iptables disable && ${path}/scripts/box.service stop"
39 | execRootCmdVoid(cmd, callback)
40 | }
41 | }
42 |
43 | fun readLog(): String {
44 | return execRootCmd("cat ${path}/run/runs.log")
45 | }
46 |
47 | val linkDBClash: String
48 | get() {
49 | return execRootCmd("grep 'external-controller:' ${path}/clash/config.yaml | awk '{print $2}'")
50 | }
51 |
52 |
53 | val linkDBSing: String
54 | get() {
55 | val cmd =
56 | "grep -w 'external_controller' ${path}/sing-box/config.json | awk '{print $2}' | sed 's/\"//g' | sed 's/,//g'"
57 | return execRootCmd(cmd)
58 | }
59 |
60 | private val proxyProviderJsonKey: HashSet
61 | get() {
62 | val s = HashSet()
63 | val cmd = "$yq -oj '.proxy-providers | keys' $path/clash/config.yaml | $EXTRACTOR"
64 | val result = execRootCmd(cmd)
65 | if (result.isEmpty())
66 | return s
67 | val list = result.split("\n")
68 | for (x in list) {
69 | s.add(x)
70 | }
71 | return s
72 | }
73 |
74 | val proxyProviderPath: HashSet
75 | @SuppressLint("SuspiciousIndentation")
76 | get() {
77 | val s = HashSet()
78 | val ppKey = proxyProviderJsonKey
79 | for (xs in ppKey) {
80 | if (SettingCmd.core == "clash") {
81 | val pth =
82 | execRootCmd("exp=$xs $yq -oj '.proxy-providers.[env(exp)].path' $getPathOnly | $QUOTES")
83 | if (pth.contains("./")) {
84 | s.add(pth.replace("./", "$path/clash/"))
85 | } else {
86 | s.add(pth)
87 | }
88 | }
89 | }
90 | s.add(getPathOnly)
91 | return s
92 | }
93 |
94 | val appidList: HashSet get() {
95 | val s = HashSet()
96 | val modeCommand = "sed -n 's/^\\(mode:[^ ]*\\).*/\\1/p' ${path}/package.list.cfg"
97 | val packageCommand = "sed -n '/^[^#]/s/^\\([^ ]*\\.[^ ]*\\).*/\\1/p' ${path}/package.list.cfg"
98 | val gidCommand = "sed -n '/^[^#]/s/^\\([0-9]\\{1,8\\}\\).*/\\1/p' ${path}/package.list.cfg"
99 |
100 | val modeResult = execRootCmd(modeCommand)
101 | val packageResult = execRootCmd(packageCommand)
102 | val gidResult = execRootCmd(gidCommand)
103 |
104 | val result = """
105 | $modeResult
106 | $packageResult
107 | $gidResult
108 | """.trimIndent()
109 |
110 | if (result.isEmpty()) {
111 | return s
112 | }
113 |
114 | val appIds = result.split("\n")
115 | for (i in appIds) {
116 | if (i.isNotEmpty() && !i.startsWith("alook")) {
117 | s.add(i.trim())
118 | }
119 | }
120 | return s
121 | }
122 |
123 | fun setAppidList(s: HashSet): Boolean {
124 | val content = s.filterNotNull()
125 | .filter { it.isNotEmpty() }
126 | .joinToString(" ")
127 |
128 | val command = """
129 | echo "$content" > "$path/package.list.cfg" &&
130 | sed -i '/^#/!s/ /\'$'\n/g' "${path}/package.list.cfg"
131 | sed -i '/alook\\|999_alook/s/^/#/' "${path}/package.list.cfg"
132 | """.trimIndent()
133 |
134 | return execRootCmdSilent(command) != -1
135 |
136 | }
137 |
138 | private fun getNameConfig(what: String, isClash: Boolean): String {
139 | val m = if (isClash) "yaml" else "json"
140 | return execRootCmd("find ${path}/$what/ -maxdepth 1 -name 'config.$m' -type f -exec basename {} \\;")
141 | }
142 |
143 | val getPathOnly: String
144 | get() {
145 | val what = SettingCmd.core
146 | val isClash = what == "clash"
147 | val name = getNameConfig(what, isClash)
148 | return "$path/$what/$name"
149 | }
150 |
151 | fun saveConfig(ctx: Context, str: String, pth: String, callback: (Boolean) -> Unit) {
152 | thread {
153 | val exFile = File(ctx.getExternalFilesDir(null), "out.txt")
154 | val fos = FileOutputStream(exFile)
155 | fos.write(str.toByteArray())
156 | val cmd = "mv -f $exFile $pth"
157 | execRootCmdVoid(cmd, callback)
158 | }
159 | }
160 |
161 | private fun yqParser(dir: String, config: String, isClash: Boolean): String {
162 | return if (isClash) {
163 | val yamlToJson = "$yq -oj ${path}/${dir}/${config} > ${path}/${dir}/xtemp.json"
164 | execRootCmd("$yamlToJson && $yq -oy ${path}/${dir}/xtemp.json > ${path}/${dir}/${config} && rm -f ${path}/${dir}/xtemp.json && cat ${path}/${dir}/${config}")
165 | } else {
166 | execRootCmd("$yq -oj ${path}/${dir}/${config} > ${path}/${dir}/xtemp.json && mv -f ${path}/${dir}/xtemp.json ${path}/${dir}/${config} && cat ${path}/${dir}/${config}")
167 | }
168 | }
169 |
170 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/modul/ModuleManager.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util.modul
2 |
3 | import xyz.chz.bfm.util.terminal.TerminalHelper
4 |
5 | object ModuleManager {
6 | val moduleVersionCode: String
7 | get() {
8 | val cmd = "cat /data/adb/modules/box_for_root/module.prop | grep '^versionCode='"
9 | val result = TerminalHelper.execRootCmd(cmd)
10 | return if (result.isEmpty()) "" else result.split("=")[1]
11 | }
12 |
13 | val moduleVersion: String
14 | get() {
15 | val cmd = "cat /data/adb/modules/box_for_root/module.prop | grep '^version='"
16 | val result = TerminalHelper.execRootCmd(cmd)
17 | return if (result.isEmpty()) "" else result.split("=")[1]
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/xyz/chz/bfm/util/terminal/TerminalHelper.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm.util.terminal
2 |
3 | import android.util.Log
4 | import xyz.chz.bfm.BuildConfig
5 |
6 | object TerminalHelper {
7 | private const val TAG = "BoxForRoot.Terminal"
8 |
9 | fun execRootCmd(cmd: String): String {
10 | return try {
11 | val process: Process = Runtime.getRuntime().exec("su -c $cmd")
12 | process.waitFor()
13 | val output = process.inputStream.bufferedReader().lineSequence().joinToString("\n")
14 | if (BuildConfig.DEBUG) Log.d(TAG, output)
15 | output
16 | } catch (e: Exception) {
17 | ""
18 | }
19 | }
20 |
21 | fun execRootCmdSilent(cmd: String): Int {
22 | return try {
23 | val process: Process = Runtime.getRuntime().exec("su -c $cmd")
24 | process.waitFor()
25 | process.exitValue()
26 | } catch (e: Exception) {
27 | -1
28 | }
29 | }
30 |
31 | fun execRootCmdVoid(cmd: String, callback: (Boolean) -> Unit) {
32 | try {
33 | val process = Runtime.getRuntime().exec("su -c $cmd")
34 | process.waitFor()
35 | callback(process.exitValue() == 0)
36 | } catch (e: Exception) {
37 | e.printStackTrace()
38 | callback(false)
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_app.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_commit.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_config.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_converter.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_dashboard.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_disabled.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_done.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
15 |
18 |
22 |
25 |
28 |
31 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_download.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_enabled.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
14 |
19 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_home.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
15 |
18 |
22 |
25 |
28 |
31 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/app/src/main/res/drawable/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_loading.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_log.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_modul.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_save.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_select_all.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_setting.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_converter.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
21 |
22 |
26 |
27 |
32 |
33 |
38 |
39 |
40 |
46 |
47 |
50 |
51 |
56 |
57 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_core.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
15 |
16 |
23 |
24 |
25 |
32 |
33 |
36 |
37 |
45 |
46 |
55 |
56 |
68 |
69 |
80 |
81 |
92 |
93 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/custom_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
20 |
21 |
29 |
30 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_app_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
16 |
17 |
22 |
23 |
31 |
32 |
42 |
43 |
53 |
54 |
64 |
65 |
66 |
67 |
68 |
69 |
77 |
78 |
84 |
85 |
96 |
97 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_config_helper.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
26 |
27 |
39 |
40 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_dashboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_applist.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
24 |
25 |
31 |
32 |
39 |
40 |
41 |
42 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
16 |
19 |
22 |
23 |
28 |
33 |
36 |
37 |
42 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/clashtemplate:
--------------------------------------------------------------------------------
1 | # port: 6550
2 | # socks-port: 6551
3 | # mixed-port: 6552
4 | redir-port: 9797
5 | tproxy-port: 9898
6 | mode: rule
7 | allow-lan: true
8 | bind-address: "*"
9 | log-level: warning
10 | unified-delay: true
11 | geodata-mode: true
12 | geodata-loader: memconservative
13 | ipv6: false
14 | external-controller: 0.0.0.0:9090
15 | secret: ""
16 | external-ui: /data/adb/box/clash/dashboard
17 | profile:
18 | store-selected: true
19 | store-fake-ip: false
20 | find-process-mode: off
21 | geox-url:
22 | geoip: "https://github.com/MetaCubeX/meta-rules-dat/raw/release/geoip-lite.dat"
23 | mmdb: "https://github.com/MetaCubeX/meta-rules-dat/raw/release/country-lite.mmdb"
24 | geosite: "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
25 | sniffer:
26 | enable: false
27 | force-dns-mapping: false
28 | parse-pure-ip: false
29 | override-destination: false
30 | sniff:
31 | TLS:
32 | ports: [443, 8443]
33 | HTTP:
34 | ports: [80, 8080-8880]
35 | override-destination: true
36 | sniffing: [tls, http]
37 | port-whitelist: [80, 443]
38 | dns:
39 | enable: true
40 | prefer-h3: false
41 | ipv6: false
42 | default-nameserver:
43 | - '8.8.8.8#match'
44 | - 1.1.1.1
45 | listen: 0.0.0.0:1053
46 | use-hosts: true
47 | enhanced-mode: fake-ip
48 | fake-ip-range: 28.0.0.1/8
49 | fake-ip-filter:
50 | - "*.lan"
51 | - "*.ntp.*"
52 | nameserver:
53 | - 9.9.9.9
54 | - 94.140.14.14
55 | - '8.8.8.8#match'
56 | # proxy-server-nameserver:
57 | # - 112.215.198.248
58 | # - "[2400:9800:2:2::246]"
59 | # nameserver-policy:
60 | # "geosite:category-ads-all": rcode://success
61 | # "geosite:nameserver-policy":
62 | # - 112.215.198.248
63 | # - '[2404:c0:1000::a:0:1]'
64 | tun:
65 | enable: false
66 | mtu: 9000
67 | device: tun9
68 | stack: system
69 | dns-hijack:
70 | - any:53
71 | - tcp://any:53
72 | auto-route: true
73 | strict-route: false
74 | auto-detect-interface: true
75 | include-android-user: [0, 10]
76 | exclude-package: []
77 | proxies:
78 | %1$sproxy-groups:
79 | %2$srules:
80 | # - DOMAIN-SUFFIX,googlesyndication.com,%3$s
81 | # - DOMAIN-SUFFIX,adtival.network,%3$s
82 | # - IP-CIDR,127.0.0.1/32,REJECT,no-resolve
83 | # - IP-CIDR,28.0.0.1/8,REJECT,no-resolve
84 | # - IP-CIDR6,::1/128,REJECT,no-resolve
85 | # - GEOSITE,category-ads-all,REJECT
86 | # - AND,((NETWORK,udp),(GEOSITE,youtube)),REJECT
87 | - MATCH,%3$s
--------------------------------------------------------------------------------
/app/src/main/res/raw/singboxtemplate:
--------------------------------------------------------------------------------
1 | {
2 | "log": {
3 | "disabled": false,
4 | "level": "panic",
5 | "output": "/data/adb/box/run/sing-box.log",
6 | "timestamp": true
7 | },
8 | "dns": {
9 | "servers": [
10 | {
11 | "tag": "AdGuard",
12 | "address": "94.140.14.14"
13 | },
14 | {
15 | "tag": "local",
16 | "address": "YOUR DNS",
17 | "detour": "direct"
18 | },
19 | {
20 | "tag": "rcode",
21 | "address": "rcode://success"
22 | }
23 | ],
24 | "rules": [
25 | {
26 | "outbound": "any",
27 | "server": "local",
28 | "disable_cache": true
29 | }
30 | ],
31 | "final": "AdGuard",
32 | "reverse_mapping": false,
33 | "disable_cache": false,
34 | "independent_cache": false
35 | },
36 | "inbounds": [
37 | {
38 | "type": "tproxy",
39 | "tag": "tproxy-in",
40 | "listen": "::",
41 | "listen_port": 9898,
42 | "sniff": true,
43 | "sniff_override_destination": false
44 | }
45 | ],
46 | "outbounds": [
47 | %1$s
48 | ],
49 | "route": {
50 | "rule_set": [
51 | {
52 | "type": "remote",
53 | "tag": "geosite-category-ads-all",
54 | "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/sing/geo/geosite/category-ads-all.srs",
55 | "download_detour": "match",
56 | "update_interval": "24h",
57 | "format": "binary"
58 | }
59 | ],
60 | "rules": [
61 | {
62 | "protocol": "dns",
63 | "outbound": "dns-out"
64 | },
65 | {
66 | "rule_set": [
67 | "geosite-category-ads-all"
68 | ],
69 | "outbound": "ads-all"
70 | }
71 | ],
72 | "final": "match",
73 | "find_process": true,
74 | "auto_detect_interface": true
75 | },
76 | "experimental": {
77 | "cache_file": {
78 | "enabled": true,
79 | "path": "cache.db",
80 | "store_fakeip": false
81 | },
82 | "clash_api": {
83 | "external_controller": "0.0.0.0:9090",
84 | "external_ui": "./dashboard"
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 | #0F2227
5 | @color/bgdialog
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2196F3
4 | #0D2196F3
5 | #192196F3
6 | #802196F3
7 | #EC7474
8 | #FCFCFC
9 | #009E00
10 | #0E121A
11 | #000000
12 | #171C26
13 | #4F565E
14 | #888F96
15 | #80888F96
16 | #FFFFFFFF
17 | #1E0304
18 | #00FFFFFF
19 | #FCFCFC
20 | #F2F5F6
21 | #80F2F5F6
22 | #7F717171
23 | @color/white
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 50dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | BoxForRoot
4 | 20230814
5 | added %d apps in mode %s
6 | mode %s
7 | OK
8 | clash only work on redir-host
9 | Module Not Found
10 | Please upgrade ur Module :)\nMinimum ver: %d
11 | https://github.com/taamarin/box_for_magisk
12 | https://github.com/taamarin/box_for_magisk/releases
13 |
14 | Success
15 | Failed
16 | Home
17 | Dashboard
18 | App
19 |
20 |
21 | select core :
22 | clash settings:
23 | find procces
24 | find config
25 | clash options
26 | unified delay[meta]
27 | sniffer[meta]
28 | redir-host[meta]
29 | geodata[meta]
30 | subscription
31 | module settings:
32 | cgroup:
33 | memcg
34 | blkio
35 | cpuset
36 | quic
37 | ipv6
38 | port detect
39 | crontab
40 | geox
41 | NetworkMode
42 | ProxyMode
43 | check module
44 | MyIP
45 | Setting
46 |
47 |
48 |
49 | - clash
50 | - sing-box
51 | - xray
52 | - hysteria
53 | - v2fly
54 |
55 |
56 |
57 | - off
58 | - strict
59 | - always
60 |
61 |
62 |
63 | - default
64 | - config2
65 | - config3
66 |
67 |
68 |
69 | - premium
70 | - mihomo
71 |
72 |
73 |
74 | - tproxy
75 | - redirect
76 | - enhance
77 | - mixed
78 | - tun
79 |
80 |
81 |
82 | - whitelist
83 | - blacklist
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
16 |
17 |
24 |
25 |
30 |
31 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 127.0.0.1
16 | 0.0.0.0
17 | ip-api.com
18 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/xyz/chz/bfm/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.chz.bfm
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = '1.8.0'
4 | dependencies {
5 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.43.2'
6 | classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1'
7 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.6.10'
8 | }
9 | }
10 | plugins {
11 | id 'com.android.application' version '7.4.2' apply false
12 | id 'com.android.library' version '7.4.2' apply false
13 | id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
14 | }
15 | task clean(type: Delete) {
16 | delete rootProject.buildDir
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/ke/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /libs
--------------------------------------------------------------------------------
/ke/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'org.jetbrains.dokka'
4 |
5 |
6 | android {
7 | compileSdkVersion 33
8 |
9 | defaultConfig {
10 | minSdkVersion 24
11 | targetSdkVersion 33
12 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
13 | }
14 |
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | compileOptions {
22 | sourceCompatibility JavaVersion.VERSION_1_8
23 | targetCompatibility JavaVersion.VERSION_1_8
24 | }
25 | kotlinOptions {
26 | jvmTarget = '1.8'
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0"
32 | // api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.25.0'
33 |
34 | implementation 'androidx.appcompat:appcompat:1.4.1'
35 | implementation 'androidx.annotation:annotation:1.3.0'
36 |
37 | // RxFlow
38 | implementation "io.github.reactivecircus.flowbinding:flowbinding-android:1.2.0"
39 | implementation 'androidx.core:core-ktx:1.9.0'
40 | implementation 'com.google.android.material:material:1.9.0'
41 |
42 | // Lifecycle
43 | def lifecycle_version = "2.4.1"
44 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
45 |
46 | // Syntax Highlighting
47 | api("com.github.markusressel.KodeHighlighter:core:v3.0.0")
48 |
49 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
50 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
51 |
52 | // Zoom Layout Container
53 | api 'com.otaliastudios:zoomlayout:1.9.0'
54 |
55 | testImplementation 'junit:junit:4.13.2'
56 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
57 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
58 | }
59 |
60 | configurations.all {
61 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
62 | }
63 |
64 | // build a jar with source files
65 | task sourcesJar(type: Jar) {
66 | from android.sourceSets.main.java.srcDirs
67 | classifier = 'sources'
68 | }
69 |
70 | task dokkaJar(type: Jar, dependsOn: dokkaHtml) {
71 | classifier = 'javadoc'
72 | from dokkaHtml.outputDirectory
73 | }
74 |
75 | dokkaHtml.configure {
76 | dokkaSourceSets {
77 | named("main") {
78 | noAndroidSdkLink.set(false)
79 | }
80 | }
81 | }
82 |
83 | artifacts {
84 | archives sourcesJar
85 | archives dokkaJar
86 | }
87 |
--------------------------------------------------------------------------------
/ke/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/ke/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ke/src/main/java/de/markusressel/kodeeditor/library/extensions/Extensions.kt:
--------------------------------------------------------------------------------
1 | package de.markusressel.kodeeditor.library.extensions
2 |
3 | import android.content.Context
4 | import android.content.res.TypedArray
5 | import android.graphics.Bitmap
6 | import android.graphics.Canvas
7 | import android.graphics.Color
8 | import android.graphics.drawable.Drawable
9 | import android.util.TypedValue
10 | import android.view.View
11 | import androidx.annotation.AttrRes
12 | import androidx.annotation.ColorInt
13 | import androidx.annotation.StyleableRes
14 | import androidx.core.content.ContextCompat
15 | import androidx.core.view.ViewCompat
16 |
17 | /**
18 | * Get a color from this TypedArray or use the first default that is found
19 | *
20 | * @param context view context
21 | * @param defaultColor default if none of the styleable or attribute values was found
22 | * @param styleableRes styleable resource
23 | * @param attr theme attribute resource
24 | */
25 | @ColorInt
26 | fun TypedArray.getColor(context: Context, @ColorInt defaultColor: Int = Color.BLACK, @StyleableRes styleableRes: Int, @AttrRes vararg attr: Int): Int {
27 | return getColor(styleableRes, attr.find { context.getThemeAttrColor(it) != null }
28 | ?: defaultColor)
29 | }
30 |
31 | /**
32 | * Get Color from Theme attribute
33 | *
34 | * @param attr Attribute resource ID
35 | * @return Color as Int
36 | */
37 | @ColorInt
38 | fun Context.getThemeAttrColor(@AttrRes attr: Int): Int? {
39 | val typedValue = TypedValue()
40 | if (theme.resolveAttribute(attr, typedValue, true)) {
41 | if (typedValue.type >= TypedValue.TYPE_FIRST_INT && typedValue.type <= TypedValue.TYPE_LAST_INT) {
42 | return typedValue.data
43 | } else if (typedValue.type == TypedValue.TYPE_STRING) {
44 | return ContextCompat.getColor(this, typedValue.resourceId)
45 | }
46 | }
47 |
48 | return null
49 | }
50 |
51 | /**
52 | * Sets a view background without resetting it's padding
53 | *
54 | * @param background the background drawable to use (may be null)
55 | */
56 | fun View.setViewBackgroundWithoutResettingPadding(background: Drawable?) {
57 | val paddingBottom = this.paddingBottom
58 | val paddingStart = ViewCompat.getPaddingStart(this)
59 | val paddingEnd = ViewCompat.getPaddingEnd(this)
60 | val paddingTop = this.paddingTop
61 | ViewCompat.setBackground(this, background)
62 | ViewCompat.setPaddingRelative(this, paddingStart, paddingTop, paddingEnd, paddingBottom)
63 | }
64 |
65 | /**
66 | * Converts the given number to a px value assuming it is a dp value.
67 | *
68 | * @return px value
69 | */
70 | fun Number.dpToPx(context: Context): Float {
71 | return TypedValue.applyDimension(
72 | TypedValue.COMPLEX_UNIT_DIP,
73 | this.toFloat(),
74 | context.resources.displayMetrics)
75 | }
76 |
77 | /**
78 | * Renders a view to a bitmap
79 | *
80 | * @param dimensionLimit the maximum image dimension
81 | * @param backgroundColor background color in case the view doesn't have one
82 | * @return the rendered image or null if the view has no measured dimensions (yet)
83 | */
84 | fun View.createSnapshot(dimensionLimit: Number = 1F, backgroundColor: Int = Color.TRANSPARENT): Bitmap? {
85 | if (measuredWidth == 0 || measuredHeight == 0) {
86 | // the view has no dimensions so it can't be rendered
87 | return null
88 | }
89 |
90 | val limitAsFloat = dimensionLimit.toFloat()
91 |
92 | // select smaller scaling factor to match dimensionLimit
93 | val scaleFactor = Math.min(
94 | Math.min(
95 | limitAsFloat / measuredWidth,
96 | limitAsFloat / measuredHeight),
97 | 1F)
98 |
99 | // Define a bitmap with the target dimensions
100 | val returnedBitmap = Bitmap.createBitmap(
101 | (this.measuredWidth * scaleFactor).toInt(),
102 | (this.measuredHeight * scaleFactor).toInt(),
103 | Bitmap.Config.ARGB_8888)
104 |
105 | // bind a canvas to the bitmap
106 | Canvas(returnedBitmap).apply {
107 | scale(scaleFactor, scaleFactor)
108 | drawColor(backgroundColor)
109 | background?.draw(this)
110 | draw(this)
111 | }
112 |
113 | return returnedBitmap
114 | }
115 |
--------------------------------------------------------------------------------
/ke/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditText.kt:
--------------------------------------------------------------------------------
1 | package de.markusressel.kodeeditor.library.view
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.text.Layout
6 | import android.util.AttributeSet
7 | import android.util.Log
8 | import androidx.appcompat.widget.AppCompatEditText
9 | import de.markusressel.kodehighlighter.core.util.EditTextHighlighter
10 | import kotlinx.coroutines.*
11 | import kotlinx.coroutines.flow.catch
12 | import kotlinx.coroutines.flow.debounce
13 | import kotlinx.coroutines.flow.launchIn
14 | import kotlinx.coroutines.flow.onEach
15 | import reactivecircus.flowbinding.android.widget.textChanges
16 | import java.util.concurrent.TimeUnit
17 |
18 | /**
19 | * EditText modified for longer texts and support for syntax highlighting
20 | */
21 | class CodeEditText
22 | @JvmOverloads
23 | constructor(context: Context,
24 | attrs: AttributeSet? = null,
25 | defStyleAttr: Int = 0)
26 | : AppCompatEditText(context, attrs, defStyleAttr) {
27 |
28 | /**
29 | * The current syntax highlighter
30 | */
31 | var highlighter: EditTextHighlighter? = null
32 | set(value) {
33 | // clear any old style
34 | field?.clearAppliedStyles()
35 |
36 | // set new highlighter
37 | field = value
38 |
39 | // and initialize it
40 | initSyntaxHighlighter()
41 | }
42 |
43 | /**
44 | * Listener for selection changes
45 | */
46 | var selectionChangedListener: SelectionChangedListener? = null
47 |
48 | private var highlightingTimeout = 50L to TimeUnit.MILLISECONDS
49 | private var highlightingJob: Job? = null
50 |
51 | init {
52 | reInit()
53 | }
54 |
55 | private fun reInit() {
56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
57 | hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
58 | }
59 |
60 | initSyntaxHighlighter()
61 | isClickable = true
62 | isFocusableInTouchMode = true
63 | }
64 |
65 | @OptIn(FlowPreview::class)
66 | private fun initSyntaxHighlighter() {
67 | highlightingJob?.cancel("Reinitializing")
68 |
69 | highlighter?.let {
70 | refreshSyntaxHighlighting()
71 | highlightingJob = textChanges()
72 | .debounce(highlightingTimeout.second.toMillis(highlightingTimeout.first))
73 | .onEach {
74 | refreshSyntaxHighlighting()
75 | }
76 | .catch {
77 | Log.e(CodeTextView.TAG, "Error while refreshing syntax highlighting", it)
78 | }
79 | .launchIn(CoroutineScope(Job() + Dispatchers.Main))
80 | }
81 |
82 | }
83 |
84 | override fun setText(text: CharSequence?, type: BufferType?) {
85 | super.setText(text, type)
86 | refreshSyntaxHighlighting()
87 | }
88 |
89 | /**
90 | * Set the timeout before new text is highlighted after the user has stopped typing.
91 | *
92 | * @param timeout arbitrary value
93 | * @param timeUnit the time unit to use
94 | */
95 | @Suppress("unused")
96 | fun setHighlightingTimeout(timeout: Long, timeUnit: TimeUnit) {
97 | highlightingTimeout = timeout to timeUnit
98 | reInit()
99 | }
100 |
101 | /**
102 | * Get the current syntax highlighter timeout in milliseconds.
103 | *
104 | * @return timeout in milliseconds
105 | */
106 | @Suppress("unused")
107 | fun getHighlightingTimeout(): Long {
108 | return highlightingTimeout.second.toMillis(highlightingTimeout.first)
109 | }
110 |
111 | /**
112 | * Force a refresh of the syntax highlighting
113 | */
114 | @Synchronized
115 | fun refreshSyntaxHighlighting() {
116 | highlighter?.refreshHighlighting()
117 | ?: Log.w(TAG, "No syntax highlighter is set!")
118 | }
119 |
120 | override fun onSelectionChanged(selStart: Int, selEnd: Int) {
121 | super.onSelectionChanged(selStart, selEnd)
122 | selectionChangedListener?.onSelectionChanged(selStart, selEnd, hasSelection())
123 | }
124 |
125 | companion object {
126 | const val TAG = "CodeEditText"
127 | }
128 |
129 | }
--------------------------------------------------------------------------------
/ke/src/main/java/de/markusressel/kodeeditor/library/view/CodeEditorView.kt:
--------------------------------------------------------------------------------
1 | package de.markusressel.kodeeditor.library.view
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.util.AttributeSet
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.widget.TextView
9 | import androidx.annotation.CallSuper
10 | import androidx.annotation.StringRes
11 | import com.otaliastudios.zoom.ZoomApi
12 | import com.otaliastudios.zoom.ZoomLayout
13 | import de.markusressel.kodeeditor.library.R
14 | import de.markusressel.kodeeditor.library.extensions.getColor
15 | import de.markusressel.kodeeditor.library.extensions.setViewBackgroundWithoutResettingPadding
16 | import de.markusressel.kodehighlighter.core.LanguageRuleBook
17 | import de.markusressel.kodehighlighter.core.util.EditTextHighlighter
18 | import de.markusressel.kodehighlighter.core.util.StatefulSpannableHighlighter
19 |
20 | /**
21 | * Code Editor that allows pinch-to-zoom
22 | */
23 | open class CodeEditorView
24 | @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
25 | : ZoomLayout(context, attrs, defStyleAttr), SelectionChangedListener {
26 |
27 | /**
28 | * The actual text editor content
29 | */
30 | lateinit var codeEditText: CodeEditText
31 |
32 | /**
33 | * A text view for the non-editable state
34 | */
35 | lateinit var codeTextView: CodeTextView
36 |
37 | /**
38 | * The currently active syntax highlighter (if any)
39 | */
40 | var languageRuleBook: LanguageRuleBook?
41 | get() = codeEditText.highlighter?.languageRuleBook
42 | set(value) {
43 | if (value != null) {
44 | codeEditText.highlighter = EditTextHighlighter(codeEditText, value)
45 | codeTextView.highlighter = StatefulSpannableHighlighter(value, value.defaultColorScheme)
46 | } else {
47 | codeEditText.highlighter = null
48 | codeTextView.highlighter = null
49 | }
50 | }
51 |
52 | /**
53 | * Listener for selection changes
54 | */
55 | var selectionChangedListener: SelectionChangedListener? = null
56 |
57 | /**
58 | * The current text
59 | */
60 | var text: String
61 | get() = codeEditText.text?.toString() ?: ""
62 | set(value) {
63 | codeEditText.setText(value)
64 | codeTextView.text = value
65 | }
66 |
67 | /** The start index of the current selection */
68 | val selectionStart: Int
69 | get() {
70 | val activeView: TextView = if (editable) codeEditText else codeTextView
71 | return activeView.selectionStart
72 | }
73 |
74 | /** The end index of the current selection */
75 | val selectionEnd: Int
76 | get() {
77 | val activeView: TextView = if (editable) codeEditText else codeTextView
78 | return activeView.selectionEnd
79 | }
80 |
81 | /** True when a range is selected */
82 | val hasSelection: Boolean
83 | get() {
84 | val activeView: TextView = if (editable) codeEditText else codeTextView
85 | return activeView.hasSelection()
86 | }
87 |
88 | /**
89 | * Set the text in the editor
90 | *
91 | * @param text string resource of the new text to set
92 | */
93 | @Suppress("unused")
94 | fun setText(@StringRes text: Int) {
95 | this.text = context.getString(text)
96 | }
97 |
98 | /**
99 | * Controls whether the text is editable
100 | */
101 | var editable: Boolean
102 | get() = codeEditText.visibility == View.VISIBLE
103 | set(value) {
104 | if (value) {
105 | codeEditText.visibility = View.VISIBLE
106 | codeTextView.visibility = View.GONE
107 | } else {
108 | codeTextView.text = codeEditText.text
109 | codeEditText.visibility = View.GONE
110 | codeTextView.visibility = View.VISIBLE
111 | }
112 | }
113 |
114 | init {
115 | setHasClickableChildren(true)
116 | isFocusableInTouchMode = true
117 |
118 | inflateViews(LayoutInflater.from(context))
119 | readParameters(attrs, defStyleAttr)
120 |
121 | setListeners()
122 | }
123 |
124 | private fun readParameters(attrs: AttributeSet?, defStyleAttr: Int) {
125 | val a = context.obtainStyledAttributes(attrs, R.styleable.CodeEditorView, defStyleAttr, 0)
126 |
127 | val editTextBackgroundColor = a.getColor(context,
128 | defaultColor = Color.WHITE,
129 | styleableRes = R.styleable.CodeEditorView_ke_editor_backgroundColor,
130 | attr = intArrayOf(R.attr.ke_editor_backgroundColor,
131 | android.R.attr.windowBackground))
132 | codeEditText.setBackgroundColor(editTextBackgroundColor)
133 |
134 | val maxRealZoom = a.getFloat(R.styleable.CodeEditorView_ke_editor_maxZoom, DEFAULT_MAX_ZOOM)
135 | setMaxZoom(maxRealZoom, ZoomApi.TYPE_REAL_ZOOM)
136 |
137 | a.recycle()
138 | }
139 |
140 | private fun inflateViews(inflater: LayoutInflater) {
141 | inflater.inflate(R.layout.view_code_editor__inner_layout, this)
142 |
143 | codeEditText = findViewById(R.id.cev_editor_codeEditText)
144 | codeEditText.setViewBackgroundWithoutResettingPadding(null)
145 | codeEditText.post {
146 | codeEditText.setSelection(0)
147 | }
148 | codeEditText.selectionChangedListener = this
149 |
150 | codeTextView = findViewById(R.id.cev_editor_codeTextView)
151 | codeTextView.setViewBackgroundWithoutResettingPadding(null)
152 | codeTextView.selectionChangedListener = this
153 | }
154 |
155 | private var firstInit = true
156 |
157 | private fun setListeners() {
158 | addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
159 | if (firstInit) {
160 | firstInit = false
161 |
162 | setMinimumDimensions()
163 | }
164 | }
165 | }
166 |
167 | /**
168 | * Applies minimum dimensions for the [CodeEditText] and [CodeTextView] so that they always
169 | * fill up the parent.
170 | */
171 | private fun setMinimumDimensions() {
172 | val containerWidth = width - (paddingLeft + paddingRight)
173 | val containerHeight = height - (paddingTop + paddingBottom)
174 |
175 | val codeEditTextLayoutParams = (codeEditText.layoutParams as MarginLayoutParams)
176 | val minimumWidth = containerWidth + (codeEditTextLayoutParams.leftMargin + codeEditTextLayoutParams.rightMargin)
177 | val minimumHeight = containerHeight - (codeEditTextLayoutParams.topMargin + codeEditTextLayoutParams.bottomMargin)
178 |
179 | codeEditText.minWidth = minimumWidth
180 | codeTextView.minWidth = minimumWidth
181 |
182 | codeEditText.minHeight = minimumHeight
183 | codeTextView.minHeight = minimumHeight
184 | }
185 |
186 | /**
187 | * @return the current count of lines of code in the editor.
188 | */
189 | fun getLineCount(): Long {
190 | val currentText = codeEditText.text
191 | return if (currentText != null) {
192 | currentText.count { it == '\n' } + 1L
193 | } else {
194 | 0L
195 | }
196 | }
197 |
198 | /**
199 | * Called when the selection changes.
200 | * Override this if you are interested in such events.
201 | */
202 | @CallSuper
203 | override fun onSelectionChanged(start: Int, end: Int, hasSelection: Boolean) {
204 | selectionChangedListener?.onSelectionChanged(start, end, hasSelection)
205 | }
206 |
207 | companion object {
208 | const val TAG = "CodeEditorView"
209 |
210 | const val DEFAULT_MAX_ZOOM = 10F
211 | }
212 |
213 | }
214 |
--------------------------------------------------------------------------------
/ke/src/main/java/de/markusressel/kodeeditor/library/view/CodeTextView.kt:
--------------------------------------------------------------------------------
1 | package de.markusressel.kodeeditor.library.view
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.text.Layout
6 | import android.text.Spannable
7 | import android.text.SpannableString
8 | import android.util.AttributeSet
9 | import android.util.Log
10 | import androidx.appcompat.widget.AppCompatTextView
11 | import de.markusressel.kodehighlighter.core.util.StatefulSpannableHighlighter
12 | import kotlinx.coroutines.*
13 | import kotlinx.coroutines.flow.catch
14 | import kotlinx.coroutines.flow.debounce
15 | import kotlinx.coroutines.flow.launchIn
16 | import kotlinx.coroutines.flow.onEach
17 | import reactivecircus.flowbinding.android.widget.textChanges
18 | import java.util.concurrent.TimeUnit
19 |
20 | /**
21 | * TextView modified for longer texts and support for syntax highlighting
22 | */
23 | class CodeTextView
24 | @JvmOverloads
25 | constructor(context: Context,
26 | attrs: AttributeSet? = null,
27 | defStyleAttr: Int = 0)
28 | : AppCompatTextView(context, attrs, defStyleAttr) {
29 |
30 | /**
31 | * The current syntax highlighter
32 | */
33 | var highlighter: StatefulSpannableHighlighter? = null
34 | set(value) {
35 | // clear any old style
36 | field?.clearAppliedStyles(text as Spannable)
37 |
38 | // set new highlighter
39 | field = value
40 |
41 | // and initialize it
42 | initSyntaxHighlighter()
43 | }
44 |
45 | /**
46 | * Listener for selection changes
47 | */
48 | var selectionChangedListener: SelectionChangedListener? = null
49 |
50 | private var highlightingTimeout = 50L to TimeUnit.MILLISECONDS
51 | private var highlightingJob: Job? = null
52 |
53 | init {
54 | CoroutineScope(Job() + Dispatchers.Main).launch {
55 | reInit()
56 | }
57 | }
58 |
59 | private fun reInit() {
60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
61 | hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
62 | }
63 |
64 | initSyntaxHighlighter()
65 | }
66 |
67 | @OptIn(FlowPreview::class)
68 | private fun initSyntaxHighlighter() {
69 | highlightingJob?.cancel("Reinitializing")
70 |
71 | if (highlighter != null) {
72 | CoroutineScope(Dispatchers.Main).launch {
73 | refreshSyntaxHighlighting()
74 | }
75 |
76 | highlightingJob = textChanges()
77 | .debounce(highlightingTimeout.second.toMillis(highlightingTimeout.first))
78 | .onEach {
79 | refreshSyntaxHighlighting()
80 | }
81 | .catch {
82 | Log.e(TAG, "Error while refreshing syntax highlighting", it)
83 | }
84 | .launchIn(CoroutineScope(Job() + Dispatchers.Main))
85 | }
86 | }
87 |
88 | override fun setText(text: CharSequence?, type: BufferType?) {
89 | super.setText(SpannableString.valueOf(text), BufferType.SPANNABLE)
90 | CoroutineScope(Job() + Dispatchers.Default).launch {
91 | refreshSyntaxHighlighting()
92 | }
93 | }
94 |
95 | /**
96 | * Set the timeout before new text is highlighted after the user has stopped typing.
97 | *
98 | * @param timeout arbitrary value
99 | * @param timeUnit the time unit to use
100 | */
101 | @Suppress("unused")
102 | fun setHighlightingTimeout(timeout: Long, timeUnit: TimeUnit) {
103 | highlightingTimeout = timeout to timeUnit
104 | reInit()
105 | }
106 |
107 | /**
108 | * Get the current syntax highlighter timeout in milliseconds.
109 | *
110 | * @return timeout in milliseconds
111 | */
112 | @Suppress("unused")
113 | fun getHighlightingTimeout(): Long {
114 | return highlightingTimeout.second.toMillis(highlightingTimeout.first)
115 | }
116 |
117 | /**
118 | * Force a refresh of the syntax highlighting
119 | */
120 | @Synchronized
121 | fun refreshSyntaxHighlighting() {
122 | if (highlighter == null) {
123 | Log.w(TAG, "No syntax highlighter is set!")
124 | }
125 |
126 | highlighter?.apply {
127 | CoroutineScope(Dispatchers.Main).launch {
128 | highlight(text as Spannable)
129 | }
130 | }
131 | }
132 |
133 | override fun onSelectionChanged(selStart: Int, selEnd: Int) {
134 | super.onSelectionChanged(selStart, selEnd)
135 | selectionChangedListener?.onSelectionChanged(selStart, selEnd, hasSelection())
136 | }
137 |
138 | companion object {
139 | const val TAG = "CodeTextView"
140 | }
141 |
142 | }
--------------------------------------------------------------------------------
/ke/src/main/java/de/markusressel/kodeeditor/library/view/SelectionChangedListener.kt:
--------------------------------------------------------------------------------
1 | package de.markusressel.kodeeditor.library.view
2 |
3 | /**
4 | * Interface for a listener of selection changes
5 | */
6 | interface SelectionChangedListener {
7 |
8 | /**
9 | * Called when the selection changes
10 | *
11 | * @param start selection start index
12 | * @param end selection end index
13 | * @param hasSelection true when a range is selected (start != end)
14 | */
15 | fun onSelectionChanged(start: Int, end: Int, hasSelection: Boolean)
16 |
17 | }
--------------------------------------------------------------------------------
/ke/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/ke/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/ke/src/main/res/layout/layout_code_editor__main_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/ke/src/main/res/layout/view_code_editor__divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ke/src/main/res/layout/view_code_editor__inner_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
25 |
26 |
36 |
37 |
--------------------------------------------------------------------------------
/ke/src/main/res/layout/view_code_editor__linenumbers.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
25 |
26 |
--------------------------------------------------------------------------------
/ke/src/main/res/layout/view_code_editor__minimap.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
26 |
27 |
31 |
32 |
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/ke/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taamarin/box.manager/09f5683086e73d66beaa6725dcaf64b19d088ed5/ke/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/ke/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/ke/src/main/res/values/attributes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/ke/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/ke/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 36dp
4 | 4dp
5 | 16sp
6 |
--------------------------------------------------------------------------------
/ke/src/main/res/values/library_kodeeditor_strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Markus Ressel
7 | http://markusressel.de
8 |
9 | KodeEditor
10 | A simple code editor with syntax highlighting and pinch to zoom
11 | @string/library_kodeeditor_repositoryLink
12 | 2.1.1
13 |
14 | true
15 | https://github.com/markusressel/KodeEditor
16 |
17 | de.markusressel.kodeeditor
18 |
19 | mit
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ke/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | KodeEditor
3 |
--------------------------------------------------------------------------------
/ke/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { url "https://jitpack.io" }
14 | }
15 | }
16 |
17 | rootProject.name = "BoxForRoot"
18 | include ':app'
19 | include ':ke'
20 |
--------------------------------------------------------------------------------