diff --git a/README.md b/README.md index bb1ce8d7..9fc189ea 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ Undocumented API list: * (since API 24) [`Landroid/net/ConnectivityManager;->getLastTetherError(Ljava/lang/String;)I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/7cb2ccf/appcompat/hiddenapi-flags.csv#122588) * (since API 24) [`Landroid/net/ConnectivityManager;->startTethering(IZLandroid/net/ConnectivityManager$OnStartTetheringCallback;Landroid/os/Handler;)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/7cb2ccf/appcompat/hiddenapi-flags.csv#122683) * (since API 24) [`Landroid/net/ConnectivityManager;->stopTethering(I)V,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/7cb2ccf/appcompat/hiddenapi-flags.csv#122685) +* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apBand:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/7cb2ccf/appcompat/hiddenapi-flags.csv#131003) +* (since API 23) [`Landroid/net/wifi/WifiConfiguration;->apChannel:I,greylist`](https://android.googlesource.com/platform/prebuilts/runtime/+/7cb2ccf/appcompat/hiddenapi-flags.csv#131004) * [`Landroid/net/wifi/WifiManager;->getWifiApConfiguration()Landroid/net/wifi/WifiConfiguration;,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/7cb2ccf/appcompat/hiddenapi-flags.csv#131756) * [`Landroid/net/wifi/WifiManager;->setWifiApConfiguration(Landroid/net/wifi/WifiConfiguration;)Z,whitelist`](https://android.googlesource.com/platform/prebuilts/runtime/+/7cb2ccf/appcompat/hiddenapi-flags.csv#131825) * (deprecated since API 26) `Landroid/net/wifi/WifiManager;->setWifiApEnabled(Landroid/net/wifi/WifiConfiguration;Z)Z` diff --git a/mobile/build.gradle b/mobile/build.gradle index 4a241049..494b814a 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -64,31 +64,30 @@ androidExtensions { } def aux = [ - 'com.crashlytics.sdk.android:crashlytics:2.9.9', - 'com.google.firebase:firebase-core:16.0.8', + 'com.crashlytics.sdk.android:crashlytics:2.10.0', + 'com.google.firebase:firebase-core:16.0.9', ] def lifecycleVersion = '2.0.0' -def roomVersion = '2.1.0-alpha07' +def roomVersion = '2.1.0-beta01' dependencies { kapt "androidx.room:room-compiler:$roomVersion" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.browser:browser:1.0.0' - implementation 'androidx.core:core-ktx:1.0.1' + implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.emoji:emoji:1.0.0' implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" - implementation 'androidx.preference:preference:1.1.0-alpha04' + implementation 'androidx.preference:preference:1.1.0-alpha05' implementation "androidx.room:room-ktx:$roomVersion" - implementation 'com.android.billingclient:billing:1.2.2' - implementation 'com.github.luongvo:BadgeView:1.1.5' + implementation 'com.android.billingclient:billing:2.0.0' implementation 'com.github.topjohnwu.libsu:core:2.5.0' - implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.material:material:1.1.0-alpha06' implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.linkedin.dexmaker:dexmaker:2.25.0' implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0' implementation 'net.glxn.qrgen:android:2.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' for (dep in aux) { freedomImplementation dep diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt index 32878a84..257f3ffb 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt @@ -50,6 +50,7 @@ class App : Application() { override fun onFailed(throwable: Throwable?) = Timber.d(throwable) }) }) + EBegFragment.init() if (DhcpWorkaround.shouldEnable) DhcpWorkaround.enable(true) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt index e8590351..f8c361f0 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/EBegFragment.kt @@ -1,40 +1,66 @@ package be.mygod.vpnhotspot -import android.content.DialogInterface import android.os.Bundle -import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Button import android.widget.Spinner -import androidx.annotation.StringRes -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDialogFragment +import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.util.launchUrl import be.mygod.vpnhotspot.widget.SmartSnackbar import com.android.billingclient.api.* -import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_ebeg.view.* import timber.log.Timber /** * Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/java/org/sufficientlysecure/donations/DonationsFragment.java */ -class EBegFragment : AppCompatDialogFragment(), PurchasesUpdatedListener, BillingClientStateListener, - SkuDetailsResponseListener, ConsumeResponseListener { - @Parcelize - data class MessageArg(@StringRes val title: Int, @StringRes val message: Int) : Parcelable - class MessageDialogFragment : AlertDialogFragment() { - override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) { - setTitle(arg.title) - setMessage(arg.message) - setNeutralButton(R.string.donations__button_close, null) +class EBegFragment : AppCompatDialogFragment(), SkuDetailsResponseListener { + companion object : BillingClientStateListener, PurchasesUpdatedListener, ConsumeResponseListener { + private lateinit var billingClient: BillingClient + + fun init() { + billingClient = BillingClient.newBuilder(app).apply { + enablePendingPurchases() + }.setListener(this).build().also { it.startConnection(this) } + } + + override fun onBillingSetupFinished(billingResult: BillingResult?) { + if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) { + billingClient.queryPurchases(BillingClient.SkuType.INAPP).apply { + if (responseCode == BillingClient.BillingResponseCode.OK) { + onPurchasesUpdated(this.billingResult, purchasesList) + } + } + } else Timber.e("onBillingSetupFinished: ${billingResult?.responseCode}") + } + + override fun onBillingServiceDisconnected() { + Timber.e("onBillingServiceDisconnected") + billingClient.startConnection(this) + } + + override fun onPurchasesUpdated(billingResult: BillingResult?, purchases: MutableList?) { + if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { + // directly consume in-app purchase, so that people can donate multiple times + for (purchase in purchases) if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { + billingClient.consumeAsync(ConsumeParams.newBuilder().apply { + setPurchaseToken(purchase.purchaseToken) + }.build(), this) + } + } else Timber.e("onPurchasesUpdated: ${billingResult?.responseCode}") + } + + override fun onConsumeResponse(billingResult: BillingResult?, purchaseToken: String?) { + if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) { + SmartSnackbar.make(R.string.donations__thanks_dialog).show() + } else Timber.e("onConsumeResponse: ${billingResult?.responseCode}") } } - private lateinit var billingClient: BillingClient private lateinit var googleSpinner: Spinner private var skus: MutableList? = null set(value) { @@ -53,62 +79,27 @@ class EBegFragment : AppCompatDialogFragment(), PurchasesUpdatedListener, Billin super.onViewCreated(view, savedInstanceState) dialog!!.setTitle(R.string.settings_misc_donate) googleSpinner = view.donations__google_android_market_spinner - onBillingServiceDisconnected() + billingClient.querySkuDetailsAsync( + SkuDetailsParams.newBuilder().apply { + setSkusList(listOf("donate001", "donate002", "donate005", "donate010", "donate020", "donate050", + "donate100", "donate200", "donatemax")) + setType(BillingClient.SkuType.INAPP) + }.build(), this) view.donations__google_android_market_donate_button.setOnClickListener { val sku = skus?.getOrNull(googleSpinner.selectedItemPosition) - if (sku == null) { - openDialog(R.string.donations__google_android_market_not_supported_title, - R.string.donations__google_android_market_not_supported) - } else billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder() - .setSkuDetails(sku).build()) + if (sku != null) billingClient.launchBillingFlow(requireActivity(), BillingFlowParams.newBuilder().apply { + setSkuDetails(sku) + }.build()) else SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show() } @Suppress("ConstantConditionIf") if (BuildConfig.DONATIONS) (view.donations__more_stub.inflate() as Button) .setOnClickListener { requireContext().launchUrl("https://mygod.be/donate/") } } - private fun openDialog(@StringRes title: Int, @StringRes message: Int) { - val fragmentManager = fragmentManager - if (fragmentManager == null) SmartSnackbar.make(message).show() else try { - MessageDialogFragment().withArg(MessageArg(title, message)).show(fragmentManager, "MessageDialogFragment") - } catch (e: IllegalStateException) { - SmartSnackbar.make(message).show() + override fun onSkuDetailsResponse(billingResult: BillingResult?, skuDetailsList: MutableList?) { + if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK) skus = skuDetailsList else { + Timber.e("onSkuDetailsResponse: ${billingResult?.responseCode}") + SmartSnackbar.make(R.string.donations__google_android_market_not_supported).show() } } - - override fun onBillingServiceDisconnected() { - skus = null - billingClient = BillingClient.newBuilder(context ?: return).setListener(this).build() - .also { it.startConnection(this) } - } - - override fun onBillingSetupFinished(responseCode: Int) { - if (responseCode == BillingClient.BillingResponse.OK) { - billingClient.querySkuDetailsAsync( - SkuDetailsParams.newBuilder().apply { - setSkusList(listOf("donate001", "donate002", "donate005", "donate010", "donate020", "donate050", - "donate100", "donate200", "donatemax")) - setType(BillingClient.SkuType.INAPP) - }.build(), this) - } else Timber.e("onBillingSetupFinished: $responseCode") - } - - override fun onSkuDetailsResponse(responseCode: Int, skuDetailsList: MutableList?) { - if (responseCode == BillingClient.BillingResponse.OK) skus = skuDetailsList - else Timber.e("onSkuDetailsResponse: $responseCode") - } - - override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) { - if (responseCode == BillingClient.BillingResponse.OK && purchases != null) { - // directly consume in-app purchase, so that people can donate multiple times - purchases.forEach { billingClient.consumeAsync(it.purchaseToken, this) } - } else Timber.e("onPurchasesUpdated: $responseCode") - } - - override fun onConsumeResponse(responseCode: Int, purchaseToken: String?) { - if (responseCode == BillingClient.BillingResponse.OK) { - openDialog(R.string.donations__thanks_dialog_title, R.string.donations__thanks_dialog) - dismissAllowingStateLoss() - } else Timber.e("onConsumeResponse: $responseCode") - } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt index 4914c620..3e48b877 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt @@ -2,7 +2,6 @@ package be.mygod.vpnhotspot import android.content.Intent import android.os.Bundle -import android.view.Gravity import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -18,13 +17,10 @@ import be.mygod.vpnhotspot.manage.TetheringFragment import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.widget.SmartSnackbar -import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationView -import q.rorbin.badgeview.QBadgeView class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { private lateinit var binding: ActivityMainBinding - private lateinit var badge: QBadgeView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -32,15 +28,15 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS binding.lifecycleOwner = this binding.navigation.setOnNavigationItemSelectedListener(this) if (savedInstanceState == null) displayFragment(TetheringFragment()) - badge = QBadgeView(this) - badge.bindTarget((binding.navigation.getChildAt(0) as BottomNavigationMenuView).getChildAt(1)) - badge.badgeBackgroundColor = ContextCompat.getColor(this, R.color.colorSecondary) - badge.badgeTextColor = ContextCompat.getColor(this, R.color.primary_text_default_material_light) - badge.badgeGravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL - badge.setGravityOffset(16f, 0f, true) val model = ViewModelProviders.of(this).get() if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class) - model.clients.observe(this, Observer { badge.badgeNumber = it.size }) + model.clients.observe(this, Observer { + if (it.isNotEmpty()) binding.navigation.showBadge(R.id.navigation_clients).apply { + backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.colorSecondary) + badgeTextColor = ContextCompat.getColor(this@MainActivity, R.color.primary_text_default_material_light) + number = it.size + } else binding.navigation.removeBadge(R.id.navigation_clients) + }) SmartSnackbar.Register(lifecycle, binding.fragmentHolder) WifiDoubleLock.ActivityListener(this) } diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index 8afd3ff0..e96761d8 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -10,7 +10,7 @@ android:orientation="vertical" tools:context="be.mygod.vpnhotspot.MainActivity"> - - Закрыть Считаете это приложение полезным?\nПоддержите его разработку, отправив пожертвование разработчику! Google Play Store - In-App пожертвования не поддерживаются. Пожертвования через приложение не поддерживаются. Google Play Store установлен правильно? Google взимает 30% комиссии с каждого пожертвования! Пожертвовать! Сколько? - Благодарю! Благодарю за пожертвование! Я очень это ценю! diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index eab8ea62..17eaee4f 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -144,11 +144,9 @@ 关闭 Google Play 商店 - 不支持 In-App 捐赠。 不支持 In-App 捐赠。你的 Google Play 商店是否安装正确了呢? 捐赠! 捐赠多少? - 谢谢! 谢谢捐赠!\n非常感谢您! 觉得此应用很有用?\n捐赠给该开发者以支持此应用的开发! diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 60f79d31..bb1eb044 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -153,11 +153,9 @@ Close Do you find this application useful?\nSupport its development by sending a donation to the developer! Google Play Store - In-App Donations are not supported. In-App Donations are not supported. Is Google Play Store installed correctly? Google charges a fee of 30% Donate! How much? - Thanks! Thanks for donating!\nI really appreciate this!