├── .circleci └── config.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── libs │ └── README.md │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── io │ │ └── github │ │ └── trojan_gfw │ │ └── igniter │ │ └── proxy │ │ └── aidl │ │ ├── ITrojanService.aidl │ │ └── ITrojanServiceCallback.aidl │ ├── cpp │ ├── CMakeLists.txt │ └── jni-helper.cpp │ ├── ic_launcher-web.png │ ├── java │ └── io │ │ └── github │ │ └── trojan_gfw │ │ └── igniter │ │ ├── AboutActivity.java │ │ ├── ClashHelper.java │ │ ├── Globals.java │ │ ├── ILogFunction.java │ │ ├── IgniterApplication.java │ │ ├── JNIHelper.java │ │ ├── LogHelper.java │ │ ├── MainActivity.java │ │ ├── PathHelper.java │ │ ├── ProxyService.java │ │ ├── TextViewListener.java │ │ ├── TrojanConfig.java │ │ ├── TrojanHelper.java │ │ ├── TrojanURLHelper.java │ │ ├── common │ │ ├── app │ │ │ ├── BaseAppCompatActivity.java │ │ │ └── BaseFragment.java │ │ ├── constants │ │ │ └── Constants.java │ │ ├── dialog │ │ │ └── LoadingDialog.java │ │ ├── mvp │ │ │ ├── BasePresenter.java │ │ │ └── BaseView.java │ │ ├── os │ │ │ ├── IThreads.java │ │ │ ├── PreferencesProvider.java │ │ │ ├── Task.java │ │ │ └── Threads.java │ │ └── utils │ │ │ ├── PermissionUtils.java │ │ │ ├── PreferenceUtils.java │ │ │ ├── ProcessUtils.java │ │ │ └── SnackbarUtils.java │ │ ├── connection │ │ ├── TestConnection.java │ │ └── TrojanConnection.java │ │ ├── exempt │ │ ├── activity │ │ │ └── ExemptAppActivity.java │ │ ├── adapter │ │ │ └── AppInfoAdapter.java │ │ ├── contract │ │ │ └── ExemptAppContract.java │ │ ├── data │ │ │ ├── AppInfo.java │ │ │ ├── ExemptAppDataManager.java │ │ │ └── ExemptAppDataSource.java │ │ ├── fragment │ │ │ └── ExemptAppFragment.java │ │ └── presenter │ │ │ └── ExemptAppPresenter.java │ │ ├── initializer │ │ ├── Initializer.java │ │ ├── InitializerHelper.java │ │ ├── MainInitializer.java │ │ ├── ProxyInitializer.java │ │ └── ToolInitializer.java │ │ ├── qrcode │ │ └── ScanQRCodeActivity.java │ │ ├── servers │ │ ├── activity │ │ │ └── ServerListActivity.java │ │ ├── contract │ │ │ └── ServerListContract.java │ │ ├── data │ │ │ ├── ServerListDataManager.java │ │ │ └── ServerListDataSource.java │ │ ├── fragment │ │ │ ├── ServerListAdapter.java │ │ │ └── ServerListFragment.java │ │ └── presenter │ │ │ └── ServerListPresenter.java │ │ └── tile │ │ ├── IgniterTileService.java │ │ └── ProxyHelper.java │ └── res │ ├── drawable-hdpi │ ├── ic_action_link.png │ ├── ic_action_name.png │ ├── ic_save.png │ ├── ic_search.png │ ├── ic_tile.png │ └── qr_code.png │ ├── drawable-mdpi │ ├── ic_action_link.png │ ├── ic_action_name.png │ ├── ic_save.png │ ├── ic_search.png │ ├── ic_tile.png │ └── qr_code.png │ ├── drawable-xhdpi │ ├── ic_action_link.png │ ├── ic_action_name.png │ ├── ic_save.png │ ├── ic_search.png │ ├── ic_tile.png │ └── qr_code.png │ ├── drawable-xxhdpi │ ├── ic_action_link.png │ ├── ic_action_name.png │ ├── ic_save.png │ ├── ic_search.png │ ├── ic_tile.png │ └── qr_code.png │ ├── drawable-xxxhdpi │ ├── ic_action_link.png │ ├── ic_action_name.png │ ├── ic_save.png │ ├── ic_search.png │ ├── ic_tile.png │ └── qr_code.png │ ├── drawable │ ├── common_round_rect_white_bg.xml │ └── qr_code.png │ ├── layout │ ├── activity_about.xml │ ├── activity_exempt_app.xml │ ├── activity_main.xml │ ├── activity_scan_qrcode.xml │ ├── activity_server_list.xml │ ├── dialog_loading.xml │ ├── fragment_exempt_app.xml │ ├── fragment_server_list.xml │ ├── item_app_info.xml │ └── item_server.xml │ ├── menu │ ├── menu_exempt_app.xml │ ├── menu_main.xml │ └── menu_server_list.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── raw │ ├── cacert.pem │ ├── clash_config.yaml │ └── country.mmdb │ ├── values │ ├── arrays.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── root_preferences.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/code 5 | docker: 6 | - image: circleci/android:api-28 7 | environment: 8 | JVM_OPTS: -Xmx4G 9 | steps: 10 | - checkout 11 | - run: 12 | name: Pull Submodules 13 | command: git submodule update --init --recursive --remote 14 | - run: 15 | name: Download Go Libs 16 | command: curl -L https://github.com/trojan-gfw/igniter-go-libs/releases/download/1.17/golibs.aar > app/src/libs/golibs.aar 17 | - restore_cache: 18 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 19 | - run: 20 | name: Download Dependencies 21 | command: ./gradlew androidDependencies 22 | - save_cache: 23 | paths: 24 | - ~/.gradle 25 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 26 | - run: 27 | name: Run Tests 28 | command: ./gradlew lint test 29 | - store_test_results: 30 | path: app/build/test-results 31 | destination: test-results/ 32 | - run: 33 | name: Initial build 34 | command: ./gradlew clean assembleRelease --no-daemon --stacktrace 35 | - store_artifacts: 36 | path: app/build/outputs/apk/ 37 | destination: apks/ 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # android lib 12 | *.aar 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | out/ 18 | app/.cxx/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | release/ 24 | debug/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | .project 38 | .settings/ 39 | 40 | # Android Studio captures folder 41 | captures/ 42 | 43 | # IntelliJ 44 | *.iml 45 | .idea/ 46 | 47 | # Keystore files 48 | # Uncomment the following line if you do not want to check your keystore files in. 49 | #*.jks 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json 56 | 57 | # Freeline 58 | freeline.py 59 | freeline/ 60 | freeline_project_description.json 61 | 62 | # fastlane 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | fastlane/readme.md 68 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/cpp/trojan"] 2 | path = app/src/main/cpp/trojan 3 | url = https://github.com/trojan-gfw/trojan.git 4 | [submodule "app/src/main/cpp/libs"] 5 | path = app/src/main/cpp/libs 6 | url = https://github.com/trojan-gfw/igniter-libs.git 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # igniter 2 | 3 | [![CircleCI](https://circleci.com/gh/trojan-gfw/igniter/tree/master.svg?style=svg)](https://circleci.com/gh/trojan-gfw/igniter/tree/master) 4 | 5 | 6 | A trojan client for Android. 7 | 8 | Get it on Google Play 9 | 10 | 11 | 12 | ## Thanks 13 | 14 | * Dreamacro/clash [GPLv3](https://github.com/Dreamacro/clash/blob/master/LICENSE) 15 | * eycorsican/go-tun2socks [MIT](https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE) 16 | * bingoogolapple/BGAQRCode-Android [Apache License 2.0](https://github.com/bingoogolapple/BGAQRCode-Android) 17 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | ext.versionMajor = 0 4 | ext.versionMinor = 9 5 | ext.versionPatch = 5 6 | ext.versionClassifier = "beta" // or null 7 | ext.isSnapshot = true // set to false when publishing new releases 8 | ext.minimumSdkVersion = 21 9 | ext.targetSdkVersion = 28 10 | 11 | private Integer GenerateVersionCode() { 12 | return ext.minimumSdkVersion * 10000000 + ext.versionMajor * 10000 + ext.versionMinor * 100 + ext.versionPatch 13 | } 14 | 15 | private String GenerateVersionName() { 16 | String versionName = "${ext.versionMajor}.${ext.versionMinor}.${ext.versionPatch}" 17 | if (ext.versionClassifier != null) { 18 | versionName += "-" + ext.versionClassifier 19 | } 20 | 21 | if (ext.isSnapshot) { 22 | versionName += "-" + "SNAPSHOT" 23 | } 24 | return versionName; 25 | } 26 | 27 | android { 28 | ndkVersion "21.1.6352462" 29 | compileSdkVersion 28 30 | 31 | applicationVariants.all { variant -> 32 | variant.resValue "string", "versionName", variant.versionName 33 | } 34 | 35 | defaultConfig { 36 | applicationId "io.github.trojan_gfw.igniter" 37 | minSdkVersion project.ext.minimumSdkVersion 38 | targetSdkVersion project.ext.targetSdkVersion 39 | versionCode GenerateVersionCode() 40 | versionName GenerateVersionName() 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | ndk { 43 | abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" 44 | } 45 | externalNativeBuild { 46 | cmake { 47 | arguments "-DANDROID_CPP_FEATURES=rtti exceptions" 48 | } 49 | } 50 | archivesBaseName = "$applicationId-v$versionName-$versionCode" 51 | } 52 | buildTypes { 53 | debug { 54 | applicationIdSuffix '.debug' 55 | debuggable true 56 | } 57 | release { 58 | minifyEnabled false 59 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 60 | } 61 | } 62 | externalNativeBuild { 63 | cmake { 64 | path file('src/main/cpp/CMakeLists.txt') 65 | } 66 | } 67 | } 68 | 69 | repositories { 70 | flatDir { 71 | dirs 'src/libs' 72 | } 73 | } 74 | 75 | dependencies { 76 | implementation 'com.google.android.material:material:1.2.0-alpha06' 77 | implementation 'androidx.appcompat:appcompat:1.2.0-beta01' 78 | implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03' 79 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' 80 | implementation 'androidx.core:core:1.3.0-rc01' 81 | implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7' 82 | implementation 'androidx.preference:preference:1.1.1' 83 | testImplementation 'junit:junit:4.13' 84 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 85 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 86 | 87 | api(name: 'golibs', ext: 'aar') 88 | } 89 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/src/libs/README.md: -------------------------------------------------------------------------------- 1 | put golibs.aar here. 2 | 3 | ref: 4 | - https://github.com/trojan-gfw/igniter-go-libs 5 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 25 | 28 | 31 | 32 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/aidl/io/github/trojan_gfw/igniter/proxy/aidl/ITrojanService.aidl: -------------------------------------------------------------------------------- 1 | // ITrojanService.aidl 2 | package io.github.trojan_gfw.igniter.proxy.aidl; 3 | import io.github.trojan_gfw.igniter.proxy.aidl.ITrojanServiceCallback; 4 | // Declare any non-default types here with import statements 5 | 6 | interface ITrojanService { 7 | int getState(); 8 | void testConnection(String testUrl); 9 | void showDevelopInfoInLogcat(); 10 | oneway void registerCallback(in ITrojanServiceCallback callback); 11 | oneway void unregisterCallback(in ITrojanServiceCallback callback); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/aidl/io/github/trojan_gfw/igniter/proxy/aidl/ITrojanServiceCallback.aidl: -------------------------------------------------------------------------------- 1 | // ITrojanServiceCallback.aidl 2 | package io.github.trojan_gfw.igniter.proxy.aidl; 3 | 4 | // Declare any non-default types here with import statements 5 | 6 | interface ITrojanServiceCallback { 7 | void onStateChanged(int state, String msg); 8 | void onTestResult(String testUrl, boolean connected, long delay, String error); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | project(igniter) 3 | add_definitions(-DENABLE_ANDROID_LOG) 4 | # As long as we use OpenSSL 1.1.1, we are safe 5 | add_definitions(-DENABLE_TLS13_CIPHERSUITES) 6 | find_library( # Sets the name of the path variable. 7 | androidLogLib 8 | 9 | # Specifies the name of the NDK library that 10 | # you want CMake to locate. 11 | log) 12 | add_library(trojan 13 | trojan/src/core/authenticator.cpp 14 | trojan/src/core/config.cpp 15 | trojan/src/core/log.cpp 16 | trojan/src/core/service.cpp 17 | trojan/src/core/version.cpp 18 | trojan/src/proto/socks5address.cpp 19 | trojan/src/proto/trojanrequest.cpp 20 | trojan/src/proto/udppacket.cpp 21 | trojan/src/session/clientsession.cpp 22 | trojan/src/session/forwardsession.cpp 23 | trojan/src/session/natsession.cpp 24 | trojan/src/session/serversession.cpp 25 | trojan/src/session/session.cpp 26 | trojan/src/session/udpforwardsession.cpp 27 | trojan/src/ssl/ssldefaults.cpp 28 | trojan/src/ssl/sslsession.cpp) 29 | target_include_directories(trojan PRIVATE 30 | ${CMAKE_SOURCE_DIR}/libs/include 31 | ${CMAKE_SOURCE_DIR}/libs/include/${ANDROID_ABI} 32 | ${CMAKE_SOURCE_DIR}/trojan/src) 33 | target_link_libraries(trojan 34 | ${CMAKE_SOURCE_DIR}/libs/lib/${ANDROID_ABI}/libboost_program_options.a 35 | ${CMAKE_SOURCE_DIR}/libs/lib/${ANDROID_ABI}/libboost_system.a 36 | ${CMAKE_SOURCE_DIR}/libs/lib/${ANDROID_ABI}/libssl.a 37 | ${CMAKE_SOURCE_DIR}/libs/lib/${ANDROID_ABI}/libcrypto.a 38 | ${androidLogLib}) 39 | add_library(jni-helper SHARED jni-helper.cpp) 40 | target_include_directories(jni-helper PRIVATE 41 | ${CMAKE_SOURCE_DIR}/trojan/src 42 | ${CMAKE_SOURCE_DIR}/libn2t/src 43 | ${CMAKE_SOURCE_DIR}/libs/include 44 | ${CMAKE_SOURCE_DIR}/libs/include/${ANDROID_ABI}) 45 | target_link_libraries(jni-helper trojan) 46 | -------------------------------------------------------------------------------- /app/src/main/cpp/jni-helper.cpp: -------------------------------------------------------------------------------- 1 | #include "jni.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | using namespace std; 8 | 9 | 10 | static thread *trojanThread = nullptr; 11 | static Config *trojanConfig = nullptr; 12 | static Service *trojanService = nullptr; 13 | 14 | 15 | static void startTrojan(const string &config) 16 | { 17 | trojanConfig = new Config(); 18 | trojanConfig->load(config); 19 | trojanService = new Service(*trojanConfig); 20 | trojanService->run(); 21 | } 22 | 23 | 24 | 25 | extern "C" { 26 | JNIEXPORT void JNICALL Java_io_github_trojan_1gfw_igniter_JNIHelper_trojan(JNIEnv *env, jclass, jstring config) { 27 | if (trojanThread != nullptr) 28 | return; 29 | const char *s = env->GetStringUTFChars(config, 0); 30 | string a(s); 31 | env->ReleaseStringUTFChars(config, s); 32 | trojanThread = new thread(startTrojan, a); 33 | } 34 | 35 | 36 | JNIEXPORT void JNICALL Java_io_github_trojan_1gfw_igniter_JNIHelper_stop(JNIEnv *env, jclass) { 37 | 38 | if (trojanThread != nullptr) { 39 | trojanService->stop(); 40 | trojanThread->join(); 41 | delete trojanService; 42 | delete trojanConfig; 43 | delete trojanThread; 44 | trojanThread = nullptr; 45 | } 46 | } 47 | } 48 | 49 | jint JNI_OnLoad(JavaVM* vm, void* reserved) 50 | { 51 | return JNI_VERSION_1_6; 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import androidx.preference.PreferenceFragmentCompat; 9 | 10 | public class AboutActivity extends AppCompatActivity { 11 | 12 | public static Intent create(Context context) { 13 | return new Intent(context, AboutActivity.class); 14 | } 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_about); 20 | getSupportFragmentManager() 21 | .beginTransaction() 22 | .replace(R.id.settings, new SettingsFragment()) 23 | .commit(); 24 | } 25 | 26 | public static class SettingsFragment extends PreferenceFragmentCompat { 27 | @Override 28 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 29 | setPreferencesFromResource(R.xml.root_preferences, rootKey); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/ClashHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileOutputStream; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class ClashHelper { 10 | 11 | private static final String TAG = "ClashConfig"; 12 | 13 | // In general, we capture only one group for replacement 14 | public static final Pattern clashSocksPortPattern = Pattern.compile("^socks.*port:\\s+(\\d+)", Pattern.MULTILINE); 15 | public static final Pattern trojanPortPattern = Pattern.compile("trojan.*socks.*port:\\s+(\\d+)", Pattern.MULTILINE); 16 | 17 | /* 18 | * Generate Clash running configuration file according to the template file 19 | */ 20 | public static void ChangeClashConfig(String clashConfigPath, long trojanPort, long clashSocksPort) { 21 | File tmpClashConfigFile = new File(clashConfigPath + ".tmp"); 22 | File clashConfigFile = new File(clashConfigPath); 23 | try { 24 | String str; 25 | try (FileInputStream fis = new FileInputStream(clashConfigFile)) { 26 | long origClashConfigLen = clashConfigFile.length(); 27 | byte[] content = new byte[(int) origClashConfigLen]; 28 | if (fis.read(content) != origClashConfigLen) { 29 | LogHelper.e(TAG, "fail to read full content of clash config file"); 30 | } 31 | str = new String(content); 32 | } 33 | 34 | String clashSocksPortStr = String.valueOf(clashSocksPort); 35 | String trojanPortStr = String.valueOf(trojanPort); 36 | str = replaceGroup(clashSocksPortPattern, str, 1, clashSocksPortStr); 37 | str = replaceGroup(trojanPortPattern, str, 1, trojanPortStr); 38 | 39 | try (FileOutputStream fos = new FileOutputStream(tmpClashConfigFile)) { 40 | fos.write(str.getBytes()); 41 | } 42 | 43 | if (!clashConfigFile.delete()) { 44 | LogHelper.e(TAG, "fail to delete old clash config file"); 45 | } 46 | if (!tmpClashConfigFile.renameTo(clashConfigFile)) { 47 | LogHelper.e(TAG, "fail to rename tmp clash config file"); 48 | } 49 | 50 | } catch (Exception e) { 51 | e.printStackTrace(); 52 | } 53 | } 54 | 55 | public static void ShowConfig(String clashConfigPath) { 56 | File file = new File(clashConfigPath); 57 | 58 | try { 59 | try (FileInputStream fis = new FileInputStream(file)) { 60 | StringBuilder sb = new StringBuilder(); 61 | byte[] content = new byte[(int) file.length()]; 62 | fis.read(content); 63 | sb.append("\r\n"); 64 | sb.append(new String(content)); 65 | LogHelper.v(TAG, sb.toString()); 66 | } 67 | } catch (Exception e) { 68 | e.printStackTrace(); 69 | } 70 | } 71 | 72 | public static String replaceGroup(Pattern regex, String source, 73 | int groupToReplace, String replacement) throws Exception { 74 | return replaceGroup(regex, source, groupToReplace, 1, replacement); 75 | } 76 | 77 | public static String replaceGroup(Pattern regex, String source, 78 | int groupToReplace, int groupOccurrence, String replacement) throws Exception { 79 | Matcher m = regex.matcher(source); 80 | for (int i = 0; i < groupOccurrence; i++) { 81 | if (!m.find()) 82 | throw new Exception("Pattern not found"); // pattern not met, may also throw an exception here 83 | } 84 | return new StringBuilder(source) 85 | .replace(m.start(groupToReplace), m.end(groupToReplace), replacement) 86 | .toString(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/Globals.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | 6 | import java.io.File; 7 | 8 | public class Globals { 9 | 10 | private static String cacheDir; 11 | private static String filesDir; 12 | private static String externalFilesDir; 13 | private static TrojanConfig trojanConfigInstance; 14 | 15 | public static void Init(Context ctx) { 16 | cacheDir = ctx.getCacheDir().getAbsolutePath(); 17 | filesDir = ctx.getFilesDir().getAbsolutePath(); 18 | File externalDocDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); 19 | File igniterExternalFileDir = new File(externalDocDir, "igniter"); 20 | externalFilesDir = igniterExternalFileDir.getAbsolutePath(); 21 | trojanConfigInstance = new TrojanConfig(); 22 | trojanConfigInstance.setCaCertPath(Globals.getCaCertPath()); 23 | } 24 | 25 | public static String getCaCertPath() { 26 | return PathHelper.combine(cacheDir, "cacert.pem"); 27 | } 28 | 29 | public static String getCountryMmdbPath() { 30 | return PathHelper.combine(filesDir, "Country.mmdb"); 31 | } 32 | 33 | public static String getClashConfigPath() { 34 | return PathHelper.combine(filesDir, "config.yaml"); 35 | } 36 | 37 | public static String getTrojanConfigPath() { 38 | return PathHelper.combine(filesDir, "config.json"); 39 | } 40 | 41 | public static String getTrojanConfigListPath() { 42 | return PathHelper.combine(filesDir, "config_list.json"); 43 | } 44 | 45 | public static String getPreferencesFilePath() { 46 | return PathHelper.combine(filesDir, "preferences.txt"); 47 | } 48 | 49 | public static String getExemptedAppListPath() { 50 | return PathHelper.combine(externalFilesDir, "exempted_app_list.txt"); 51 | } 52 | 53 | public static void setTrojanConfigInstance(TrojanConfig config) { 54 | trojanConfigInstance = config; 55 | } 56 | 57 | public static TrojanConfig getTrojanConfigInstance() { 58 | return trojanConfigInstance; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/ILogFunction.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | public interface ILogFunction { 4 | public int output(String tag, String msg); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/IgniterApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import io.github.trojan_gfw.igniter.initializer.InitializerHelper; 7 | 8 | public class IgniterApplication extends Application { 9 | @Override 10 | protected void attachBaseContext(Context base) { 11 | super.attachBaseContext(base); 12 | InitializerHelper.runInit(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/JNIHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | public class JNIHelper { 4 | static { 5 | System.loadLibrary("jni-helper"); 6 | } 7 | 8 | public static native void trojan(String config); 9 | 10 | public static native void stop(); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/LogHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import android.util.Log; 4 | 5 | public final class LogHelper { 6 | 7 | // Logcat is line-buffered 8 | 9 | private static final int maxLogSize = 1000; 10 | 11 | private static final ILogFunction _v = new ILogFunction() { 12 | @Override 13 | public int output(String tag, String msg) { 14 | return Log.v(tag, msg); 15 | } 16 | }; 17 | private static final ILogFunction _d = new ILogFunction() { 18 | @Override 19 | public int output(String tag, String msg) { 20 | return Log.d(tag, msg); 21 | } 22 | }; 23 | 24 | private static final ILogFunction _i = new ILogFunction() { 25 | @Override 26 | public int output(String tag, String msg) { 27 | return Log.i(tag, msg); 28 | } 29 | }; 30 | 31 | private static final ILogFunction _w = new ILogFunction() { 32 | @Override 33 | public int output(String tag, String msg) { 34 | return Log.w(tag, msg); 35 | } 36 | }; 37 | 38 | private static final ILogFunction _e = new ILogFunction() { 39 | @Override 40 | public int output(String tag, String msg) { 41 | return Log.e(tag, msg); 42 | } 43 | }; 44 | 45 | private static void UnderlyingLog(String veryLongString, ILogFunction func, String TAG) { 46 | 47 | if (veryLongString.length() < maxLogSize) { 48 | func.output(TAG, veryLongString); 49 | return; 50 | } 51 | 52 | for (int i = 0; i <= veryLongString.length() / maxLogSize; i++) { 53 | int start = i * maxLogSize; 54 | int end = (i + 1) * maxLogSize; 55 | end = Math.min(end, veryLongString.length()); 56 | func.output(TAG, veryLongString.substring(start, end)); 57 | } 58 | } 59 | 60 | public static void v(String tag, String msg) { 61 | UnderlyingLog(msg, _v, tag); 62 | } 63 | 64 | public static void d(String tag, String msg) { 65 | UnderlyingLog(msg, _d, tag); 66 | } 67 | 68 | public static void i(String tag, String msg) { 69 | UnderlyingLog(msg, _i, tag); 70 | } 71 | 72 | public static void w(String tag, String msg) { 73 | UnderlyingLog(msg, _w, tag); 74 | } 75 | 76 | public static void e(String tag, String msg) { 77 | UnderlyingLog(msg, _e, tag); 78 | } 79 | 80 | public static void showDevelopInfoInLogcat() { 81 | util.Util.logGoRoutineCount(); 82 | util.Util.logGoroutineStackTrace(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/PathHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import java.io.File; 4 | 5 | public class PathHelper { 6 | public static String combine(String... paths) { 7 | File file = new File(paths[0]); 8 | 9 | for (int i = 1; i < paths.length; i++) { 10 | file = new File(file, paths[i]); 11 | } 12 | 13 | return file.getPath(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/TextViewListener.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import android.text.Editable; 4 | import android.text.TextWatcher; 5 | 6 | 7 | /** 8 | * Text view listener which splits the update text event in four parts: 9 | *
    10 | *
  • The text placed before the updated part.
  • 11 | *
  • The old text in the updated part.
  • 12 | *
  • The new text in the updated part.
  • 13 | *
  • The text placed after the updated part.
  • 14 | *
