diff --git a/.github/workflows/build-playstore-android.yaml b/.github/workflows/build-playstore-android.yaml index f623aec..b2ed222 100644 --- a/.github/workflows/build-playstore-android.yaml +++ b/.github/workflows/build-playstore-android.yaml @@ -10,13 +10,15 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: gradle - name: key run: openssl base64 -d -A -out /home/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/app/key.keystore <<< ${{ secrets.SIGNING_PLAYSTORE_KEYSTORE }} - name: build AAB run: ./gradlew bundleProductionPlaystoreRelease --parallel env: + GITHUB_USERNAME: ${{ secrets.USERNAME_GITHUB }} + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_PLAYSTORE_KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_PLAYSTORE_KEY_PASSWORD }} SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_PLAYSTORE_STORE_PASSWORD }} diff --git a/.github/workflows/build-web-android.yaml b/.github/workflows/build-web-android.yaml index d1003c8..fed76fd 100644 --- a/.github/workflows/build-web-android.yaml +++ b/.github/workflows/build-web-android.yaml @@ -10,7 +10,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: gradle - name: use web version code run: | @@ -27,6 +27,8 @@ jobs: - name: build APK run: ./gradlew assembleProductionNoinappRelease --parallel env: + GITHUB_USERNAME: ${{ secrets.USERNAME_GITHUB }} + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_WEB_KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_WEB_KEY_PASSWORD }} SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_WEB_STORE_PASSWORD }} diff --git a/.github/workflows/test-android.yaml b/.github/workflows/test-android.yaml index 5853830..281f472 100644 --- a/.github/workflows/test-android.yaml +++ b/.github/workflows/test-android.yaml @@ -10,7 +10,10 @@ jobs: - uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: gradle - name: unit-tests run: ./gradlew testProductionPlaystoreReleaseUnitTest testProductionNoinappReleaseUnitTest --parallel + env: + GITHUB_USERNAME: ${{ secrets.USERNAME_GITHUB }} + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} diff --git a/account/account-release.aar b/account/account-release.aar deleted file mode 100644 index a39d470..0000000 Binary files a/account/account-release.aar and /dev/null differ diff --git a/account/build.gradle b/account/build.gradle deleted file mode 100644 index 90374f0..0000000 --- a/account/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('account-release.aar')) \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 696f869..1af20f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,15 +12,14 @@ buildscript { apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlinx-serialization' apply plugin: 'kape.licenses' - repositories { google() jcenter() mavenCentral() + mavenLocal() maven { url "https://jitpack.io" } } @@ -31,41 +30,37 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.preference:preference:1.1.0' - implementation 'com.madgag.spongycastle:core:1.54.0.0' + implementation 'com.madgag.spongycastle:core:1.58.0.0' implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.appcompat:appcompat:1.2.0-alpha03' implementation "androidx.core:core-ktx:1.3.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + implementation 'com.google.code.gson:gson:2.10.1' + implementation "io.ktor:ktor-client-okhttp:2.3.3" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" + implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" // PIA implementation project(':openvpnTunnel') implementation project(":core") - implementation project(":regions") - implementation project(":account") - implementation project(":csi") - implementation project(":kpi") // Commons - implementation "io.ktor:ktor-client-okhttp:1.6.1" - implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.2.1" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.2.1" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1" - implementation "androidx.security:security-crypto:1.1.0-alpha03" - implementation "com.russhwolf:multiplatform-settings:0.8" - implementation 'com.google.code.gson:gson:2.10.1' + implementation("com.kape.android:account-android:1.4.5") + implementation("com.kape.android:regions-android:1.6.2") + implementation("com.kape.android:csi-android:1.3.2") + implementation("com.kape.android:kpi-android:1.2.2") // Tests testImplementation 'com.github.gmazzo:okhttp-mock:1.3.2' - testImplementation "org.json:json:20180813" + testImplementation "org.json:json:20231013" testImplementation 'junit:junit:4.13.2' - testImplementation 'androidx.test:core:1.4.0' - testImplementation 'org.robolectric:robolectric:4.4' - testImplementation 'org.mockito:mockito-inline:2.23.0' - testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" + testImplementation 'androidx.test:core:1.5.0' + testImplementation 'org.robolectric:robolectric:4.10.3' + testImplementation 'org.mockito:mockito-inline:4.4.0' + testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" androidTestImplementation 'com.github.gmazzo:okhttp-mock:1.3.2' androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:core:1.4.0' @@ -77,8 +72,8 @@ dependencies { implementation 'org.greenrobot:eventbus:3.1.1' //ButterKnife - implementation 'com.jakewharton:butterknife:10.2.1' - annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1' + implementation 'com.jakewharton:butterknife:10.2.3' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3' //Wireguard implementation "com.jakewharton.threetenabp:threetenabp:1.2.1" @@ -115,19 +110,24 @@ dependencies { } android { + namespace 'com.privateinternetaccess.android' compileSdkVersion rootProject.compileSdkVersion buildToolsVersion rootProject.buildToolsVersion - + ndkVersion '21.1.6352462' dataBinding.enabled true buildFeatures { + buildConfig = true viewBinding = true } compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" } defaultConfig { @@ -163,9 +163,6 @@ android { } } - lintOptions { - disable 'InvalidPackage', 'MissingTranslation', 'MissingDefaultResource', 'ExtraTranslation' - } flavorDimensions "pia", "store" @@ -249,7 +246,14 @@ android { buildConfigField 'String', 'PIA_VALID_DIP_TOKEN', "\"${System.getenv("PIA_VALID_DIP_TOKEN")}\"" } } - ndkVersion '21.1.6352462' + packagingOptions { + jniLibs { + useLegacyPackaging true + } + } + lint { + disable 'InvalidPackage', 'MissingTranslation', 'MissingDefaultResource', 'ExtraTranslation' + } } task fetchRegions { diff --git a/app/src/amazonstore/AndroidManifest.xml b/app/src/amazonstore/AndroidManifest.xml index 4e43a8b..58ad1aa 100644 --- a/app/src/amazonstore/AndroidManifest.xml +++ b/app/src/amazonstore/AndroidManifest.xml @@ -17,7 +17,6 @@ --> + xmlns:tools="http://schemas.android.com/tools"> \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6490ebe..00d9ffe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,8 +18,7 @@ --> + xmlns:tools="http://schemas.android.com/tools"> @@ -69,7 +68,6 @@ android:roundIcon="@mipmap/ic_launcher_round" android:theme="@style/pia_theme_3_0" android:name=".PIAApplication" - android:extractNativeLibs="true" android:usesCleartextTraffic="true" android:label="@string/launcher_name" android:appCategory="productivity" diff --git a/app/src/main/java/com/privateinternetaccess/android/PIAApplication.java b/app/src/main/java/com/privateinternetaccess/android/PIAApplication.java index adb91b0..9304d6c 100644 --- a/app/src/main/java/com/privateinternetaccess/android/PIAApplication.java +++ b/app/src/main/java/com/privateinternetaccess/android/PIAApplication.java @@ -45,6 +45,7 @@ import com.privateinternetaccess.android.pia.handlers.PIAServerHandler; import com.privateinternetaccess.android.pia.handlers.PiaPrefHandler; import com.privateinternetaccess.android.pia.handlers.ThemeHandler; +import com.privateinternetaccess.android.pia.interfaces.IAccount; import com.privateinternetaccess.android.pia.model.events.VpnStateEvent; import com.privateinternetaccess.android.pia.utils.DLog; import com.privateinternetaccess.android.pia.utils.Prefs; @@ -200,8 +201,6 @@ public void onCreate() { .enableLogging(notRelease) .setDebugParameters(debugMode, debugLevel, context.getFilesDir()); - PIAServerHandler.startup(context); - updateOrResetValues(); ThemeHandler.setAppTheme(this); diff --git a/app/src/main/java/com/privateinternetaccess/android/PIALifecycleObserver.kt b/app/src/main/java/com/privateinternetaccess/android/PIALifecycleObserver.kt index 69dd7a6..39d72a4 100644 --- a/app/src/main/java/com/privateinternetaccess/android/PIALifecycleObserver.kt +++ b/app/src/main/java/com/privateinternetaccess/android/PIALifecycleObserver.kt @@ -40,8 +40,12 @@ class PIALifecycleObserver(private val context: Context) : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onForeground() { DLog.d(TAG, "Application Foregrounded") - PIAServerHandler.getInstance(context).triggerLatenciesUpdate() - migrateApiTokenIfNeeded() + val account = PIAFactory.getInstance().getAccount(context) + if (account.loggedIn()) { + PIAServerHandler.startup(context) + PIAServerHandler.getInstance(context).triggerLatenciesUpdate() + migrateApiTokenIfNeeded() + } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) diff --git a/app/src/main/java/com/privateinternetaccess/android/pia/api/Gen4PortForwardApi.kt b/app/src/main/java/com/privateinternetaccess/android/pia/api/Gen4PortForwardApi.kt index b0491a0..74ea70c 100644 --- a/app/src/main/java/com/privateinternetaccess/android/pia/api/Gen4PortForwardApi.kt +++ b/app/src/main/java/com/privateinternetaccess/android/pia/api/Gen4PortForwardApi.kt @@ -204,7 +204,6 @@ class Gen4PortForwardApi : PIACertPinningAPI() { } } - @UseExperimental(ExperimentalTime::class) private fun tokenExpirationDateDaysLeft( tokenExpirationDate: String ): Long { diff --git a/app/src/main/java/com/privateinternetaccess/android/pia/handlers/PIAServerHandler.java b/app/src/main/java/com/privateinternetaccess/android/pia/handlers/PIAServerHandler.java index 66e9fd9..1615810 100644 --- a/app/src/main/java/com/privateinternetaccess/android/pia/handlers/PIAServerHandler.java +++ b/app/src/main/java/com/privateinternetaccess/android/pia/handlers/PIAServerHandler.java @@ -22,12 +22,14 @@ import static com.privateinternetaccess.android.pia.api.PiaApi.ANDROID_HTTP_CLIENT; import android.app.AlarmManager; +import android.app.Application; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.os.ConfigurationCompat; @@ -37,6 +39,7 @@ import com.privateinternetaccess.android.model.events.SeverListUpdateEvent; import com.privateinternetaccess.android.model.events.SeverListUpdateEvent.ServerListUpdateState; import com.privateinternetaccess.android.pia.PIAFactory; +import com.privateinternetaccess.android.pia.interfaces.IAccount; import com.privateinternetaccess.android.pia.providers.ModuleClientStateProvider; import com.privateinternetaccess.android.pia.receivers.FetchServersReceiver; import com.privateinternetaccess.android.pia.receivers.PingReceiver; @@ -49,11 +52,12 @@ import com.privateinternetaccess.core.model.PIAServer; import com.privateinternetaccess.core.model.PIAServerInfo; import com.privateinternetaccess.core.model.ServerResponse; +import com.privateinternetaccess.regions.PlatformInstancesProvider; import com.privateinternetaccess.regions.RegionLowerLatencyInformation; import com.privateinternetaccess.regions.RegionsAPI; import com.privateinternetaccess.regions.RegionsBuilder; import com.privateinternetaccess.regions.RegionsUtils; -import com.privateinternetaccess.regions.model.RegionsResponse; +import com.privateinternetaccess.regions.model.VpnRegionsResponse; import org.greenrobot.eventbus.EventBus; @@ -80,7 +84,7 @@ * Handler class for helping with the servers and pings to those servers. * */ -public class PIAServerHandler { +public class PIAServerHandler implements PlatformInstancesProvider { private static final String TAG = "PIAServerHandler"; public static final String LAST_SERVER_GRAB = "LAST_SERVER_GRAB"; @@ -96,7 +100,7 @@ public class PIAServerHandler { private static PIAServer randomServer; public static PIAServerHandler getInstance(Context context){ - if(instance == null){ + if (instance == null){ startup(context); } return instance; @@ -189,10 +193,20 @@ private void prepareRegionModule() { .setEndpointProvider(new ModuleClientStateProvider(context)) .setCertificate(ModuleClientStateProvider.Companion.getCERTIFICATE()) .setUserAgent(ANDROID_HTTP_CLIENT) + .setMetadataRequestPath("/vpninfo/regions/v2") + .setVpnRegionsRequestPath("/vpninfo/servers/v6") + .setShadowsocksRegionsRequestPath("/shadow_socks") + .setPlatformInstancesProvider(this) .build(); } } + @NonNull + @Override + public Application provideApplicationContext() { + return (Application) context.getApplicationContext(); + } + private void offLoadResponse(ServerResponse response, boolean fromWeb){ PIAServerHandler handler = getInstance(null); if (handler != null) { @@ -228,7 +242,7 @@ public void loadPersistedServersIfAny() { } } - RegionsResponse regionsResponse = RegionsUtils.INSTANCE.parse(gen4LastBody); + VpnRegionsResponse regionsResponse = RegionsUtils.INSTANCE.parse(gen4LastBody); Map serverMap = ServerResponseHelper.Companion.adaptServers(context, regionsResponse); PIAServerInfo serverInfo = @@ -259,6 +273,14 @@ public void triggerLatenciesUpdate() { } public void triggerLatenciesUpdate(@Nullable Function1 callback) { + IAccount account = PIAFactory.getInstance().getAccount(context); + if (!account.loggedIn()) { + if (callback != null) { + callback.invoke(new Error("Error when updating latencies. User not logged in")); + } + return; + } + if (PiaPrefHandler.isStopPingingServersEnabled(context)) { DLog.e(TAG, "Error when updating latencies. Disabled on developer options"); if (callback != null) { @@ -283,12 +305,11 @@ public void triggerLatenciesUpdate(@Nullable Function1 callback) { return; } - regionModule.pingRequests((response, errors) -> { - if (errors.size() > 0) { - Error lastKnownError = errors.get(errors.size() - 1); - DLog.e(TAG, "Error when updating latencies " + lastKnownError.getMessage()); + regionModule.pingRequests((response, error) -> { + if (error != null) { + DLog.e(TAG, "Error when updating latencies " + error.getMessage()); if (callback != null) { - callback.invoke(lastKnownError); + callback.invoke(error); } return null; } @@ -339,6 +360,14 @@ public void triggerFetchServers() { } public void triggerFetchServers(@Nullable Function1 callback) { + IAccount account = PIAFactory.getInstance().getAccount(context); + if (!account.loggedIn()) { + if (callback != null) { + callback.invoke(new Error("Fetch servers cancelled. User not logged in")); + } + return; + } + if (SystemUtils.INSTANCE.isDozeModeEnabled(context)) { DLog.e(TAG, "Error when updating list of servers. Doze mode enabled."); if (callback != null) { @@ -356,12 +385,11 @@ public void triggerFetchServers(@Nullable Function1 callback) { } Locale locale = ConfigurationCompat.getLocales(context.getResources().getConfiguration()).get(0); - regionModule.fetchRegions(locale.getLanguage(), (regionsResponse, errors) -> { - if (errors.size() > 0) { - Error lastKnownError = errors.get(errors.size() - 1); - DLog.e(TAG, "Error fetching the list of servers " + lastKnownError.getMessage()); + regionModule.fetchVpnRegions(locale.getLanguage(), (regionsResponse, error) -> { + if (error != null) { + DLog.e(TAG, "Error fetching the list of servers " + error.getMessage()); if (callback != null) { - callback.invoke(lastKnownError); + callback.invoke(error); } return null; } diff --git a/app/src/main/java/com/privateinternetaccess/android/pia/handlers/ThemeHandler.java b/app/src/main/java/com/privateinternetaccess/android/pia/handlers/ThemeHandler.java index f0dd7e3..2102bf0 100644 --- a/app/src/main/java/com/privateinternetaccess/android/pia/handlers/ThemeHandler.java +++ b/app/src/main/java/com/privateinternetaccess/android/pia/handlers/ThemeHandler.java @@ -103,6 +103,10 @@ public static void updateActivityTheme(AppCompatActivity activity){ * @param app */ public static void setAppTheme(Application app){ + if (PIAApplication.isAndroidTV(app)) { + return; + } + Theme theme = getPrefTheme(app); int mode = AppCompatDelegate.MODE_NIGHT_NO; switch (theme){ diff --git a/app/src/main/java/com/privateinternetaccess/android/pia/utils/Prefs.java b/app/src/main/java/com/privateinternetaccess/android/pia/utils/Prefs.java index 12fb323..4678ebe 100644 --- a/app/src/main/java/com/privateinternetaccess/android/pia/utils/Prefs.java +++ b/app/src/main/java/com/privateinternetaccess/android/pia/utils/Prefs.java @@ -60,8 +60,6 @@ public class Prefs { PiaPrefHandler.TRIAL_EMAIL, PiaPrefHandler.TRIAL_EMAIL_TEMP, PiaPrefHandler.DIP_TOKENS, - PiaPrefHandler.GEN4_LAST_SERVER_BODY, - PiaPrefHandler.GEN4_QUICK_CONNECT_LIST, PiaPrefHandler.FAVORITE_REGIONS, PiaPrefHandler.SELECTED_REGION, }; diff --git a/app/src/main/java/com/privateinternetaccess/android/pia/utils/ServerResponseHelper.kt b/app/src/main/java/com/privateinternetaccess/android/pia/utils/ServerResponseHelper.kt index 6cafc20..6b66353 100644 --- a/app/src/main/java/com/privateinternetaccess/android/pia/utils/ServerResponseHelper.kt +++ b/app/src/main/java/com/privateinternetaccess/android/pia/utils/ServerResponseHelper.kt @@ -21,15 +21,15 @@ package com.privateinternetaccess.android.pia.utils import android.content.Context import com.privateinternetaccess.android.pia.handlers.PiaPrefHandler import com.privateinternetaccess.regions.RegionsProtocol -import com.privateinternetaccess.regions.model.RegionsResponse import com.privateinternetaccess.core.model.PIAServer import com.privateinternetaccess.core.model.PIAServer.PIAServerEndpointDetails import com.privateinternetaccess.core.model.PIAServerInfo +import com.privateinternetaccess.regions.model.VpnRegionsResponse class ServerResponseHelper { companion object { - fun adaptServers(context: Context, regionsResponse: RegionsResponse): Map { + fun adaptServers(context: Context, regionsResponse: VpnRegionsResponse): Map { val servers = mutableMapOf() for (region in regionsResponse.regions) { val wireguardEndpoints = region.servers[RegionsProtocol.WIREGUARD.protocol] @@ -125,7 +125,7 @@ class ServerResponseHelper { return servers } - fun adaptServersInfo(regionsResponse: RegionsResponse): PIAServerInfo { + fun adaptServersInfo(regionsResponse: VpnRegionsResponse): PIAServerInfo { val autoRegions = mutableListOf() regionsResponse.regions.filter { it.autoRegion }.forEach { region -> autoRegions.add(region.id) diff --git a/app/src/main/java/com/privateinternetaccess/android/ui/DialogFactory.java b/app/src/main/java/com/privateinternetaccess/android/ui/DialogFactory.java index 1d942a2..89cc1d5 100644 --- a/app/src/main/java/com/privateinternetaccess/android/ui/DialogFactory.java +++ b/app/src/main/java/com/privateinternetaccess/android/ui/DialogFactory.java @@ -40,7 +40,6 @@ import butterknife.BindView; import butterknife.ButterKnife; -import io.ktor.http.cio.websocket.Frame; public class DialogFactory { diff --git a/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/AmazonGetStartedFragment.kt b/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/AmazonGetStartedFragment.kt index ec98832..ec80f33 100644 --- a/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/AmazonGetStartedFragment.kt +++ b/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/AmazonGetStartedFragment.kt @@ -12,12 +12,12 @@ import android.view.ViewGroup import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import butterknife.ButterKnife import com.amazon.device.iap.model.PurchaseResponse import com.privateinternetaccess.account.model.response.AmazonSubscriptionsInformation import com.privateinternetaccess.android.BuildConfig import com.privateinternetaccess.android.PIAApplication import com.privateinternetaccess.android.R +import com.privateinternetaccess.android.databinding.FragmentGetStartedNewBinding import com.privateinternetaccess.android.model.events.PricingLoadedEvent import com.privateinternetaccess.android.pia.PIAFactory import com.privateinternetaccess.android.pia.handlers.PiaPrefHandler @@ -28,41 +28,40 @@ import com.privateinternetaccess.android.pia.utils.DLog import com.privateinternetaccess.android.pia.utils.Toaster import com.privateinternetaccess.android.ui.drawer.settings.DeveloperActivity import com.privateinternetaccess.android.utils.toAndroidSubscription -import kotlinx.android.synthetic.main.fragment_get_started_new.* import java.text.DecimalFormat import java.util.* class AmazonGetStartedFragment : Fragment() { private var pricesLoaded = false + private lateinit var binding: FragmentGetStartedNewBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val view: View = inflater.inflate(R.layout.fragment_get_started_new, container, false) - ButterKnife.bind(this, view) - return view + binding = FragmentGetStartedNewBinding.inflate(inflater) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - yearlySpinner.isVisible = true - monthlySpinner.isVisible = true + binding.yearlySpinner.isVisible = true + binding.monthlySpinner.isVisible = true if (PIAApplication.isRelease()) { - devButton.visibility = View.GONE + binding.devButton.visibility = View.GONE } else { - devButton.visibility = View.VISIBLE - devButton.setOnClickListener { + binding.devButton.visibility = View.VISIBLE + binding.devButton.setOnClickListener { val i = Intent(context, DeveloperActivity::class.java) startActivity(i) } } - footer.movementMethod = LinkMovementMethod.getInstance() + binding.footer.movementMethod = LinkMovementMethod.getInstance() if (PiaPrefHandler.availableSubscriptions(requireContext()) == null) { requestPrices() @@ -70,16 +69,16 @@ class AmazonGetStartedFragment : Fragment() { pricesLoaded() } - yearly.setOnClickListener { + binding.yearly.setOnClickListener { handleSelection(true) } - monthly.setOnClickListener { + binding.monthly.setOnClickListener { handleSelection(false) } - subscribe.setOnClickListener { subscribe() } - login.setOnClickListener { login() } + binding.subscribe.setOnClickListener { subscribe() } + binding.login.setOnClickListener { login() } PIAApplication.amazonPurchaseUtil.observableProducts.observe(viewLifecycleOwner) { showAmazonPricing(it!!) @@ -91,10 +90,10 @@ class AmazonGetStartedFragment : Fragment() { } private fun showAmazonPricing(event: PricingLoadedEvent) { - yearlySpinner.isGone = true - monthlySpinner.isGone = true + binding.yearlySpinner.isGone = true + binding.monthlySpinner.isGone = true pricesLoaded = !TextUtils.isEmpty(event.yearlyCost) - description.text = getString(R.string.startup_region_message_new, event.yearlyCost) + binding.description.text = getString(R.string.startup_region_message_new, event.yearlyCost) setUpCosts(event.monthlyCost, event.yearlyCost) } @@ -160,23 +159,23 @@ class AmazonGetStartedFragment : Fragment() { private fun handleSelection(yearlySelected: Boolean) { val theme = ThemeHandler.getCurrentTheme(requireContext()) if (yearlySelected) { - yearly.isSelected = true - monthly.isSelected = false + binding.yearly.isSelected = true + binding.monthly.isSelected = false PIAApplication.amazonPurchaseUtil.selectProduct(true) - yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) - monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) + binding.yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) + binding.monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) } else { - monthly.isSelected = true - yearly.isSelected = false + binding.monthly.isSelected = true + binding.yearly.isSelected = false PIAApplication.amazonPurchaseUtil.selectProduct(false) - yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) - monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) + binding.yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) + binding.monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) } } private fun setUpCosts(monthly: String, yearly: String) { - monthlyCost.text = String.format(getString(R.string.purchasing_monthly_ending), monthly) - yearlyCost.text = getString(R.string.yearly_sub_text, yearly) + binding.monthlyCost.text = String.format(getString(R.string.purchasing_monthly_ending), monthly) + binding.yearlyCost.text = getString(R.string.yearly_sub_text, yearly) if (!TextUtils.isEmpty(yearly)) { pricesLoaded = true val sb = StringBuilder() @@ -197,7 +196,7 @@ class AmazonGetStartedFragment : Fragment() { DLog.d("Purchasing", "year = $year cleaned = $cleaned") year = year / 100 / 12 DLog.d("PurchasingFragment", "mYearlyCost = " + format.format(year)) - yearlyTotal.text = String.format( + binding.yearlyTotal.text = String.format( getString(R.string.purchasing_yearly_month_ending), format.format(year), currency diff --git a/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/NewGetStartedFragment.kt b/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/NewGetStartedFragment.kt index 5b1220d..4540b76 100644 --- a/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/NewGetStartedFragment.kt +++ b/app/src/main/java/com/privateinternetaccess/android/ui/loginpurchasing/NewGetStartedFragment.kt @@ -10,10 +10,10 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isGone import androidx.fragment.app.Fragment -import butterknife.ButterKnife import com.privateinternetaccess.account.model.response.AndroidSubscriptionsInformation import com.privateinternetaccess.android.PIAApplication import com.privateinternetaccess.android.R +import com.privateinternetaccess.android.databinding.FragmentGetStartedNewBinding import com.privateinternetaccess.android.model.events.PricingLoadedEvent import com.privateinternetaccess.android.pia.PIAFactory import com.privateinternetaccess.android.pia.handlers.PiaPrefHandler @@ -26,7 +26,6 @@ import com.privateinternetaccess.android.ui.drawer.settings.DeveloperActivity import com.privateinternetaccess.android.utils.SubscriptionsUtils.getMonthlySubscriptionId import com.privateinternetaccess.android.utils.SubscriptionsUtils.getYearlySubscriptionId import com.privateinternetaccess.android.utils.SubscriptionsUtils.isPlayStoreFlavour -import kotlinx.android.synthetic.main.fragment_get_started_new.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import java.text.DecimalFormat @@ -36,31 +35,31 @@ class NewGetStartedFragment : Fragment() { private var pricesLoaded = false private var selectedProduct: String? = null + private lateinit var binding: FragmentGetStartedNewBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val view: View = inflater.inflate(R.layout.fragment_get_started_new, container, false) - ButterKnife.bind(this, view) - return view + binding = FragmentGetStartedNewBinding.inflate(inflater) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (PIAApplication.isRelease()) { - devButton.visibility = View.GONE + binding.devButton.visibility = View.GONE } else { - devButton.visibility = View.VISIBLE - devButton.setOnClickListener { + binding.devButton.visibility = View.VISIBLE + binding.devButton.setOnClickListener { val i = Intent(context, DeveloperActivity::class.java) startActivity(i) } } - footer.movementMethod = LinkMovementMethod.getInstance() + binding.footer.movementMethod = LinkMovementMethod.getInstance() if (PiaPrefHandler.availableSubscriptions(requireContext()) == null) { requestPrices() @@ -68,16 +67,16 @@ class NewGetStartedFragment : Fragment() { pricesLoaded() } - yearly.setOnClickListener { + binding.yearly.setOnClickListener { handleSelection(true) } - monthly.setOnClickListener { + binding.monthly.setOnClickListener { handleSelection(false) } - subscribe.setOnClickListener { subscribe() } - login.setOnClickListener { login() } + binding.subscribe.setOnClickListener { subscribe() } + binding.login.setOnClickListener { login() } } override fun onResume() { @@ -94,7 +93,7 @@ class NewGetStartedFragment : Fragment() { fun loadPricing(event: PricingLoadedEvent) { requireActivity().runOnUiThread { pricesLoaded = !TextUtils.isEmpty(event.yearlyCost) - description.text = String.format( + binding.description.text = String.format( getString(R.string.startup_region_message_new), event.yearlyCost, getString(R.string.startup_region_message_cancel_anytime) @@ -104,8 +103,8 @@ class NewGetStartedFragment : Fragment() { } private fun pricesLoaded() { - yearlySpinner.isGone = true - monthlySpinner.isGone = true + binding.yearlySpinner.isGone = true + binding.monthlySpinner.isGone = true EventBus.getDefault().postSticky(SubscriptionsEvent()) selectedProduct = getYearlySubscriptionId(requireContext()) handleSelection(true) @@ -137,23 +136,23 @@ class NewGetStartedFragment : Fragment() { private fun handleSelection(yearlySelected: Boolean) { val theme = ThemeHandler.getCurrentTheme(requireContext()) if (yearlySelected) { - yearly.isSelected = true - monthly.isSelected = false + binding.yearly.isSelected = true + binding.monthly.isSelected = false selectedProduct = getYearlySubscriptionId(requireContext()) - yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) - monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) + binding.yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) + binding.monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) } else { - monthly.isSelected = true - yearly.isSelected = false + binding.monthly.isSelected = true + binding.yearly.isSelected = false selectedProduct = getMonthlySubscriptionId(requireContext()) - yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) - monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) + binding.yearlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection else R.drawable.ic_selection_dark) + binding.monthlyIcon.setImageResource(if (theme == ThemeHandler.Theme.DAY) R.drawable.ic_selection_checked else R.drawable.ic_selection_checked_dark) } } private fun setUpCosts(monthly: String, yearly: String) { - monthlyCost.text = String.format(getString(R.string.purchasing_monthly_ending), monthly) - yearlyCost.text = getString(R.string.yearly_sub_text, yearly) + binding.monthlyCost.text = String.format(getString(R.string.purchasing_monthly_ending), monthly) + binding.yearlyCost.text = getString(R.string.yearly_sub_text, yearly) if (!TextUtils.isEmpty(yearly)) { pricesLoaded = true val sb = StringBuilder() @@ -174,7 +173,7 @@ class NewGetStartedFragment : Fragment() { DLog.d("Purchasing", "year = $year cleaned = $cleaned") year = year / 100 / 12 DLog.d("PurchasingFragment", "mYearlyCost = " + format.format(year)) - yearlyTotal.text = String.format( + binding.yearlyTotal.text = String.format( getString(R.string.purchasing_yearly_month_ending), format.format(year), currency diff --git a/app/src/main/java/com/privateinternetaccess/android/ui/rating/Rating.kt b/app/src/main/java/com/privateinternetaccess/android/ui/rating/Rating.kt index d210b9d..fd30f4d 100644 --- a/app/src/main/java/com/privateinternetaccess/android/ui/rating/Rating.kt +++ b/app/src/main/java/com/privateinternetaccess/android/ui/rating/Rating.kt @@ -218,7 +218,6 @@ class Rating(val context: Context) : CoroutineScope { } ?: RatingState(active = true, counter = 0) } - @UseExperimental(ExperimentalTime::class) private fun daysPassedSinceNotEnjoyingReply( dateString: String ): Long { diff --git a/app/src/main/java/com/privateinternetaccess/android/utils/CSIHelper.kt b/app/src/main/java/com/privateinternetaccess/android/utils/CSIHelper.kt index ad87c84..6cbd944 100644 --- a/app/src/main/java/com/privateinternetaccess/android/utils/CSIHelper.kt +++ b/app/src/main/java/com/privateinternetaccess/android/utils/CSIHelper.kt @@ -19,7 +19,7 @@ import com.privateinternetaccess.csi.ProviderType import com.privateinternetaccess.csi.ReportType import com.privateinternetaccess.regions.RegionsUtils.parse import com.privateinternetaccess.regions.RegionsUtils.stringify -import com.privateinternetaccess.regions.model.RegionsResponse +import com.privateinternetaccess.regions.model.VpnRegionsResponse import java.util.* @@ -190,11 +190,11 @@ class CSIHelper(private val context: Context) { return "Unknown" } - val redactedRegions: ArrayList = ArrayList() + val redactedRegions: ArrayList = ArrayList() val (groups, regions) = parse(lastServerBody) for ((id, name, country, dns, geo, offline, latitude, longitude, autoRegion, portForward, proxy) in regions) { redactedRegions.add( - RegionsResponse.Region( + VpnRegionsResponse.Region( id, name, country, @@ -209,7 +209,7 @@ class CSIHelper(private val context: Context) { ) ) } - val redactedResponse = RegionsResponse( + val redactedResponse = VpnRegionsResponse( groups, redactedRegions ) diff --git a/app/src/noinapp/AndroidManifest.xml b/app/src/noinapp/AndroidManifest.xml index 198937c..72e751d 100644 --- a/app/src/noinapp/AndroidManifest.xml +++ b/app/src/noinapp/AndroidManifest.xml @@ -17,8 +17,7 @@ --> + xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/test/java/com/privateinternetaccess/android/pia/handler/PIAServerHandlerTest.kt b/app/src/test/java/com/privateinternetaccess/android/pia/handler/PIAServerHandlerTest.kt index 5d6f256..428de1e 100644 --- a/app/src/test/java/com/privateinternetaccess/android/pia/handler/PIAServerHandlerTest.kt +++ b/app/src/test/java/com/privateinternetaccess/android/pia/handler/PIAServerHandlerTest.kt @@ -20,35 +20,65 @@ package com.privateinternetaccess.android.pia.handler import android.content.Context import androidx.test.core.app.ApplicationProvider +import com.privateinternetaccess.account.AndroidAccountAPI +import com.privateinternetaccess.android.pia.PIAFactory import com.privateinternetaccess.android.pia.handlers.PIAServerHandler +import com.privateinternetaccess.android.pia.impl.AccountImpl +import com.privateinternetaccess.android.pia.interfaces.IAccount +import com.privateinternetaccess.android.pia.interfaces.IFactory +import com.privateinternetaccess.android.pia.interfaces.IVPN import com.privateinternetaccess.android.pia.utils.Prefs import com.privateinternetaccess.android.utils.KeyStoreUtils import com.privateinternetaccess.regions.RegionLowerLatencyInformation import com.privateinternetaccess.regions.RegionsAPI -import com.privateinternetaccess.regions.model.RegionsResponse +import com.privateinternetaccess.regions.model.ShadowsocksRegionsResponse +import com.privateinternetaccess.regions.model.VpnRegionsResponse +import org.junit.After import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import org.mockito.kotlin.any +import org.mockito.kotlin.mock import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PIAServerHandlerTest { private lateinit var context: Context + private lateinit var mockedStatic: MockedStatic @Before fun setup() { MockitoAnnotations.initMocks(this) context = ApplicationProvider.getApplicationContext() Prefs.setKeyStoreUtils(Mockito.mock(KeyStoreUtils::class.java)) + + val accountMock = mock() + Mockito.`when`(accountMock.loggedIn()).thenReturn(true) + + val vpnMock = mock() + Mockito.`when`(vpnMock.isVPNActive()).thenReturn(false) + + val factoryMock = mock() + Mockito.`when`(factoryMock.getAccount(context)).thenReturn(accountMock) + Mockito.`when`(factoryMock.getVPN(context)).thenReturn(vpnMock) + + mockedStatic = Mockito.mockStatic(PIAFactory::class.java) + mockedStatic.`when` { PIAFactory.getInstance() }.thenReturn(factoryMock) + PIAServerHandler.releaseInstance() } + @After + fun cleanup() { + mockedStatic.close() + } + @Test fun testGen4WorkersAllocation() { PIAServerHandler.getInstance(context).fetchServers(context, true) @@ -62,7 +92,7 @@ class PIAServerHandlerTest { PIAServerHandler.setRegionModule(regionsSpy) PIAServerHandler.getInstance(context) - verify(regionsSpy).fetchRegions(any(), any()) + verify(regionsSpy).fetchVpnRegions(any(), any()) } @Test @@ -71,7 +101,7 @@ class PIAServerHandlerTest { PIAServerHandler.setRegionModule(regionsSpy) PIAServerHandler.getInstance(context) - verify(regionsSpy).fetchRegions(any(), any()) + verify(regionsSpy).fetchVpnRegions(any(), any()) } @Test @@ -88,15 +118,27 @@ class PIAServerHandlerTest { private class MockRegionsApi(private val mockResponse: Boolean) : RegionsAPI { - override fun fetchRegions(locale: String, callback: (response: RegionsResponse?, List) -> Unit) { + override fun fetchShadowsocksRegions( + locale: String, + callback: (response: List, error: Error?) -> Unit + ) { + callback(emptyList(), null) + } + + override fun fetchVpnRegions( + locale: String, + callback: (response: VpnRegionsResponse?, Error?) -> Unit + ) { if (mockResponse) { - callback(Mockito.mock(RegionsResponse::class.java), emptyList()) + callback(Mockito.mock(VpnRegionsResponse::class.java), null) } else { - callback(null, listOf(Error("Tests"))) + callback(null, Error("Tests")) } } - override fun pingRequests(callback: (response: List, List) -> Unit) { - callback(emptyList(), emptyList()) + override fun pingRequests( + callback: (response: List, Error?) -> Unit + ) { + callback(emptyList(), null) } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0e323b7..ac2e910 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.5.31' + ext.kotlin_version = '1.9.20' repositories { google() jcenter() @@ -19,12 +19,41 @@ allprojects{ google() jcenter() mavenCentral() + mavenLocal() maven { url "https://jitpack.io" } maven { url 'https://maven.google.com' } maven { url 'https://maven.google.com/' name 'Google' } + maven { + url = uri("https://maven.pkg.github.com/pia-foss/mobile-shared-regions") + credentials { + username = System.getenv("GITHUB_USERNAME") + password = System.getenv("GITHUB_TOKEN") + } + } + maven { + url = uri("https://maven.pkg.github.com/pia-foss/mobile-shared-account") + credentials { + username = System.getenv("GITHUB_USERNAME") + password = System.getenv("GITHUB_TOKEN") + } + } + maven { + url = uri("https://maven.pkg.github.com/pia-foss/mobile-shared-csi") + credentials { + username = System.getenv("GITHUB_USERNAME") + password = System.getenv("GITHUB_TOKEN") + } + } + maven { + url = uri("https://maven.pkg.github.com/pia-foss/mobile-shared-kpi") + credentials { + username = System.getenv("GITHUB_USERNAME") + password = System.getenv("GITHUB_TOKEN") + } + } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6fc8933..a6a4363 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -22,19 +22,19 @@ gradlePlugin { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } tasks { compileKotlin { kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } } compileTestKotlin { kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } } } diff --git a/core/build.gradle b/core/build.gradle index c2e51cd..c8d0151 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -7,5 +7,5 @@ dependencies { implementation "org.json:json:20180813" } -sourceCompatibility = "1.7" -targetCompatibility = "1.7" \ No newline at end of file +sourceCompatibility = "17" +targetCompatibility = "17" \ No newline at end of file diff --git a/csi/build.gradle b/csi/build.gradle deleted file mode 100644 index 34925a8..0000000 --- a/csi/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('csi-release.aar')) \ No newline at end of file diff --git a/csi/csi-release.aar b/csi/csi-release.aar deleted file mode 100644 index 2c09d32..0000000 Binary files a/csi/csi-release.aar and /dev/null differ diff --git a/gradle.properties b/gradle.properties index 03ff063..80a3a00 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,10 @@ android.bundle.enableUncompressedNativeLibs=false android.useDeprecatedNdk=true android.useAndroidX=true android.enableJetifier=true -org.gradle.jvmargs=-Xmx2g +org.gradle.jvmargs=-Xmx2g \ +--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ +--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ +--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED # R8 Full mode android.enableR8.fullMode=true diff --git a/kpi/build.gradle b/kpi/build.gradle deleted file mode 100644 index 0af9d47..0000000 --- a/kpi/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('kpi-release.aar')) \ No newline at end of file diff --git a/kpi/kpi-release.aar b/kpi/kpi-release.aar deleted file mode 100644 index fd13972..0000000 Binary files a/kpi/kpi-release.aar and /dev/null differ diff --git a/regions/build.gradle b/regions/build.gradle deleted file mode 100644 index 8a5ed4d..0000000 --- a/regions/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('regions-release.aar')) \ No newline at end of file diff --git a/regions/regions-release.aar b/regions/regions-release.aar deleted file mode 100644 index 6c8f70b..0000000 Binary files a/regions/regions-release.aar and /dev/null differ