Browse Source

Refine profile activity

世界 4 years ago
parent
commit
a2e3ed0164

+ 37 - 0
app/build.gradle

@@ -6,6 +6,31 @@ plugins {
 
 }
 
+
+def keystorePwd = null
+def alias = null
+def pwd = null
+
+Properties properties
+def base64 = System.getenv("LOCAL_PROPERTIES")
+if (base64 != null && !base64.isBlank()) {
+    properties = new Properties()
+    properties.load(new ByteArrayInputStream(Base64.decoder.decode(base64)))
+} else if (project.rootProject.file("local.properties").exists()) {
+    properties = new Properties()
+    properties.load(project.rootProject.file("local.properties").newDataInputStream())
+}
+
+if (properties != null) {
+    keystorePwd = properties.getProperty("KEYSTORE_PASS")
+    alias = properties.getProperty("ALIAS_NAME")
+    pwd = properties.getProperty("ALIAS_PASS")
+}
+
+keystorePwd = keystorePwd ?: System.getenv("KEYSTORE_PASS")
+alias = alias ?: System.getenv("ALIAS_NAME")
+pwd = pwd ?: System.getenv("ALIAS_PASS")
+
 android {
     compileSdkVersion 30
     buildToolsVersion "30.0.3"
@@ -36,12 +61,24 @@ android {
 
     externalNativeBuild.ndkBuild.path "src/main/jni/Android.mk"
 
+    signingConfigs {
+        release {
+            storeFile project.file("release.keystore")
+            storePassword keystorePwd
+            keyAlias alias
+            keyPassword pwd
+        }
+    }
 
     buildTypes {
+        debug {
+            signingConfig keystorePwd == null ? signingConfigs.debug : signingConfigs.release
+        }
         release {
             minifyEnabled true
             shrinkResources true
             proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+            signingConfig keystorePwd == null ? signingConfigs.debug : signingConfigs.release
         }
     }
 

BIN
app/release.keystore


+ 1 - 0
app/src/main/java/io/nekohasekai/sagernet/Constants.kt

@@ -11,6 +11,7 @@ object Key {
     const val MODE_PROXY = 1
     const val MODE_TRANS = 2
 
+    const val PROFILE_DIRTY = "profileDirty"
     const val PROFILE_ID = "profileId"
     const val PROFILE_NAME = "profileName"
     const val PROFILE_GROUP = "profileGroup"

+ 0 - 1
app/src/main/java/io/nekohasekai/sagernet/bg/ProxyInstance.kt

@@ -31,7 +31,6 @@ class ProxyInstance(val profile: ProxyEntity) {
 
     fun start() {
         v2rayPoint.runLoop(DataStore.preferIpv6)
-        println("Satrted")
     }
 
     fun stop() {

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

@@ -38,6 +38,8 @@ object DataStore {
     var proxyApps by configurationStore.int("proxyApps")
     var individual by configurationStore.string("individual")
 
+    // cache
+    var dirty by profileCacheStore.boolean(Key.PROFILE_DIRTY)
     var editingId by profileCacheStore.long(Key.PROFILE_ID)
     var editingGroup by profileCacheStore.long(Key.PROFILE_GROUP)
     var profileName by profileCacheStore.string(Key.PROFILE_NAME)

+ 8 - 0
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt

@@ -44,6 +44,14 @@ class ProxyEntity(
         }
     }
 
+    fun putBean(bean: AbstractBean) {
+        when (bean) {
+            is SOCKSBean -> socksBean = bean
+            is VMessBean -> vmessBean = bean
+            else -> error("Undefined type $type")
+        }
+    }
+
     fun requireVMess() = requireBean() as VMessBean
     fun requireSOCKS() = requireBean() as SOCKSBean
 

+ 4 - 4
app/src/main/java/io/nekohasekai/sagernet/ktx/Asyncs.kt

@@ -2,11 +2,11 @@ package io.nekohasekai.sagernet.ktx
 
 import kotlinx.coroutines.*
 
-fun runOnIoDispatcher(block: suspend CoroutineScope.() -> Unit) =
-    GlobalScope.launch(Dispatchers.IO, block = block)
+fun runOnDefaultDispatcher(block: suspend CoroutineScope.() -> Unit) =
+    GlobalScope.launch(Dispatchers.Default, block = block)
 
-suspend fun onIoDispatcher(block: suspend CoroutineScope.() -> Unit) =
-    withContext(Dispatchers.IO, block = block)
+suspend fun onDefaultDispatcher(block: suspend CoroutineScope.() -> Unit) =
+    withContext(Dispatchers.Default, block = block)
 
 fun runOnMainDispatcher(block: suspend CoroutineScope.() -> Unit) =
     GlobalScope.launch(Dispatchers.Main, block = block)

+ 8 - 3
app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt

@@ -1,10 +1,15 @@
 package io.nekohasekai.sagernet.ui
 
 import android.os.Bundle
-import android.view.LayoutInflater
 import android.view.View
-import android.view.ViewGroup
+import io.nekohasekai.sagernet.R
 
-class AboutFragment : ToolbarFragment() {
+class AboutFragment : ToolbarFragment(R.layout.fragment_about) {
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        toolbar.setTitle(R.string.menu_about)
+    }
 
 }

+ 2 - 2
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

@@ -11,7 +11,7 @@ import com.google.android.material.tabs.TabLayout
 import com.google.android.material.tabs.TabLayoutMediator
 import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.ktx.dp2px
-import io.nekohasekai.sagernet.ktx.runOnIoDispatcher
+import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
 import io.nekohasekai.sagernet.ui.configuration.GroupPagerAdapter
 
 class ConfigurationFragment : ToolbarFragment(R.layout.group_list_main),
@@ -42,7 +42,7 @@ class ConfigurationFragment : ToolbarFragment(R.layout.group_list_main),
             }
         }.attach()
 
-        runOnIoDispatcher {
+        runOnDefaultDispatcher {
             adapter.reloadList {
                 tabLayout.isGone = it
                 toolbar.elevation = if (it) 0F else dp2px(4).toFloat()

+ 2 - 2
app/src/main/java/io/nekohasekai/sagernet/ui/configuration/ConfigurationAdapter.kt

@@ -25,11 +25,11 @@ class ConfigurationAdapter(private val groupIdToQuery: Long) :
     var configurationList: List<ProxyEntity> = listOf()
 
     fun reloadList() {
-        runOnIoDispatcher {
+        runOnDefaultDispatcher {
             configurationList = SagerDatabase.proxyDao.getByGroup(groupIdToQuery)
             if (configurationList.isEmpty() &&
                 (SagerDatabase.groupDao.getById(groupIdToQuery)
-                    ?: return@runOnIoDispatcher).isDefault
+                    ?: return@runOnDefaultDispatcher).isDefault
             ) {
                 SagerDatabase.proxyDao.addProxy(ProxyEntity(
                     groupId = groupIdToQuery,

+ 4 - 4
app/src/main/java/io/nekohasekai/sagernet/ui/configuration/ConfigurationHolder.kt

@@ -33,8 +33,8 @@ class ConfigurationHolder(val view: View) : RecyclerView.ViewHolder(view), Event
 
     override fun onEvent(eventId: Int, vararg args: Any) {
         if (args[0] != profileId) return
-        runOnIoDispatcher {
-            val profile = SagerDatabase.proxyDao.getById(profileId) ?: return@runOnIoDispatcher
+        runOnDefaultDispatcher {
+            val profile = SagerDatabase.proxyDao.getById(profileId) ?: return@runOnDefaultDispatcher
             onMainDispatcher {
                 bind(profile)
             }
@@ -45,7 +45,7 @@ class ConfigurationHolder(val view: View) : RecyclerView.ViewHolder(view), Event
         profileId = proxyEntity.id
 
         view.setOnClickListener {
-            runOnIoDispatcher {
+            runOnDefaultDispatcher {
                 if (DataStore.selectedProxy != proxyEntity.id) {
                     DataStore.selectedProxy = proxyEntity.id
                     onMainDispatcher {
@@ -82,7 +82,7 @@ class ConfigurationHolder(val view: View) : RecyclerView.ViewHolder(view), Event
             })
         }
 
-        runOnIoDispatcher {
+        runOnDefaultDispatcher {
             if (DataStore.selectedProxy == proxyEntity.id) {
                 onMainDispatcher {
                     selectedView.visibility = View.VISIBLE

+ 102 - 14
app/src/main/java/io/nekohasekai/sagernet/ui/settings/ProfileSettingsActivity.kt

@@ -5,24 +5,32 @@ import android.os.Bundle
 import android.os.Parcelable
 import android.view.Menu
 import android.view.MenuItem
+import android.view.View
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.Fragment
+import androidx.core.view.ViewCompat
 import androidx.preference.EditTextPreference
 import androidx.preference.Preference
+import androidx.preference.PreferenceDataStore
 import androidx.preference.PreferenceFragmentCompat
+import io.nekohasekai.sagernet.Key
 import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.database.ProxyEntity
 import io.nekohasekai.sagernet.database.SagerDatabase
+import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
 import io.nekohasekai.sagernet.fmt.AbstractBean
 import io.nekohasekai.sagernet.ktx.EVENT_UPDATE_GROUP
+import io.nekohasekai.sagernet.ktx.EVENT_UPDATE_PROFILE
 import io.nekohasekai.sagernet.ktx.Empty
 import io.nekohasekai.sagernet.ktx.postNotification
 import io.nekohasekai.sagernet.utils.AlertDialogFragment
+import io.nekohasekai.sagernet.widget.ListListener
 import kotlinx.parcelize.Parcelize
 
 @Suppress("UNCHECKED_CAST")
-abstract class ProfileSettingsActivity<T : AbstractBean> : AppCompatActivity() {
+abstract class ProfileSettingsActivity<T : AbstractBean> : AppCompatActivity(),
+    OnPreferenceDataStoreChangeListener {
 
     class UnsavedChangesDialogFragment : AlertDialogFragment<Empty, Empty>() {
         override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
@@ -53,25 +61,31 @@ abstract class ProfileSettingsActivity<T : AbstractBean> : AppCompatActivity() {
         const val EXTRA_GROUP_ID = "group"
     }
 
-    abstract fun createFragment(): Fragment
-    abstract fun init(bean: T?)
+    abstract val type: String
+    abstract fun createEntity(): T
+    abstract fun init()
+    abstract fun init(bean: T)
+    abstract fun T.serialize()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_activity)
         setSupportActionBar(findViewById(R.id.toolbar))
         supportActionBar?.apply {
+            setTitle(R.string.profile_config)
             setDisplayHomeAsUpEnabled(true)
             setHomeAsUpIndicator(R.drawable.ic_navigation_close)
         }
+        DataStore.profileCacheStore.registerChangeListener(this)
 
         if (savedInstanceState == null) {
             val editingId = intent.getLongExtra(EXTRA_PROFILE_ID, 0L)
+            DataStore.dirty = false
             DataStore.editingId = editingId
             if (editingId == 0L) {
                 val editingGroup = intent.getLongExtra(EXTRA_GROUP_ID, 0L)
                 DataStore.editingGroup = editingGroup
-                init(null)
+                init()
             } else {
                 val proxyEntity = SagerDatabase.proxyDao.getById(editingId)
                 if (proxyEntity == null) {
@@ -82,13 +96,37 @@ abstract class ProfileSettingsActivity<T : AbstractBean> : AppCompatActivity() {
                 init(proxyEntity.requireBean() as T)
             }
             supportFragmentManager.beginTransaction()
-                .replace(R.id.settings, createFragment())
+                .replace(R.id.settings,
+                    MyPreferenceFragmentCompat().apply { activity = this@ProfileSettingsActivity })
                 .commit()
         }
 
     }
 
-    private val child by lazy { supportFragmentManager.findFragmentById(R.id.settings) as MyPreferenceFragmentCompat<T, ProfileSettingsActivity<T>> }
+    fun saveAndExit() {
+
+        val editingId = DataStore.editingId
+        if (editingId == 0L) {
+            val editingGroup = DataStore.editingGroup
+            SagerDatabase.proxyDao.addProxy(ProxyEntity(
+                groupId = editingGroup,
+                type = type
+            ).apply { putBean(createEntity().apply { serialize() }) })
+            postNotification(EVENT_UPDATE_GROUP, editingGroup)
+        } else {
+            val entity = SagerDatabase.proxyDao.getById(DataStore.editingId)
+            if (entity == null) {
+                finish()
+                return
+            }
+            SagerDatabase.proxyDao.updateProxy(entity.apply { (requireBean() as T).serialize() })
+            postNotification(EVENT_UPDATE_PROFILE, editingId)
+        }
+        finish()
+
+    }
+
+    private val child by lazy { supportFragmentManager.findFragmentById(R.id.settings) as MyPreferenceFragmentCompat }
 
     override fun onCreateOptionsMenu(menu: Menu?): Boolean {
         menuInflater.inflate(R.menu.profile_config_menu, menu)
@@ -98,7 +136,7 @@ abstract class ProfileSettingsActivity<T : AbstractBean> : AppCompatActivity() {
     override fun onOptionsItemSelected(item: MenuItem) = child.onOptionsItemSelected(item)
 
     override fun onBackPressed() {
-        if (child.unsaved) UnsavedChangesDialogFragment().show(child, REQUEST_UNSAVED_CHANGES)
+        if (DataStore.dirty) UnsavedChangesDialogFragment().show(child, REQUEST_UNSAVED_CHANGES)
         else super.onBackPressed()
     }
 
@@ -107,15 +145,65 @@ abstract class ProfileSettingsActivity<T : AbstractBean> : AppCompatActivity() {
         return true
     }
 
-    abstract class MyPreferenceFragmentCompat<T : AbstractBean, P : ProfileSettingsActivity<T>> :
-        PreferenceFragmentCompat() {
-        val activity get() = requireActivity() as P
-        var unsaved = false
+    override fun onDestroy() {
+        DataStore.profileCacheStore.unregisterChangeListener(this)
+        super.onDestroy()
+    }
+
+    override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
+        if (key != Key.PROFILE_DIRTY) {
+            DataStore.dirty = true
+        }
+    }
+
+    abstract fun PreferenceFragmentCompat.createPreferences(
+        savedInstanceState: Bundle?,
+        rootKey: String?,
+    )
+
+    class MyPreferenceFragmentCompat : PreferenceFragmentCompat() {
+
+        lateinit var activity: ProfileSettingsActivity<*>
+
+        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+            preferenceManager.preferenceDataStore = DataStore.profileCacheStore
+            activity.apply {
+                createPreferences(savedInstanceState, rootKey)
+            }
+        }
+
+        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+            super.onViewCreated(view, savedInstanceState)
+
+            ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)
+        }
+
+        override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+            R.id.action_delete -> {
+                DeleteConfirmationDialogFragment().withArg(ProfileIdArg(DataStore.editingId,
+                    DataStore.editingGroup))
+                    .show(this)
+                true
+            }
+            R.id.action_apply -> {
+                activity.saveAndExit()
+                true
+            }
+            else -> false
+        }
+
     }
 
     object PasswordSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
-        override fun provideSummary(preference: EditTextPreference?) =
-            "\u2022".repeat(preference?.text?.length ?: 0)
+
+        override fun provideSummary(preference: EditTextPreference): CharSequence {
+            return if (preference.text.isNullOrBlank()) {
+                preference.context.getString(androidx.preference.R.string.not_set)
+            } else {
+                "\u2022".repeat(preference.text.length)
+            }
+
+        }
     }
 
 }

+ 37 - 79
app/src/main/java/io/nekohasekai/sagernet/ui/settings/SocksSettingsActivity.kt

@@ -5,6 +5,7 @@ import android.view.MenuItem
 import android.view.View
 import androidx.core.view.ViewCompat
 import androidx.preference.EditTextPreference
+import androidx.preference.PreferenceFragmentCompat
 import cn.hutool.core.util.NumberUtil
 import io.nekohasekai.sagernet.Key
 import io.nekohasekai.sagernet.R
@@ -20,93 +21,50 @@ import io.nekohasekai.sagernet.widget.ListListener
 
 class SocksSettingsActivity : ProfileSettingsActivity<SOCKSBean>() {
 
-    override fun createFragment() = SocksPreferenceFragment()
+    override val type = "socks"
+    override fun createEntity() = SOCKSBean()
 
-    override fun init(bean: SOCKSBean?) {
-        DataStore.profileName = bean?.name ?: ""
-        DataStore.serverAddress = bean?.serverAddress ?: ""
-        DataStore.serverPort = (bean?.serverPort ?: 1080).toString()
-        DataStore.serverUsername = bean?.username ?: ""
-        DataStore.serverPassword = bean?.password ?: ""
-        DataStore.serverUdp = bean?.udp ?: false
+    override fun init() {
+        DataStore.profileName = ""
+        DataStore.serverAddress = ""
+        DataStore.serverPort = "1080"
+        DataStore.serverUsername = ""
+        DataStore.serverPassword = ""
+        DataStore.serverUdp = false
     }
 
-    fun saveAndExit() {
-        val editingId = DataStore.editingId
-        if (editingId == 0L) {
-            val editingGroup = DataStore.editingGroup
-            // create new entity
-            SagerDatabase.proxyDao.addProxy(ProxyEntity(
-                groupId = editingGroup,
-                type = "socks",
-                socksBean = SOCKSBean().apply {
-                    name = DataStore.profileName
-                    serverAddress = DataStore.serverAddress
-                    serverPort =
-                        DataStore.serverPort
-                            .takeIf { !it.isNullOrBlank() && NumberUtil.isInteger(it) }?.toInt()
-                            ?: 1080
-                    username = DataStore.serverUsername
-                    password = DataStore.serverPassword
-                    udp = DataStore.serverUdp
-                }
-            ))
-            postNotification(EVENT_UPDATE_GROUP, editingGroup)
-        } else {
-            val entity = SagerDatabase.proxyDao.getById(DataStore.editingId)
-            if (entity == null) {
-                finish()
-                return
-            }
-            SagerDatabase.proxyDao.updateProxy(entity.apply {
-                requireSOCKS().apply {
-                    name = DataStore.profileName
-                    serverAddress = DataStore.serverAddress
-                    serverPort = DataStore.serverPort
-                        .takeIf { !it.isNullOrBlank() && NumberUtil.isInteger(it) }?.toInt()
-                        ?: 1080
-                    username = DataStore.serverUsername
-                    password = DataStore.serverPassword
-                    udp = DataStore.serverUdp
-                }
-            })
-            postNotification(EVENT_UPDATE_PROFILE, editingId)
-        }
-        finish()
+    override fun init(bean: SOCKSBean) {
+        DataStore.profileName = bean.name
+        DataStore.serverAddress = bean.serverAddress
+        DataStore.serverPort = "${bean.serverPort}"
+        DataStore.serverUsername = bean.username
+        DataStore.serverPassword = bean.password
+        DataStore.serverUdp = bean.udp
     }
 
-    class SocksPreferenceFragment : MyPreferenceFragmentCompat<SOCKSBean, SocksSettingsActivity>() {
-
-        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-            preferenceManager.preferenceDataStore = DataStore.profileCacheStore
-            addPreferencesFromResource(R.xml.socks_preferences)
-            findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {
-                setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
-            }
-            findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {
-                summaryProvider = PasswordSummaryProvider
-            }
-        }
+    override fun SOCKSBean.serialize() {
+        name = DataStore.profileName
+        serverAddress = DataStore.serverAddress
+        serverPort =
+            DataStore.serverPort
+                .takeIf { !it.isNullOrBlank() && NumberUtil.isInteger(it) }?.toInt()
+                ?: 1080
+        username = DataStore.serverUsername
+        password = DataStore.serverPassword
+        udp = DataStore.serverUdp
+    }
 
-        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-            super.onViewCreated(view, savedInstanceState)
-            ViewCompat.setOnApplyWindowInsetsListener(listView, ListListener)
+    override fun PreferenceFragmentCompat.createPreferences(
+        savedInstanceState: Bundle?,
+        rootKey: String?,
+    ) {
+        addPreferencesFromResource(R.xml.socks_preferences)
+        findPreference<EditTextPreference>(Key.SERVER_PORT)!!.apply {
+            setOnBindEditTextListener(EditTextPreferenceModifiers.Port)
         }
-
-        override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
-            R.id.action_delete -> {
-                DeleteConfirmationDialogFragment().withArg(ProfileIdArg(DataStore.editingId,
-                    DataStore.editingGroup))
-                    .show(this)
-                true
-            }
-            R.id.action_apply -> {
-                activity.saveAndExit()
-                true
-            }
-            else -> false
+        findPreference<EditTextPreference>(Key.SERVER_PASSWORD)!!.apply {
+            summaryProvider = PasswordSummaryProvider
         }
-
     }
 
 }

+ 8 - 2
app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt

@@ -34,6 +34,7 @@ import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.bg.BaseService
 import io.nekohasekai.sagernet.ui.MainActivity
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
 class StatsBar @JvmOverloads constructor(
@@ -81,17 +82,22 @@ class StatsBar @JvmOverloads constructor(
     fun changeState(state: BaseService.State) {
         val activity = context as MainActivity
         fun postWhenStarted(what: () -> Unit) = activity.lifecycleScope.launch(Dispatchers.Main) {
+            delay(100L)
             activity.whenStarted { what() }
         }
         if ((state == BaseService.State.Connected).also { hideOnScroll = it }) {
-            postWhenStarted { performShow() }
+            postWhenStarted {
+                performShow()
+            }
             /*  tester.status.observe(activity) {
                   it.retrieve(this::setStatus) { msg ->
                       activity.snackbar(msg).show()
                   }
               }*/
         } else {
-            postWhenStarted { performHide() }
+            postWhenStarted {
+                performHide()
+            }
             updateTraffic(0, 0, 0, 0)
             /* tester.status.removeObservers(activity)
              if (state != BaseService.State.Idle) tester.invalidate()*/

+ 15 - 12
app/src/main/res/layout/fragment_about.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
@@ -9,33 +10,35 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content" />
 
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:padding="16dp">
-
         <com.google.android.material.card.MaterialCardView
             android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            app:cardElevation="4dp"
+            app:cardCornerRadius="1dp">
 
             <LinearLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical"
-                android:padding="8dp">
+                android:padding="16dp">
 
                 <TextView
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:text="@string/app_name"
-                    android:textAppearance="?attr/textAppearanceSubtitle1"
-                    />
+                    android:textAppearance="?textAppearanceBody2"
+                    android:textColor="@color/black" />
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="8dp"
+                    android:autoLink="web"
+                    android:text="@string/about_text"/>
 
             </LinearLayout>
 
         </com.google.android.material.card.MaterialCardView>
 
-    </LinearLayout>
-
 </LinearLayout>

+ 1 - 1
app/src/main/res/layout/layout_profile.xml

@@ -6,7 +6,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_margin="8dp"
-    android:elevation="4dp">
+    app:cardElevation="4dp">
 
     <LinearLayout
         android:id="@+id/content_lin"

+ 8 - 0
app/src/main/res/values/strings.xml

@@ -196,4 +196,12 @@
     <string name="no">No</string>
     <string name="apply">Apply</string>
 
+    <string name="about_text">The universal proxy toolchain for Android, written in Kotlin.
+
+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/.</string>
+
 </resources>