Unverified Commit 8ae3f5ce authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1903 from Mygod/bottomappbar

Is this a Material Design 2?
parents dc72fc00 2ea09791
......@@ -63,9 +63,7 @@ dependencies {
implementation project(':core')
implementation "android.arch.work:work-runtime-ktx:$workVersion"
implementation "androidx.browser:browser:$androidxVersion"
implementation "androidx.gridlayout:gridlayout:$androidxVersion"
implementation "androidx.room:room-runtime:$roomVersion"
implementation "com.google.android.material:material:$androidxVersion"
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.4'
implementation 'com.google.android.gms:play-services-vision:15.0.2'
implementation 'com.google.firebase:firebase-ads:15.0.1'
......
......@@ -31,12 +31,9 @@ import android.net.VpnService
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.os.SystemClock
import android.text.format.Formatter
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
......@@ -49,7 +46,6 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceDataStore
import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.acl.CustomRulesFragment
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
......@@ -60,16 +56,10 @@ import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.responseLength
import com.github.shadowsocks.utils.thread
import com.github.shadowsocks.widget.ServiceButton
import com.github.shadowsocks.widget.StatsBar
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
import java.util.*
class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPreferenceDataStoreChangeListener,
......@@ -86,15 +76,10 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
// UI
private lateinit var fab: ServiceButton
private lateinit var stats: StatsBar
internal lateinit var drawer: DrawerLayout
private lateinit var navigation: NavigationView
private var testCount = 0
private lateinit var statusText: TextView
private lateinit var txText: TextView
private lateinit var rxText: TextView
private lateinit var txRateText: TextView
private lateinit var rxRateText: TextView
val snackbar by lazy { findViewById<View>(R.id.snackbar) }
fun snackbar(text: CharSequence = "") = Snackbar.make(snackbar, text, Snackbar.LENGTH_LONG)
......@@ -115,7 +100,12 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
app.handler.post { changeState(state, msg, true) }
}
override fun trafficUpdated(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
app.handler.post { updateTraffic(profileId, txRate, rxRate, txTotal, rxTotal) }
app.handler.post {
stats.updateTraffic(txRate, rxRate, txTotal, rxTotal)
val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment?
if (state != BaseService.STOPPING)
child?.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal)
}
}
override fun trafficPersisted(profileId: Long) {
app.handler.post { ProfilesFragment.instance?.onTrafficPersisted(profileId) }
......@@ -123,69 +113,14 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
}
}
fun changeState(state: Int, msg: String? = null, animate: Boolean = false) {
private fun changeState(state: Int, msg: String? = null, animate: Boolean = false) {
fab.changeState(state, animate)
when (state) {
BaseService.CONNECTING -> statusText.setText(R.string.connecting)
BaseService.CONNECTED -> statusText.setText(R.string.vpn_connected)
BaseService.STOPPING -> statusText.setText(R.string.stopping)
else -> {
stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error).format(Locale.ENGLISH, msg)).show()
statusText.setText(R.string.not_connected)
}
}
this.state = state
if (state != BaseService.CONNECTED) {
updateTraffic(-1, 0, 0, 0, 0)
testCount += 1 // suppress previous test messages
}
ProfilesFragment.instance?.profilesAdapter?.notifyDataSetChanged() // refresh button enabled state
stateListener?.invoke(state)
}
fun updateTraffic(profileId: Long, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.text = Formatter.formatFileSize(this, txTotal)
rxText.text = Formatter.formatFileSize(this, rxTotal)
txRateText.text = getString(R.string.speed, Formatter.formatFileSize(this, txRate))
rxRateText.text = getString(R.string.speed, Formatter.formatFileSize(this, rxRate))
val child = supportFragmentManager.findFragmentById(R.id.fragment_holder) as ToolbarFragment?
if (state != BaseService.STOPPING)
child?.onTrafficUpdated(profileId, txRate, rxRate, txTotal, rxTotal)
}
/**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/97bfd27/services/core/java/com/android/server/connectivity/NetworkMonitor.java#879
*/
private fun testConnection(id: Int) {
val url = URL("https", when (app.currentProfile!!.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (BaseService.usingVpnMode) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS,
InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
val (success, result) = try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L)
Pair(true, getString(R.string.connection_test_available, elapsed))
else throw IOException(getString(R.string.connection_test_error_status_code, code))
} catch (e: IOException) {
Pair(false, getString(R.string.connection_test_error, e.message))
} finally {
conn.disconnect()
}
if (testCount == id) app.handler.post {
if (success) statusText.text = result else {
statusText.setText(R.string.connection_test_fail)
snackbar(result).show()
}
}
}
override val listenForDeath: Boolean get() = true
override fun onServiceConnected(service: IShadowsocksService) = changeState(service.state)
......@@ -210,6 +145,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
stats = findViewById(R.id.stats)
stats.setOnClickListener { if (state == BaseService.CONNECTED) stats.testConnection() }
drawer = findViewById(R.id.drawer)
navigation = findViewById(R.id.navigation)
navigation.setNavigationItemSelectedListener(this)
......@@ -218,20 +155,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, OnPre
displayFragment(ProfilesFragment())
}
statusText = findViewById(R.id.status)
txText = findViewById(R.id.tx)
txRateText = findViewById(R.id.txRate)
rxText = findViewById(R.id.rx)
rxRateText = findViewById(R.id.rxRate)
findViewById<View>(R.id.stat).setOnClickListener {
if (state == BaseService.CONNECTED) {
++testCount
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
thread("ConnectionTest") { testConnection(id) }
}
}
fab = findViewById(R.id.fab)
fab.setOnClickListener {
when {
......
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.util.AttributeSet
import androidx.gridlayout.widget.GridLayout
import com.github.shadowsocks.R
/**
* Based on: http://stackoverflow.com/a/6212120/2245107
*/
class BoundedGridLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
defStyleRes: Int = 0) : GridLayout(context, attrs, defStyleAttr) {
private val boundedWidth: Int
private val boundedHeight: Int
init {
val arr = context.obtainStyledAttributes(attrs, R.styleable.BoundedGridLayout, defStyleAttr, defStyleRes)
boundedWidth = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_width, 0)
boundedHeight = arr.getDimensionPixelSize(R.styleable.BoundedGridLayout_bounded_height, 0)
arr.recycle()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) = super.onMeasure(
if (boundedWidth <= 0 || boundedWidth >= MeasureSpec.getSize(widthSpec)) widthSpec
else MeasureSpec.makeMeasureSpec(boundedWidth, MeasureSpec.getMode(widthSpec)),
if (boundedHeight <= 0 || boundedHeight >= MeasureSpec.getSize(heightSpec)) heightSpec
else MeasureSpec.makeMeasureSpec(boundedHeight, MeasureSpec.getMode(heightSpec)))
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.accessibility.AccessibilityManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.getSystemService
import com.google.android.material.animation.AnimationUtils
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.SnackbarConsts
/**
* Full credits go to: https://stackoverflow.com/a/35904421/2245107
*/
class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet?) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val accessibility = context.getSystemService<AccessibilityManager>()!!
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean =
dependency is Snackbar.SnackbarLayout
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
child.layoutParams.height = dependency.y.toInt()
child.requestLayout()
return true
}
/**
* Based on: https://android.googlesource.com/platform/frameworks/support.git/+/fa0f82f/design/src/android/support/design/widget/BaseTransientBottomBar.java#558
*/
override fun onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) {
if (accessibility.isEnabled) child.layoutParams.height = parent.height else {
val animator = ValueAnimator()
val start = child.height
animator.setIntValues(start, parent.height)
animator.interpolator = AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR
animator.duration = SnackbarConsts.ANIMATION_DURATION
animator.addUpdateListener {
child.layoutParams.height = it.animatedValue as Int
child.requestLayout()
}
animator.start()
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2018 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2018 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.github.shadowsocks.widget
import android.content.Context
import android.os.SystemClock
import android.text.format.Formatter
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.github.shadowsocks.App.Companion.app
import com.github.shadowsocks.MainActivity
import com.github.shadowsocks.R
import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.responseLength
import com.github.shadowsocks.utils.thread
import com.google.android.material.bottomappbar.BottomAppBar
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
class StatsBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.bottomAppBarStyle) :
BottomAppBar(context, attrs, defStyleAttr) {
private var testCount = 0
private lateinit var statusText: TextView
private lateinit var txText: TextView
private lateinit var rxText: TextView
private lateinit var txRateText: TextView
private lateinit var rxRateText: TextView
override fun getBehavior() = object : Behavior() {
val threshold = context.resources.getDimensionPixelSize(R.dimen.stats_bar_scroll_threshold)
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar, target: View,
dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
val dy = dyConsumed + dyUnconsumed
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, if (Math.abs(dy) >= threshold) dy else 0,
dxUnconsumed, 0, type)
}
}
override fun setOnClickListener(l: OnClickListener?) {
statusText = findViewById(R.id.status)
txText = findViewById(R.id.tx)
txRateText = findViewById(R.id.txRate)
rxText = findViewById(R.id.rx)
rxRateText = findViewById(R.id.rxRate)
super.setOnClickListener(l)
}
fun changeState(state: Int) {
statusText.setText(when (state) {
BaseService.CONNECTING -> R.string.connecting
BaseService.CONNECTED -> R.string.vpn_connected
BaseService.STOPPING -> R.string.stopping
else -> R.string.not_connected
})
if (state != BaseService.CONNECTED) {
updateTraffic(0, 0, 0, 0)
testCount += 1 // suppress previous test messages
}
}
fun updateTraffic(txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) {
txText.text = "▲ ${Formatter.formatFileSize(context, txTotal)}"
rxText.text = "▼ ${Formatter.formatFileSize(context, rxTotal)}"
txRateText.text = context.getString(R.string.speed, Formatter.formatFileSize(context, txRate))
rxRateText.text = context.getString(R.string.speed, Formatter.formatFileSize(context, rxRate))
}
/**
* Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
*/
fun testConnection() {
++testCount
statusText.setText(R.string.connection_test_testing)
val id = testCount // it would change by other code
thread("ConnectionTest") {
val url = URL("https", when (app.currentProfile!!.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (BaseService.usingVpnMode) url.openConnection() else
url.openConnection(Proxy(Proxy.Type.SOCKS,
InetSocketAddress("127.0.0.1", DataStore.portProxy))))
as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
val context = context as MainActivity
val (success, result) = try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L)
Pair(true, context.getString(R.string.connection_test_available, elapsed))
else throw IOException(context.getString(R.string.connection_test_error_status_code, code))
} catch (e: IOException) {
Pair(false, context.getString(R.string.connection_test_error, e.message))
} finally {
conn.disconnect()
}
if (testCount == id) app.handler.post {
if (success) statusText.text = result else {
statusText.setText(R.string.connection_test_fail)
context.snackbar(result).show()
}
}
}
}
}
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package com.google.android.material.snackbar
object SnackbarConsts {
const val ANIMATION_DURATION = BaseTransientBottomBar.ANIMATION_DURATION.toLong()
}
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/background_stat"/>
</item>
<item android:drawable="?android:attr/selectableItemBackground"/>
</layer-list>
......@@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingBottom="32dp"
android:paddingBottom="88dp"
android:scrollbars="vertical"
android:clipChildren="false"
android:clipToPadding="false"/>
......
......@@ -13,160 +13,110 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.github.shadowsocks.widget.ShrinkUpwardBehavior">
<FrameLayout
android:id="@+id/fragment_holder"
android:background="?android:attr/windowBackground"
android:layout_above="@+id/stat"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/stat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_stat"
android:elevation="4dp"
android:focusable="true"
android:nextFocusRight="@+id/fab"
android:orientation="vertical"
android:paddingBottom="16dip"
android:paddingEnd="16dip"
android:paddingStart="16dip"
android:paddingTop="16dip"
android:layout_alignParentBottom="true">
android:layout_height="match_parent"/>
<TextView
android:id="@+id/status"
<com.github.shadowsocks.widget.ServiceButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingEnd="72dip"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
tools:ignore="RtlSymmetry"/>
android:elevation="6dp"
app:layout_anchor="@id/stats"
app:backgroundTint="@color/background_service"
app:fabSize="normal"
app:pressedTranslationZ="6dp"
app:srcCompat="@drawable/ic_service_idle"/>
<com.github.shadowsocks.widget.BoundedGridLayout
<com.github.shadowsocks.widget.StatsBar
android:id="@+id/stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:bounded_width="288dp"
app:columnCount="4">
android:layout_gravity="bottom"
app:backgroundTint="@color/background_stat"
app:contentInsetStart="0dp"
app:layout_dodgeInsetEdges="bottom"
app:layout_scrollFlags="enterAlways|scroll"
app:hideOnScroll="true">
<TextView
style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content"
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:ellipsize="marquee"
android:gravity="end"
android:text="@string/sent"
android:textColor="?android:attr/textColorPrimary"
app:layout_column="0"/>
android:background="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="16dip">
<TextView
android:id="@+id/tx"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="fill_horizontal"
android:ellipsize="marquee"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_column="1"
app:layout_columnWeight="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:text="▲"
android:singleLine="true"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_column="2"/>
android:layout_gravity="fill_horizontal"
android:layout_columnWeight="1"/>
<TextView
android:id="@+id/txRate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="fill_horizontal"
android:ellipsize="marquee"
android:singleLine="true"
android:gravity="end"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_column="3"
app:layout_columnWeight="1"/>
<TextView
style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:ellipsize="marquee"
android:gravity="end"
android:text="@string/received"
android:textColor="?android:attr/textColorPrimary"
app:layout_column="0"
app:layout_row="1"/>
android:layout_gravity="fill_horizontal"
android:layout_row="0"
android:layout_column="2"
android:layout_columnWeight="1"/>
<TextView
android:id="@+id/rx"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="fill_horizontal"
android:ellipsize="marquee"
android:singleLine="true"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_column="1"
app:layout_columnWeight="1"
app:layout_row="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:text="▼"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_column="2"
app:layout_row="1"/>
android:layout_gravity="fill_horizontal"
android:layout_column="0"
android:layout_columnWeight="1"
android:layout_row="1"/>
<TextView
android:id="@+id/rxRate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="fill_horizontal"
android:ellipsize="marquee"
android:singleLine="true"
android:gravity="end"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_column="3"
app:layout_columnWeight="1"
app:layout_row="1"/>
</com.github.shadowsocks.widget.BoundedGridLayout>
</LinearLayout>
</RelativeLayout>
android:layout_gravity="fill_horizontal"
android:layout_column="2"
android:layout_columnWeight="1"
android:layout_row="1"/>
<!-- workaround for 0 elevation in disabled state -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginTop="-28dp"
android:elevation="6dp"
app:layout_anchor="@id/stat"
app:layout_anchorGravity="top|end">
<com.github.shadowsocks.widget.ServiceButton
android:id="@+id/fab"
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/background_service"
app:fabSize="normal"
app:pressedTranslationZ="6dp"
app:srcCompat="@drawable/ic_service_idle"/>
</FrameLayout>
android:ellipsize="marquee"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:layout_gravity="fill_horizontal"
android:layout_row="2"
android:layout_column="0"
android:layout_columnSpan="3"
tools:text="@string/connection_test_available"/>
</GridLayout>
</com.github.shadowsocks.widget.StatsBar>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BoundedGridLayout">
<attr name="bounded_width" format="dimension" />
<attr name="bounded_height" format="dimension" />
</declare-styleable>
</resources>
......@@ -3,4 +3,5 @@
<dimen name="qr_code_size">250dp</dimen>
<dimen name="profile_padding">8dp</dimen>
<dimen name="bottom_sheet_padding">8dp</dimen>
<dimen name="stats_bar_scroll_threshold">4dp</dimen>
</resources>
......@@ -126,8 +126,6 @@
<string name="profile_default">Use the current profile</string>
<!-- status -->
<string name="sent">Sent:</string>
<string name="received">Received:</string>
<string name="connecting">Connecting…</string>
<string name="vpn_connected">Connected, tap to check connection</string>
<string name="not_connected">Not connected</string>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment