import java.util.regex.Matcher import java.util.regex.Pattern apply plugin: 'com.android.application' android { compileSdkVersion 28 buildToolsVersion '28.0.3' compileOptions { targetCompatibility 1.8 sourceCompatibility 1.8 } defaultConfig { applicationId "se.leap.bitmaskclient" versionCode 147 versionName "1.0.3" minSdkVersion 16 targetSdkVersion 28 vectorDrawables.useSupportLibrary = true buildConfigField 'boolean', 'openvpn3', 'false' //Build Config Fields for default donation details //This is the default donation URL and should be set to the donation page of LEAP // and this should not be set/altered anywhere else. buildConfigField 'String', 'default_donation_url', '"https://leap.se/en/about-us/donate"' //This is the donation URL and should be set to the relevant donation page. buildConfigField 'String', 'donation_url', 'null' //The field to enable donations in the app. buildConfigField 'boolean', 'enable_donation', 'true' //The field to enable donation reminder popup in the app if enable_donation is set to 'false' this will be disabled. buildConfigField 'boolean', 'enable_donation_reminder', 'true' //The duration in days to trigger the donation reminder buildConfigField 'int', 'donation_reminder_duration', '30' //skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead buildConfigField 'boolean', 'priotize_anonymous_usage', 'false' //ignore the following configs, only used in custom flavor buildConfigField "String", "customProviderUrl", '""' buildConfigField "String", "customProviderIp", '""' buildConfigField "String", "customProviderApiIp", '""' buildConfigField "String", "geoipUrl", '""' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" dexOptions { jumboMode true multiDexEnabled true } } signingConfigs { release { storeFile project.hasProperty('storeFileProperty') ? file(storeFileProperty) : null storePassword project.hasProperty('storePasswordProperty') ? storePasswordProperty : "" keyAlias project.hasProperty('keyAliasProperty') ? keyAliasProperty : "" keyPassword project.hasProperty('keyPasswordProperty') ? keyPasswordProperty : "" } } flavorDimensions "branding", "implementation", "abi" productFlavors { productFlavors.all { ext.appName = null ext.splitApk = false } production { dimension "implementation" } insecure { dimension "implementation" } normal { dimension "branding" appName = "Bitmask" splitApk = true } custom { dimension "branding" //************************************************************************** //************************************************************************** //Configurations for custom branded app. //Change the package name as needed, e.g. "org.example.myapp" applicationId "se.leap.riseupvpn" //Set app name here appName = "Riseup VPN" //Provider base url, e.g. '"https://example.com"' def customProviderUrl = '"https://riseup.net"' buildConfigField "String", "customProviderUrl", customProviderUrl //static ip address of provider, using a commercially validated CA certificate to serve the provider.json def customProviderIp = '"198.252.153.70"' buildConfigField "String", "customProviderIp", customProviderIp //static ip address of the provider api, using a self signed certificate to serve provider.json, eip-service.json etc. def customProviderApiIp = '"198.252.153.107"' buildConfigField "String", "customProviderApiIp", customProviderApiIp def geoipUrl = '"https://api.black.riseup.net:9001/json"' buildConfigField "String", "geoipUrl", geoipUrl //Change the versionCode as needed //versionCode 1 //Change the versionName as needed //versionName "0.9.9RC1" //skip the account creation / login screen if the provider offers anonymous vpn usage, use directly the anonymous cert instead buildConfigField 'boolean', 'priotize_anonymous_usage', 'true' //Build Config Fields for default donation details //This is the donation URL and should be set to the relevant donation page. buildConfigField 'String', 'donation_url', '"https://riseup.net/vpn/donate"' //The field to enable donations in the app. buildConfigField 'boolean', 'enable_donation', 'true' //The field to enable donation reminder popup in the app if enable_donation is set to 'false' this will be disabled. buildConfigField 'boolean', 'enable_donation_reminder', 'true' //The duration in days to trigger the donation reminder buildConfigField 'int', 'donation_reminder_duration', '30' // Build apks for each architecture, in addition to one 'fat' apk containing libraries for all all architectures // enable this if you're publishing in gplay ext { splitApk = true } //************************************************************************** //************************************************************************** } fat { dimension "abi" ext { abiVersionCode = 0 abiFilter = "" } } x86 { dimension "abi" ndk { abiFilters "x86" } ext { abiVersionCode = 1 abiFilter = "x86" } } armv7 { dimension "abi" ndk { abiFilters "armeabi-v7a" } ext { abiVersionCode = 2 abiFilter = "armeabi-v7a" } } x86_64 { dimension "abi" ndk { abiFilters "x86_64" } ext { abiVersionCode = 3 abiFilter = "x86_64" } } arm64 { dimension "abi" ndk { abiFilters "arm64-v8a" } ext { abiVersionCode = 4 abiFilter = "arm64-v8a" } } } buildTypes { buildTypes.all { ext.appSuffix = "" } release { //runProguard true if(signingConfigs.contains(release)) signingConfig signingConfigs.release.isSigningReady() ? signingConfigs.release : signingConfigs.debug //minifyEnabled = true //shrinkResources true } beta { initWith release applicationIdSuffix ".beta" appSuffix = " Beta" } debug { testCoverageEnabled = true } } lintOptions { abortOnError false } sourceSets { main { assets.srcDirs = ['assets', 'ovpnlibs/assets', '../ics-openvpn/main/build/ovpnassets', // '../go/out' TODO: uncomment this line as soon as we want to use PT in production ] jniLibs.srcDirs = ['../ics-openvpn/main/build/intermediates/cmake/skeletonRelease/obj/'] jni.srcDirs = [] //disable automatic ndk-build } debug { assets.srcDirs = ['src/debug/assets', /*'../go/out'*/] } test { resources.srcDirs += ['src/test/resources'] java.srcDirs += ['src/sharedTest/java'] } androidTest { java.srcDirs += ['src/sharedTest/java'] } } /** * BUILD VARAIANTS: * ================= * Development builds: * -------------------- * customProductionFatDebug -> branded development build, includes all ABIs * normalProductionFatDebug -> Bitmask development build, includes all ABIS * customInsecureFatDebug -> branded development build, doesn't checks certificates (for test server setup w/o valid certificates), includes all ABIs * normalInsecureFatDebug -> Bitmask development build, doesn't checks certificates (for test server setup w/o valid certificates), includes all ABIs * * Branded Releases: * ----------------- * customProductionFatBeta -> branded build, includes all ABI's, Beta release * customProductionFatRelease -> branded build, includes all ABI's, stable release (-> F-Droid, GPlay if not splitApk is set to true) * * Bitmask Beta releases: * ---------------------- * normalProductionArm64Beta -> Bitmask build, only for ABI arm64, for GPlay Beta channel with split apks (1 of 4) * normalProductionArmv7Beta -> Bitmask build, only for ABI armeabi-v7a, for GPlay Beta channel with split apks (2 of 4) * normalProductionX86Beta -> Bitmask build, only for ABI x86, for GPlay Beta channel with split apks (3 of 4) * normalProductionX86_64Beta -> Bitmask build, only for ABI x86 64 bit, for GPlay Beta channel with split apks (4 of 4) * normalProductionFatBeta -> Bitmask build, including all ABIS, for izzysoft's F-Droid repo and beta link on download page * * Bitmask Stable releases: * ------------------------ * normalProductionArm64Release -> Bitmask build, only for ABI arm64, for GPlay releases with split apks (1 of 4) * normalProductionArmv7Release -> Bitmask build, only for ABI armeabi-v7a, for GPlay releases with split apks (2 of 4) * normalProductionX86Release -> Bitmask build, only for ABI x86, for GPlay releases with split apks (3 of 4) * normalProductionX86_64Release -> Bitmask build, only for ABI x86 64 bit, for GPlay releases with split apks (4 of 4) * normalProductionFatRelease -> Bitmask build, including all ABIS, for official F-Droid repo and stable link on download page */ variantFilter { variant -> def names = variant.flavors*.name def buildTypeName = variant.buildType.name // flavorDimensions "branding" -> 0, "implementation" -> 1, "abi" -> 2 def supportsSplitApk = variant.flavors[0].splitApk // To check for a certain build type, use variant.buildType.name == "" if (((names.contains("insecure") && !names.contains("fat")) || (names.contains("insecure") && buildTypeName.contains("beta")) || (names.contains("insecure") && buildTypeName.contains("release")) || (buildTypeName.contains("debug") && !names.contains("fat")) || (!supportsSplitApk && !names.contains("fat"))) ) { // Gradle ignores any variants that satisfy the conditions above. setIgnore(true) } } } dependencies { testImplementation 'junit:junit:4.12' //outdated mockito-core version due to powermock dependency testImplementation 'org.mockito:mockito-core:2.8.9' testImplementation('org.powermock:powermock-api-mockito2:1.7.3') { exclude group: 'junit' exclude group: 'org.mockito' } testImplementation 'org.powermock:powermock-module-junit4:1.7.3' testImplementation 'org.powermock:powermock-core:1.7.3' testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.3' testImplementation group: 'com.tngtech.java', name: 'junit-dataprovider', version: '1.10.0' androidTestImplementation 'org.mockito:mockito-core:2.8.9' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0' //TODO: remove that library androidTestImplementation 'com.jayway.android.robotium:robotium-solo:5.6.3' testImplementation 'junit:junit:4.12' testImplementation 'org.json:json:20170516' debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2' betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2' annotationProcessor 'com.jakewharton:butterknife:6.1.0' annotationProcessor 'com.squareup.dagger:dagger-compiler:1.2.2' implementation 'com.jakewharton:butterknife:6.1.0' //TODO: replace that library compileOnly 'com.squareup.dagger:dagger-compiler:1.2.2' implementation 'com.github.pedrovgs:renderers:1.5' implementation 'com.intellij:annotations:12.0' implementation 'com.google.code.gson:gson:2.8.2' implementation 'com.squareup.okhttp3:okhttp:3.9.0' implementation 'androidx.legacy:legacy-support-core-utils:1.0.0' implementation 'androidx.annotation:annotation:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.fragment:fragment:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.multidex:multidex:2.0.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'de.hdodenhof:circleimageview:3.1.0' implementation project(path: ':shapeshifter') } android.applicationVariants.all { variant -> // configure app name and apk file name for different build variants def flavors = variant.productFlavors // flavorDimensions "branding" -> 0, "implementation" -> 1, "abi" -> 2 def branding = flavors[0] def trimmedAppName = branding.appName.replaceAll(' ', '') def abiDimension = flavors[2] def abiFilter = abiDimension.abiFilter if (abiFilter.length() > 0) { abiFilter = "_"+abiFilter } def buildType = variant.buildType def tag = getTag() if (tag.length() > 0) { tag = "_"+tag } variant.resValue "string", "app_name", "\"${branding.appName}${buildType.appSuffix}\"" variant.outputs.all { output -> output.outputFileName = "${trimmedAppName}${abiFilter}_${buildType.name}${tag}.apk" } // reconfigure version codes for split builds variant.outputs.each { output -> // if not a fat build if (abiDimension.abiVersionCode > 0) { output.versionCodeOverride = android.defaultConfig.versionCode * 1000 + abiDimension.abiVersionCode } } // remove unrelated abi specific assets variant.mergeAssets.doLast { // if not a fat build if (abiDimension.abiVersionCode > 0) { def filesToDelete = fileTree(dir: variant.mergeAssets.outputDir, excludes: ["*pie_openvpn.${abiDimension.abiFilter}", 'urls/', '*.url', '*.json', '*.pem']) delete(filesToDelete) } } } // Ensure the no-op dependency is always used in JVM tests. configurations.all { config -> if (config.name.contains('UnitTest')) { config.resolutionStrategy.eachDependency { details -> if (details.requested.group == 'com.squareup.leakcanary' && details.requested.name == 'leakcanary-android') { details.useTarget(group: details.requested.group, name: 'leakcanary-android-no-op', version: details.requested.version) } } } resolutionStrategy.force "com.android.support:support-annotations:28.0.0" resolutionStrategy.force "com.android.support:support-v4:28.0.0" resolutionStrategy.force "com.android.support:support-core-utils:28.0.0" resolutionStrategy.force "com.android.support:appcompat-v7:28.0.0" resolutionStrategy.force "com.android.support:design:28.0.0" resolutionStrategy.force "com.android.support:support-fragment:28.0.0" } subprojects { afterEvaluate {project -> if (project.hasProperty("android")) { android { compileSdkVersion 28 buildToolsVersion "28.0.3" } } } } def processFileInplace(file, Closure processText) { def text = file.text file.write(processText(text)) } task copyIcsOpenVPNClasses(dependsOn: 'copyIcsOpenVPNFiles', type: Copy ) { println "copyIcsOpenVPNClasses" // from ('../ics-openvpn/main/') { // include '**/LaunchVPN.java' // include '**/OpenVPNSservice.java' // include '**/VpnProfile.java' // include '**/DisconnectVPN.java' // include '**/VpnProfile.java' // include '**/SeekBarTicks.java' // include '**/core/**.java' // include '**/activities/BaseActivity.java' // include '**/APIVpnProfile.java' // include '**/aidl/**/api/**.aidl' // include '**/aidl/**/core/**.aidl' // // includeEmptyDirs = false // // filter { // line -> line.replaceAll('de.blinkt.openvpn.R', 'se.leap.bitmaskclient.R') // } // filter { // line -> line.replaceAll('de.blinkt.openvpn.BuildConfig', 'se.leap.bitmaskclient.BuildConfig') // } // filter { // line -> line.replace('package de.blinkt.openvpn;', 'package de.blinkt.openvpn;\n\nimport se.leap.bitmaskclient.R;') // } // } into '.' } task copyIcsOpenVPNXml(dependsOn: 'copyIcsOpenVPNFiles', type: Copy ) { println "copyIcsOpenVPNXml" from ('../ics-openvpn/main/') { include '**/strings.xml' include '**/refs.xml' include '**/white_rect.xml' include '**/plurals.xml' includeEmptyDirs = false rename 'strings.xml', 'strings-icsopenvpn.xml' rename 'plurals.xml', 'plurals-icsopenvpn.xml' filter { line -> line.replaceAll('.*name="app".*', '') } } into '.' } task copyIcsOpenVPNImages(dependsOn: 'copyIcsOpenVPNFiles', type: Copy ) { println "copyIcsOpenVPNImages" // from ('../ics-openvpn/main/') { // include '**/ic_filter*.png' // include '**/ic_delete*.png' // include '**/ic_share*.png' // include '**/ic_close*.png' // include '**/ic_edit*.png' // include '**/ic_check*.png' // include '**/ic_pause*.png' // include '**/ic_play*.png' // include '**/ic_content_copy_white_*.png' // include '**/ic_add_circle_outline_white_*.png' // include '**/ic_warning_black_*.png' // include '**/ic_add_circle_outline_grey600_*.png' // include '**/ic_archive_grey600_*.png' // include '**/ic_receipt_white_*.png' // include '**/ic_sort_white_*.png' // include '**/ic_content_copy_white_*.png' // include '**/ic_archive_white_*.png' // include '**/ic_menu_archive*.png' // include '**/vpn_item_settings*.png' // include '**/ic_menu_log*.png' // include '**/ic_menu_copy_holo_light*.png' // // includeEmptyDirs = false // } into '.' } // thanks to http://pleac.sourceforge.net/pleac_groovy/fileaccess.html def removeDuplicatedStrings() { println "removeDuplicatedStrings" new File('.').eachFileRecurse { if(it.name.equals('strings.xml') || it.name.equals('plurals.xml')) { replaceDuplicatesForSource(it, it.name.substring(0, it.name.lastIndexOf('.'))) } } } def replaceDuplicatesForSource(File it, String type) { def ics_openvpn_file = file(it.absolutePath.replace(type+'.xml', type+'-icsopenvpn.xml')) if(ics_openvpn_file.exists()) { def ics_openvpn_strings_names = new XmlParser().parse(ics_openvpn_file) def current_file = it ics_openvpn_strings_names.string.each { processFileInplace(current_file) { text -> text.replaceAll('.*name=\"' + it.attribute('name') + '\".*(\n)*.*string>.*\n+', '') } } } } task mergeUntranslatable(dependsOn: 'copyIcsOpenVPNFiles', type: Copy ) { println "mergeUntranslatable" from ('../ics-openvpn/main/') { include '**/untranslatable.xml' rename 'untranslatable.xml', 'untranslatable-icsopenvpn.xml' } into '.' def bitmask_untranslatable = file('src/main/res/values/untranslatable.xml') def ics_openvpn_untranslatable = new File(bitmask_untranslatable.path.replace('untranslatable.xml', 'untranslatable-icsopenvpn.xml')) ics_openvpn_untranslatable.createNewFile() def string_continuation = false; ics_openvpn_untranslatable.eachLine { text -> if(text.contains('string name=')) { if(!bitmask_untranslatable.text.contains(text)) bitmask_untranslatable << text if(text.contains('')) string_continuation = true } else if(string_continuation) { bitmask_untranslatable << text } if(text.contains('')) { string_continuation = false bitmask_untranslatable << System.getProperty("line.separator") } } bitmask_untranslatable.write(bitmask_untranslatable.text.replaceAll("", "")) bitmask_untranslatable << "" delete ics_openvpn_untranslatable } task copyIcsOpenVPNFiles(dependsOn: 'updateIcsOpenVpn') { println "copyIcsOpenVPNFiles" copyIcsOpenVPNClasses copyIcsOpenVPNXml copyIcsOpenVPNImages //mergeUntranslatable.execute() removeDuplicatedStrings() } task updateIcsOpenVpn( type: Exec ) { commandLine 'git', 'submodule', 'sync' commandLine 'git', 'submodule', 'update', '--init', '--recursive' } def getTag() { String commit = "git log --pretty=format:'%h' -n 1".execute().text.trim().replaceAll("'", "") return ("git describe --tags --exact-match "+ commit).execute().text.trim() } task cleanNative( type: Delete ) { def shouldClean = getCurrentFlavorForBetaOrRelease() == "production" println "cleanNative: " + shouldClean if (shouldClean) { def dirName = "obj" file( dirName ).list().each{ f -> delete "${dirName}/${f}" } } } task updateSdkLicences ( type: Exec ) { println "say yes to licenses" commandLine 'sh', 'yes', '|', 'sdkmanager', '--licenses' } def getCurrentFlavorForBetaOrRelease() { Gradle gradle = getGradle() String tskReqStr = gradle.getStartParameter().getTaskRequests().toString() Pattern pattern; if( tskReqStr.contains( "assemble" ) ) pattern = Pattern.compile("assemble(\\w+)(Beta|Release)") else pattern = Pattern.compile("generate(\\w+)(Beta|Release)") Matcher matcher = pattern.matcher( tskReqStr ) if( matcher.find() ) return matcher.group(1).toLowerCase() else { return ""; } }