15 | * 16 | * myEditText.addTextChangedListener(new TextViewListener() { 17 | * \@Override 18 | * protected void onTextChanged(String before, String old, String aNew, String after) { 19 | * // intuitive usation of parametters 20 | * String completeOldText = before + old + after; 21 | * String completeNewText = before + aNew + after; 22 | * 23 | * // update TextView 24 | * startUpdates(); // to prevent infinite loop. 25 | * myEditText.setText(myNewText); 26 | * endUpdates(); 27 | * } 28 | * } 29 | * 30 | * Created by Jeremy B. 31 | */ 32 | 33 | public abstract class TextViewListener implements TextWatcher { 34 | /** 35 | * Unchanged sequence which is placed before the updated sequence. 36 | */ 37 | private String _before; 38 | 39 | /** 40 | * Updated sequence before the update. 41 | */ 42 | private String _old; 43 | 44 | /** 45 | * Updated sequence after the update. 46 | */ 47 | private String _new; 48 | 49 | /** 50 | * Unchanged sequence which is placed after the updated sequence. 51 | */ 52 | private String _after; 53 | 54 | /** 55 | * Indicates when changes are made from within the listener, should be omitted. 56 | */ 57 | private boolean _ignore = false; 58 | 59 | @Override 60 | public void beforeTextChanged(CharSequence sequence, int start, int count, int after) { 61 | _before = sequence.subSequence(0,start).toString(); 62 | _old = sequence.subSequence(start, start+count).toString(); 63 | _after = sequence.subSequence(start+count, sequence.length()).toString(); 64 | } 65 | 66 | @Override 67 | public void onTextChanged(CharSequence sequence, int start, int before, int count) { 68 | _new = sequence.subSequence(start, start+count).toString(); 69 | } 70 | 71 | @Override 72 | public void afterTextChanged(Editable sequence) { 73 | if (_ignore) 74 | return; 75 | 76 | onTextChanged(_before, _old, _new, _after); 77 | } 78 | 79 | /** 80 | * Triggered method when the text in the text view has changed. 81 | *
82 | * You can apply changes to the text view from this method 83 | * with the condition to call {@link #startUpdates()} before any update, 84 | * and to call {@link #endUpdates()} after them. 85 | * 86 | * @param before Unchanged part of the text placed before the updated part. 87 | * @param old Old updated part of the text. 88 | * @param aNew New updated part of the text? 89 | * @param after Unchanged part of the text placed after the updated part. 90 | */ 91 | protected abstract void onTextChanged(String before, String old, String aNew, String after); 92 | 93 | /** 94 | * Call this method when you start to update the text view, so it stops listening to it and then prevent an infinite loop. 95 | * @see #endUpdates() 96 | */ 97 | protected void startUpdates(){ 98 | _ignore = true; 99 | } 100 | 101 | /** 102 | * Call this method when you finished to update the text view in order to restart to listen to it. 103 | * @see #startUpdates() 104 | */ 105 | protected void endUpdates(){ 106 | _ignore = false; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/TrojanConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.text.TextUtils; 6 | 7 | import androidx.annotation.Nullable; 8 | 9 | import org.json.JSONArray; 10 | import org.json.JSONObject; 11 | 12 | public class TrojanConfig implements Parcelable { 13 | 14 | private String localAddr; 15 | private int localPort; 16 | private String remoteAddr; 17 | private int remotePort; 18 | private String password; 19 | private boolean verifyCert; 20 | private String caCertPath; 21 | private boolean enableIpv6; 22 | private String cipherList; 23 | private String tls13CipherList; 24 | 25 | 26 | public TrojanConfig() { 27 | // defaults 28 | this.localAddr = "127.0.0.1"; 29 | this.localPort = 1081; 30 | this.remotePort = 443; 31 | this.verifyCert = true; 32 | this.cipherList = "ECDHE-ECDSA-AES128-GCM-SHA256:" 33 | + "ECDHE-RSA-AES128-GCM-SHA256:" 34 | + "ECDHE-ECDSA-CHACHA20-POLY1305:" 35 | + "ECDHE-RSA-CHACHA20-POLY1305:" 36 | + "ECDHE-ECDSA-AES256-GCM-SHA384:" 37 | + "ECDHE-RSA-AES256-GCM-SHA384:" 38 | + "ECDHE-ECDSA-AES256-SHA:" 39 | + "ECDHE-ECDSA-AES128-SHA:" 40 | + "ECDHE-RSA-AES128-SHA:" 41 | + "ECDHE-RSA-AES256-SHA:" 42 | + "DHE-RSA-AES128-SHA:" 43 | + "DHE-RSA-AES256-SHA:" 44 | + "AES128-SHA:" 45 | + "AES256-SHA:" 46 | + "DES-CBC3-SHA"; 47 | this.tls13CipherList = "TLS_AES_128_GCM_SHA256:" 48 | + "TLS_CHACHA20_POLY1305_SHA256:" 49 | + "TLS_AES_256_GCM_SHA384"; 50 | } 51 | 52 | protected TrojanConfig(Parcel in) { 53 | localAddr = in.readString(); 54 | localPort = in.readInt(); 55 | remoteAddr = in.readString(); 56 | remotePort = in.readInt(); 57 | password = in.readString(); 58 | verifyCert = in.readByte() != 0; 59 | caCertPath = in.readString(); 60 | enableIpv6 = in.readByte() != 0; 61 | cipherList = in.readString(); 62 | tls13CipherList = in.readString(); 63 | } 64 | 65 | public static final Creator CREATOR = new Creator() { 66 | @Override 67 | public TrojanConfig createFromParcel(Parcel in) { 68 | return new TrojanConfig(in); 69 | } 70 | 71 | @Override 72 | public TrojanConfig[] newArray(int size) { 73 | return new TrojanConfig[size]; 74 | } 75 | }; 76 | 77 | public String generateTrojanConfigJSON() { 78 | try { 79 | return new JSONObject() 80 | .put("local_addr", this.localAddr) 81 | .put("local_port", this.localPort) 82 | .put("remote_addr", this.remoteAddr) 83 | .put("remote_port", this.remotePort) 84 | .put("password", new JSONArray().put(password)) 85 | .put("log_level", 2) // WARN 86 | .put("ssl", new JSONObject() 87 | .put("verify", this.verifyCert) 88 | .put("cert", this.caCertPath) 89 | .put("cipher", this.cipherList) 90 | .put("cipher_tls13", this.tls13CipherList) 91 | .put("alpn", new JSONArray().put("h2").put("http/1.1"))) 92 | .put("enable_ipv6", this.enableIpv6) 93 | .toString(); 94 | } catch (Exception e) { 95 | e.printStackTrace(); 96 | return null; 97 | } 98 | } 99 | 100 | public void fromJSON(String jsonStr) { 101 | try { 102 | JSONObject json = new JSONObject(jsonStr); 103 | this.setLocalAddr(json.getString("local_addr")) 104 | .setLocalPort(json.getInt("local_port")) 105 | .setRemoteAddr(json.getString("remote_addr")) 106 | .setRemotePort(json.getInt("remote_port")) 107 | .setPassword(json.getJSONArray("password").getString(0)) 108 | .setEnableIpv6(json.getBoolean("enable_ipv6")) 109 | .setVerifyCert(json.getJSONObject("ssl").getBoolean("verify")); 110 | 111 | } catch (Exception e) { 112 | e.printStackTrace(); 113 | } 114 | } 115 | 116 | public void copyFrom(TrojanConfig that) { 117 | this 118 | .setLocalAddr(that.localAddr) 119 | .setLocalPort(that.localPort) 120 | .setRemoteAddr(that.remoteAddr) 121 | .setRemotePort(that.remotePort) 122 | .setPassword(that.password) 123 | .setEnableIpv6(that.enableIpv6) 124 | .setVerifyCert(that.verifyCert) 125 | .setCaCertPath(that.caCertPath) 126 | .setCipherList(that.cipherList) 127 | .setTls13CipherList(that.tls13CipherList); 128 | 129 | } 130 | 131 | public boolean isValidRunningConfig() { 132 | return !TextUtils.isEmpty(this.caCertPath) 133 | && !TextUtils.isEmpty(this.remoteAddr) 134 | && !TextUtils.isEmpty(this.password); 135 | } 136 | 137 | public String getLocalAddr() { 138 | return localAddr; 139 | } 140 | 141 | public TrojanConfig setLocalAddr(String localAddr) { 142 | this.localAddr = localAddr; 143 | return this; 144 | } 145 | 146 | public int getLocalPort() { 147 | return localPort; 148 | } 149 | 150 | public TrojanConfig setLocalPort(int localPort) { 151 | this.localPort = localPort; 152 | return this; 153 | } 154 | 155 | public String getRemoteAddr() { 156 | return remoteAddr; 157 | } 158 | 159 | public TrojanConfig setRemoteAddr(String remoteAddr) { 160 | this.remoteAddr = remoteAddr; 161 | return this; 162 | } 163 | 164 | public int getRemotePort() { 165 | return remotePort; 166 | } 167 | 168 | public TrojanConfig setRemotePort(int remotePort) { 169 | this.remotePort = remotePort; 170 | return this; 171 | } 172 | 173 | public String getPassword() { 174 | return password; 175 | } 176 | 177 | public TrojanConfig setPassword(String password) { 178 | this.password = password; 179 | return this; 180 | } 181 | 182 | public boolean getVerifyCert() { 183 | return verifyCert; 184 | } 185 | 186 | public TrojanConfig setVerifyCert(boolean verifyCert) { 187 | this.verifyCert = verifyCert; 188 | return this; 189 | } 190 | 191 | public String getCaCertPath() { 192 | return caCertPath; 193 | } 194 | 195 | public TrojanConfig setCaCertPath(String caCertPath) { 196 | this.caCertPath = caCertPath; 197 | return this; 198 | } 199 | 200 | public boolean getEnableIpv6() { 201 | return enableIpv6; 202 | } 203 | 204 | public TrojanConfig setEnableIpv6(boolean enableIpv6) { 205 | this.enableIpv6 = enableIpv6; 206 | return this; 207 | } 208 | 209 | public String getCipherList() { 210 | return cipherList; 211 | } 212 | 213 | public TrojanConfig setCipherList(String cipherList) { 214 | this.cipherList = cipherList; 215 | return this; 216 | } 217 | 218 | public String getTls13CipherList() { 219 | return tls13CipherList; 220 | } 221 | 222 | public TrojanConfig setTls13CipherList(String tls13CipherList) { 223 | this.tls13CipherList = tls13CipherList; 224 | return this; 225 | } 226 | 227 | @Override 228 | public boolean equals(@Nullable Object obj) { 229 | if (!(obj instanceof TrojanConfig)) { 230 | return false; 231 | } 232 | TrojanConfig that = (TrojanConfig) obj; 233 | return (paramEquals(remoteAddr, that.remoteAddr) && paramEquals(remotePort, that.remotePort) 234 | && paramEquals(localAddr, that.localAddr) && paramEquals(localPort, that.localPort)) 235 | && paramEquals(password, that.password) && paramEquals(verifyCert, that.verifyCert) 236 | && paramEquals(caCertPath, that.caCertPath) && paramEquals(enableIpv6, that.enableIpv6) 237 | && paramEquals(cipherList, that.cipherList) && paramEquals(tls13CipherList, that.tls13CipherList); 238 | } 239 | 240 | private static boolean paramEquals(Object a, Object b) { 241 | if (a == b) { 242 | return true; 243 | } 244 | if (a == null || b == null) { 245 | return false; 246 | } 247 | return a.equals(b); 248 | } 249 | 250 | @Override 251 | public int describeContents() { 252 | return 0; 253 | } 254 | 255 | @Override 256 | public void writeToParcel(Parcel dest, int flags) { 257 | dest.writeString(localAddr); 258 | dest.writeInt(localPort); 259 | dest.writeString(remoteAddr); 260 | dest.writeInt(remotePort); 261 | dest.writeString(password); 262 | dest.writeByte((byte) (verifyCert ? 1 : 0)); 263 | dest.writeString(caCertPath); 264 | dest.writeByte((byte) (enableIpv6 ? 1 : 0)); 265 | dest.writeString(cipherList); 266 | dest.writeString(tls13CipherList); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/TrojanHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.File; 12 | import java.io.FileInputStream; 13 | import java.io.FileOutputStream; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.InputStreamReader; 17 | import java.io.OutputStream; 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | import java.util.List; 21 | 22 | public class TrojanHelper { 23 | private static final String SINGLE_CONFIG_TAG = "TrojanConfig"; 24 | private static final String CONFIG_LIST_TAG = "TrojanConfigList"; 25 | 26 | public static boolean writeTrojanServerConfigList(List configList, String trojanConfigListPath) { 27 | JSONArray jsonArray = new JSONArray(); 28 | for (TrojanConfig config : configList) { 29 | try { 30 | JSONObject jsonObject = new JSONObject(config.generateTrojanConfigJSON()); 31 | jsonArray.put(jsonObject); 32 | } catch (JSONException e) { 33 | e.printStackTrace(); 34 | return false; 35 | } 36 | } 37 | String configStr = jsonArray.toString(); 38 | File file = new File(trojanConfigListPath); 39 | if (file.exists()) { 40 | file.delete(); 41 | } 42 | try (OutputStream fos = new FileOutputStream(file)) { 43 | fos.write(configStr.getBytes()); 44 | fos.flush(); 45 | } catch (IOException e) { 46 | e.printStackTrace(); 47 | return false; 48 | } 49 | return true; 50 | } 51 | 52 | @NonNull 53 | public static List readTrojanServerConfigList(String trojanConfigListPath) { 54 | File file = new File(trojanConfigListPath); 55 | if (!file.exists()) { 56 | return Collections.emptyList(); 57 | } 58 | try (InputStream fis = new FileInputStream(file)) { 59 | byte[] data = new byte[(int) file.length()]; 60 | fis.read(data); 61 | String json = new String(data); 62 | JSONArray jsonArr = new JSONArray(json); 63 | int len = jsonArr.length(); 64 | List list = new ArrayList<>(len); 65 | for (int i = 0; i < len; i++) { 66 | list.add(parseTrojanConfigFromJSON(jsonArr.getJSONObject(i).toString())); 67 | } 68 | return list; 69 | } catch (IOException | JSONException e) { 70 | e.printStackTrace(); 71 | } 72 | return Collections.emptyList(); 73 | } 74 | 75 | public static void ShowTrojanConfigList(String trojanConfigListPath) { 76 | File file = new File(trojanConfigListPath); 77 | 78 | try { 79 | try (FileInputStream fis = new FileInputStream(file)) { 80 | byte[] content = new byte[(int) file.length()]; 81 | fis.read(content); 82 | LogHelper.v(CONFIG_LIST_TAG, new String(content)); 83 | } 84 | } catch (Exception e) { 85 | e.printStackTrace(); 86 | } 87 | } 88 | 89 | private static String parseTrojanConfigToJSON(TrojanConfig config) { 90 | try { 91 | /*JSONObject json = new JSONObject(); 92 | json.put("local_addr", config.getLocalAddr()); 93 | json.put("local_port", config.getLocalPort()); 94 | json.put("remote_addr", config.getRemoteAddr()); 95 | json.put("remote_port", config.getRemotePort()); 96 | json.put("password", config.getPassword()); 97 | json.put("verify_cert", config.getVerifyCert()); 98 | json.put("ca_cert_path", config.getCaCertPath()); 99 | json.put("enable_ipv6", config.getEnableIpv6()); 100 | json.put("cipher_list", config.getCipherList()); 101 | json.put("tls13_cipher_list", config.getTls13CipherList()); 102 | return json.toString();*/ 103 | return config.generateTrojanConfigJSON(); 104 | } catch (Exception e) { 105 | e.printStackTrace(); 106 | } 107 | return ""; 108 | } 109 | 110 | @Nullable 111 | public static TrojanConfig readTrojanConfig(String trojanConfigPath) { 112 | File file = new File(trojanConfigPath); 113 | if (!file.exists()) { 114 | return null; 115 | } 116 | StringBuilder sb = new StringBuilder(); 117 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) { 118 | String line; 119 | while ((line = reader.readLine()) != null) { 120 | sb.append(line); 121 | } 122 | TrojanConfig trojanConfig = new TrojanConfig(); 123 | trojanConfig.fromJSON(sb.toString()); 124 | return trojanConfig; 125 | } catch (IOException e) { 126 | e.printStackTrace(); 127 | } 128 | return null; 129 | } 130 | 131 | @NonNull 132 | private static TrojanConfig parseTrojanConfigFromJSON(String json) { 133 | TrojanConfig config = new TrojanConfig(); 134 | config.fromJSON(json); 135 | return config; 136 | } 137 | 138 | public static void WriteTrojanConfig(TrojanConfig trojanConfig, String trojanConfigPath) { 139 | String config = trojanConfig.generateTrojanConfigJSON(); 140 | File file = new File(trojanConfigPath); 141 | try { 142 | try (FileOutputStream fos = new FileOutputStream(file)) { 143 | fos.write(config.getBytes()); 144 | } 145 | } catch (Exception e) { 146 | e.printStackTrace(); 147 | } 148 | } 149 | 150 | public static void ChangeListenPort(String trojanConfigPath, long port) { 151 | File file = new File(trojanConfigPath); 152 | if (file.exists()) { 153 | try { 154 | String str; 155 | try (FileInputStream fis = new FileInputStream(file)) { 156 | byte[] content = new byte[(int) file.length()]; 157 | fis.read(content); 158 | str = new String(content); 159 | 160 | } 161 | JSONObject json = new JSONObject(str); 162 | json.put("local_port", port); 163 | try (FileOutputStream fos = new FileOutputStream(file)) { 164 | 165 | fos.write(json.toString().getBytes()); 166 | } 167 | } catch (Exception e) { 168 | e.printStackTrace(); 169 | } 170 | } 171 | } 172 | 173 | public static void ShowConfig(String trojanConfigPath) { 174 | File file = new File(trojanConfigPath); 175 | 176 | try { 177 | try (FileInputStream fis = new FileInputStream(file)) { 178 | byte[] content = new byte[(int) file.length()]; 179 | fis.read(content); 180 | LogHelper.v(SINGLE_CONFIG_TAG, new String(content)); 181 | } 182 | } catch (Exception e) { 183 | e.printStackTrace(); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/TrojanURLHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter; 2 | 3 | import java.net.URI; 4 | 5 | public class TrojanURLHelper { 6 | public static String GenerateTrojanURL(TrojanConfig trojanConfig) { 7 | 8 | URI trojanUri; 9 | try { 10 | trojanUri = new URI("trojan", 11 | trojanConfig.getPassword(), 12 | trojanConfig.getRemoteAddr(), 13 | trojanConfig.getRemotePort(), 14 | null, null, null); 15 | } catch (java.net.URISyntaxException e) { 16 | e.printStackTrace(); 17 | return null; 18 | } 19 | 20 | return trojanUri.toString(); 21 | } 22 | 23 | public static TrojanConfig ParseTrojanURL(String trojanURLStr) { 24 | URI trojanUri; 25 | try { 26 | trojanUri = new URI(trojanURLStr); 27 | } catch (java.net.URISyntaxException e) { 28 | e.printStackTrace(); 29 | return null; 30 | } 31 | String scheme = trojanUri.getScheme(); 32 | if (scheme == null) { 33 | return null; 34 | } 35 | if (!scheme.equals("trojan")) 36 | return null; 37 | String host = trojanUri.getHost(); 38 | int port = trojanUri.getPort(); 39 | String userInfo = trojanUri.getUserInfo(); 40 | 41 | TrojanConfig retConfig = new TrojanConfig(); 42 | retConfig.setRemoteAddr(host); 43 | retConfig.setRemotePort(port); 44 | retConfig.setPassword(userInfo); 45 | return retConfig; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/app/BaseAppCompatActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.app; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | 6 | import androidx.annotation.Nullable; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | public abstract class BaseAppCompatActivity extends AppCompatActivity { 10 | protected Context mContext; 11 | 12 | @Override 13 | protected void onCreate(@Nullable Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | mContext = this; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/app/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.app; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import androidx.annotation.IdRes; 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.fragment.app.Fragment; 11 | import androidx.fragment.app.FragmentActivity; 12 | 13 | public abstract class BaseFragment extends Fragment { 14 | protected View mRootView; 15 | protected Context mContext; 16 | 17 | @Override 18 | public void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setRetainInstance(true); 21 | } 22 | 23 | @Override 24 | public void onAttach(Context context) { 25 | super.onAttach(context); 26 | mContext = context; 27 | } 28 | 29 | @Override 30 | public void onDetach() { 31 | super.onDetach(); 32 | mContext = null; 33 | } 34 | 35 | @Override 36 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 37 | super.onViewCreated(view, savedInstanceState); 38 | mRootView = view; 39 | } 40 | 41 | protected T findViewById(@IdRes int id) { 42 | return mRootView.findViewById(id); 43 | } 44 | 45 | protected void runOnUiThread(Runnable runnable) { 46 | FragmentActivity activity = getActivity(); 47 | if (activity != null) { 48 | activity.runOnUiThread(runnable); 49 | } 50 | } 51 | 52 | protected void finishActivity() { 53 | FragmentActivity activity = getActivity(); 54 | if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) { 55 | activity.finish(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/constants/Constants.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.constants; 2 | 3 | public abstract class Constants { 4 | public static final String PREFERENCE_AUTHORITY = "io.github.trojan_gfw.igniter"; 5 | public static final String PREFERENCE_PATH = "preferences"; 6 | public static final String PREFERENCE_URI = "content://" + PREFERENCE_AUTHORITY + "/" + PREFERENCE_PATH; 7 | public static final String PREFERENCE_KEY_ENABLE_CLASH = "enable_clash"; 8 | public static final String PREFERENCE_KEY_FIRST_START = "first_start"; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/dialog/LoadingDialog.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.dialog; 2 | 3 | import android.app.Dialog; 4 | import android.content.Context; 5 | import android.graphics.Color; 6 | import android.graphics.PorterDuff; 7 | import android.graphics.drawable.ColorDrawable; 8 | import android.view.Window; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.core.content.ContextCompat; 13 | import androidx.core.widget.ContentLoadingProgressBar; 14 | 15 | import io.github.trojan_gfw.igniter.R; 16 | 17 | public class LoadingDialog extends Dialog { 18 | private TextView mMsgTv; 19 | 20 | public LoadingDialog(@NonNull Context context) { 21 | super(context); 22 | init(context); 23 | } 24 | 25 | private void init(Context context) { 26 | Window window =getWindow(); 27 | if (window != null) { 28 | window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 29 | } 30 | setContentView(R.layout.dialog_loading); 31 | ContentLoadingProgressBar pb = findViewById(R.id.dialogLoadingPb); 32 | pb.getIndeterminateDrawable().setColorFilter(ContextCompat.getColor(context, R.color.colorPrimary), 33 | PorterDuff.Mode.MULTIPLY); 34 | mMsgTv = findViewById(R.id.dialogLoadingMsgTv); 35 | } 36 | 37 | public void setMsg(String msg) { 38 | mMsgTv.setText(msg); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/mvp/BasePresenter.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.mvp; 2 | 3 | public interface BasePresenter { 4 | void start(); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/mvp/BaseView.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.mvp; 2 | 3 | public interface BaseView { 4 | void setPresenter(T presenter); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/os/IThreads.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.os; 2 | 3 | import java.util.concurrent.Executor; 4 | 5 | /** 6 | * Interface of thread pool. Provides methods to run {@link Task} or {@link Runnable} in main thread 7 | * or in background. 8 | */ 9 | public interface IThreads { 10 | Executor getThreadPoolExecutor(); 11 | 12 | void runOnUiThread(Runnable runnable); 13 | 14 | void runOnUiThread(Runnable runnable, long delayMillis); 15 | 16 | void runOnWorkThread(Task task); 17 | 18 | void runOnWorkThread(final Task task, long delayMillis); 19 | 20 | void removeDelayedAction(Runnable action); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/os/PreferencesProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.os; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.content.UriMatcher; 7 | import android.content.pm.ProviderInfo; 8 | import android.database.Cursor; 9 | import android.database.MatrixCursor; 10 | import android.net.Uri; 11 | 12 | import org.json.JSONException; 13 | import org.json.JSONObject; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.File; 17 | import java.io.FileInputStream; 18 | import java.io.FileOutputStream; 19 | import java.io.IOException; 20 | import java.io.InputStreamReader; 21 | import java.io.OutputStreamWriter; 22 | import java.util.Iterator; 23 | import java.util.LinkedHashMap; 24 | import java.util.Map; 25 | import java.util.Objects; 26 | import java.util.Set; 27 | 28 | import io.github.trojan_gfw.igniter.Globals; 29 | import io.github.trojan_gfw.igniter.LogHelper; 30 | import io.github.trojan_gfw.igniter.common.constants.Constants; 31 | 32 | public class PreferencesProvider extends ContentProvider { 33 | private static final String TAG = "PreferencesProvider"; 34 | public static final String AUTHORITY = Constants.PREFERENCE_AUTHORITY; 35 | public static final String PATH = Constants.PREFERENCE_PATH; 36 | private static final int CODE_PREFERENCES = 2077; 37 | 38 | private Map mCachePreferences; 39 | private final UriMatcher mUriMatcher; 40 | private String mPreferencesFilePath; 41 | 42 | public PreferencesProvider() { 43 | mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 44 | mUriMatcher.addURI(AUTHORITY, PATH, CODE_PREFERENCES); 45 | } 46 | 47 | @Override 48 | public void attachInfo(Context context, ProviderInfo info) { 49 | super.attachInfo(context, info); 50 | LogHelper.e(TAG, "attachInfo"); 51 | mPreferencesFilePath = Globals.getPreferencesFilePath(); 52 | } 53 | 54 | private boolean isUriNotValid(Uri uri) { 55 | return mUriMatcher.match(uri) != CODE_PREFERENCES; 56 | } 57 | 58 | @Override 59 | public int delete(Uri uri, String selection, String[] selectionArgs) { 60 | if (isUriNotValid(uri)) return 0; 61 | mCachePreferences.clear(); 62 | File preferencesFile = new File(mPreferencesFilePath); 63 | if (preferencesFile.exists()) { 64 | preferencesFile.delete(); 65 | return 1; 66 | } 67 | return 0; 68 | } 69 | 70 | @Override 71 | public String getType(Uri uri) { 72 | return "text/plain"; 73 | } 74 | 75 | @Override 76 | public Uri insert(Uri uri, ContentValues values) { 77 | if (isUriNotValid(uri)) return null; 78 | update(uri, values, null, null); 79 | return uri; 80 | } 81 | 82 | @Override 83 | public boolean onCreate() { 84 | return true; 85 | } 86 | 87 | private void ensureCacheReady() { 88 | if (mCachePreferences == null) { 89 | mCachePreferences = new LinkedHashMap<>(); 90 | readPreferencesToCache(); 91 | } 92 | } 93 | 94 | private void readPreferencesToCache() { 95 | File preferencesFile = new File(mPreferencesFilePath); 96 | if (!preferencesFile.exists()) return; 97 | try (FileInputStream fis = new FileInputStream(preferencesFile); 98 | InputStreamReader isr = new InputStreamReader(fis); 99 | BufferedReader reader = new BufferedReader(isr)) { 100 | StringBuilder sb = new StringBuilder(); 101 | String tmp; 102 | while ((tmp = reader.readLine()) != null) { 103 | sb.append(tmp); 104 | } 105 | JSONObject jsonObject = new JSONObject(sb.toString()); 106 | Iterator it = jsonObject.keys(); 107 | while (it.hasNext()) { 108 | String key = it.next(); 109 | mCachePreferences.put(key, jsonObject.opt(key)); 110 | } 111 | } catch (IOException | JSONException e) { 112 | e.printStackTrace(); 113 | } 114 | } 115 | 116 | @Override 117 | public Cursor query(Uri uri, String[] projection, String selection, 118 | String[] selectionArgs, String sortOrder) { 119 | LogHelper.i(TAG, "query values with: " + this); 120 | if (isUriNotValid(uri)) { 121 | return new MatrixCursor(new String[0], 1); 122 | } 123 | ensureCacheReady(); 124 | String[] queryKeys; 125 | if (projection == null) { 126 | Set keySet = mCachePreferences.keySet(); 127 | queryKeys = new String[keySet.size()]; 128 | int i = 0; 129 | for (String key : keySet) { 130 | queryKeys[i++] = key; 131 | } 132 | } else { 133 | queryKeys = projection; 134 | } 135 | MatrixCursor cursor = new MatrixCursor(queryKeys, 1); 136 | Object[] values = new Object[queryKeys.length]; 137 | for (int i = 0; i < queryKeys.length; i++) { 138 | values[i] = mCachePreferences.get(queryKeys[i]); 139 | } 140 | cursor.addRow(values); 141 | return cursor; 142 | } 143 | 144 | @Override 145 | public int update(Uri uri, ContentValues values, String selection, 146 | String[] selectionArgs) { 147 | LogHelper.i(TAG, "update values with: " + this); 148 | if (isUriNotValid(uri)) { 149 | return 0; 150 | } 151 | ensureCacheReady(); 152 | Set keySet = values.keySet(); 153 | boolean valueChanged = false; 154 | for (String key : keySet) { 155 | Object previousValue = mCachePreferences.get(key); 156 | Object nextValue = values.get(key); 157 | if (!Objects.equals(previousValue, nextValue)) { 158 | valueChanged = true; 159 | mCachePreferences.put(key, nextValue); 160 | } 161 | } 162 | if (valueChanged) { 163 | writeCacheIntoFile(); 164 | return 1; 165 | } 166 | return 0; 167 | } 168 | 169 | private void writeCacheIntoFile() { 170 | if (mCachePreferences == null) return; 171 | LogHelper.i(TAG, "write preferences to file: " + this); 172 | JSONObject jsonObject = new JSONObject(); 173 | try { 174 | for (String key : mCachePreferences.keySet()) { 175 | jsonObject.put(key, mCachePreferences.get(key)); 176 | } 177 | } catch (JSONException e) { 178 | e.printStackTrace(); 179 | } 180 | File preferencesFile = new File(mPreferencesFilePath); 181 | try (FileOutputStream fos = new FileOutputStream(preferencesFile); 182 | OutputStreamWriter osw = new OutputStreamWriter(fos)) { 183 | osw.write(jsonObject.toString()); 184 | osw.flush(); 185 | } catch (IOException e) { 186 | e.printStackTrace(); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/os/Task.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.os; 2 | 3 | 4 | import android.os.Process; 5 | 6 | import androidx.annotation.WorkerThread; 7 | 8 | /** 9 | * A wrapper of Runnable. 10 | */ 11 | public abstract class Task implements Runnable { 12 | private int priority = Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE; 13 | 14 | public Task() { 15 | } 16 | 17 | /** 18 | * Construct a task with priority. 19 | * 20 | * @param priority {@link Process#THREAD_PRIORITY_BACKGROUND} 21 | */ 22 | public Task(int priority) { 23 | this.priority = priority; 24 | } 25 | 26 | @Override 27 | public void run() { 28 | Process.setThreadPriority(priority); 29 | onRun(); 30 | } 31 | 32 | @WorkerThread 33 | public abstract void onRun(); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/os/Threads.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.os; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | import java.util.concurrent.Executor; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.SynchronousQueue; 9 | import java.util.concurrent.ThreadFactory; 10 | import java.util.concurrent.ThreadPoolExecutor; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | /** 15 | * Singleton implementation of {@link IThreads}. Call {@link #instance()} to get the instance. 16 | */ 17 | public final class Threads implements IThreads { 18 | private ExecutorService mThreadPool; 19 | private Handler mHandler; 20 | 21 | private Threads() { 22 | mThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 23 | 30L, TimeUnit.SECONDS, 24 | new SynchronousQueue(), 25 | new DefaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); 26 | mHandler = new Handler(Looper.getMainLooper()); 27 | } 28 | 29 | /** 30 | * The default thread factory 31 | */ 32 | private static class DefaultThreadFactory implements ThreadFactory { 33 | private static final AtomicInteger poolNumber = new AtomicInteger(1); 34 | private final ThreadGroup group; 35 | private final AtomicInteger threadNumber = new AtomicInteger(1); 36 | private final String namePrefix; 37 | 38 | DefaultThreadFactory() { 39 | SecurityManager s = System.getSecurityManager(); 40 | group = (s != null) ? s.getThreadGroup() : 41 | Thread.currentThread().getThreadGroup(); 42 | namePrefix = "ThreadHelperPool-" + 43 | poolNumber.getAndIncrement() + 44 | "-thread-"; 45 | } 46 | 47 | @Override 48 | public Thread newThread(Runnable r) { 49 | Thread t = new Thread(group, r, 50 | namePrefix + threadNumber.getAndIncrement(), 51 | 0); 52 | if (t.isDaemon()) 53 | t.setDaemon(false); 54 | return t; 55 | } 56 | } 57 | 58 | public static IThreads instance() { 59 | return Holder.INSTANCE; 60 | } 61 | 62 | private static class Holder { 63 | private static final Threads INSTANCE = new Threads(); 64 | } 65 | 66 | @Override 67 | public Executor getThreadPoolExecutor() { 68 | return mThreadPool; 69 | } 70 | 71 | @Override 72 | public void runOnWorkThread(Task task) { 73 | mThreadPool.execute(task); 74 | } 75 | 76 | @Override 77 | public void runOnWorkThread(final Task task, long delayMillis) { 78 | mHandler.postDelayed(new Runnable() { 79 | @Override 80 | public void run() { 81 | mThreadPool.execute(task); 82 | } 83 | }, delayMillis); 84 | } 85 | 86 | @Override 87 | public void runOnUiThread(Runnable action) { 88 | mHandler.post(action); 89 | } 90 | 91 | @Override 92 | public void runOnUiThread(Runnable action, long delayMillis) { 93 | mHandler.postDelayed(action, delayMillis); 94 | } 95 | 96 | @Override 97 | public void removeDelayedAction(Runnable action) { 98 | mHandler.removeCallbacks(action); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/utils/PermissionUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.utils; 2 | 3 | import android.Manifest; 4 | import android.content.Context; 5 | import android.content.pm.PackageManager; 6 | 7 | import androidx.core.content.ContextCompat; 8 | 9 | public abstract class PermissionUtils { 10 | 11 | public static boolean hasReadWriteExtStoragePermission(Context context) { 12 | return ContextCompat.checkSelfPermission(context, 13 | Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED 14 | && ContextCompat.checkSelfPermission(context, 15 | Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/utils/PreferenceUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.utils; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.ContentValues; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | 8 | import androidx.core.content.ContentResolverCompat; 9 | 10 | public abstract class PreferenceUtils { 11 | 12 | public static boolean getBooleanPreference(ContentResolver resolver, Uri uri, String key, boolean defaultValue) { 13 | try (Cursor query = ContentResolverCompat.query(resolver, uri, new String[]{key}, null, 14 | null, null, null)) { 15 | if (query.moveToFirst()) { 16 | int columnIndex = query.getColumnIndex(key); 17 | if (columnIndex >= 0) { 18 | int type = query.getType(columnIndex); 19 | switch (type) { 20 | case Cursor.FIELD_TYPE_STRING: 21 | return Boolean.parseBoolean(query.getString(columnIndex)); 22 | case Cursor.FIELD_TYPE_INTEGER: 23 | return query.getInt(columnIndex) == 1; 24 | default: 25 | return defaultValue; 26 | } 27 | } 28 | } 29 | } 30 | return defaultValue; 31 | } 32 | 33 | public static void putBooleanPreference(ContentResolver resolver, Uri uri, String key, boolean value) { 34 | ContentValues contentValues = new ContentValues(1); 35 | contentValues.put(key, value); 36 | resolver.update(uri, contentValues, null, null); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/utils/ProcessUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.utils; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import java.util.List; 9 | 10 | import static android.content.Context.ACTIVITY_SERVICE; 11 | 12 | public class ProcessUtils { 13 | 14 | @Nullable 15 | public static String getProcessNameByPID(Context context, int pid) { 16 | ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); 17 | List runningAppProcesses = am.getRunningAppProcesses(); 18 | if (runningAppProcesses == null) { 19 | return null; 20 | } 21 | for (ActivityManager.RunningAppProcessInfo info : runningAppProcesses) { 22 | if (info.pid == pid) { 23 | return info.processName; 24 | } 25 | } 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/common/utils/SnackbarUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.common.utils; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.StringRes; 6 | 7 | import com.google.android.material.snackbar.Snackbar; 8 | 9 | public class SnackbarUtils { 10 | 11 | public static void showTextShort(View view, @StringRes int id) { 12 | Snackbar.make(view, id, Snackbar.LENGTH_SHORT).show(); 13 | } 14 | 15 | public static void showTextShort(View view, @StringRes int id, @StringRes int actionId, View.OnClickListener listener) { 16 | Snackbar.make(view, id, Snackbar.LENGTH_SHORT).setAction(actionId, listener).show(); 17 | } 18 | 19 | public static void showTextShort(View view, String text, String actionText, View.OnClickListener listener) { 20 | Snackbar.make(view, text, Snackbar.LENGTH_SHORT).setAction(actionText, listener).show(); 21 | } 22 | 23 | public static void showTextShort(View view, String text) { 24 | Snackbar.make(view, text, Snackbar.LENGTH_SHORT).show(); 25 | } 26 | 27 | public static void showTextLong(View view, @StringRes int id) { 28 | Snackbar.make(view, id, Snackbar.LENGTH_LONG).show(); 29 | } 30 | 31 | public static void showTextLong(View view, @StringRes int id, @StringRes int actionId, View.OnClickListener listener) { 32 | Snackbar.make(view, id, Snackbar.LENGTH_LONG).setAction(actionId, listener).show(); 33 | } 34 | 35 | public static void showTextLong(View view, String text) { 36 | Snackbar.make(view, text, Snackbar.LENGTH_LONG).show(); 37 | } 38 | 39 | public static void showTextLong(View view, String text, String actionText, View.OnClickListener listener) { 40 | Snackbar.make(view, text, Snackbar.LENGTH_LONG).setAction(actionText, listener).show(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/connection/TestConnection.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.connection; 2 | 3 | import android.os.AsyncTask; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import java.lang.ref.WeakReference; 8 | import java.net.InetSocketAddress; 9 | import java.net.Proxy; 10 | import java.net.URL; 11 | import java.net.URLConnection; 12 | 13 | public class TestConnection extends AsyncTask { 14 | private static final int DEFAULT_TIMEOUT = 10 * 1000; // 10 seconds 15 | private final String mProxyHost; 16 | private final long mProxyPort; 17 | private final WeakReference mOnResultListenerRef; 18 | 19 | public TestConnection(String proxyHost, long proxyPort, OnResultListener onResultListener) { 20 | mProxyHost = proxyHost; 21 | mProxyPort = proxyPort; 22 | mOnResultListenerRef = new WeakReference<>(onResultListener); 23 | } 24 | 25 | @Override 26 | protected TestResult doInBackground(String... strings) { 27 | String testUrl = strings[0]; 28 | try { 29 | long startTime = System.currentTimeMillis(); 30 | InetSocketAddress proxyAddress = new InetSocketAddress(mProxyHost, (int) mProxyPort); 31 | Proxy proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress); 32 | URLConnection connection = new URL(testUrl).openConnection(proxy); 33 | connection.setConnectTimeout(DEFAULT_TIMEOUT); 34 | connection.setReadTimeout(DEFAULT_TIMEOUT); 35 | connection.connect(); 36 | return new TestResult(testUrl, true, "", 37 | System.currentTimeMillis() - startTime); 38 | } catch (Exception e) { 39 | return new TestResult(testUrl, false, e.getMessage(), 0); 40 | } 41 | } 42 | 43 | @Override 44 | protected void onPostExecute(TestResult testResult) { 45 | OnResultListener listener = mOnResultListenerRef.get(); 46 | if (listener != null) { 47 | listener.onResult(testResult.url, testResult.connected, testResult.delay, testResult.error); 48 | } 49 | } 50 | 51 | public interface OnResultListener { 52 | void onResult(String testUrl, boolean connected, long delay, String error); 53 | } 54 | } 55 | 56 | class TestResult { 57 | boolean connected; 58 | String url; 59 | String error; 60 | long delay; 61 | 62 | TestResult(String url, boolean connected, @NonNull String error, long delay) { 63 | this.connected = connected; 64 | this.url = url; 65 | this.error = error; 66 | this.delay = delay; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/connection/TrojanConnection.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.connection; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.os.Binder; 8 | import android.os.Handler; 9 | import android.os.IBinder; 10 | import android.os.RemoteException; 11 | 12 | import androidx.annotation.NonNull; 13 | 14 | import io.github.trojan_gfw.igniter.ProxyService; 15 | import io.github.trojan_gfw.igniter.R; 16 | import io.github.trojan_gfw.igniter.proxy.aidl.ITrojanService; 17 | import io.github.trojan_gfw.igniter.proxy.aidl.ITrojanServiceCallback; 18 | 19 | /** 20 | * A class that delegates interaction with {@link ProxyService}. You should call {@link #connect(Context, Callback)} 21 | * when you are ready for interacting with {@link ProxyService} and call {@link #disconnect(Context)} 22 | * in the end. {@link TrojanConnection} would bind {@link ProxyService} and register {@link ITrojanServiceCallback}. 23 | * You can easily obtain {@link ProxyService} and get state change as well as connection test result 24 | * by implementing {@link Callback}. 25 | * 26 | * @see ProxyService 27 | * @see ITrojanService 28 | * @see ITrojanServiceCallback 29 | */ 30 | public class TrojanConnection implements ServiceConnection, Binder.DeathRecipient { 31 | private final Handler mHandler = new Handler(); 32 | private ITrojanService mTrojanService; 33 | private Callback mCallback; 34 | private boolean mServiceCallbackRegistered; 35 | private final boolean mListenToDeath; 36 | private boolean mAlreadyConnected; 37 | private IBinder mBinder; 38 | /** 39 | * Implementation of {@link ITrojanServiceCallback}. The callback is registered in {@link #onServiceConnected(ComponentName, IBinder)}, 40 | * and unregistered in {@link #onServiceDisconnected(ComponentName)}. The callback is considered 41 | * to be invoked by {@link ITrojanService}, in this case, a field of {@link ProxyService} implements 42 | * {@link ITrojanService}. 43 | * 44 | * @see ProxyService 45 | * @see ITrojanService 46 | */ 47 | private ITrojanServiceCallback mTrojanServiceCallback = new ITrojanServiceCallback.Stub() { 48 | @Override 49 | public void onStateChanged(final int state, final String msg) throws RemoteException { 50 | if (mCallback != null) { 51 | mHandler.post(new Runnable() { 52 | @Override 53 | public void run() { 54 | mCallback.onStateChanged(state, msg); 55 | } 56 | }); 57 | } 58 | } 59 | 60 | @Override 61 | public void onTestResult(final String testUrl, final boolean connected, final long delay, final String error) throws RemoteException { 62 | if (mCallback != null) { 63 | mHandler.post(new Runnable() { 64 | @Override 65 | public void run() { 66 | mCallback.onTestResult(testUrl, connected, delay, error); 67 | } 68 | }); 69 | } 70 | } 71 | }; 72 | 73 | public TrojanConnection(boolean listenToDeath) { 74 | super(); 75 | mListenToDeath = listenToDeath; 76 | } 77 | 78 | /** 79 | * Callback for events that are relative to {@link ProxyService}. 80 | */ 81 | public interface Callback { 82 | void onServiceConnected(ITrojanService service); 83 | 84 | void onServiceDisconnected(); 85 | 86 | void onStateChanged(int state, String msg); 87 | 88 | void onTestResult(String testUrl, boolean connected, long delay, @NonNull String error); 89 | 90 | void onBinderDied(); 91 | } 92 | 93 | public void connect(Context context, Callback callback) { 94 | if (mAlreadyConnected) { 95 | return; 96 | } 97 | mAlreadyConnected = true; 98 | if (mCallback != null) { 99 | throw new IllegalStateException("Required to call disconnect(Context) first."); 100 | } 101 | mCallback = callback; 102 | 103 | // todo: choose the service class dynamically. 104 | Intent intent = new Intent(context, ProxyService.class); 105 | intent.setAction(context.getString(R.string.bind_service)); 106 | context.bindService(intent, this, Context.BIND_AUTO_CREATE); 107 | } 108 | 109 | public void disconnect(Context context) { 110 | unregisterServiceCallback(); 111 | if (mAlreadyConnected) { 112 | try { 113 | context.unbindService(this); 114 | } catch (IllegalArgumentException e) { 115 | e.printStackTrace(); 116 | } 117 | mAlreadyConnected = false; 118 | if (mListenToDeath && mBinder != null) { 119 | mBinder.unlinkToDeath(this, 0); 120 | } 121 | mBinder = null; 122 | mTrojanService = null; 123 | mCallback = null; 124 | } 125 | } 126 | 127 | private void unregisterServiceCallback() { 128 | ITrojanService service = mTrojanService; 129 | if (service != null && mServiceCallbackRegistered) { 130 | try { 131 | service.unregisterCallback(mTrojanServiceCallback); 132 | } catch (RemoteException e) { 133 | e.printStackTrace(); 134 | } 135 | mServiceCallbackRegistered = false; 136 | } 137 | } 138 | 139 | public ITrojanService getService() { 140 | return mTrojanService; 141 | } 142 | 143 | /** 144 | * Obtain the binder {@link ITrojanService} returned by {@link ProxyService#onBind(Intent)} and 145 | * register callback {@link #mTrojanServiceCallback} with the binder. 146 | */ 147 | @Override 148 | public void onServiceConnected(ComponentName name, IBinder binder) { 149 | mBinder = binder; 150 | ITrojanService service = ITrojanService.Stub.asInterface(binder); 151 | mTrojanService = service; 152 | try { 153 | if (mListenToDeath) { 154 | binder.linkToDeath(this, 0); 155 | } 156 | if (mServiceCallbackRegistered) { 157 | throw new IllegalStateException("TrojanServiceCallback already registered!"); 158 | } 159 | service.registerCallback(mTrojanServiceCallback); 160 | mServiceCallbackRegistered = true; 161 | } catch (RemoteException e) { 162 | e.printStackTrace(); 163 | } 164 | if (mCallback != null) { 165 | mCallback.onServiceConnected(service); 166 | } 167 | } 168 | 169 | @Override 170 | public void onServiceDisconnected(ComponentName name) { 171 | unregisterServiceCallback(); 172 | if (mCallback != null) { 173 | mCallback.onServiceDisconnected(); 174 | } 175 | mTrojanService = null; 176 | mBinder = null; 177 | } 178 | 179 | @Override 180 | public void binderDied() { 181 | mTrojanService = null; 182 | mServiceCallbackRegistered = false; 183 | if (mCallback != null) { 184 | mHandler.post(new Runnable() { 185 | @Override 186 | public void run() { 187 | mCallback.onBinderDied(); 188 | } 189 | }); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/activity/ExemptAppActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.Window; 7 | 8 | import androidx.fragment.app.FragmentManager; 9 | 10 | import io.github.trojan_gfw.igniter.Globals; 11 | import io.github.trojan_gfw.igniter.R; 12 | import io.github.trojan_gfw.igniter.common.app.BaseAppCompatActivity; 13 | import io.github.trojan_gfw.igniter.exempt.contract.ExemptAppContract; 14 | import io.github.trojan_gfw.igniter.exempt.data.ExemptAppDataManager; 15 | import io.github.trojan_gfw.igniter.exempt.fragment.ExemptAppFragment; 16 | import io.github.trojan_gfw.igniter.exempt.presenter.ExemptAppPresenter; 17 | 18 | public class ExemptAppActivity extends BaseAppCompatActivity { 19 | private ExemptAppContract.Presenter mPresenter; 20 | 21 | public static Intent create(Context context) { 22 | return new Intent(context, ExemptAppActivity.class); 23 | } 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | requestWindowFeature(Window.FEATURE_NO_TITLE); 29 | setContentView(R.layout.activity_exempt_app); 30 | FragmentManager fm = getSupportFragmentManager(); 31 | ExemptAppFragment fragment = (ExemptAppFragment) fm.findFragmentByTag(ExemptAppFragment.TAG); 32 | if (fragment == null) { 33 | fragment = ExemptAppFragment.newInstance(); 34 | } 35 | mPresenter = new ExemptAppPresenter(fragment, new ExemptAppDataManager(getApplicationContext(), 36 | Globals.getExemptedAppListPath())); 37 | fm.beginTransaction() 38 | .replace(R.id.parent_fl, fragment, ExemptAppFragment.TAG) 39 | .commitAllowingStateLoss(); 40 | } 41 | 42 | @Override 43 | public void onBackPressed() { 44 | if (mPresenter == null || !mPresenter.handleBackPressed()) { 45 | super.onBackPressed(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/adapter/AppInfoAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.adapter; 2 | 3 | import android.content.res.Resources; 4 | import android.graphics.Rect; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.CompoundButton; 9 | import android.widget.Switch; 10 | import android.widget.TextView; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.core.widget.TextViewCompat; 14 | import androidx.recyclerview.widget.RecyclerView; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import io.github.trojan_gfw.igniter.R; 20 | import io.github.trojan_gfw.igniter.exempt.data.AppInfo; 21 | 22 | public class AppInfoAdapter extends RecyclerView.Adapter { 23 | private final List mData = new ArrayList<>(); 24 | private OnItemOperationListener mOnItemOperationListener; 25 | private final Rect mIconBound = new Rect(); 26 | 27 | public AppInfoAdapter() { 28 | super(); 29 | final int size = Resources.getSystem().getDimensionPixelSize(android.R.dimen.app_icon_size); 30 | mIconBound.right = size; 31 | mIconBound.bottom = size; 32 | } 33 | 34 | @NonNull 35 | @Override 36 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { 37 | View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_app_info, viewGroup, false); 38 | return new ViewHolder(v); 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { 43 | if (i != RecyclerView.NO_POSITION) { 44 | viewHolder.bindData(mData.get(i)); 45 | } 46 | } 47 | 48 | public void removeData(int position) { 49 | mData.remove(position); 50 | notifyItemRemoved(position); 51 | } 52 | 53 | public void refreshData(List data) { 54 | mData.clear(); 55 | mData.addAll(data); 56 | notifyDataSetChanged(); 57 | } 58 | 59 | @Override 60 | public int getItemCount() { 61 | return mData.size(); 62 | } 63 | 64 | public void setOnItemOperationListener(OnItemOperationListener onItemOperationListener) { 65 | mOnItemOperationListener = onItemOperationListener; 66 | } 67 | 68 | public interface OnItemOperationListener { 69 | void onToggle(boolean exempt, AppInfo appInfo, int position); 70 | } 71 | 72 | class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener { 73 | private TextView mNameTv; 74 | private Switch mExemptSwitch; 75 | private AppInfo mCurrentInfo; 76 | 77 | ViewHolder(@NonNull View itemView) { 78 | super(itemView); 79 | mNameTv = itemView.findViewById(R.id.appNameTv); 80 | TextViewCompat.setAutoSizeTextTypeWithDefaults(mNameTv, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE); 81 | mExemptSwitch = itemView.findViewById(R.id.appExemptSwitch); 82 | } 83 | 84 | @Override 85 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 86 | if (mOnItemOperationListener != null) { 87 | mOnItemOperationListener.onToggle(isChecked, mCurrentInfo, getBindingAdapterPosition()); 88 | } 89 | } 90 | 91 | void bindData(AppInfo appInfo) { 92 | mCurrentInfo = appInfo; 93 | mNameTv.setText(appInfo.getAppName()); 94 | appInfo.getIcon().setBounds(mIconBound); 95 | mNameTv.setCompoundDrawables(appInfo.getIcon(), null, null, null); 96 | mExemptSwitch.setOnCheckedChangeListener(null); 97 | mExemptSwitch.setChecked(appInfo.isExempt()); 98 | mExemptSwitch.setOnCheckedChangeListener(this); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/contract/ExemptAppContract.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.contract; 2 | 3 | import androidx.annotation.AnyThread; 4 | import androidx.annotation.UiThread; 5 | 6 | import java.util.List; 7 | 8 | import io.github.trojan_gfw.igniter.common.mvp.BasePresenter; 9 | import io.github.trojan_gfw.igniter.common.mvp.BaseView; 10 | import io.github.trojan_gfw.igniter.exempt.data.AppInfo; 11 | 12 | public interface ExemptAppContract { 13 | interface Presenter extends BasePresenter { 14 | void updateAppInfo(AppInfo appInfo, int position, boolean exempt); 15 | 16 | void saveExemptAppInfoList(); 17 | 18 | /** 19 | * @return true if exit directly, false to cancel exiting. 20 | */ 21 | boolean handleBackPressed(); 22 | 23 | void filterAppsByName(String name); 24 | 25 | void exit(); 26 | } 27 | 28 | interface View extends BaseView { 29 | @UiThread 30 | void showLoading(); 31 | 32 | @UiThread 33 | void dismissLoading(); 34 | 35 | @UiThread 36 | void showSaveSuccess(); 37 | 38 | @UiThread 39 | void showExitConfirm(); 40 | 41 | @UiThread 42 | void showAppList(List packageNames); 43 | 44 | @AnyThread 45 | void exit(boolean configurationChanged); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/data/AppInfo.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.data; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | public class AppInfo implements Cloneable { 6 | private String appName; 7 | private String appNameInLowercase; 8 | private Drawable icon; 9 | private String packageName; 10 | private boolean exempt; 11 | 12 | public String getAppName() { 13 | return appName; 14 | } 15 | 16 | public String getAppNameInLowercase() { 17 | if (appNameInLowercase == null) { 18 | // lazy loading. 19 | appNameInLowercase = appName.toLowerCase(); 20 | } 21 | return appNameInLowercase; 22 | } 23 | 24 | public void setAppName(String appName) { 25 | this.appName = appName; 26 | } 27 | 28 | public Drawable getIcon() { 29 | return icon; 30 | } 31 | 32 | public void setIcon(Drawable icon) { 33 | this.icon = icon; 34 | } 35 | 36 | public String getPackageName() { 37 | return packageName; 38 | } 39 | 40 | public void setPackageName(String packageName) { 41 | this.packageName = packageName; 42 | } 43 | 44 | public boolean isExempt() { 45 | return exempt; 46 | } 47 | 48 | public void setExempt(boolean exempt) { 49 | this.exempt = exempt; 50 | } 51 | 52 | @Override 53 | protected Object clone() throws CloneNotSupportedException { 54 | AppInfo appInfo = (AppInfo) super.clone(); 55 | appInfo.appName = appName; 56 | appInfo.icon = icon; 57 | appInfo.packageName = packageName; 58 | appInfo.exempt = exempt; 59 | return appInfo; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/data/ExemptAppDataManager.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.data; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import android.content.pm.PackageManager; 6 | import android.os.Build; 7 | import android.text.TextUtils; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | import java.io.BufferedReader; 12 | import java.io.BufferedWriter; 13 | import java.io.File; 14 | import java.io.FileInputStream; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.io.InputStreamReader; 18 | import java.io.OutputStreamWriter; 19 | import java.util.ArrayList; 20 | import java.util.HashSet; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | /** 25 | * Implementation of {@link ExemptAppDataSource}. This class reads and writes exempted app list in a 26 | * file. The exempted app package names will be written line by line in the file. 27 | *
28 | * Example: 29 | *
30 | * com.google.playstore 31 | *
32 | * io.github.trojan_gfw.igniter 33 | *
34 | * com.android.something 35 | */ 36 | public class ExemptAppDataManager implements ExemptAppDataSource { 37 | private final PackageManager mPackageManager; 38 | private final String mExemptAppListFilePath; 39 | 40 | public ExemptAppDataManager(Context context, String exemptAppListFilePath) { 41 | super(); 42 | mPackageManager = context.getPackageManager(); 43 | mExemptAppListFilePath = exemptAppListFilePath; 44 | } 45 | 46 | @Override 47 | public void saveExemptAppInfoSet(Set exemptAppPackageNames) { 48 | File file = new File(mExemptAppListFilePath); 49 | if (file.exists()) { 50 | file.delete(); 51 | } 52 | if (exemptAppPackageNames == null || exemptAppPackageNames.isEmpty()) { 53 | return; 54 | } 55 | File dir = file.getParentFile(); 56 | if (!dir.exists()) { 57 | dir.mkdirs(); 58 | } 59 | try (FileOutputStream fos = new FileOutputStream(file); 60 | OutputStreamWriter osw = new OutputStreamWriter(fos); 61 | BufferedWriter bw = new BufferedWriter(osw)) { 62 | for (String name : exemptAppPackageNames) { 63 | bw.write(name); 64 | bw.write('\n'); 65 | } 66 | bw.flush(); 67 | } catch (IOException e) { 68 | e.printStackTrace(); 69 | } 70 | } 71 | 72 | @NonNull 73 | private Set readExemptAppListConfig() { 74 | File file = new File(mExemptAppListFilePath); 75 | Set exemptAppSet = new HashSet<>(); 76 | if (!file.exists()) { 77 | return exemptAppSet; 78 | } 79 | try (FileInputStream fis = new FileInputStream(file); 80 | InputStreamReader isr = new InputStreamReader(fis); 81 | BufferedReader reader = new BufferedReader(isr)) { 82 | String tmp = reader.readLine(); 83 | while (!TextUtils.isEmpty(tmp)) { 84 | exemptAppSet.add(tmp); 85 | tmp = reader.readLine(); 86 | } 87 | } catch (IOException e) { 88 | e.printStackTrace(); 89 | } 90 | return exemptAppSet; 91 | } 92 | 93 | @Override 94 | public Set loadExemptAppPackageNameSet() { 95 | Set exemptAppPackageNames = readExemptAppListConfig(); 96 | // filter uninstalled apps 97 | List applicationInfoList = queryCurrentInstalledApps(); 98 | Set installedAppPackageNames = new HashSet<>(); 99 | for (ApplicationInfo applicationInfo : applicationInfoList) { 100 | installedAppPackageNames.add(applicationInfo.packageName); 101 | } 102 | Set ret = new HashSet<>(); 103 | for (String packageName : exemptAppPackageNames) { 104 | if (installedAppPackageNames.contains(packageName)) { 105 | ret.add(packageName); 106 | } 107 | } 108 | return ret; 109 | } 110 | 111 | private List queryCurrentInstalledApps() { 112 | int flags = 0; 113 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 114 | flags |= PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.MATCH_DISABLED_COMPONENTS; 115 | } else { // These flags are deprecated since Nougat. 116 | flags |= PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_DISABLED_COMPONENTS; 117 | } 118 | return mPackageManager.getInstalledApplications(flags); 119 | } 120 | 121 | @Override 122 | public List getAllAppInfoList() { 123 | List applicationInfoList = queryCurrentInstalledApps(); 124 | List appInfoList = new ArrayList<>(applicationInfoList.size()); 125 | for (ApplicationInfo applicationInfo : applicationInfoList) { 126 | AppInfo appInfo = new AppInfo(); 127 | appInfo.setAppName(mPackageManager.getApplicationLabel(applicationInfo).toString()); 128 | appInfo.setPackageName(applicationInfo.packageName); 129 | appInfo.setIcon(mPackageManager.getApplicationIcon(applicationInfo)); 130 | appInfoList.add(appInfo); 131 | } 132 | return appInfoList; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/data/ExemptAppDataSource.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.data; 2 | 3 | import androidx.annotation.WorkerThread; 4 | 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | public interface ExemptAppDataSource { 9 | /** 10 | * Load exempt applications' package names. 11 | * 12 | * @return exempt applications' package names.. 13 | */ 14 | @WorkerThread 15 | Set loadExemptAppPackageNameSet(); 16 | 17 | /** 18 | * Save exempt applications' package names. 19 | * 20 | * @param exemptAppPackageNames exempt app package name set 21 | */ 22 | @WorkerThread 23 | void saveExemptAppInfoSet(Set exemptAppPackageNames); 24 | 25 | /** 26 | * Load all application info list, including exempt apps and non-exempt apps. 27 | * @return all application info list 28 | */ 29 | @WorkerThread 30 | List getAllAppInfoList(); 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/fragment/ExemptAppFragment.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.fragment; 2 | 3 | import android.app.Activity; 4 | import android.content.DialogInterface; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.Menu; 8 | import android.view.MenuInflater; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | import androidx.appcompat.app.AlertDialog; 16 | import androidx.appcompat.app.AppCompatActivity; 17 | import androidx.appcompat.widget.SearchView; 18 | import androidx.appcompat.widget.Toolbar; 19 | import androidx.fragment.app.FragmentActivity; 20 | import androidx.recyclerview.widget.DividerItemDecoration; 21 | import androidx.recyclerview.widget.LinearLayoutManager; 22 | import androidx.recyclerview.widget.RecyclerView; 23 | 24 | import java.util.List; 25 | 26 | import io.github.trojan_gfw.igniter.R; 27 | import io.github.trojan_gfw.igniter.common.app.BaseFragment; 28 | import io.github.trojan_gfw.igniter.common.dialog.LoadingDialog; 29 | import io.github.trojan_gfw.igniter.common.utils.SnackbarUtils; 30 | import io.github.trojan_gfw.igniter.exempt.adapter.AppInfoAdapter; 31 | import io.github.trojan_gfw.igniter.exempt.contract.ExemptAppContract; 32 | import io.github.trojan_gfw.igniter.exempt.data.AppInfo; 33 | 34 | public class ExemptAppFragment extends BaseFragment implements ExemptAppContract.View { 35 | public static final String TAG = "ExemptAppFragment"; 36 | private ExemptAppContract.Presenter mPresenter; 37 | private Toolbar mTopBar; 38 | private RecyclerView mAppRv; 39 | private AppInfoAdapter mAppInfoAdapter; 40 | private LoadingDialog mLoadingDialog; 41 | 42 | public ExemptAppFragment() { 43 | // Required empty public constructor 44 | } 45 | 46 | public static ExemptAppFragment newInstance() { 47 | return new ExemptAppFragment(); 48 | } 49 | 50 | @Override 51 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 52 | Bundle savedInstanceState) { 53 | // Inflate the layout for this fragment 54 | return inflater.inflate(R.layout.fragment_exempt_app, container, false); 55 | } 56 | 57 | @Override 58 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 59 | super.onViewCreated(view, savedInstanceState); 60 | findViews(); 61 | initViews(); 62 | initListeners(); 63 | mPresenter.start(); 64 | } 65 | 66 | private void findViews() { 67 | mTopBar = findViewById(R.id.exemptAppTopBar); 68 | mAppRv = findViewById(R.id.exemptAppRv); 69 | } 70 | 71 | private void initViews() { 72 | FragmentActivity activity = getActivity(); 73 | if (activity instanceof AppCompatActivity) { 74 | ((AppCompatActivity) activity).setSupportActionBar(mTopBar); 75 | setHasOptionsMenu(true); 76 | } 77 | mAppInfoAdapter = new AppInfoAdapter(); 78 | mAppRv.setAdapter(mAppInfoAdapter); 79 | mAppRv.addItemDecoration(new DividerItemDecoration(mContext, LinearLayoutManager.VERTICAL)); 80 | } 81 | 82 | private void initListeners() { 83 | mAppInfoAdapter.setOnItemOperationListener(new AppInfoAdapter.OnItemOperationListener() { 84 | @Override 85 | public void onToggle(boolean exempt, AppInfo appInfo, int position) { 86 | mPresenter.updateAppInfo(appInfo, position, exempt); 87 | } 88 | }); 89 | } 90 | 91 | @Override 92 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 93 | menu.clear(); 94 | inflater.inflate(R.menu.menu_exempt_app, menu); 95 | 96 | MenuItem item = menu.findItem(R.id.action_search_app); 97 | SearchView searchView = null; 98 | if (item != null) { 99 | searchView = (SearchView) item.getActionView(); 100 | } 101 | if (searchView != null) { 102 | searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 103 | @Override 104 | public boolean onQueryTextSubmit(String s) { 105 | return false; 106 | } 107 | 108 | @Override 109 | public boolean onQueryTextChange(String s) { 110 | mPresenter.filterAppsByName(s); 111 | return true; 112 | } 113 | }); 114 | } 115 | } 116 | 117 | @Override 118 | public boolean onOptionsItemSelected(MenuItem item) { 119 | if (item.getItemId() == R.id.action_save_exempt_apps) { 120 | mPresenter.saveExemptAppInfoList(); 121 | return true; 122 | } 123 | return false; 124 | } 125 | 126 | @Override 127 | public void showSaveSuccess() { 128 | SnackbarUtils.showTextShort(mRootView, R.string.common_save_success, R.string.exempt_app_exit, new View.OnClickListener() { 129 | @Override 130 | public void onClick(View v) { 131 | mPresenter.exit(); 132 | } 133 | }); 134 | } 135 | 136 | @Override 137 | public void showExitConfirm() { 138 | new AlertDialog.Builder(mContext) 139 | .setTitle(R.string.common_alert) 140 | .setMessage(R.string.exempt_app_exit_without_saving_confirm) 141 | .setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { 142 | @Override 143 | public void onClick(DialogInterface dialog, int which) { 144 | dialog.dismiss(); 145 | } 146 | }) 147 | .setPositiveButton(R.string.common_confirm, new DialogInterface.OnClickListener() { 148 | @Override 149 | public void onClick(DialogInterface dialog, int which) { 150 | dialog.dismiss(); 151 | mPresenter.exit(); 152 | } 153 | }).create().show(); 154 | } 155 | 156 | @Override 157 | public void showAppList(final List appInfoList) { 158 | mAppInfoAdapter.refreshData(appInfoList); 159 | } 160 | 161 | @Override 162 | public void showLoading() { 163 | if (mLoadingDialog == null) { 164 | mLoadingDialog = new LoadingDialog(requireContext()); 165 | mLoadingDialog.setMsg(getString(R.string.exempt_app_loading_tip)); 166 | } 167 | mLoadingDialog.show(); 168 | } 169 | 170 | @Override 171 | public void dismissLoading() { 172 | if (mLoadingDialog != null && mLoadingDialog.isShowing()) { 173 | mLoadingDialog.dismiss(); 174 | } 175 | } 176 | 177 | @Override 178 | public void exit(boolean configurationChanged) { 179 | Activity activity = getActivity(); 180 | if (activity != null) { 181 | activity.setResult(configurationChanged ? Activity.RESULT_OK : Activity.RESULT_CANCELED); 182 | activity.finish(); 183 | } 184 | } 185 | 186 | @Override 187 | public void setPresenter(ExemptAppContract.Presenter presenter) { 188 | mPresenter = presenter; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/exempt/presenter/ExemptAppPresenter.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.exempt.presenter; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | import io.github.trojan_gfw.igniter.common.os.Task; 12 | import io.github.trojan_gfw.igniter.common.os.Threads; 13 | import io.github.trojan_gfw.igniter.exempt.contract.ExemptAppContract; 14 | import io.github.trojan_gfw.igniter.exempt.data.AppInfo; 15 | import io.github.trojan_gfw.igniter.exempt.data.ExemptAppDataSource; 16 | 17 | public class ExemptAppPresenter implements ExemptAppContract.Presenter { 18 | private final ExemptAppContract.View mView; 19 | private final ExemptAppDataSource mDataSource; 20 | private boolean mDirty; 21 | private boolean mConfigurationChanged; 22 | private List mAllAppInfoList; 23 | private Set mExemptAppPackageNameSet; 24 | 25 | public ExemptAppPresenter(ExemptAppContract.View view, ExemptAppDataSource dataSource) { 26 | super(); 27 | mView = view; 28 | mDataSource = dataSource; 29 | view.setPresenter(this); 30 | } 31 | 32 | @Override 33 | public void updateAppInfo(AppInfo appInfo, int position, boolean exempt) { 34 | mDirty = true; 35 | String packageName = appInfo.getPackageName(); 36 | if (mExemptAppPackageNameSet.contains(packageName)) { 37 | if (!exempt) { 38 | mExemptAppPackageNameSet.remove(packageName); 39 | } 40 | } else if (exempt) { 41 | mExemptAppPackageNameSet.add(packageName); 42 | } 43 | appInfo.setExempt(exempt); 44 | } 45 | 46 | @Override 47 | public void filterAppsByName(final String name) { 48 | if (TextUtils.isEmpty(name)) { 49 | mView.showAppList(mAllAppInfoList); 50 | return; 51 | } 52 | Threads.instance().runOnWorkThread(new Task() { 53 | @Override 54 | public void onRun() { 55 | final List tmpInfoList = new ArrayList<>(); 56 | final String lowercaseName = name.toLowerCase(); 57 | for (AppInfo appInfo : mAllAppInfoList) { 58 | if (appInfo.getAppNameInLowercase().contains(lowercaseName)) { 59 | tmpInfoList.add(appInfo); 60 | } 61 | } 62 | Threads.instance().runOnUiThread(new Runnable() { 63 | @Override 64 | public void run() { 65 | mView.showAppList(tmpInfoList); 66 | } 67 | }); 68 | } 69 | }); 70 | } 71 | 72 | @Override 73 | public void saveExemptAppInfoList() { 74 | if (!mDirty) { 75 | mView.showSaveSuccess(); 76 | return; 77 | } 78 | mConfigurationChanged = true; 79 | mView.showLoading(); 80 | Threads.instance().runOnWorkThread(new Task() { 81 | @Override 82 | public void onRun() { 83 | mDataSource.saveExemptAppInfoSet(mExemptAppPackageNameSet); 84 | mDirty = false; 85 | Threads.instance().runOnUiThread(new Runnable() { 86 | @Override 87 | public void run() { 88 | mView.dismissLoading(); 89 | mView.showSaveSuccess(); 90 | } 91 | }); 92 | } 93 | }); 94 | } 95 | 96 | @Override 97 | public boolean handleBackPressed() { 98 | if (mDirty) { 99 | mView.showExitConfirm(); 100 | } 101 | return mDirty; 102 | } 103 | 104 | @Override 105 | public void exit() { 106 | mView.exit(mConfigurationChanged); 107 | } 108 | 109 | @Override 110 | public void start() { 111 | mView.showLoading(); 112 | Threads.instance().runOnWorkThread(new Task() { 113 | @Override 114 | public void onRun() { 115 | final List allAppInfoList = mDataSource.getAllAppInfoList(); 116 | mExemptAppPackageNameSet = mDataSource.loadExemptAppPackageNameSet(); 117 | for (AppInfo appInfo : allAppInfoList) { 118 | if (mExemptAppPackageNameSet.contains(appInfo.getPackageName())) { 119 | appInfo.setExempt(true); 120 | } 121 | } 122 | // cluster exempted apps. 123 | Collections.sort(allAppInfoList, new Comparator() { 124 | @Override 125 | public int compare(AppInfo o1, AppInfo o2) { 126 | if (o1.isExempt() != o2.isExempt()) { 127 | return o1.isExempt() ? -1 : 1; 128 | } 129 | return o1.getAppName().compareTo(o2.getAppName()); 130 | } 131 | }); 132 | mAllAppInfoList = allAppInfoList; 133 | Threads.instance().runOnUiThread(new Runnable() { 134 | @Override 135 | public void run() { 136 | mView.showAppList(allAppInfoList); 137 | mView.dismissLoading(); 138 | } 139 | }); 140 | } 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/initializer/Initializer.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.initializer; 2 | 3 | import android.content.Context; 4 | 5 | public abstract class Initializer { 6 | 7 | public abstract void init(Context context); 8 | 9 | public abstract boolean runsInWorkerThread(); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/initializer/InitializerHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.initializer; 2 | 3 | import android.content.Context; 4 | import android.os.Process; 5 | import android.text.TextUtils; 6 | 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | import io.github.trojan_gfw.igniter.common.os.Task; 11 | import io.github.trojan_gfw.igniter.common.os.Threads; 12 | import io.github.trojan_gfw.igniter.common.utils.ProcessUtils; 13 | 14 | /** 15 | * Helper class of application initializations. You can just extend {@link Initializer} to create your 16 | * own initializer and register it in {@link #registerMainInitializers()} or {@link #registerToolsInitializers()}. 17 | * You should consider carefully to determine which process your initializers are run in. 18 | */ 19 | public class InitializerHelper { 20 | private static final String TOOL_PROCESS_POSTFIX = ":tools"; 21 | private static final String PROXY_PROCESS_POSTFIX = ":proxy"; 22 | private static List sInitializerList; 23 | 24 | private static void createInitializerList() { 25 | sInitializerList = new LinkedList<>(); 26 | } 27 | 28 | private static void registerMainInitializers() { 29 | createInitializerList(); 30 | sInitializerList.add(new MainInitializer()); 31 | } 32 | 33 | private static void registerToolsInitializers() { 34 | createInitializerList(); 35 | sInitializerList.add(new ToolInitializer()); 36 | } 37 | 38 | private static void registerProxyInitializers() { 39 | createInitializerList(); 40 | sInitializerList.add(new ProxyInitializer()); 41 | } 42 | 43 | public static void runInit(Context context) { 44 | final String processName = ProcessUtils.getProcessNameByPID(context, Process.myPid()); 45 | if (isToolProcess(processName)) { 46 | registerToolsInitializers(); 47 | } else if (isProxyProcess(processName)) { 48 | registerProxyInitializers(); 49 | } else { 50 | registerMainInitializers(); 51 | } 52 | runInit(context, sInitializerList); 53 | clearInitializerLists(); 54 | } 55 | 56 | private static void clearInitializerLists() { 57 | sInitializerList = null; 58 | } 59 | 60 | private static void runInit(final Context context, List initializerList) { 61 | final List runInWorkerThreadList = new LinkedList<>(); 62 | for (int i = initializerList.size() - 1; i >= 0; i--) { 63 | if (initializerList.get(i).runsInWorkerThread()) { 64 | runInWorkerThreadList.add(initializerList.remove(i)); 65 | } 66 | } 67 | Threads.instance().runOnWorkThread(new Task() { 68 | @Override 69 | public void onRun() { 70 | runInitList(context, runInWorkerThreadList); 71 | } 72 | }); 73 | runInitList(context, initializerList); 74 | } 75 | 76 | private static void runInitList(Context context, List initializers) { 77 | for (int i = initializers.size() - 1; i >= 0; i--) { 78 | initializers.remove(i).init(context); 79 | } 80 | } 81 | 82 | private static boolean isMainProcess(String processName) { 83 | return !isToolProcess(processName) && !isProxyProcess(processName); 84 | } 85 | 86 | private static boolean isToolProcess(String processName) { 87 | return TextUtils.equals(processName, TOOL_PROCESS_POSTFIX); 88 | } 89 | 90 | private static boolean isProxyProcess(String processName) { 91 | return TextUtils.equals(processName, PROXY_PROCESS_POSTFIX); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/initializer/MainInitializer.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.initializer; 2 | 3 | import android.content.Context; 4 | 5 | import io.github.trojan_gfw.igniter.Globals; 6 | 7 | /** 8 | * Initializer that runs in Main Process (Default process). 9 | */ 10 | public class MainInitializer extends Initializer { 11 | 12 | @Override 13 | public void init(Context context) { 14 | Globals.Init(context); 15 | } 16 | 17 | @Override 18 | public boolean runsInWorkerThread() { 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/initializer/ProxyInitializer.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.initializer; 2 | 3 | import android.content.Context; 4 | 5 | import io.github.trojan_gfw.igniter.Globals; 6 | import io.github.trojan_gfw.igniter.LogHelper; 7 | import io.github.trojan_gfw.igniter.TrojanConfig; 8 | import io.github.trojan_gfw.igniter.TrojanHelper; 9 | 10 | public class ProxyInitializer extends Initializer { 11 | private static final String TAG = "ProxyInitializer"; 12 | 13 | @Override 14 | public void init(Context context) { 15 | Globals.Init(context); 16 | TrojanConfig cacheConfig = TrojanHelper.readTrojanConfig(Globals.getTrojanConfigPath()); 17 | if (cacheConfig == null) { 18 | LogHelper.e(TAG, "read null trojan config"); 19 | } else { 20 | cacheConfig.setCaCertPath(Globals.getCaCertPath()); 21 | Globals.setTrojanConfigInstance(cacheConfig); 22 | } 23 | if (!Globals.getTrojanConfigInstance().isValidRunningConfig()) { 24 | LogHelper.e(TAG, "Invalid trojan config!"); 25 | } 26 | } 27 | 28 | @Override 29 | public boolean runsInWorkerThread() { 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/initializer/ToolInitializer.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.initializer; 2 | 3 | import android.content.Context; 4 | 5 | import io.github.trojan_gfw.igniter.Globals; 6 | 7 | /** 8 | * Initializer that runs in Tools Process. 9 | */ 10 | public class ToolInitializer extends Initializer { 11 | 12 | @Override 13 | public void init(Context context) { 14 | Globals.Init(context); 15 | } 16 | 17 | @Override 18 | public boolean runsInWorkerThread() { 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/qrcode/ScanQRCodeActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.qrcode; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.widget.Toast; 7 | 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import cn.bingoogolapple.qrcode.core.BarcodeType; 11 | import cn.bingoogolapple.qrcode.zxing.ZXingView; 12 | import io.github.trojan_gfw.igniter.R; 13 | 14 | public class ScanQRCodeActivity extends AppCompatActivity implements ZXingView.Delegate { 15 | public static final String KEY_SCAN_CONTENT = "content"; 16 | // private static final String TAG = "ScanQRCodeActivity"; 17 | 18 | public static Intent create(Context context) { 19 | return new Intent(context, ScanQRCodeActivity.class); 20 | } 21 | 22 | private ZXingView mZXingView; 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_scan_qrcode); 27 | mZXingView = findViewById(R.id.zxingview); 28 | mZXingView.setType(BarcodeType.ONLY_QR_CODE, null); 29 | mZXingView.setDelegate(this); 30 | } 31 | 32 | @Override 33 | protected void onResume() { 34 | super.onResume(); 35 | mZXingView.startCamera(); 36 | mZXingView.startSpotAndShowRect(); 37 | } 38 | 39 | @Override 40 | protected void onPause() { 41 | super.onPause(); 42 | mZXingView.stopSpot(); 43 | mZXingView.stopCamera(); 44 | } 45 | 46 | @Override 47 | public void onScanQRCodeSuccess(String result) { 48 | Intent intent = new Intent(); 49 | intent.putExtra(KEY_SCAN_CONTENT, result); 50 | setResult(RESULT_OK, intent); 51 | finish(); 52 | } 53 | 54 | @Override 55 | public void onCameraAmbientBrightnessChanged(boolean isDark) { 56 | 57 | } 58 | 59 | @Override 60 | public void onScanQRCodeOpenCameraError() { 61 | runOnUiThread(new Runnable() { 62 | @Override 63 | public void run() { 64 | Toast.makeText(getApplicationContext(), R.string.scan_qr_code_camera_error, Toast.LENGTH_SHORT).show(); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/servers/activity/ServerListActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.servers.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | 7 | import androidx.fragment.app.FragmentManager; 8 | 9 | import io.github.trojan_gfw.igniter.Globals; 10 | import io.github.trojan_gfw.igniter.R; 11 | import io.github.trojan_gfw.igniter.common.app.BaseAppCompatActivity; 12 | import io.github.trojan_gfw.igniter.servers.data.ServerListDataManager; 13 | import io.github.trojan_gfw.igniter.servers.fragment.ServerListFragment; 14 | import io.github.trojan_gfw.igniter.servers.presenter.ServerListPresenter; 15 | 16 | public class ServerListActivity extends BaseAppCompatActivity { 17 | public static final String KEY_TROJAN_CONFIG = "trojan_config"; 18 | 19 | public static Intent create(Context context) { 20 | return new Intent(context, ServerListActivity.class); 21 | } 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_server_list); 27 | 28 | FragmentManager fm = getSupportFragmentManager(); 29 | ServerListFragment fragment = (ServerListFragment) fm.findFragmentByTag(ServerListFragment.TAG); 30 | if (fragment == null) { 31 | fragment = ServerListFragment.newInstance(); 32 | } 33 | new ServerListPresenter(fragment, new ServerListDataManager(Globals.getTrojanConfigListPath())); 34 | fm.beginTransaction() 35 | .replace(R.id.parent_fl, fragment, ServerListFragment.TAG) 36 | .commitAllowingStateLoss(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/servers/contract/ServerListContract.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.servers.contract; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | 6 | import java.util.List; 7 | 8 | import io.github.trojan_gfw.igniter.TrojanConfig; 9 | import io.github.trojan_gfw.igniter.common.mvp.BasePresenter; 10 | import io.github.trojan_gfw.igniter.common.mvp.BaseView; 11 | 12 | public interface ServerListContract { 13 | interface Presenter extends BasePresenter { 14 | void addServerConfig(String trojanUrl); 15 | void handleServerSelection(TrojanConfig config); 16 | void deleteServerConfig(TrojanConfig config, int pos); 17 | void gotoScanQRCode(); 18 | void displayImportFileDescription(); 19 | void hideImportFileDescription(); 20 | void importConfigFromFile(); 21 | void parseConfigsInFileStream(Context context, Uri fileUri); 22 | } 23 | 24 | interface View extends BaseView { 25 | void showAddTrojanConfigSuccess(); 26 | void showQRCodeScanError(String scanContent); 27 | void selectServerConfig(TrojanConfig config); 28 | void showServerConfigList(List configs); 29 | void removeServerConfig(TrojanConfig config, int pos); 30 | void gotoScanQRCode(); 31 | void showImportFileDescription(); 32 | void dismissImportFileDescription(); 33 | void openFileChooser(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/servers/data/ServerListDataManager.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.servers.data; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import io.github.trojan_gfw.igniter.TrojanConfig; 9 | import io.github.trojan_gfw.igniter.TrojanHelper; 10 | 11 | public class ServerListDataManager implements ServerListDataSource { 12 | private final String mConfigFilePath; 13 | 14 | public ServerListDataManager(String configFilePath) { 15 | mConfigFilePath = configFilePath; 16 | } 17 | 18 | @Override 19 | @NonNull 20 | public List loadServerConfigList() { 21 | return new ArrayList<>(TrojanHelper.readTrojanServerConfigList(mConfigFilePath)); 22 | } 23 | 24 | @Override 25 | public void deleteServerConfig(TrojanConfig config) { 26 | List trojanConfigs = loadServerConfigList(); 27 | for (int i = trojanConfigs.size() - 1; i >= 0; i--) { 28 | if (trojanConfigs.get(i).getRemoteAddr().equals(config.getRemoteAddr())) { 29 | trojanConfigs.remove(i); 30 | replaceServerConfigs(trojanConfigs); 31 | break; 32 | } 33 | } 34 | } 35 | 36 | @Override 37 | public void saveServerConfig(TrojanConfig config) { 38 | if (config == null) { 39 | return; 40 | } 41 | final String remoteAddr = config.getRemoteAddr(); 42 | if (remoteAddr == null) { 43 | return; 44 | } 45 | boolean configRemoteAddrExists = false; 46 | List trojanConfigs = loadServerConfigList(); 47 | for (int i = trojanConfigs.size() - 1; i >= 0; i--) { 48 | TrojanConfig cacheConfig = trojanConfigs.get(i); 49 | if (cacheConfig == null) continue; 50 | if (remoteAddr.equals(cacheConfig.getRemoteAddr())) { 51 | trojanConfigs.set(i, config); 52 | configRemoteAddrExists = true; 53 | break; 54 | } 55 | } 56 | if (!configRemoteAddrExists) { 57 | trojanConfigs.add(config); 58 | } 59 | replaceServerConfigs(trojanConfigs); 60 | } 61 | 62 | @Override 63 | public void replaceServerConfigs(List list) { 64 | TrojanHelper.writeTrojanServerConfigList(list, mConfigFilePath); 65 | TrojanHelper.ShowTrojanConfigList(mConfigFilePath); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/servers/data/ServerListDataSource.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.servers.data; 2 | 3 | import androidx.annotation.WorkerThread; 4 | 5 | import java.util.List; 6 | 7 | import io.github.trojan_gfw.igniter.TrojanConfig; 8 | 9 | public interface ServerListDataSource { 10 | @WorkerThread 11 | List loadServerConfigList(); 12 | @WorkerThread 13 | void deleteServerConfig(TrojanConfig config); 14 | @WorkerThread 15 | void saveServerConfig(TrojanConfig config); 16 | @WorkerThread 17 | void replaceServerConfigs(List list); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/servers/fragment/ServerListAdapter.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.servers.fragment; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import io.github.trojan_gfw.igniter.R; 16 | import io.github.trojan_gfw.igniter.TrojanConfig; 17 | 18 | public class ServerListAdapter extends RecyclerView.Adapter { 19 | private final LayoutInflater mInflater; 20 | private final List mData; 21 | private OnItemClickListener mOnItemClickListener; 22 | 23 | public ServerListAdapter(Context context, List data) { 24 | super(); 25 | this.mData = new ArrayList<>(data); 26 | mInflater = LayoutInflater.from(context); 27 | } 28 | 29 | @NonNull 30 | @Override 31 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { 32 | ViewHolder vh = new ViewHolder(mInflater.inflate(R.layout.item_server, viewGroup, false)); 33 | vh.bindListener(mOnItemClickListener); 34 | return vh; 35 | } 36 | 37 | public void replaceData(List data) { 38 | mData.clear(); 39 | mData.addAll(data); 40 | notifyDataSetChanged(); 41 | } 42 | 43 | public void removeItemOnPosition(int pos) { 44 | mData.remove(pos); 45 | notifyItemRemoved(pos); 46 | } 47 | 48 | @Override 49 | public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { 50 | viewHolder.bindData(mData.get(i)); 51 | } 52 | 53 | @Override 54 | public int getItemCount() { 55 | return mData.size(); 56 | } 57 | 58 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) { 59 | this.mOnItemClickListener = onItemClickListener; 60 | } 61 | 62 | public interface OnItemClickListener { 63 | void onItemSelected(TrojanConfig config, int pos); 64 | 65 | void onItemDelete(TrojanConfig config, int pos); 66 | } 67 | } 68 | 69 | class ViewHolder extends RecyclerView.ViewHolder { 70 | private TrojanConfig mConfig; 71 | private TextView mRemoteAddrTv; 72 | private ServerListAdapter.OnItemClickListener itemClickListener; 73 | 74 | public ViewHolder(@NonNull final View itemView) { 75 | super(itemView); 76 | mRemoteAddrTv = itemView.findViewById(R.id.serverAddrTv); 77 | itemView.setOnClickListener(new View.OnClickListener() { 78 | @Override 79 | public void onClick(View v) { 80 | if (itemClickListener != null) { 81 | itemClickListener.onItemSelected(mConfig, getBindingAdapterPosition()); 82 | } 83 | } 84 | }); 85 | itemView.findViewById(R.id.deleteServerBtn).setOnClickListener(new View.OnClickListener() { 86 | @Override 87 | public void onClick(View v) { 88 | if (itemClickListener != null) { 89 | itemClickListener.onItemDelete(mConfig, getBindingAdapterPosition()); 90 | } 91 | } 92 | }); 93 | } 94 | 95 | public void bindData(TrojanConfig config) { 96 | this.mConfig = config; 97 | mRemoteAddrTv.setText(config.getRemoteAddr()); 98 | } 99 | 100 | public void bindListener(ServerListAdapter.OnItemClickListener listener) { 101 | itemClickListener = listener; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/servers/fragment/ServerListFragment.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.servers.fragment; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.Dialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.graphics.drawable.Drawable; 11 | import android.net.Uri; 12 | import android.os.Bundle; 13 | import android.view.LayoutInflater; 14 | import android.view.Menu; 15 | import android.view.MenuInflater; 16 | import android.view.MenuItem; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import android.widget.Toast; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.annotation.Nullable; 23 | import androidx.appcompat.app.AlertDialog; 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.appcompat.widget.Toolbar; 26 | import androidx.core.content.ContextCompat; 27 | import androidx.core.graphics.drawable.DrawableCompat; 28 | import androidx.fragment.app.FragmentActivity; 29 | import androidx.recyclerview.widget.LinearLayoutManager; 30 | import androidx.recyclerview.widget.RecyclerView; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | 35 | import io.github.trojan_gfw.igniter.R; 36 | import io.github.trojan_gfw.igniter.TrojanConfig; 37 | import io.github.trojan_gfw.igniter.common.app.BaseFragment; 38 | import io.github.trojan_gfw.igniter.qrcode.ScanQRCodeActivity; 39 | import io.github.trojan_gfw.igniter.servers.activity.ServerListActivity; 40 | import io.github.trojan_gfw.igniter.servers.contract.ServerListContract; 41 | 42 | public class ServerListFragment extends BaseFragment implements ServerListContract.View { 43 | private static final int FILE_IMPORT_REQUEST_CODE = 120; 44 | private static final int SCAN_QR_CODE_REQUEST_CODE = 110; 45 | private static final int REQUEST_CAMERA_CODE = 114; 46 | public static final String TAG = "ServerListFragment"; 47 | public static final String KEY_TROJAN_CONFIG = ServerListActivity.KEY_TROJAN_CONFIG; 48 | private ServerListContract.Presenter mPresenter; 49 | private RecyclerView mServerListRv; 50 | private ServerListAdapter mServerListAdapter; 51 | private Dialog mImportConfigDialog; 52 | 53 | public ServerListFragment() { 54 | // Required empty public constructor 55 | } 56 | 57 | public static ServerListFragment newInstance() { 58 | return new ServerListFragment(); 59 | } 60 | 61 | @Override 62 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 63 | Bundle savedInstanceState) { 64 | // Inflate the layout for this fragment 65 | return inflater.inflate(R.layout.fragment_server_list, container, false); 66 | } 67 | 68 | @Override 69 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 70 | super.onViewCreated(view, savedInstanceState); 71 | findViews(); 72 | initViews(); 73 | initListeners(); 74 | mPresenter.start(); 75 | } 76 | 77 | private void findViews() { 78 | mServerListRv = findViewById(R.id.serverListRv); 79 | } 80 | 81 | private void initViews() { 82 | FragmentActivity activity = getActivity(); 83 | if (activity instanceof AppCompatActivity) { 84 | ((AppCompatActivity) activity).setSupportActionBar((Toolbar)findViewById(R.id.toolbar)); 85 | setHasOptionsMenu(true); 86 | } 87 | mServerListRv.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false)); 88 | mServerListAdapter = new ServerListAdapter(getContext(), new ArrayList()); 89 | mServerListRv.setAdapter(mServerListAdapter); 90 | } 91 | 92 | private void initListeners() { 93 | mServerListAdapter.setOnItemClickListener(new ServerListAdapter.OnItemClickListener() { 94 | @Override 95 | public void onItemSelected(TrojanConfig config, int pos) { 96 | mPresenter.handleServerSelection(config); 97 | } 98 | 99 | @Override 100 | public void onItemDelete(TrojanConfig config, int pos) { 101 | mPresenter.deleteServerConfig(config, pos); 102 | } 103 | }); 104 | } 105 | 106 | private Context getApplicationContext() { 107 | if (getActivity() != null) { 108 | return getActivity().getApplicationContext(); 109 | } 110 | return null; 111 | } 112 | 113 | @Override 114 | public void showAddTrojanConfigSuccess() { 115 | mRootView.post(new Runnable() { 116 | @Override 117 | public void run() { 118 | Toast.makeText(getApplicationContext(), R.string.scan_qr_code_success, Toast.LENGTH_SHORT).show(); 119 | } 120 | }); 121 | } 122 | 123 | @Override 124 | public void showQRCodeScanError(final String scanContent) { 125 | mRootView.post(new Runnable() { 126 | @Override 127 | public void run() { 128 | Toast.makeText(getApplicationContext(), getString(R.string.scan_qr_code_failed, scanContent), Toast.LENGTH_SHORT).show(); 129 | } 130 | }); 131 | } 132 | 133 | @Override 134 | public void gotoScanQRCode() { 135 | if (PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA)) { 136 | gotoScanQRCodeInner(); 137 | } else { 138 | requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_CODE); 139 | } 140 | } 141 | 142 | private void gotoScanQRCodeInner() { 143 | startActivityForResult(ScanQRCodeActivity.create(mContext), SCAN_QR_CODE_REQUEST_CODE); 144 | } 145 | 146 | @Override 147 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 148 | super.onActivityResult(requestCode, resultCode, data); 149 | if (SCAN_QR_CODE_REQUEST_CODE == requestCode && resultCode == Activity.RESULT_OK && data != null) { 150 | mPresenter.addServerConfig(data.getStringExtra(ScanQRCodeActivity.KEY_SCAN_CONTENT)); 151 | } else if (FILE_IMPORT_REQUEST_CODE == requestCode && resultCode == Activity.RESULT_OK && data != null) { 152 | Uri uri = data.getData(); 153 | if (uri != null) { 154 | mPresenter.parseConfigsInFileStream(getContext(), uri); 155 | } 156 | } 157 | } 158 | 159 | @Override 160 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 161 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 162 | if (REQUEST_CAMERA_CODE == requestCode) { 163 | if (PackageManager.PERMISSION_GRANTED == grantResults[0]) { 164 | gotoScanQRCodeInner(); 165 | } else { 166 | Toast.makeText(getContext(), R.string.server_list_lack_of_camera_permission, Toast.LENGTH_SHORT).show(); 167 | } 168 | } 169 | } 170 | 171 | @Override 172 | public void selectServerConfig(TrojanConfig config) { 173 | FragmentActivity activity = getActivity(); 174 | if (activity != null) { 175 | Intent intent = new Intent(); 176 | intent.putExtra(KEY_TROJAN_CONFIG, config); 177 | activity.setResult(Activity.RESULT_OK, intent); 178 | activity.finish(); 179 | } 180 | } 181 | 182 | 183 | 184 | @Override 185 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 186 | menu.clear(); 187 | inflater.inflate(R.menu.menu_server_list, menu); 188 | MenuItem item = menu.getItem(0); 189 | // Tint scan QRCode icon to white. 190 | if (item.getIcon() != null) { 191 | Drawable drawable = item.getIcon(); 192 | Drawable wrapper = DrawableCompat.wrap(drawable); 193 | drawable.mutate(); 194 | DrawableCompat.setTint(wrapper, ContextCompat.getColor(mContext, android.R.color.white)); 195 | item.setIcon(drawable); 196 | } 197 | } 198 | 199 | @Override 200 | public boolean onOptionsItemSelected(MenuItem item) { 201 | switch (item.getItemId()) { 202 | case R.id.action_scan_qr_code: 203 | mPresenter.gotoScanQRCode(); 204 | return true; 205 | case R.id.action_import_from_file: 206 | mPresenter.displayImportFileDescription(); 207 | return true; 208 | default: 209 | break; 210 | } 211 | return super.onOptionsItemSelected(item); 212 | } 213 | 214 | @Override 215 | public void showImportFileDescription() { 216 | mImportConfigDialog = new AlertDialog.Builder(mContext).setTitle(R.string.common_alert) 217 | .setMessage(R.string.server_list_import_file_desc) 218 | .setPositiveButton(R.string.common_confirm, new DialogInterface.OnClickListener() { 219 | @Override 220 | public void onClick(DialogInterface dialog, int which) { 221 | mPresenter.importConfigFromFile(); 222 | } 223 | }).setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() { 224 | @Override 225 | public void onClick(DialogInterface dialog, int which) { 226 | mPresenter.hideImportFileDescription(); 227 | } 228 | }).create(); 229 | mImportConfigDialog.show(); 230 | } 231 | 232 | @Override 233 | public void dismissImportFileDescription() { 234 | if (mImportConfigDialog != null && mImportConfigDialog.isShowing()) { 235 | mImportConfigDialog.dismiss(); 236 | mImportConfigDialog = null; 237 | } 238 | } 239 | 240 | @Override 241 | public void openFileChooser() { 242 | Intent intent = new Intent() 243 | .setType("text/plain") 244 | .setAction(Intent.ACTION_GET_CONTENT); 245 | startActivityForResult(Intent.createChooser(intent, getString(R.string.server_list_file_chooser_msg)), FILE_IMPORT_REQUEST_CODE); 246 | } 247 | 248 | @Override 249 | public void showServerConfigList(final List configs) { 250 | mRootView.post(new Runnable() { 251 | @Override 252 | public void run() { 253 | mServerListAdapter.replaceData(configs); 254 | } 255 | }); 256 | } 257 | 258 | @Override 259 | public void removeServerConfig(TrojanConfig config, final int pos) { 260 | mRootView.post(new Runnable() { 261 | @Override 262 | public void run() { 263 | mServerListAdapter.removeItemOnPosition(pos); 264 | } 265 | }); 266 | } 267 | 268 | @Override 269 | public void setPresenter(ServerListContract.Presenter presenter) { 270 | mPresenter = presenter; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/servers/presenter/ServerListPresenter.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.servers.presenter; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.util.ArrayList; 14 | import java.util.Collections; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Set; 18 | 19 | import io.github.trojan_gfw.igniter.TrojanConfig; 20 | import io.github.trojan_gfw.igniter.TrojanURLHelper; 21 | import io.github.trojan_gfw.igniter.common.os.Task; 22 | import io.github.trojan_gfw.igniter.common.os.Threads; 23 | import io.github.trojan_gfw.igniter.servers.contract.ServerListContract; 24 | import io.github.trojan_gfw.igniter.servers.data.ServerListDataSource; 25 | 26 | public class ServerListPresenter implements ServerListContract.Presenter { 27 | private final ServerListContract.View mView; 28 | private final ServerListDataSource mDataManager; 29 | 30 | public ServerListPresenter(ServerListContract.View view, ServerListDataSource dataManager) { 31 | mView = view; 32 | mDataManager = dataManager; 33 | view.setPresenter(this); 34 | } 35 | 36 | @Override 37 | public void hideImportFileDescription() { 38 | mView.dismissImportFileDescription(); 39 | } 40 | 41 | @Override 42 | public void displayImportFileDescription() { 43 | mView.showImportFileDescription(); 44 | } 45 | 46 | @Override 47 | public void importConfigFromFile() { 48 | mView.openFileChooser(); 49 | } 50 | 51 | @Override 52 | public void parseConfigsInFileStream(final Context context, final Uri fileUri) { 53 | Threads.instance().runOnWorkThread(new Task() { 54 | @Override 55 | public void onRun() { 56 | StringBuilder sb = new StringBuilder(); 57 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getContentResolver().openInputStream(fileUri)))) { 58 | String line; 59 | while ((line = reader.readLine()) != null) { 60 | sb.append(line); 61 | } 62 | } catch (IOException e) { 63 | e.printStackTrace(); 64 | } 65 | List trojanConfigs = parseTrojanConfigsFromFileContent(sb.toString()); 66 | List currentConfigs = mDataManager.loadServerConfigList(); 67 | currentConfigs.addAll(trojanConfigs); 68 | // remove repeated configurations 69 | Set newTrojanConfigRemoteAddrSet = new HashSet<>(); 70 | for (TrojanConfig config : trojanConfigs) { 71 | newTrojanConfigRemoteAddrSet.add(config.getRemoteAddr()); 72 | } 73 | for (int i = currentConfigs.size() - 1; i >= 0; i--) { 74 | if (newTrojanConfigRemoteAddrSet.contains(currentConfigs.get(i).getRemoteAddr())) { 75 | currentConfigs.remove(i); 76 | } 77 | } 78 | currentConfigs.addAll(trojanConfigs); 79 | mDataManager.replaceServerConfigs(currentConfigs); 80 | loadConfigs(); 81 | mView.showAddTrojanConfigSuccess(); 82 | } 83 | }); 84 | } 85 | 86 | private List parseTrojanConfigsFromFileContent(String fileContent) { 87 | try { 88 | JSONObject jsonObject = new JSONObject(fileContent); 89 | JSONArray configs = jsonObject.optJSONArray("configs"); 90 | if (configs == null) { 91 | return Collections.emptyList(); 92 | } 93 | final int len = configs.length(); 94 | List list = new ArrayList<>(len); 95 | for (int i = 0; i < len; i++) { 96 | JSONObject config = configs.getJSONObject(i); 97 | String remoteAddr = config.optString("server", null); 98 | if (remoteAddr == null) { 99 | continue; 100 | } 101 | TrojanConfig tmp = new TrojanConfig(); 102 | tmp.setRemoteAddr(remoteAddr); 103 | tmp.setRemotePort(config.optInt("server_port")); 104 | tmp.setPassword(config.optString("password")); 105 | tmp.setVerifyCert(config.optBoolean("verify")); 106 | list.add(tmp); 107 | } 108 | return list; 109 | } catch (JSONException e) { 110 | e.printStackTrace(); 111 | } 112 | return Collections.emptyList(); 113 | } 114 | 115 | @Override 116 | public void addServerConfig(final String trojanUrl) { 117 | Threads.instance().runOnWorkThread(new Task() { 118 | @Override 119 | public void onRun() { 120 | TrojanConfig config = TrojanURLHelper.ParseTrojanURL(trojanUrl); 121 | if (config != null) { 122 | mDataManager.saveServerConfig(config); 123 | loadConfigs(); 124 | mView.showAddTrojanConfigSuccess(); 125 | } else { 126 | mView.showQRCodeScanError(trojanUrl); 127 | } 128 | } 129 | }); 130 | } 131 | 132 | @Override 133 | public void handleServerSelection(TrojanConfig config) { 134 | mView.selectServerConfig(config); 135 | } 136 | 137 | @Override 138 | public void deleteServerConfig(final TrojanConfig config, final int pos) { 139 | Threads.instance().runOnWorkThread(new Task() { 140 | @Override 141 | public void onRun() { 142 | mDataManager.deleteServerConfig(config); 143 | mView.removeServerConfig(config, pos); 144 | } 145 | }); 146 | } 147 | 148 | @Override 149 | public void start() { 150 | Threads.instance().runOnWorkThread(new Task() { 151 | @Override 152 | public void onRun() { 153 | loadConfigs(); 154 | } 155 | }); 156 | } 157 | 158 | @Override 159 | public void gotoScanQRCode() { 160 | mView.gotoScanQRCode(); 161 | } 162 | 163 | private void loadConfigs() { 164 | List trojanConfigs = mDataManager.loadServerConfigList(); 165 | mView.showServerConfigList(trojanConfigs); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/tile/IgniterTileService.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.tile; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Build; 6 | import android.os.RemoteException; 7 | import android.service.quicksettings.Tile; 8 | import android.service.quicksettings.TileService; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.RequiresApi; 12 | 13 | import io.github.trojan_gfw.igniter.LogHelper; 14 | import io.github.trojan_gfw.igniter.MainActivity; 15 | import io.github.trojan_gfw.igniter.ProxyService; 16 | import io.github.trojan_gfw.igniter.common.constants.Constants; 17 | import io.github.trojan_gfw.igniter.common.utils.PreferenceUtils; 18 | import io.github.trojan_gfw.igniter.connection.TrojanConnection; 19 | import io.github.trojan_gfw.igniter.proxy.aidl.ITrojanService; 20 | 21 | /** 22 | * Igniter's implementation of TileService, showing current state of {@link ProxyService} and providing a 23 | * shortcut to start or stop {@link ProxyService} by the help of {@link ProxyHelper}. This 24 | * service receives state change by the help of {@link TrojanConnection}. 25 | * 26 | * @see ProxyService 27 | * @see io.github.trojan_gfw.igniter.ProxyService.ProxyState 28 | */ 29 | @RequiresApi(api = Build.VERSION_CODES.N) 30 | public class IgniterTileService extends TileService implements TrojanConnection.Callback { 31 | private static final String TAG = "IgniterTile"; 32 | private final TrojanConnection mConnection = new TrojanConnection(false); 33 | /** 34 | * Indicates that user had tapped the tile before {@link TrojanConnection} connects {@link ProxyService}. 35 | * Generally speaking, when the connection is built, we should call {@link #onClick()} again if 36 | * the value is true. 37 | */ 38 | private boolean mTapPending; 39 | 40 | @Override 41 | public void onStartListening() { 42 | super.onStartListening(); 43 | LogHelper.i(TAG, "onStartListening"); 44 | mConnection.connect(this, this); 45 | } 46 | 47 | @Override 48 | public void onStopListening() { 49 | super.onStopListening(); 50 | LogHelper.i(TAG, "onStopListening"); 51 | mConnection.disconnect(this); 52 | } 53 | 54 | @Override 55 | public void onServiceConnected(ITrojanService service) { 56 | LogHelper.i(TAG, "onServiceConnected"); 57 | try { 58 | int state = service.getState(); 59 | updateTile(state); 60 | if (mTapPending) { 61 | mTapPending = false; 62 | onClick(); 63 | } 64 | } catch (RemoteException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | 69 | @Override 70 | public void onServiceDisconnected() { 71 | LogHelper.i(TAG, "onServiceDisconnected"); 72 | } 73 | 74 | @Override 75 | public void onStateChanged(int state, String msg) { 76 | LogHelper.i(TAG, "onStateChanged# state: " + state + ", msg: " + msg); 77 | updateTile(state); 78 | } 79 | 80 | @Override 81 | public void onTestResult(String testUrl, boolean connected, long delay, @NonNull String error) { 82 | // Do nothing, since TileService will not submit test request. 83 | } 84 | 85 | @Override 86 | public void onBinderDied() { 87 | LogHelper.i(TAG, "onBinderDied"); 88 | } 89 | 90 | private void updateTile(final @ProxyService.ProxyState int state) { 91 | Tile tile = getQsTile(); 92 | if (tile == null) { 93 | return; 94 | } 95 | LogHelper.i(TAG, "updateTile with state: " + state); 96 | switch (state) { 97 | case ProxyService.STATE_NONE: 98 | tile.setState(Tile.STATE_INACTIVE); 99 | break; 100 | case ProxyService.STOPPED: 101 | break; 102 | case ProxyService.STARTED: 103 | tile.setState(Tile.STATE_ACTIVE); 104 | break; 105 | case ProxyService.STARTING: 106 | case ProxyService.STOPPING: 107 | tile.setState(Tile.STATE_UNAVAILABLE); 108 | break; 109 | default: 110 | LogHelper.e(TAG, "Unknown state: " + state); 111 | break; 112 | } 113 | tile.updateTile(); 114 | } 115 | 116 | private boolean isFirstStart() { 117 | return PreferenceUtils.getBooleanPreference(getContentResolver(), Uri.parse(Constants.PREFERENCE_URI), 118 | Constants.PREFERENCE_KEY_FIRST_START, true); 119 | } 120 | 121 | @Override 122 | public void onClick() { 123 | super.onClick(); 124 | LogHelper.i(TAG, "onClick"); 125 | if (isFirstStart()) { 126 | // if user never open Igniter before, when he/she clicks the tile, it is necessary 127 | // to start the launcher activity for resource preparation. 128 | Intent intent = new Intent(this, MainActivity.class); 129 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 130 | startActivity(intent); 131 | return; 132 | } 133 | ITrojanService service = mConnection.getService(); 134 | if (service == null) { 135 | mTapPending = true; 136 | updateTile(ProxyService.STARTING); 137 | } else { 138 | try { 139 | @ProxyService.ProxyState int state = service.getState(); 140 | updateTile(state); 141 | switch (state) { 142 | case ProxyService.STARTED: 143 | stopProxyService(); 144 | break; 145 | case ProxyService.STARTING: 146 | case ProxyService.STOPPING: 147 | break; 148 | case ProxyService.STATE_NONE: 149 | case ProxyService.STOPPED: 150 | startProxyService(); 151 | break; 152 | default: 153 | LogHelper.e(TAG, "Unknown state: " + state); 154 | break; 155 | } 156 | } catch (RemoteException e) { 157 | e.printStackTrace(); 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * Start ProxyService if everything is ready. Otherwise start the launcher Activity. 164 | */ 165 | private void startProxyService() { 166 | if (ProxyHelper.isTrojanConfigValid() && ProxyHelper.isVPNServiceConsented(this)) { 167 | ProxyHelper.startProxyService(this); 168 | } else { 169 | ProxyHelper.startLauncherActivity(this); 170 | } 171 | } 172 | 173 | private void stopProxyService() { 174 | ProxyHelper.stopProxyService(this); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/trojan_gfw/igniter/tile/ProxyHelper.java: -------------------------------------------------------------------------------- 1 | package io.github.trojan_gfw.igniter.tile; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.VpnService; 6 | 7 | import androidx.core.content.ContextCompat; 8 | 9 | import io.github.trojan_gfw.igniter.BuildConfig; 10 | import io.github.trojan_gfw.igniter.Globals; 11 | import io.github.trojan_gfw.igniter.MainActivity; 12 | import io.github.trojan_gfw.igniter.ProxyService; 13 | import io.github.trojan_gfw.igniter.R; 14 | import io.github.trojan_gfw.igniter.TrojanConfig; 15 | import io.github.trojan_gfw.igniter.TrojanHelper; 16 | 17 | /** 18 | * Helper class for starting or stopping {@link ProxyService}. Before starting {@link ProxyService}, 19 | * make sure the TrojanConfig is valid (with the help of {@link #isTrojanConfigValid()} and whether 20 | * user has consented VPN Service (with the help of {@link #isVPNServiceConsented(Context)}. 21 | *
22 | * It's recommended to start launcher activity when the config is invalid or user hasn't consented 23 | * VPN service. 24 | */ 25 | public abstract class ProxyHelper { 26 | public static boolean isTrojanConfigValid() { 27 | TrojanConfig cacheConfig = TrojanHelper.readTrojanConfig(Globals.getTrojanConfigPath()); 28 | if (cacheConfig == null) { 29 | return false; 30 | } 31 | cacheConfig.setCaCertPath(Globals.getCaCertPath()); 32 | if (BuildConfig.DEBUG) { 33 | TrojanHelper.ShowConfig(Globals.getTrojanConfigPath()); 34 | } 35 | return cacheConfig.isValidRunningConfig(); 36 | } 37 | 38 | public static boolean isVPNServiceConsented(Context context) { 39 | return VpnService.prepare(context.getApplicationContext()) == null; 40 | } 41 | 42 | public static void startProxyService(Context context) { 43 | ContextCompat.startForegroundService(context, new Intent(context, ProxyService.class)); 44 | } 45 | 46 | public static void startLauncherActivity(Context context) { 47 | Intent intent = new Intent(context, MainActivity.class); 48 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 49 | context.startActivity(intent); 50 | } 51 | 52 | public static void stopProxyService(Context context) { 53 | Intent intent = new Intent(context.getString(R.string.stop_service)); 54 | intent.setPackage(context.getPackageName()); 55 | context.sendBroadcast(intent); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-hdpi/ic_action_link.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-hdpi/ic_action_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-hdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-hdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-hdpi/ic_tile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-hdpi/qr_code.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-mdpi/ic_action_link.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-mdpi/ic_action_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-mdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-mdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-mdpi/ic_tile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-mdpi/qr_code.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xhdpi/ic_action_link.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xhdpi/ic_action_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xhdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xhdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xhdpi/ic_tile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xhdpi/qr_code.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxhdpi/ic_action_link.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxhdpi/ic_action_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxhdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxhdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxhdpi/ic_tile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxhdpi/qr_code.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxxhdpi/ic_action_link.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_action_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxxhdpi/ic_action_name.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxxhdpi/ic_save.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxxhdpi/ic_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxxhdpi/ic_tile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable-xxxhdpi/qr_code.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/common_round_rect_white_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oasiscifr/igniter/37eb3a3463aec9c03da01ed4dedce97149360b78/app/src/main/res/drawable/qr_code.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_exempt_app.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 24 | 25 | 34 | 35 | 50 | 51 | 68 | 69 | 84 | 85 | 97 | 98 | 112 | 113 | 125 | 126 | 136 | 137 | 143 | 144 | 145 |