diff --git a/app/src/main/java/org/torproject/android/service/OrbotService.java b/app/src/main/java/org/torproject/android/service/OrbotService.java index 8dfc5f5745..82e8cc47e1 100644 --- a/app/src/main/java/org/torproject/android/service/OrbotService.java +++ b/app/src/main/java/org/torproject/android/service/OrbotService.java @@ -754,10 +754,6 @@ public void onReceive(Context context, Intent intent) { // hack for https://github.com/guardianproject/tor-android/issues/73 remove when fixed var newStatus = intent.getStringExtra(EXTRA_STATUS); - if (STATUS_ON.equals(newStatus) && Prefs.getTransport() == Transport.NONE && !Prefs.getHasDirectConnected()) { - Prefs.setHasDirectConnected(true); - } - if (STATUS_OFF.equals(mCurrentStatus) && STATUS_STOPPING.equals(newStatus)) break; mCurrentStatus = newStatus; diff --git a/app/src/main/java/org/torproject/android/ui/OrbotBottomSheetDialogFragment.kt b/app/src/main/java/org/torproject/android/ui/OrbotBottomSheetDialogFragment.kt index 263dd453aa..526c900f85 100644 --- a/app/src/main/java/org/torproject/android/ui/OrbotBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/torproject/android/ui/OrbotBottomSheetDialogFragment.kt @@ -6,6 +6,7 @@ import android.graphics.Color import android.os.Bundle import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import android.widget.EditText import android.widget.FrameLayout @@ -21,7 +22,9 @@ import org.torproject.android.R Class to set up default bottom sheet behavior for Config Connection, MOAT and any other bottom sheets to come */ -open class OrbotBottomSheetDialogFragment : BottomSheetDialogFragment() { +open class OrbotBottomSheetDialogFragment( + val minMode: Boolean = false +) : BottomSheetDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = BottomSheetDialog(requireActivity(), theme) dialog.setOnShowListener { @@ -30,8 +33,8 @@ open class OrbotBottomSheetDialogFragment : BottomSheetDialogFragment() { bottomSheetView?.let { it.setBackgroundResource(R.drawable.bottom_sheet_rounded) it.setBackgroundColor(Color.TRANSPARENT) - setHeightResponsive(it) val behavior = BottomSheetBehavior.from(it) + setHeightResponsive(it, behavior) behavior.state = BottomSheetBehavior.STATE_EXPANDED } } @@ -39,16 +42,24 @@ open class OrbotBottomSheetDialogFragment : BottomSheetDialogFragment() { return dialog } - private fun setHeightResponsive(bottomSheet: View) { + private fun setHeightResponsive(bottomSheet: View, behavior: BottomSheetBehavior<*>) { val windowMetrics = WindowMetricsCalculator .getOrCreate() .computeCurrentWindowMetrics(requireActivity()) val windowHeight = windowMetrics.bounds.height() val height = (windowHeight * getHeightRatio()).toInt() - val layoutParams = bottomSheet.layoutParams - layoutParams.height = height + + if (minMode) { + behavior.maxHeight = height + + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + else { + layoutParams.height = height + } + bottomSheet.layoutParams = layoutParams } diff --git a/app/src/main/java/org/torproject/android/ui/kindness/KindnessConfigBottomSheet.kt b/app/src/main/java/org/torproject/android/ui/kindness/KindnessConfigBottomSheet.kt index 44beadad06..6a378b2666 100644 --- a/app/src/main/java/org/torproject/android/ui/kindness/KindnessConfigBottomSheet.kt +++ b/app/src/main/java/org/torproject/android/ui/kindness/KindnessConfigBottomSheet.kt @@ -4,45 +4,45 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button -import androidx.appcompat.widget.SwitchCompat -import androidx.fragment.app.FragmentActivity -import org.torproject.android.R -import org.torproject.android.util.Prefs +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.setFragmentResult +import org.torproject.android.databinding.KindnessConfigBottomSheetBinding import org.torproject.android.ui.OrbotBottomSheetDialogFragment +import org.torproject.android.util.Prefs -class KindnessConfigBottomSheet : OrbotBottomSheetDialogFragment() { +class KindnessConfigBottomSheet : OrbotBottomSheetDialogFragment(true) { - private lateinit var btnAction: Button + private lateinit var mBinding: KindnessConfigBottomSheetBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val v = inflater.inflate(R.layout.kindess_config_bottom_sheet, container, false) - v.findViewById(R.id.tvCancel).setOnClickListener { dismiss() } - btnAction = v.findViewById(R.id.btnAction) + ): View { + mBinding = KindnessConfigBottomSheetBinding.inflate(inflater, container, false) + + mBinding.tvCancel.setOnClickListener { dismiss() } - val configWifi = v.findViewById(R.id.swKindnessConfigWifi) - val configCharging = v.findViewById(R.id.swKindnessConfigCharging) + mBinding.btnAction.setOnClickListener { + Prefs.setBeSnowflakeProxyLimitWifi(mBinding.swKindnessConfigWifi.isChecked) + Prefs.setBeSnowflakeProxyLimitCharging(mBinding.swKindnessConfigCharging.isChecked) - btnAction.setOnClickListener { - Prefs.setBeSnowflakeProxyLimitWifi(configWifi.isChecked) - Prefs.setBeSnowflakeProxyLimitCharging(configCharging.isChecked) + setFragmentResult(KEY_CONFIG_CHANGED, Bundle()) dismiss() } - configWifi.isChecked = Prefs.limitSnowflakeProxyingWifi() - configCharging.isChecked = Prefs.limitSnowflakeProxyingCharging() - return v + mBinding.swKindnessConfigWifi.isChecked = Prefs.limitSnowflakeProxyingWifi() + mBinding.swKindnessConfigCharging.isChecked = Prefs.limitSnowflakeProxyingCharging() + + return mBinding.root } companion object { - fun openKindnessSettings(fragmentActivity: FragmentActivity) { + const val KEY_CONFIG_CHANGED = "kindness_config_changed" + + fun show(fragmentManager: FragmentManager) { KindnessConfigBottomSheet().show( - fragmentActivity.supportFragmentManager, + fragmentManager, "KindnessConfig" ) } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/torproject/android/ui/kindness/KindnessFragment.kt b/app/src/main/java/org/torproject/android/ui/kindness/KindnessFragment.kt index c1f053c0c5..a4e7483c7f 100644 --- a/app/src/main/java/org/torproject/android/ui/kindness/KindnessFragment.kt +++ b/app/src/main/java/org/torproject/android/ui/kindness/KindnessFragment.kt @@ -1,118 +1,209 @@ package org.torproject.android.ui.kindness +import IPtProxy.IPtProxy import android.app.AlertDialog +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.os.Bundle +import android.os.IBinder import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button -import android.widget.TextView -import androidx.appcompat.widget.SwitchCompat +import androidx.core.net.toUri import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.launch import org.torproject.android.R -import org.torproject.android.service.circumvention.BuiltInBridges -import org.torproject.android.service.circumvention.Transport +import org.torproject.android.databinding.FragmentKindnessBinding import org.torproject.android.util.Prefs -import java.util.Locale -import kotlin.collections.contains class KindnessFragment : Fragment() { - private lateinit var tvAllTimeTotal: TextView - private lateinit var tvWeeklyTotal: TextView - private lateinit var swVolunteerMode: SwitchCompat - private lateinit var btnActionActivate: Button - private lateinit var pnlActivate: View - private lateinit var pnlStatus: View + private lateinit var mBinding: FragmentKindnessBinding + private var mService: SnowflakeProxyService? = null + private var mBound = false + + private val connection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as SnowflakeProxyService.LocalBinder + mService = binder.getService() + mBound = true + observeNatType() + } + + override fun onServiceDisconnected(arg0: ComponentName) { + mBound = false + mService = null + } + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - val view = inflater.inflate(R.layout.fragment_kindness, container, false) - tvAllTimeTotal = view.findViewById(R.id.tvAlltimeTotal) - tvWeeklyTotal = view.findViewById(R.id.tvWeeklyTotal) - swVolunteerMode = view.findViewById(R.id.swVolunteerMode) - btnActionActivate = view.findViewById(R.id.btnActionActivate) - pnlActivate = view.findViewById(R.id.panel_kindness_activate) - pnlStatus = view.findViewById(R.id.panel_kindness_status) - getErrorStringIfAny()?.let { - Prefs.setBeSnowflakeProxy(false) - } - swVolunteerMode.isChecked = Prefs.beSnowflakeProxy() - swVolunteerMode.setOnCheckedChangeListener { _, isChecked -> + ): View { + mBinding = FragmentKindnessBinding.inflate(inflater) + + mBinding.swVolunteerMode.isChecked = Prefs.beSnowflakeProxy() + mBinding.swVolunteerMode.setOnCheckedChangeListener { _, isChecked -> Prefs.setBeSnowflakeProxy(isChecked) - showPanelStatus(isChecked) activity?.let { if (isChecked) { SnowflakeProxyService.startSnowflakeProxyForegroundService(it) } else { SnowflakeProxyService.stopSnowflakeProxyForegroundService(it) + + updateNatTypeUi(IPtProxy.NATUnknown) } } } - view.findViewById(R.id.ivGear).setOnClickListener { - KindnessConfigBottomSheet.openKindnessSettings(requireActivity()) + mBinding.rowUsageLimits.setOnClickListener { + KindnessConfigBottomSheet.show(parentFragmentManager) } - view.findViewById(R.id.swVolunteerAdjust) - .setOnClickListener { KindnessConfigBottomSheet.openKindnessSettings(requireActivity()) } + updateNatTypeUi(IPtProxy.NATUnknown) - btnActionActivate.setOnClickListener { - getErrorStringIfAny()?.let { - showDisabledDialog(it) - return@setOnClickListener + mBinding.rowProxyQuality.setOnClickListener { + if (mService?.natType?.value == IPtProxy.NATRestricted) { + showQualityHint() } - swVolunteerMode.isChecked = true } - showPanelStatus(Prefs.beSnowflakeProxy()) - return view - } + mBinding.btnActionActivate.setOnClickListener { + TestingDialogFragment.show(parentFragmentManager) + } - private fun getErrorStringIfAny(): Int? { - val country = Prefs.bridgeCountry?.lowercase(Locale.getDefault()) - if (BuiltInBridges.dnsCountries.contains(country)) - return R.string.kindness_mode_cant_run_in_your_country - if (Prefs.useVpn() && Prefs.transport != Transport.NONE) - R.string.kindness_mode_cant_run_with_bridge - if (!Prefs.hasDirectConnected) { - return R.string.kindness_never_had_a_direct_connection + mBinding.btnActionLearnMore.setOnClickListener { + val i = Intent(Intent.ACTION_VIEW, "https://orbot.app/kindness".toUri()) + val pm = context?.packageManager + + if (pm != null && i.resolveActivity(pm) != null) { + startActivity(i) + } } - return null + + showPanelStatus(!Prefs.snowflakeNeedsQualityCheck) + + parentFragmentManager.setFragmentResultListener( + KindnessConfigBottomSheet.KEY_CONFIG_CHANGED, + viewLifecycleOwner) { _, _ -> + updateUsageLimitsUi() + } + + parentFragmentManager.setFragmentResultListener( + TestingDialogFragment.KEY_RESULT, + viewLifecycleOwner) { _, bundle -> + + if (bundle.getBoolean(TestingDialogFragment.KEY_RESULT)) { + if (!Prefs.snowflakeNeedsQualityCheck) { + mBinding.swVolunteerMode.isChecked = true + showPanelStatus(true) + } + } + } + + return mBinding.root } - private fun showDisabledDialog(msg: Int) { - AlertDialog.Builder(requireContext()) - .setTitle(R.string.kindness_mode_cant_start) - .setMessage(msg) - .setPositiveButton(android.R.string.ok, null) - .show() + override fun onStart() { + super.onStart() + + // TODO: We need this, to receive the proxy quality, but on the other hand, this + // will unintentionally start the SnowflakeProxyService, which we don't want, because + // it will automatically start SnowflakeProxy if there are no Wi-Fi/battery limits. + // -> hence a redesign of the SnowflakeProxyService seems to await us. + +// context?.let { +// it.bindService(SnowflakeProxyService.getIntent(it), connection, Context.BIND_AUTO_CREATE) +// } } override fun onResume() { super.onResume() - // updates these values when user returns to screen after running snowflake proxy for some time - tvAllTimeTotal.text = "${Prefs.snowflakesServed}" - tvWeeklyTotal.text = "${Prefs.snowflakesServedWeekly}" + + // Updates these values when user returns to screen after running snowflake proxy for some time. + + updateUsageLimitsUi() + + mBinding.tvAlltimeTotal.text = "${Prefs.snowflakesServed}" + mBinding.tvWeeklyTotal.text = "${Prefs.snowflakesServedWeekly}" + } + + override fun onStop() { + super.onStop() + + if (mBound) { + context?.unbindService(connection) + mBound = false + } + } + + private fun observeNatType() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + mService?.natType?.collect { natType -> + updateNatTypeUi(natType) + } + } + } + } + + private fun updateNatTypeUi(natType: String) { + mBinding.tvProxyQualityStatus.text = when (natType) { + IPtProxy.NATUnknown -> getString(R.string.kindness_proxy_quality_unknown) + IPtProxy.NATRestricted -> getString(R.string.kindness_proxy_quality_restricted) + IPtProxy.NATUnrestricted -> getString(R.string.kindness_proxy_quality_unrestricted) + else -> natType + } + + if (natType == IPtProxy.NATRestricted) { + mBinding.redDot.visibility = View.VISIBLE + mBinding.chevron2.visibility = View.VISIBLE + } + else { + mBinding.redDot.visibility = View.GONE + mBinding.chevron2.visibility = View.GONE + } + } + + private fun updateUsageLimitsUi() { + mBinding.tvUsageLimitsStatus.text = + getString(if (Prefs.limitSnowflakeProxyingWifi() || Prefs.limitSnowflakeProxyingCharging()) + R.string.kindness_usage_limits_status_on + else R.string.kindness_usage_limits_status_off) + } + + private fun showQualityHint() { + val context = context ?: return + + AlertDialog.Builder(context) + .setTitle(R.string.kindness_quality_upgrade_title) + .setMessage(String.format("%s\n\n%s", + getString(R.string.kindness_quality_upgrade_line1), + getString(R.string.kindness_quality_upgrade_line2))) + .setPositiveButton(android.R.string.ok, null) + .show() } private fun showPanelStatus(isActivated: Boolean) { val duration = 250L if (isActivated) { - pnlActivate.animate().alpha(0f).setDuration(0).withEndAction { - pnlActivate.visibility = View.GONE + mBinding.panelKindnessActivate.animate().alpha(0f).setDuration(0).withEndAction { + mBinding.panelKindnessActivate.visibility = View.GONE } - pnlStatus.visibility = View.VISIBLE - pnlStatus.animate().alpha(1f).duration = duration + mBinding.panelKindnessStatus.visibility = View.VISIBLE + mBinding.panelKindnessStatus.animate().alpha(1f).duration = duration } else { - pnlActivate.visibility = View.VISIBLE - pnlActivate.animate().alpha(1f).duration = duration + mBinding.panelKindnessActivate.visibility = View.VISIBLE + mBinding.panelKindnessActivate.animate().alpha(1f).duration = duration - pnlStatus.animate().alpha(0f).setDuration(0).withEndAction { - pnlStatus.visibility = View.GONE + mBinding.panelKindnessStatus.animate().alpha(0f).setDuration(0).withEndAction { + mBinding.panelKindnessStatus.visibility = View.GONE } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyService.kt b/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyService.kt index ee3824e4c0..cdf4d041a0 100644 --- a/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyService.kt +++ b/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyService.kt @@ -10,26 +10,38 @@ import android.content.IntentFilter import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities +import android.os.Binder import android.os.Build import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import org.torproject.android.R import org.torproject.android.util.NetworkUtils import org.torproject.android.util.Prefs class SnowflakeProxyService : Service() { + inner class LocalBinder : Binder() { + fun getService(): SnowflakeProxyService = this@SnowflakeProxyService + } + private val binder = LocalBinder() + private lateinit var snowflakeProxyWrapper: SnowflakeProxyWrapper private lateinit var powerConnectionReceiver: PowerConnectionReceiver private lateinit var notificationChannelId: String private lateinit var networkCallbacks: ConnectivityManager.NetworkCallback - override fun onBind(intent: Intent?): IBinder? { + private val _natType = MutableStateFlow(IPtProxy.IPtProxy.NATUnknown) + val natType: StateFlow = _natType.asStateFlow() + + override fun onBind(intent: Intent?): IBinder { Log.d(TAG, "onBind: $intent") - return null + return binder } override fun onCreate() { @@ -53,6 +65,10 @@ class SnowflakeProxyService : Service() { return START_STICKY } + fun updateNatType(type: String) { + _natType.value = type + } + fun refreshNotification(contentText: String? = null) { val title = if (snowflakeProxyWrapper.isProxyRunning()) getString(R.string.kindness_mode_is_running) @@ -170,7 +186,7 @@ class SnowflakeProxyService : Service() { private const val CHANNEL_ID = "snowflake" private const val ACTION_STOP_SNOWFLAKE_SERVICE = "ACTION_STOP_SNOWFLAKE_SERVICE" - private fun getIntent(context: Context) = Intent(context, SnowflakeProxyService::class.java) + fun getIntent(context: Context) = Intent(context, SnowflakeProxyService::class.java) // start this service, but not necessarily snowflake proxy from the app UI fun startSnowflakeProxyForegroundService(context: Context) { diff --git a/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyWrapper.kt b/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyWrapper.kt index 26323bbbb5..6ae9cc3052 100644 --- a/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyWrapper.kt +++ b/app/src/main/java/org/torproject/android/ui/kindness/SnowflakeProxyWrapper.kt @@ -89,8 +89,8 @@ class SnowflakeProxyWrapper(private val service: SnowflakeProxyService) { // Ignored. } - override fun natTypeUpdated(natType: String?) { - // TODO feature added in IPtProxy 5.4.1 + override fun natTypeUpdated(natType: String) { + service.updateNatType(natType) } } @@ -107,10 +107,13 @@ class SnowflakeProxyWrapper(private val service: SnowflakeProxyService) { @Synchronized fun stopProxy() { if (proxy == null) return + proxy?.stop() proxy = null releaseMappedPorts() + + service.updateNatType(IPtProxy.IPtProxy.NATUnknown) } fun isProxyRunning(): Boolean = proxy != null diff --git a/app/src/main/java/org/torproject/android/ui/kindness/TestingDialogFragment.kt b/app/src/main/java/org/torproject/android/ui/kindness/TestingDialogFragment.kt new file mode 100644 index 0000000000..fa8a415389 --- /dev/null +++ b/app/src/main/java/org/torproject/android/ui/kindness/TestingDialogFragment.kt @@ -0,0 +1,111 @@ +package org.torproject.android.ui.kindness + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.setFragmentResult +import androidx.lifecycle.lifecycleScope +import androidx.window.layout.WindowMetricsCalculator +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.torproject.android.R +import org.torproject.android.databinding.FragmentTestingBinding +import org.torproject.android.service.circumvention.Transport +import org.torproject.android.util.Prefs + +class TestingDialogFragment : DialogFragment() { + + private lateinit var mBinding: FragmentTestingBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + mBinding = FragmentTestingBinding.inflate(inflater, container, false) + + mBinding.tvTitleApproved.text = getString(R.string.testing_title_approved, "✅") + mBinding.tvTitleDeclined.text = getString(R.string.testing_title_declined, "\uD83D\uDEAB") + + mBinding.boxTesting.visibility = View.VISIBLE + mBinding.boxApproved.visibility = View.GONE + mBinding.boxDeclined.visibility = View.GONE + + mBinding.btContinue.setOnClickListener { + val bundle = Bundle() + bundle.putBoolean(KEY_RESULT, true) + + setFragmentResult(KEY_RESULT, bundle) + dismiss() + } + + mBinding.btOk.setOnClickListener { + setFragmentResult(KEY_RESULT, Bundle()) + dismiss() + } + + return mBinding.root + } + + override fun onStart() { + super.onStart() + + dialog?.window?.let { window -> + val metrics = WindowMetricsCalculator.getOrCreate() + .computeCurrentWindowMetrics(requireActivity()) + val width = metrics.bounds.width() + val height = metrics.bounds.height() + + val dialogHeight = if (width > height) (height * 0.9f).toInt() else (height * 0.33f).toInt() + + val dialogWidth = if (width > height) (width * 0.33f).toInt() else (width * 0.9f).toInt() + + window.setLayout(dialogWidth, dialogHeight) + window.setBackgroundDrawableResource(android.R.color.transparent) + } + + // TODO: Add actual test: + // - If connected to Tor without a bridge right now, we're fine: dismiss immediately like `btContinue`. + // - If last test success timestamp is younger than 1 day, dismiss immediately like `btContinue`. + // - Else, set bridge to `NONE`, start Tor, wait until success or timeout. + // - Stop Tor again, after test and restore original settings. + // - Store timestamp of success in `Prefs`. + + if (!Prefs.snowflakeNeedsQualityCheck) { + mBinding.btContinue.callOnClick() + + return + } + + // TODO: @bitmold: Is this the correct way to test, if Tor is currently running without any bridges? + if (Prefs.useVpn() && Prefs.transport == Transport.NONE) { + Prefs.snowflakeNeedsQualityCheck = false + + mBinding.btContinue.callOnClick() + + return + } + + lifecycleScope.launch { + // TODO: Replace with proper start of Tor (VPN vs. expert mode?) + // and wait until successful connect or 90 second timeout. + delay(3000) + + Prefs.snowflakeNeedsQualityCheck = false + + mBinding.boxTesting.visibility = View.GONE + mBinding.boxApproved.visibility = View.VISIBLE + } + } + + companion object { + const val KEY_RESULT = "kindness_test_result" + + fun show(fragmentManager: FragmentManager) { + TestingDialogFragment().show(fragmentManager, "TestingFragment") + } + } +} diff --git a/app/src/main/java/org/torproject/android/util/Prefs.kt b/app/src/main/java/org/torproject/android/util/Prefs.kt index 690fb7eac7..45d8db7df7 100644 --- a/app/src/main/java/org/torproject/android/util/Prefs.kt +++ b/app/src/main/java/org/torproject/android/util/Prefs.kt @@ -21,7 +21,7 @@ object Prefs { private const val PREF_ALLOW_BACKGROUND_STARTS = "pref_allow_background_starts" private const val PREF_OPEN_PROXY_ON_ALL_INTERFACES = "pref_open_proxy_on_all_interfaces" private const val PREF_USE_VPN = "pref_vpn" - private const val PREF_DIRECT_CONNECT_SUCCESS = "pref_direct_connect" + private const val PREF_LAST_SNOWFLAKE_QUALITY_CHECK = "pref_last_snowflake_quality_check" private const val PREF_EXIT_NODES = "pref_exit_nodes" private const val PREF_BE_A_SNOWFLAKE = "pref_be_a_snowflake" private const val PREF_SHOW_SNOWFLAKE_MSG = "pref_show_snowflake_proxy_msg" @@ -163,11 +163,16 @@ object Prefs { cr?.putPref(PREF_USE_VPN, value) } - @JvmStatic - var hasDirectConnected: Boolean - get() = cr?.getPrefBoolean(PREF_DIRECT_CONNECT_SUCCESS) ?: false - set(value) = cr?.putPref(PREF_DIRECT_CONNECT_SUCCESS, value) ?: Unit + var snowflakeNeedsQualityCheck: Boolean + get() { + val last = cr?.getPrefLong(PREF_LAST_SNOWFLAKE_QUALITY_CHECK) ?: 0 + // A new quality check should be done every 24 hours. + return last <= System.currentTimeMillis() - 24 * 60 * 60 * 1000 + } + set(value) { + cr?.putPref(PREF_LAST_SNOWFLAKE_QUALITY_CHECK, if (value) 0 else System.currentTimeMillis()) + } fun startOnBoot(): Boolean { return cr?.getPrefBoolean(PREF_START_ON_BOOT, true) ?: true diff --git a/app/src/main/res/drawable/bg_modal_rounded.xml b/app/src/main/res/drawable/bg_modal_rounded.xml new file mode 100644 index 0000000000..2be0a25b6e --- /dev/null +++ b/app/src/main/res/drawable/bg_modal_rounded.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rounded_orange.xml b/app/src/main/res/drawable/bg_rounded_orange.xml new file mode 100644 index 0000000000..35f0504b4e --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_orange.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml index 93e83fc3f4..eedd464160 100644 --- a/app/src/main/res/drawable/ic_heart.xml +++ b/app/src/main/res/drawable/ic_heart.xml @@ -1,10 +1,12 @@ + android:viewportWidth="24" + android:viewportHeight="24"> - \ No newline at end of file + android:pathData="M12,9C12,9 12,3 17.25,3C22.5,3 22.5,7.5 22.5,9C22.5,15.75 12,22.5 12,22.5V9Z" + android:fillColor="#7D7C80"/> + + diff --git a/app/src/main/res/drawable/ic_snowflake.png b/app/src/main/res/drawable/ic_snowflake.png new file mode 100644 index 0000000000..3a554d917e Binary files /dev/null and b/app/src/main/res/drawable/ic_snowflake.png differ diff --git a/app/src/main/res/drawable/kindness.png b/app/src/main/res/drawable/kindness.png deleted file mode 100644 index cc379915b7..0000000000 Binary files a/app/src/main/res/drawable/kindness.png and /dev/null differ diff --git a/app/src/main/res/drawable/orbiesnoozing.png b/app/src/main/res/drawable/orbiesnoozing.png deleted file mode 100644 index 35eac329b7..0000000000 Binary files a/app/src/main/res/drawable/orbiesnoozing.png and /dev/null differ diff --git a/app/src/main/res/layout-land/fragment_kindness.xml b/app/src/main/res/layout-land/fragment_kindness.xml new file mode 100644 index 0000000000..319c43c808 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_kindness.xml @@ -0,0 +1,420 @@ + + + + + + + + + + + + + + + +