Redesign UI and introduce dark theme (#519)

Caveats:

- The openpgp preference had to be removed because the open-intents developers are too lazy to update their libraries. Over the coming weeks I will be reimplementing a local solution for this instead.

- The autofill dialog is broken but I since it is being worked on in #410 already I'm not going to bother fixing it.
This commit is contained in:
Harsh Shandilya 2019-10-01 22:14:28 +05:30 committed by GitHub
parent c9dc4034f1
commit 073346c157
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 2474 additions and 2433 deletions

3
.gitignore vendored
View file

@ -34,4 +34,7 @@ project.properties
.idea
*.iml
# Visual Studio Code
.vscode/
captures/

View file

@ -1,4 +1,4 @@
import org.gradle.api.JavaVersion.*
import org.gradle.api.JavaVersion.VERSION_1_8
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

View file

@ -5,13 +5,17 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import com.zeapo.pwdstore.crypto.PgpActivity
import kotlinx.android.synthetic.main.decrypt_layout.*
import kotlinx.android.synthetic.main.decrypt_layout.crypto_extra_show
import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_category_decrypt
import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_file
import kotlinx.android.synthetic.main.decrypt_layout.crypto_password_show
import kotlinx.android.synthetic.main.decrypt_layout.crypto_username_show
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertEquals

View file

@ -4,7 +4,9 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText

View file

@ -3,24 +3,23 @@
android:height="108dp"
android:viewportWidth="110.34687"
android:viewportHeight="110.34687">
<group
android:translateX="24.828047"
android:translateY="24.828047">
<path
android:fillColor="#00000000"
android:pathData="m18.8,30.2129v-11.546c0,-6.4144 5.1315,-11.546 11.546,-11.546 6.4144,0 11.546,5.1315 11.546,11.546v11.546"
android:strokeWidth="5.349"
android:strokeColor="#013e5b" />
<path
android:fillColor="#c74c00"
android:pathData="M15.4099,21.8429L45.2811,21.8429A2.2639,2.2639 0,0 1,47.545 24.1068L47.545,53.977A2.2639,2.2639 0,0 1,45.2811 56.2409L15.4099,56.2409A2.2639,2.2639 0,0 1,13.146 53.977L13.146,24.1068A2.2639,2.2639 0,0 1,15.4099 21.8429z" />
<path
android:fillColor="#fff"
android:pathData="m44.8267,37.6961 l-13.1408,-13.1393c-0.7569,-0.7566 -1.9838,-0.7566 -2.7408,0l-13.08,13.0785c-0.7567,0.7573 -0.7567,1.9846 0,2.7419l13.1415,13.14c0.7572,0.7567 1.9842,0.7567 2.7414,0l13.0778,-13.0785c0.7572,-0.7572 0.7572,-1.9849 0,-2.7421"
android:strokeWidth=".35344" />
<path
android:fillColor="#f47a68"
android:pathData="m30.3156,23.9881c-0.496,0 -0.992,0.1893 -1.3705,0.5676l-2.7282,2.7288 3.4612,3.4606c0.8044,-0.2715 1.727,-0.0892 2.368,0.5517 0.6237,0.624 0.8361,1.5493 0.5471,2.3828l3.3357,3.3357c0.8076,-0.2777 1.738,-0.098 2.3828,0.5476 0.9008,0.9005 0.9008,2.361 0,3.2615 -1.7823,1.7848 -4.7253,-0.1767 -3.7641,-2.5087l-3.1111,-3.1106c-2.2315,0.5285 -3.8934,-1.2655 -3.149,-3.1674l-0.6863,-0.6863l0,15.9165l5.4913,0l0,-5.8608c-0.0315,-0.7566 1.1201,-0.7566 1.0886,0l0,6.4043c0.0005,0.3013 -0.2438,0.5457 -0.545,0.5455l-6.5804,0c-0.3015,0.0005 -0.546,-0.2441 -0.5456,-0.5455l0,-17.4333c-0.0005,-0.0363 0.0029,-0.0728 0.0097,-0.1085l-1.6444,-1.6444 -9.0106,9.0084c-0.7567,0.7573 -0.7567,1.9848 0,2.7421l13.1415,13.14c0.7572,0.7567 1.9844,0.7567 2.7416,0l13.0778,-13.0785c0.7572,-0.7572 0.7572,-1.9849 0,-2.7421l-13.14,-13.1393c-0.3785,-0.3783 -0.8746,-0.5676 -1.3705,-0.5676zM29.9512,39.1825c0.1001,0 0.1808,0.0381 0.2426,0.1146 0.0648,0.0736 0.1326,0.1975 0.2032,0.371 0.0705,0.1706 0.1089,0.2615 0.1146,0.2733 0.0059,-0.0119 0.0424,-0.1026 0.11,-0.2733 0.0707,-0.1705 0.1401,-0.2946 0.2078,-0.371 0.0677,-0.0765 0.1513,-0.1146 0.2513,-0.1146 0.1412,0 0.2646,0.047 0.3705,0.1412 0.1059,0.0941 0.1592,0.2103 0.1592,0.3485 0,0.0676 -0.0179,0.1368 -0.0532,0.2073 -0.0323,0.0707 -0.0777,0.1444 -0.1366,0.2211 -0.056,0.0734 -0.1164,0.1571 -0.1812,0.2513 0.0648,-0.0089 0.1589,-0.0251 0.2825,-0.0486 0.1265,-0.0236 0.2268,-0.0354 0.3004,-0.0354 0.0969,0 0.1762,0.0224 0.238,0.0666 0.0648,0.0442 0.1118,0.1042 0.1413,0.1807 0.0294,0.0734 0.044,0.1544 0.044,0.2426 0,0.1384 -0.0337,0.2544 -0.1013,0.3485 -0.0677,0.0912 -0.1749,0.1372 -0.3219,0.1372 -0.0472,0 -0.14,-0.0119 -0.2784,-0.0354 -0.1382,-0.0236 -0.2397,-0.0386 -0.3045,-0.0446 0.1531,0.2177 0.2534,0.3652 0.3004,0.4417 0.047,0.0736 0.0706,0.1544 0.0706,0.2426 0,0.1412 -0.0533,0.2556 -0.1592,0.3439 -0.1028,0.0853 -0.2264,0.1279 -0.3705,0.1279 -0.1028,0 -0.1882,-0.0366 -0.2559,-0.11 -0.0646,-0.0767 -0.1309,-0.1955 -0.1986,-0.3572 -0.0646,-0.1647 -0.1028,-0.259 -0.1146,-0.2825 -0.0117,0.0235 -0.0516,0.1178 -0.1192,0.2825 -0.0648,0.1646 -0.1281,0.2836 -0.1899,0.3572 -0.0617,0.0735 -0.1454,0.11 -0.2513,0.11 -0.15,0 -0.2779,-0.0427 -0.3838,-0.1279 -0.1059,-0.0883 -0.1587,-0.2027 -0.1587,-0.3439 0,-0.0618 0.0156,-0.1263 0.0481,-0.194 0.0323,-0.0707 0.0665,-0.1311 0.1018,-0.1812 0.0382,-0.0499 0.1118,-0.1532 0.2206,-0.3091 -0.0736,0.0059 -0.1751,0.021 -0.3045,0.0446 -0.1294,0.0236 -0.2176,0.0354 -0.2646,0.0354 -0.1472,0 -0.2577,-0.046 -0.3311,-0.1372 -0.0707,-0.0941 -0.1059,-0.2101 -0.1059,-0.3485 0,-0.1411 0.0352,-0.2573 0.1059,-0.3485 0.0734,-0.0942 0.184,-0.1412 0.3311,-0.1412 0.0736,0 0.1628,0.0107 0.2687,0.0312 0.1059,0.0206 0.2062,0.038 0.3004,0.0527 -0.0824,-0.1177 -0.1648,-0.2366 -0.2472,-0.3572 -0.0824,-0.1206 -0.1233,-0.2282 -0.1233,-0.3224 0,-0.1382 0.0543,-0.2544 0.1633,-0.3485 0.1088,-0.0942 0.2351,-0.1412 0.3792,-0.1412zM29.9512,43.2235c0.1001,0 0.1808,0.0383 0.2426,0.1146 0.0648,0.0735 0.1326,0.197 0.2032,0.3705 0.0705,0.1707 0.1089,0.262 0.1146,0.2738 0.0059,-0.0119 0.0424,-0.1031 0.11,-0.2738 0.0707,-0.1705 0.1401,-0.2941 0.2078,-0.3705 0.0677,-0.0765 0.1513,-0.1146 0.2513,-0.1146 0.1412,0 0.2646,0.047 0.3705,0.1412 0.1059,0.0941 0.1592,0.2102 0.1592,0.3485 0,0.0675 -0.0179,0.1366 -0.0532,0.2073 -0.0323,0.0705 -0.0777,0.1441 -0.1366,0.2206 -0.056,0.0735 -0.1164,0.1576 -0.1812,0.2518 0.0648,-0.0089 0.1589,-0.0251 0.2825,-0.0486 0.1265,-0.0236 0.2268,-0.0353 0.3004,-0.0353 0.0969,0 0.1762,0.0219 0.238,0.0661 0.0648,0.0442 0.1118,0.1047 0.1413,0.1812 0.0294,0.0736 0.044,0.1542 0.044,0.2426 0,0.1382 -0.0337,0.2544 -0.1013,0.3485 -0.0677,0.0912 -0.1749,0.1366 -0.3219,0.1366 -0.0472,0 -0.14,-0.0119 -0.2784,-0.0354 -0.1382,-0.0236 -0.2397,-0.038 -0.3045,-0.044 0.1531,0.2177 0.2534,0.3647 0.3004,0.4411 0.047,0.0736 0.0706,0.1549 0.0706,0.2431 0,0.1411 -0.0533,0.2558 -0.1592,0.3439 -0.1028,0.0854 -0.2264,0.1279 -0.3705,0.1279 -0.1028,0 -0.1882,-0.0371 -0.2559,-0.1105 -0.0646,-0.0765 -0.1309,-0.1955 -0.1986,-0.3572 -0.0646,-0.1647 -0.1028,-0.259 -0.1146,-0.2825 -0.0117,0.0236 -0.0516,0.1178 -0.1192,0.2825 -0.0648,0.1648 -0.1281,0.2836 -0.1899,0.3572 -0.0617,0.0735 -0.1454,0.1105 -0.2513,0.1105 -0.15,0 -0.2779,-0.0425 -0.3838,-0.1279 -0.1059,-0.0881 -0.1587,-0.2028 -0.1587,-0.3439 0,-0.0617 0.0156,-0.1268 0.0481,-0.1945 0.0323,-0.0705 0.0665,-0.1311 0.1018,-0.1812 0.0382,-0.0499 0.1118,-0.1527 0.2206,-0.3086 -0.0736,0.0059 -0.1751,0.0205 -0.3045,0.044 -0.1294,0.0236 -0.2176,0.0354 -0.2646,0.0354 -0.1472,0 -0.2577,-0.0454 -0.3311,-0.1366 -0.0707,-0.0941 -0.1059,-0.2103 -0.1059,-0.3485 0,-0.1412 0.0352,-0.2573 0.1059,-0.3485 0.0734,-0.0942 0.184,-0.1412 0.3311,-0.1412 0.0736,0 0.1628,0.0101 0.2687,0.0307 0.1059,0.0204 0.2062,0.0384 0.3004,0.0532 -0.0824,-0.1177 -0.1648,-0.2371 -0.2472,-0.3577 -0.0824,-0.1206 -0.1233,-0.2278 -0.1233,-0.3219 0,-0.1384 0.0543,-0.2544 0.1633,-0.3485 0.1088,-0.0942 0.2351,-0.1412 0.3792,-0.1412z"
android:strokeWidth="1.3358" />
<group android:translateX="24.828047"
android:translateY="24.828047">
<path
android:pathData="m18.8,30.2129v-11.546c0,-6.4144 5.1315,-11.546 11.546,-11.546 6.4144,0 11.546,5.1315 11.546,11.546v11.546"
android:strokeWidth="5.349"
android:fillColor="#00000000"
android:strokeColor="#013e5b"/>
<path
android:pathData="M15.4099,21.8429L45.2811,21.8429A2.2639,2.2639 0,0 1,47.545 24.1068L47.545,53.977A2.2639,2.2639 0,0 1,45.2811 56.2409L15.4099,56.2409A2.2639,2.2639 0,0 1,13.146 53.977L13.146,24.1068A2.2639,2.2639 0,0 1,15.4099 21.8429z"
android:fillColor="#c74c00"/>
<path
android:pathData="m44.8267,37.6961 l-13.1408,-13.1393c-0.7569,-0.7566 -1.9838,-0.7566 -2.7408,0l-13.08,13.0785c-0.7567,0.7573 -0.7567,1.9846 0,2.7419l13.1415,13.14c0.7572,0.7567 1.9842,0.7567 2.7414,0l13.0778,-13.0785c0.7572,-0.7572 0.7572,-1.9849 0,-2.7421"
android:strokeWidth=".35344"
android:fillColor="#fff"/>
<path
android:pathData="m30.3156,23.9881c-0.496,0 -0.992,0.1893 -1.3705,0.5676l-2.7282,2.7288 3.4612,3.4606c0.8044,-0.2715 1.727,-0.0892 2.368,0.5517 0.6237,0.624 0.8361,1.5493 0.5471,2.3828l3.3357,3.3357c0.8076,-0.2777 1.738,-0.098 2.3828,0.5476 0.9008,0.9005 0.9008,2.361 0,3.2615 -1.7823,1.7848 -4.7253,-0.1767 -3.7641,-2.5087l-3.1111,-3.1106c-2.2315,0.5285 -3.8934,-1.2655 -3.149,-3.1674l-0.6863,-0.6863l0,15.9165l5.4913,0l0,-5.8608c-0.0315,-0.7566 1.1201,-0.7566 1.0886,0l0,6.4043c0.0005,0.3013 -0.2438,0.5457 -0.545,0.5455l-6.5804,0c-0.3015,0.0005 -0.546,-0.2441 -0.5456,-0.5455l0,-17.4333c-0.0005,-0.0363 0.0029,-0.0728 0.0097,-0.1085l-1.6444,-1.6444 -9.0106,9.0084c-0.7567,0.7573 -0.7567,1.9848 0,2.7421l13.1415,13.14c0.7572,0.7567 1.9844,0.7567 2.7416,0l13.0778,-13.0785c0.7572,-0.7572 0.7572,-1.9849 0,-2.7421l-13.14,-13.1393c-0.3785,-0.3783 -0.8746,-0.5676 -1.3705,-0.5676zM29.9512,39.1825c0.1001,0 0.1808,0.0381 0.2426,0.1146 0.0648,0.0736 0.1326,0.1975 0.2032,0.371 0.0705,0.1706 0.1089,0.2615 0.1146,0.2733 0.0059,-0.0119 0.0424,-0.1026 0.11,-0.2733 0.0707,-0.1705 0.1401,-0.2946 0.2078,-0.371 0.0677,-0.0765 0.1513,-0.1146 0.2513,-0.1146 0.1412,0 0.2646,0.047 0.3705,0.1412 0.1059,0.0941 0.1592,0.2103 0.1592,0.3485 0,0.0676 -0.0179,0.1368 -0.0532,0.2073 -0.0323,0.0707 -0.0777,0.1444 -0.1366,0.2211 -0.056,0.0734 -0.1164,0.1571 -0.1812,0.2513 0.0648,-0.0089 0.1589,-0.0251 0.2825,-0.0486 0.1265,-0.0236 0.2268,-0.0354 0.3004,-0.0354 0.0969,0 0.1762,0.0224 0.238,0.0666 0.0648,0.0442 0.1118,0.1042 0.1413,0.1807 0.0294,0.0734 0.044,0.1544 0.044,0.2426 0,0.1384 -0.0337,0.2544 -0.1013,0.3485 -0.0677,0.0912 -0.1749,0.1372 -0.3219,0.1372 -0.0472,0 -0.14,-0.0119 -0.2784,-0.0354 -0.1382,-0.0236 -0.2397,-0.0386 -0.3045,-0.0446 0.1531,0.2177 0.2534,0.3652 0.3004,0.4417 0.047,0.0736 0.0706,0.1544 0.0706,0.2426 0,0.1412 -0.0533,0.2556 -0.1592,0.3439 -0.1028,0.0853 -0.2264,0.1279 -0.3705,0.1279 -0.1028,0 -0.1882,-0.0366 -0.2559,-0.11 -0.0646,-0.0767 -0.1309,-0.1955 -0.1986,-0.3572 -0.0646,-0.1647 -0.1028,-0.259 -0.1146,-0.2825 -0.0117,0.0235 -0.0516,0.1178 -0.1192,0.2825 -0.0648,0.1646 -0.1281,0.2836 -0.1899,0.3572 -0.0617,0.0735 -0.1454,0.11 -0.2513,0.11 -0.15,0 -0.2779,-0.0427 -0.3838,-0.1279 -0.1059,-0.0883 -0.1587,-0.2027 -0.1587,-0.3439 0,-0.0618 0.0156,-0.1263 0.0481,-0.194 0.0323,-0.0707 0.0665,-0.1311 0.1018,-0.1812 0.0382,-0.0499 0.1118,-0.1532 0.2206,-0.3091 -0.0736,0.0059 -0.1751,0.021 -0.3045,0.0446 -0.1294,0.0236 -0.2176,0.0354 -0.2646,0.0354 -0.1472,0 -0.2577,-0.046 -0.3311,-0.1372 -0.0707,-0.0941 -0.1059,-0.2101 -0.1059,-0.3485 0,-0.1411 0.0352,-0.2573 0.1059,-0.3485 0.0734,-0.0942 0.184,-0.1412 0.3311,-0.1412 0.0736,0 0.1628,0.0107 0.2687,0.0312 0.1059,0.0206 0.2062,0.038 0.3004,0.0527 -0.0824,-0.1177 -0.1648,-0.2366 -0.2472,-0.3572 -0.0824,-0.1206 -0.1233,-0.2282 -0.1233,-0.3224 0,-0.1382 0.0543,-0.2544 0.1633,-0.3485 0.1088,-0.0942 0.2351,-0.1412 0.3792,-0.1412zM29.9512,43.2235c0.1001,0 0.1808,0.0383 0.2426,0.1146 0.0648,0.0735 0.1326,0.197 0.2032,0.3705 0.0705,0.1707 0.1089,0.262 0.1146,0.2738 0.0059,-0.0119 0.0424,-0.1031 0.11,-0.2738 0.0707,-0.1705 0.1401,-0.2941 0.2078,-0.3705 0.0677,-0.0765 0.1513,-0.1146 0.2513,-0.1146 0.1412,0 0.2646,0.047 0.3705,0.1412 0.1059,0.0941 0.1592,0.2102 0.1592,0.3485 0,0.0675 -0.0179,0.1366 -0.0532,0.2073 -0.0323,0.0705 -0.0777,0.1441 -0.1366,0.2206 -0.056,0.0735 -0.1164,0.1576 -0.1812,0.2518 0.0648,-0.0089 0.1589,-0.0251 0.2825,-0.0486 0.1265,-0.0236 0.2268,-0.0353 0.3004,-0.0353 0.0969,0 0.1762,0.0219 0.238,0.0661 0.0648,0.0442 0.1118,0.1047 0.1413,0.1812 0.0294,0.0736 0.044,0.1542 0.044,0.2426 0,0.1382 -0.0337,0.2544 -0.1013,0.3485 -0.0677,0.0912 -0.1749,0.1366 -0.3219,0.1366 -0.0472,0 -0.14,-0.0119 -0.2784,-0.0354 -0.1382,-0.0236 -0.2397,-0.038 -0.3045,-0.044 0.1531,0.2177 0.2534,0.3647 0.3004,0.4411 0.047,0.0736 0.0706,0.1549 0.0706,0.2431 0,0.1411 -0.0533,0.2558 -0.1592,0.3439 -0.1028,0.0854 -0.2264,0.1279 -0.3705,0.1279 -0.1028,0 -0.1882,-0.0371 -0.2559,-0.1105 -0.0646,-0.0765 -0.1309,-0.1955 -0.1986,-0.3572 -0.0646,-0.1647 -0.1028,-0.259 -0.1146,-0.2825 -0.0117,0.0236 -0.0516,0.1178 -0.1192,0.2825 -0.0648,0.1648 -0.1281,0.2836 -0.1899,0.3572 -0.0617,0.0735 -0.1454,0.1105 -0.2513,0.1105 -0.15,0 -0.2779,-0.0425 -0.3838,-0.1279 -0.1059,-0.0881 -0.1587,-0.2028 -0.1587,-0.3439 0,-0.0617 0.0156,-0.1268 0.0481,-0.1945 0.0323,-0.0705 0.0665,-0.1311 0.1018,-0.1812 0.0382,-0.0499 0.1118,-0.1527 0.2206,-0.3086 -0.0736,0.0059 -0.1751,0.0205 -0.3045,0.044 -0.1294,0.0236 -0.2176,0.0354 -0.2646,0.0354 -0.1472,0 -0.2577,-0.0454 -0.3311,-0.1366 -0.0707,-0.0941 -0.1059,-0.2103 -0.1059,-0.3485 0,-0.1412 0.0352,-0.2573 0.1059,-0.3485 0.0734,-0.0942 0.184,-0.1412 0.3311,-0.1412 0.0736,0 0.1628,0.0101 0.2687,0.0307 0.1059,0.0204 0.2062,0.0384 0.3004,0.0532 -0.0824,-0.1177 -0.1648,-0.2371 -0.2472,-0.3577 -0.0824,-0.1206 -0.1233,-0.2278 -0.1233,-0.3219 0,-0.1384 0.0543,-0.2544 0.1633,-0.3485 0.1088,-0.0942 0.2351,-0.1412 0.3792,-0.1412z"
android:strokeWidth="1.3358"
android:fillColor="#f47a68"/>
</group>
</vector>

View file

@ -28,13 +28,8 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".git.GitActivity"
android:parentActivityName=".PasswordStore">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.zeapo.pwdstore.PasswordStore" />
</activity>
<activity android:name=".git.GitActivity" />
<activity
android:name=".UserPreference"
@ -67,7 +62,7 @@
android:name=".autofill.AutofillActivity"
android:documentLaunchMode="intoExisting"
android:excludeFromRecents="true"
android:parentActivityName=".PasswordStore"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<meta-data
android:name="android.support.PARENT_ACTIVITY"

View file

@ -70,7 +70,7 @@ class PasswordEntry(private val content: String) {
val extraLines = extraContent!!.split("\n".toRegex())
for (line in extraLines) {
for (field in USERNAME_FIELDS) {
if (line.toLowerCase().startsWith("$field:")) {
if (line.toLowerCase().startsWith("$field:", ignoreCase = true)) {
return line.split(": *".toRegex(), 2).toTypedArray()[1]
}
}
@ -143,7 +143,7 @@ class PasswordEntry(private val content: String) {
val extraContent = if (passContent.size > 1) passContent[1] else ""
// if there is a HOTP URI, we must return the extra content with the counter incremented
return if (hasHotp()) {
extraContent.replaceFirst("counter=[0-9]+".toRegex(), "counter=" + java.lang.Long.toString(hotpCounter!!))
extraContent.replaceFirst("counter=[0-9]+".toRegex(), "counter=" + (hotpCounter!!).toString())
} else extraContent
}

View file

@ -211,8 +211,8 @@ public class PasswordFragment extends Fragment {
}
public void dismissActionMode() {
if (recyclerAdapter != null && recyclerAdapter.mActionMode != null) {
recyclerAdapter.mActionMode.finish();
if (recyclerAdapter != null && recyclerAdapter.getActionMode() != null) {
recyclerAdapter.getActionMode().finish();
}
}

View file

@ -12,12 +12,14 @@ import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.zeapo.pwdstore.pwgen.PasswordGenerator;
import org.jetbrains.annotations.NotNull;
@ -37,7 +39,7 @@ public class PasswordGeneratorDialogFragment extends DialogFragment {
@SuppressLint("SetTextI18n")
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
final Activity callingActivity = requireActivity();
LayoutInflater inflater = callingActivity.getLayoutInflater();
@SuppressLint("InflateParams") final View view = inflater.inflate(R.layout.fragment_pwgen, null);
@ -66,10 +68,10 @@ public class PasswordGeneratorDialogFragment extends DialogFragment {
checkBox = view.findViewById(R.id.pronounceable);
checkBox.setChecked(!prefs.getBoolean("s", true));
TextView textView = view.findViewById(R.id.lengthNumber);
AppCompatEditText textView = view.findViewById(R.id.lengthNumber);
textView.setText(Integer.toString(prefs.getInt("length", 20)));
TextView passwordText = view.findViewById(R.id.passwordText);
AppCompatTextView passwordText = view.findViewById(R.id.passwordText);
passwordText.setTypeface(monoTypeface);
builder.setPositiveButton(getResources().getString(R.string.dialog_ok), (dialog, which) -> {

View file

@ -18,11 +18,10 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.appcompat.widget.SearchView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
@ -30,6 +29,7 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import com.zeapo.pwdstore.crypto.PgpActivity;
import com.zeapo.pwdstore.git.GitActivity;
@ -143,7 +143,7 @@ public class PasswordStore extends AppCompatActivity {
REQUEST_EXTERNAL_STORAGE));
snack.show();
View view = snack.getView();
TextView tv = view.findViewById(com.google.android.material.R.id.snackbar_text);
AppCompatTextView tv = view.findViewById(com.google.android.material.R.id.snackbar_text);
tv.setTextColor(Color.WHITE);
tv.setMaxLines(10);
} else {
@ -217,7 +217,7 @@ public class PasswordStore extends AppCompatActivity {
int id = item.getItemId();
Intent intent;
AlertDialog.Builder initBefore = new AlertDialog.Builder(this)
final MaterialAlertDialogBuilder initBefore = new MaterialAlertDialogBuilder(this)
.setMessage(this.getResources().getString(R.string.creation_dialog_text))
.setPositiveButton(this.getResources().getString(R.string.dialog_ok), null);
@ -342,7 +342,7 @@ public class PasswordStore extends AppCompatActivity {
final Set<String> keyIds = settings.getStringSet("openpgp_key_ids_set", new HashSet<>());
if (keyIds.isEmpty())
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setMessage(this.getResources().getString(R.string.key_dialog_text))
.setPositiveButton(this.getResources().getString(R.string.dialog_positive), (dialogInterface, i) -> {
Intent intent = new Intent(activity, UserPreference.class);
@ -498,7 +498,7 @@ public class PasswordStore extends AppCompatActivity {
public void createPassword() {
if (!PasswordRepository.isInitialized()) {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setMessage(this.getResources().getString(R.string.creation_dialog_text))
.setPositiveButton(this.getResources().getString(R.string.dialog_ok), (dialogInterface, i) -> {
}).show();
@ -506,7 +506,7 @@ public class PasswordStore extends AppCompatActivity {
}
if (settings.getStringSet("openpgp_key_ids_set", new HashSet<>()).isEmpty()) {
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(this.getResources().getString(R.string.no_key_selected_dialog_title))
.setMessage(this.getResources().getString(R.string.no_key_selected_dialog_text))
.setPositiveButton(this.getResources().getString(R.string.dialog_ok), (dialogInterface, i) -> {
@ -534,7 +534,7 @@ public class PasswordStore extends AppCompatActivity {
}
final int position = (int) it.next();
final PasswordItem item = adapter.getValues().get(position);
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setMessage(getResources().getString(R.string.delete_dialog_text, item.getLongName()))
.setPositiveButton(getResources().getString(R.string.dialog_yes), (dialogInterface, i) -> {
item.getFile().delete();
@ -642,6 +642,7 @@ public class PasswordStore extends AppCompatActivity {
refreshListAdapter();
break;
case GitActivity.REQUEST_INIT:
case NEW_REPO_BUTTON:
initializeRepositoryInfo();
break;
case GitActivity.REQUEST_SYNC:
@ -651,9 +652,6 @@ public class PasswordStore extends AppCompatActivity {
case HOME:
checkLocalRepository();
break;
case NEW_REPO_BUTTON:
initializeRepositoryInfo();
break;
case CLONE_REPO_BUTTON:
// duplicate code
if (settings.getBoolean("git_external", false) && settings.getString("git_external_repo", null) != null) {
@ -708,7 +706,7 @@ public class PasswordStore extends AppCompatActivity {
if (destinationFile.exists()) {
Log.e(TAG, "Trying to move a file that already exists.");
// TODO: Add option to cancel overwrite. Will be easier once this is an async task.
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(getResources().getString(R.string.password_exists_title))
.setMessage(getResources().getString(R.string.password_exists_message,
destinationLongName, sourceLongName))
@ -739,7 +737,7 @@ public class PasswordStore extends AppCompatActivity {
private void initRepository(final int operation) {
PasswordRepository.closeRepository();
new AlertDialog.Builder(this)
new MaterialAlertDialogBuilder(this)
.setTitle(this.getResources().getString(R.string.location_dialog_title))
.setMessage(this.getResources().getString(R.string.location_dialog_text))
.setPositiveButton(this.getResources().getString(R.string.location_hidden), (dialog, whichButton) -> {
@ -768,7 +766,7 @@ public class PasswordStore extends AppCompatActivity {
intent.putExtra("operation", "git_external");
startActivityForResult(intent, operation);
} else {
new AlertDialog.Builder(activity)
new MaterialAlertDialogBuilder(activity)
.setTitle(getResources().getString(R.string.directory_selected_title))
.setMessage(getResources().getString(R.string.directory_selected_message, externalRepo))
.setPositiveButton(getResources().getString(R.string.use), (dialog1, which) -> {

View file

@ -24,7 +24,7 @@ class SelectFolderActivity : AppCompatActivity() {
passwordList = SelectFolderFragment()
val args = Bundle()
args.putString("Path", PasswordRepository.getRepositoryDirectory(applicationContext).absolutePath)
args.putString("Path", PasswordRepository.getRepositoryDirectory(applicationContext)?.absolutePath)
passwordList.arguments = args

View file

@ -50,8 +50,12 @@ public class SelectFolderFragment extends Fragment {
String path = getArguments().getString("Path");
pathStack = new Stack<>();
recyclerAdapter = new FolderRecyclerAdapter((SelectFolderActivity) requireActivity(), mListener,
PasswordRepository.getPasswords(new File(path), PasswordRepository.getRepositoryDirectory(requireActivity()), getSortOrder()));
recyclerAdapter = new FolderRecyclerAdapter(mListener,
PasswordRepository.getPasswords(
new File(path),
PasswordRepository.getRepositoryDirectory(requireActivity()), getSortOrder()
)
);
}
@Override

View file

@ -2,8 +2,6 @@ package com.zeapo.pwdstore;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
@ -22,13 +20,19 @@ import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.PreferenceManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputEditText;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.KeyPair;
@ -37,6 +41,7 @@ import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.ref.WeakReference;
import java.nio.charset.StandardCharsets;
public class SshKeyGen extends AppCompatActivity {
@ -50,7 +55,7 @@ public class SshKeyGen extends AppCompatActivity {
setTitle("Generate SSH Key");
if (savedInstanceState == null) {
getFragmentManager().beginTransaction()
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, new SshKeyGenFragment()).commit();
}
}
@ -77,20 +82,20 @@ public class SshKeyGen extends AppCompatActivity {
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.fragment_ssh_keygen, container, false);
Typeface monoTypeface = Typeface.createFromAsset(getActivity().getAssets(), "fonts/sourcecodepro.ttf");
Typeface monoTypeface = Typeface.createFromAsset(requireContext().getAssets(), "fonts/sourcecodepro.ttf");
Spinner spinner = v.findViewById(R.id.length);
Integer[] lengths = new Integer[]{2048, 4096};
ArrayAdapter<Integer> adapter = new ArrayAdapter<>(getActivity(),
ArrayAdapter<Integer> adapter = new ArrayAdapter<>(requireContext(),
android.R.layout.simple_spinner_dropdown_item, lengths);
spinner.setAdapter(adapter);
((EditText) v.findViewById(R.id.passphrase)).setTypeface(monoTypeface);
((TextInputEditText) v.findViewById(R.id.passphrase)).setTypeface(monoTypeface);
CheckBox checkbox = v.findViewById(R.id.show_passphrase);
final CheckBox checkbox = v.findViewById(R.id.show_passphrase);
checkbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
EditText editText = v.findViewById(R.id.passphrase);
int selection = editText.getSelectionEnd();
final TextInputEditText editText = v.findViewById(R.id.passphrase);
final int selection = editText.getSelectionEnd();
if (isChecked) {
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
} else {
@ -108,25 +113,27 @@ public class SshKeyGen extends AppCompatActivity {
public ShowSshKeyFragment() {
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
LayoutInflater inflater = getActivity().getLayoutInflater();
final FragmentActivity activity = requireActivity();
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
LayoutInflater inflater = activity.getLayoutInflater();
@SuppressLint("InflateParams") final View v = inflater.inflate(R.layout.fragment_show_ssh_key, null);
builder.setView(v);
TextView textView = v.findViewById(R.id.public_key);
File file = new File(getActivity().getFilesDir() + "/.ssh_key.pub");
AppCompatTextView textView = v.findViewById(R.id.public_key);
File file = new File(activity.getFilesDir() + "/.ssh_key.pub");
try {
textView.setText(FileUtils.readFileToString(file));
textView.setText(FileUtils.readFileToString(file, StandardCharsets.UTF_8));
} catch (Exception e) {
System.out.println("Exception caught :(");
e.printStackTrace();
}
builder.setPositiveButton(getResources().getString(R.string.dialog_ok), (dialog, which) -> {
if (getActivity() instanceof SshKeyGen)
getActivity().finish();
if (activity instanceof SshKeyGen)
activity.finish();
});
builder.setNegativeButton(getResources().getString(R.string.dialog_cancel), (dialog, which) -> {
@ -139,8 +146,8 @@ public class SshKeyGen extends AppCompatActivity {
ad.setOnShowListener(dialog -> {
Button b = ad.getButton(AlertDialog.BUTTON_NEUTRAL);
b.setOnClickListener(v1 -> {
TextView textView1 = getDialog().findViewById(R.id.public_key);
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
AppCompatTextView textView1 = getDialog().findViewById(R.id.public_key);
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("public key", textView1.getText().toString());
clipboard.setPrimaryClip(clip);
});
@ -198,20 +205,19 @@ public class SshKeyGen extends AppCompatActivity {
if (e == null) {
Toast.makeText(weakReference.get(), "SSH-key generated", Toast.LENGTH_LONG).show();
DialogFragment df = new ShowSshKeyFragment();
df.show(weakReference.get().getFragmentManager(), "public_key");
df.show(weakReference.get().getSupportFragmentManager(), "public_key");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(weakReference.get());
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("use_generated_key", true);
editor.apply();
} else {
new AlertDialog.Builder(weakReference.get())
new MaterialAlertDialogBuilder(weakReference.get())
.setTitle("Error while trying to generate the ssh-key")
.setMessage(weakReference.get().getResources().getString(R.string.ssh_key_error_dialog_text) + e.getMessage())
.setPositiveButton(weakReference.get().getResources().getString(R.string.dialog_ok), (dialogInterface, i) -> {
// pass
}).show();
}
}
}
}

View file

@ -8,19 +8,19 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.preference.CheckBoxPreference
import android.preference.Preference
import android.preference.PreferenceFragment
import android.preference.PreferenceManager
import android.provider.DocumentsContract
import android.provider.Settings
import android.util.Log
import android.view.MenuItem
import android.view.accessibility.AccessibilityManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
import com.zeapo.pwdstore.crypto.PgpActivity
import com.zeapo.pwdstore.git.GitActivity
@ -35,74 +35,135 @@ import java.util.Calendar
import java.util.HashSet
import java.util.TimeZone
typealias ClickListener = Preference.OnPreferenceClickListener
typealias ChangeListener = Preference.OnPreferenceChangeListener
class UserPreference : AppCompatActivity() {
private lateinit var prefsFragment: PrefsFragment
class PrefsFragment : PreferenceFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callingActivity = activity as UserPreference
class PrefsFragment : PreferenceFragmentCompat() {
private var autofillDependencies = listOf<Preference?>()
private var autoFillEnablePreference: CheckBoxPreference? = null
private lateinit var callingActivity: UserPreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
callingActivity = requireActivity() as UserPreference
val context = requireContext()
val sharedPreferences = preferenceManager.sharedPreferences
addPreferencesFromResource(R.xml.preference)
findPreference("app_version").summary = "Version: ${BuildConfig.VERSION_NAME}"
// Git preferences
val gitServerPreference = findPreference<Preference>("git_server_info")
val gitConfigPreference = findPreference<Preference>("git_config")
val sshKeyPreference = findPreference<Preference>("ssh_key")
val sshKeygenPreference = findPreference<Preference>("ssh_keygen")
val sshClearPassphrasePreference = findPreference<Preference>("ssh_key_clear_passphrase")
val clearHotpIncrementPreference = findPreference<Preference>("hotp_remember_clear_choice")
val viewSshKeyPreference = findPreference<Preference>("ssh_see_key")
val deleteRepoPreference = findPreference<Preference>("git_delete_repo")
val externalGitRepositoryPreference = findPreference<Preference>("git_external")
val selectExternalGitRepositoryPreference = findPreference<Preference>("pref_select_external")
findPreference("openpgp_key_id_pref").onPreferenceClickListener = Preference.OnPreferenceClickListener {
// Crypto preferences
val keyPreference = findPreference<Preference>("openpgp_key_id_pref")
// General preferences
val clearAfterCopyPreference = findPreference<CheckBoxPreference>("clear_after_copy")
val clearClipboard20xPreference = findPreference<CheckBoxPreference>("clear_clipboard_20x")
// Autofill preferences
autoFillEnablePreference = findPreference<CheckBoxPreference>("autofill_enable")
val autoFillAppsPreference = findPreference<Preference>("autofill_apps")
val autoFillDefaultPreference = findPreference<CheckBoxPreference>("autofill_default")
val autoFillAlwaysShowDialogPreference = findPreference<CheckBoxPreference>("autofill_always")
autofillDependencies = listOf(
autoFillAppsPreference,
autoFillDefaultPreference,
autoFillAlwaysShowDialogPreference
)
// Misc preferences
val appVersionPreference = findPreference<Preference>("app_version")
selectExternalGitRepositoryPreference?.summary = sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected))
viewSshKeyPreference?.isVisible = sharedPreferences.getBoolean("use_generated_key", false)
deleteRepoPreference?.isVisible = !sharedPreferences.getBoolean("git_external", false)
sshClearPassphrasePreference?.isVisible = sharedPreferences.getString("ssh_key_passphrase", null)?.isNotEmpty()
?: false
clearHotpIncrementPreference?.isVisible = sharedPreferences.getBoolean("hotp_remember_check", false)
clearAfterCopyPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
clearClipboard20xPreference?.isVisible = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null)
?: HashSet<String>()).toTypedArray()
keyPreference?.summary = if (selectedKeys.isEmpty()) {
this.resources.getString(R.string.pref_no_key_selected)
} else {
selectedKeys.joinToString(separator = ";") { s ->
OpenPgpUtils.convertKeyIdToHex(java.lang.Long.valueOf(s))
}
}
// see if the autofill service is enabled and check the preference accordingly
autoFillEnablePreference?.isChecked = callingActivity.isServiceEnabled
autofillDependencies.forEach { it?.isVisible = callingActivity.isServiceEnabled }
appVersionPreference?.summary = "Version: ${BuildConfig.VERSION_NAME}"
keyPreference?.onPreferenceClickListener = ClickListener {
val intent = Intent(callingActivity, PgpActivity::class.java)
intent.putExtra("OPERATION", "GET_KEY_ID")
startActivityForResult(intent, IMPORT_PGP_KEY)
true
}
findPreference("ssh_key").onPreferenceClickListener = Preference.OnPreferenceClickListener {
sshKeyPreference?.onPreferenceClickListener = ClickListener {
callingActivity.getSshKey()
true
}
findPreference("ssh_keygen").onPreferenceClickListener = Preference.OnPreferenceClickListener {
sshKeygenPreference?.onPreferenceClickListener = ClickListener {
callingActivity.makeSshKey(true)
true
}
findPreference("ssh_see_key").onPreferenceClickListener = Preference.OnPreferenceClickListener {
viewSshKeyPreference?.onPreferenceClickListener = ClickListener {
val df = SshKeyGen.ShowSshKeyFragment()
df.show(fragmentManager, "public_key")
df.show(requireFragmentManager(), "public_key")
true
}
findPreference("ssh_key_clear_passphrase").onPreferenceClickListener =
Preference.OnPreferenceClickListener {
sharedPreferences.edit().putString("ssh_key_passphrase", null).apply()
it.isEnabled = false
true
}
sshClearPassphrasePreference?.onPreferenceClickListener = ClickListener {
sharedPreferences.edit().putString("ssh_key_passphrase", null).apply()
it.isVisible = false
true
}
findPreference("hotp_remember_clear_choice").onPreferenceClickListener =
Preference.OnPreferenceClickListener {
sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply()
it.isEnabled = false
true
}
clearHotpIncrementPreference?.onPreferenceClickListener = ClickListener {
sharedPreferences.edit().putBoolean("hotp_remember_check", false).apply()
it.isVisible = false
true
}
findPreference("git_server_info").onPreferenceClickListener = Preference.OnPreferenceClickListener {
gitServerPreference?.onPreferenceClickListener = ClickListener {
val intent = Intent(callingActivity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.EDIT_SERVER)
startActivityForResult(intent, EDIT_GIT_INFO)
true
}
findPreference("git_config").onPreferenceClickListener = Preference.OnPreferenceClickListener {
gitConfigPreference?.onPreferenceClickListener = ClickListener {
val intent = Intent(callingActivity, GitActivity::class.java)
intent.putExtra("Operation", GitActivity.EDIT_GIT_CONFIG)
startActivityForResult(intent, EDIT_GIT_CONFIG)
true
}
findPreference("git_delete_repo").onPreferenceClickListener = Preference.OnPreferenceClickListener {
deleteRepoPreference?.onPreferenceClickListener = ClickListener {
val repoDir = PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext)
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(R.string.pref_dialog_delete_title)
.setMessage(resources.getString(R.string.dialog_delete_msg, repoDir))
.setCancelable(false)
@ -110,8 +171,8 @@ class UserPreference : AppCompatActivity() {
try {
FileUtils.cleanDirectory(PasswordRepository.getRepositoryDirectory(callingActivity.applicationContext))
PasswordRepository.closeRepository()
} catch (e: Exception) {
//TODO Handle the different cases of exceptions
} catch (ignored: Exception) {
// TODO Handle the different cases of exceptions
}
sharedPreferences.edit().putBoolean("repository_initialized", false).apply()
@ -124,90 +185,77 @@ class UserPreference : AppCompatActivity() {
true
}
val externalRepo = findPreference("pref_select_external")
externalRepo.summary =
sharedPreferences.getString("git_external_repo", callingActivity.getString(R.string.no_repo_selected))
externalRepo.onPreferenceClickListener = Preference.OnPreferenceClickListener {
selectExternalGitRepositoryPreference?.summary =
sharedPreferences.getString("git_external_repo", context.getString(R.string.no_repo_selected))
selectExternalGitRepositoryPreference?.onPreferenceClickListener = ClickListener {
callingActivity.selectExternalGitRepository()
true
}
val resetRepo = Preference.OnPreferenceChangeListener { _, o ->
findPreference("git_delete_repo").isEnabled = !(o as Boolean)
deleteRepoPreference?.isVisible = !(o as Boolean)
PasswordRepository.closeRepository()
sharedPreferences.edit().putBoolean("repo_changed", true).apply()
true
}
findPreference("pref_select_external").onPreferenceChangeListener = resetRepo
findPreference("git_external").onPreferenceChangeListener = resetRepo
selectExternalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
externalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
findPreference("autofill_apps").onPreferenceClickListener = Preference.OnPreferenceClickListener {
autoFillAppsPreference?.onPreferenceClickListener = ClickListener {
val intent = Intent(callingActivity, AutofillPreferenceActivity::class.java)
startActivity(intent)
true
}
findPreference("autofill_enable").onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(callingActivity).setTitle(R.string.pref_autofill_enable_title)
.setView(R.layout.autofill_instructions).setPositiveButton(R.string.dialog_ok) { _, _ ->
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
startActivity(intent)
}.setNegativeButton(R.string.dialog_cancel, null).setOnDismissListener {
(findPreference("autofill_enable") as CheckBoxPreference).isChecked =
(activity as UserPreference).isServiceEnabled
}.show()
autoFillEnablePreference?.onPreferenceClickListener = ClickListener {
var isEnabled = callingActivity.isServiceEnabled
if (isEnabled) {
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
} else {
MaterialAlertDialogBuilder(callingActivity)
.setTitle(R.string.pref_autofill_enable_title)
.setView(R.layout.autofill_instructions)
.setPositiveButton(R.string.dialog_ok) { _, _ ->
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
}
.setNegativeButton(R.string.dialog_cancel, null)
.setOnDismissListener {
isEnabled = callingActivity.isServiceEnabled
autoFillEnablePreference?.isChecked = isEnabled
autofillDependencies.forEach { it?.isVisible = isEnabled }
}
.show()
}
true
}
findPreference("export_passwords").apply {
isEnabled = sharedPreferences.getBoolean("repository_initialized", false)
findPreference<Preference>("export_passwords")?.apply {
isVisible = sharedPreferences.getBoolean("repository_initialized", false)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
callingActivity.exportPasswords()
true
}
}
findPreference("general_show_time").onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
try {
findPreference("clear_after_copy").isEnabled = newValue.toString().toInt() != 0
findPreference("clear_clipboard_20x").isEnabled = newValue.toString().toInt() != 0
true
} catch (e: NumberFormatException) {
false
}
}
findPreference<Preference>("general_show_time")?.onPreferenceChangeListener =
ChangeListener { _, newValue: Any? ->
try {
val isEnabled = newValue.toString().toInt() != 0
clearAfterCopyPreference?.isVisible = isEnabled
clearClipboard20xPreference?.isVisible = isEnabled
true
} catch (e: NumberFormatException) {
false
}
}
}
override fun onStart() {
super.onStart()
val sharedPreferences = preferenceManager.sharedPreferences
findPreference("pref_select_external").summary =
preferenceManager.sharedPreferences.getString("git_external_repo", getString(R.string.no_repo_selected))
findPreference("ssh_see_key").isEnabled = sharedPreferences.getBoolean("use_generated_key", false)
findPreference("git_delete_repo").isEnabled = !sharedPreferences.getBoolean("git_external", false)
findPreference("ssh_key_clear_passphrase").isEnabled = sharedPreferences.getString(
"ssh_key_passphrase",
null
)?.isNotEmpty() ?: false
findPreference("hotp_remember_clear_choice").isEnabled =
sharedPreferences.getBoolean("hotp_remember_check", false)
findPreference("clear_after_copy").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
findPreference("clear_clipboard_20x").isEnabled = sharedPreferences.getString("general_show_time", "45")?.toInt() != 0
val keyPref = findPreference("openpgp_key_id_pref")
val selectedKeys = (sharedPreferences.getStringSet("openpgp_key_ids_set", null)
?: HashSet<String>()).toTypedArray()
if (selectedKeys.isEmpty()) {
keyPref.summary = this.resources.getString(R.string.pref_no_key_selected)
} else {
keyPref.summary = selectedKeys.joinToString(separator = ";") { s ->
OpenPgpUtils.convertKeyIdToHex(java.lang.Long.valueOf(s))
}
}
// see if the autofill service is enabled and check the preference accordingly
(findPreference("autofill_enable") as CheckBoxPreference).isChecked =
(activity as UserPreference).isServiceEnabled
override fun onResume() {
super.onResume()
val isEnabled = callingActivity.isServiceEnabled
autoFillEnablePreference?.isChecked = isEnabled
autofillDependencies.forEach { it?.isVisible = isEnabled }
}
}
@ -220,19 +268,24 @@ class UserPreference : AppCompatActivity() {
}
prefsFragment = PrefsFragment()
fragmentManager.beginTransaction().replace(android.R.id.content, prefsFragment).commit()
supportFragmentManager
.beginTransaction()
.replace(android.R.id.content, prefsFragment)
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
fun selectExternalGitRepository() {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(this.resources.getString(R.string.external_repository_dialog_title))
.setMessage(this.resources.getString(R.string.external_repository_dialog_text))
.setPositiveButton(R.string.dialog_ok) { _, _ ->
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(Intent.createChooser(i, "Choose Directory"), SELECT_GIT_DIRECTORY)
}.setNegativeButton(R.string.dialog_cancel, null).show()
}
.setNegativeButton(R.string.dialog_cancel, null)
.show()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -350,7 +403,7 @@ class UserPreference : AppCompatActivity() {
finish()
} catch (e: IOException) {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(this.resources.getString(R.string.ssh_key_error_dialog_title))
.setMessage(this.resources.getString(R.string.ssh_key_error_dialog_text) + e.message)
.setPositiveButton(this.resources.getString(R.string.dialog_ok), null)
@ -372,7 +425,7 @@ class UserPreference : AppCompatActivity() {
Log.d(TAG, "Selected repository path is $repoPath")
if (Environment.getExternalStorageDirectory().path == repoPath) {
AlertDialog.Builder(this)
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.sdcard_root_warning_title))
.setMessage(getString(R.string.sdcard_root_warning_message))
.setPositiveButton("Remove everything") { _, _ ->
@ -380,7 +433,9 @@ class UserPreference : AppCompatActivity() {
.edit()
.putString("git_external_repo", uri?.path)
.apply()
}.setNegativeButton(R.string.dialog_cancel, null).show()
}
.setNegativeButton(R.string.dialog_cancel, null)
.show()
}
PreferenceManager.getDefaultSharedPreferences(applicationContext)
@ -413,7 +468,7 @@ class UserPreference : AppCompatActivity() {
*/
private fun exportPasswords(targetDirectory: DocumentFile) {
val repositoryDirectory = PasswordRepository.getRepositoryDirectory(applicationContext)
val repositoryDirectory = requireNotNull(PasswordRepository.getRepositoryDirectory(applicationContext))
val sourcePassDir = DocumentFile.fromFile(repositoryDirectory)
Log.d(TAG, "Copying ${repositoryDirectory.path} to $targetDirectory")

View file

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
@ -15,21 +16,23 @@ import android.widget.EditText
import android.widget.ListView
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.appcompat.widget.AppCompatTextView
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.PasswordStore
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.resolveAttribute
import com.zeapo.pwdstore.utils.splitLines
class AutofillFragment : DialogFragment() {
private var adapter: ArrayAdapter<String>? = null
private var isWeb: Boolean = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
val builder = MaterialAlertDialogBuilder(requireContext())
// this fragment is only created from the settings page (AutofillPreferenceActivity)
// need to interact with the recyclerAdapter which is a member of activity
val callingActivity = requireActivity() as AutofillPreferenceActivity
@ -51,9 +54,13 @@ class AutofillFragment : DialogFragment() {
builder.setTitle(appName)
view.findViewById<View>(R.id.webURL).visibility = View.GONE
} else {
iconPackageName = "com.android.browser"
val browserIntent = Intent("android.intent.action.VIEW", Uri.parse("http://"))
val resolveInfo = requireContext().packageManager
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
iconPackageName = resolveInfo?.activityInfo?.packageName
builder.setTitle("Website")
(view.findViewById<View>(R.id.webURL) as EditText).setText(packageName)
(view.findViewById<View>(R.id.webURL) as EditText).setText(packageName
?: "com.android.browser")
}
try {
builder.setIcon(callingActivity.packageManager.getApplicationIcon(iconPackageName))
@ -65,15 +72,17 @@ class AutofillFragment : DialogFragment() {
adapter = object : ArrayAdapter<String>(requireContext(), android.R.layout.simple_list_item_1, android.R.id.text1) {
// set text color to black because default is white...
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val textView = super.getView(position, convertView, parent) as TextView
textView.setTextColor(ContextCompat.getColor(context, R.color.grey_black_1000))
val textView = super.getView(position, convertView, parent) as AppCompatTextView
textView.setTextColor(requireContext().resolveAttribute(android.R.attr.textColor))
return textView
}
}
(view.findViewById<View>(R.id.matched) as ListView).adapter = adapter
// delete items by clicking them
(view.findViewById<View>(R.id.matched) as ListView).onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ -> adapter!!.remove(adapter!!.getItem(position)) }
AdapterView.OnItemClickListener { _, _, position, _ ->
adapter!!.remove(adapter!!.getItem(position))
}
// set the existing preference, if any
val prefs: SharedPreferences = if (!isWeb) {

View file

@ -7,8 +7,8 @@ import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
@ -68,7 +68,6 @@ internal class AutofillRecyclerAdapter(
holder.name.text = app.appName
holder.secondary.visibility = View.VISIBLE
holder.view.setBackgroundResource(R.color.grey_white_1000)
val prefs: SharedPreferences
prefs = if (app.appName != app.packageName) {
@ -151,9 +150,9 @@ internal class AutofillRecyclerAdapter(
}
internal inner class ViewHolder(var view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
var name: TextView = view.findViewById(R.id.app_name)
var icon: ImageView = view.findViewById(R.id.app_icon)
var secondary: TextView = view.findViewById(R.id.secondary_text)
var name: AppCompatTextView = view.findViewById(R.id.app_name)
var icon: AppCompatImageView = view.findViewById(R.id.app_icon)
var secondary: AppCompatTextView = view.findViewById(R.id.secondary_text)
var packageName: String? = null
var appName: String? = null
var isWeb: Boolean = false

View file

@ -12,7 +12,6 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.provider.Settings
import android.util.Log
import android.view.WindowManager
@ -21,6 +20,8 @@ import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.PasswordRepository
@ -30,7 +31,6 @@ import org.openintents.openpgp.IOpenPgpService2
import org.openintents.openpgp.OpenPgpError
import org.openintents.openpgp.util.OpenPgpApi
import org.openintents.openpgp.util.OpenPgpServiceConnection
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
@ -291,7 +291,7 @@ class AutofillService : AccessibilityService() {
when (preference) {
"/first" -> {
if (!PasswordRepository.isInitialized()) {
if (!PasswordRepository.isInitialized) {
PasswordRepository.initialize(this)
}
items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), webViewTitle)
@ -313,7 +313,7 @@ class AutofillService : AccessibilityService() {
when (preference) {
"/first" -> {
if (!PasswordRepository.isInitialized()) {
if (!PasswordRepository.isInitialized) {
PasswordRepository.initialize(this)
}
items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName)
@ -326,7 +326,7 @@ class AutofillService : AccessibilityService() {
// Put the newline separated list of passwords from the SharedPreferences
// file into the items list.
private fun getPreferredPasswords(preference: String) {
if (!PasswordRepository.isInitialized()) {
if (!PasswordRepository.isInitialized) {
PasswordRepository.initialize(this)
}
val preferredPasswords = preference.splitLines()
@ -366,7 +366,7 @@ class AutofillService : AccessibilityService() {
dialog = null
}
val builder = AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog)
val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog)
builder.setNegativeButton(R.string.dialog_cancel) { _, _ ->
dialog!!.dismiss()
dialog = null
@ -391,7 +391,7 @@ class AutofillService : AccessibilityService() {
dialog = null
}
val builder = AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog)
val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog)
builder.setNegativeButton(R.string.dialog_cancel) { _, _ ->
dialog!!.dismiss()
dialog = null
@ -525,11 +525,13 @@ class AutofillService : AccessibilityService() {
}
OpenPgpApi.RESULT_CODE_ERROR -> {
val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
Toast.makeText(this@AutofillService,
"Error from OpenKeyChain : " + error.message,
Toast.LENGTH_LONG).show()
Log.e(Constants.TAG, "onError getErrorId:" + error.errorId)
Log.e(Constants.TAG, "onError getMessage:" + error.message)
if (error != null) {
Toast.makeText(this@AutofillService,
"Error from OpenKeyChain : " + error.message,
Toast.LENGTH_LONG).show()
Log.e(Constants.TAG, "onError getErrorId:" + error.errorId)
Log.e(Constants.TAG, "onError getMessage:" + error.message)
}
}
}
}

View file

@ -12,7 +12,6 @@ import android.os.AsyncTask
import android.os.Bundle
import android.os.ConditionVariable
import android.os.Handler
import android.preference.PreferenceManager
import android.text.TextUtils
import android.text.format.DateUtils
import android.text.method.PasswordTransformationMethod
@ -29,8 +28,9 @@ import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.PasswordGeneratorDialogFragment
import com.zeapo.pwdstore.R
@ -156,8 +156,8 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
if (passwordEntry?.hotpIsIncremented() == false) {
setResult(RESULT_CANCELED)
@ -196,10 +196,10 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
private fun handleUserInteractionRequest(result: Intent, requestCode: Int) {
Log.i(TAG, "RESULT_CODE_USER_INTERACTION_REQUIRED")
val pi: PendingIntent = result.getParcelableExtra(RESULT_INTENT)
val pi: PendingIntent? = result.getParcelableExtra(RESULT_INTENT)
try {
this@PgpActivity.startIntentSenderFromChild(
this@PgpActivity, pi.intentSender, requestCode,
this@PgpActivity, pi?.intentSender, requestCode,
null, 0, 0, 0
)
} catch (e: IntentSender.SendIntentException) {
@ -219,10 +219,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
*
* Check in open-pgp-lib how their definitions and error code
*/
val error: OpenPgpError = result.getParcelableExtra(RESULT_ERROR)
showToast("Error from OpenKeyChain : " + error.message)
Log.e(TAG, "onError getErrorId:" + error.errorId)
Log.e(TAG, "onError getMessage:" + error.message)
val error: OpenPgpError? = result.getParcelableExtra(RESULT_ERROR)
if (error != null) {
showToast("Error from OpenKeyChain : " + error.message)
Log.e(TAG, "onError getErrorId:" + error.errorId)
Log.e(TAG, "onError getMessage:" + error.message)
}
}
private fun initOpenPgpApi() {
@ -354,7 +356,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val checkLayout = checkInflater.inflate(R.layout.otp_confirm_layout, null)
val rememberCheck: CheckBox =
checkLayout.findViewById(R.id.hotp_remember_checkbox)
val dialogBuilder = AlertDialog.Builder(this)
val dialogBuilder = MaterialAlertDialogBuilder(this)
dialogBuilder.setView(checkLayout)
dialogBuilder.setMessage(R.string.dialog_update_body)
.setCancelable(false)
@ -554,6 +556,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
RESULT_CODE_SUCCESS -> {
try {
val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)
?: LongArray(0)
val keys = ids.map { it.toString() }.toSet()
// use Long
@ -754,7 +757,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val extraText = findViewById<TextView>(R.id.crypto_extra_show)
if (extraText?.text?.isNotEmpty() ?: false)
if (extraText?.text?.isNotEmpty() == true)
findViewById<View>(R.id.crypto_extra_show_layout)?.visibility = View.VISIBLE
if (showTime == 0) {

View file

@ -1,7 +1,7 @@
package com.zeapo.pwdstore.git
import android.app.Activity
import android.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.GitCommand
@ -37,7 +37,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: Activity) : GitOperatio
override fun execute() {
val git = Git(repository)
if (!git.repository.repositoryState.isRebasing) {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
.setMessage("The repository is not rebasing, no need to push to another branch")
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
@ -59,7 +59,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: Activity) : GitOperatio
}
override fun onError(errorMessage: String) {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occurred when checking out another branch operation $errorMessage")
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
@ -68,7 +68,7 @@ class BreakOutOfDetached(fileDir: File, callingActivity: Activity) : GitOperatio
}
override fun onSuccess() {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
.setMessage("There was a conflict when trying to rebase. " +
"Your local master branch was pushed to another branch named conflicting-master-....\n" +

View file

@ -1,11 +1,10 @@
package com.zeapo.pwdstore.git
import android.app.Activity
import android.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import org.eclipse.jgit.api.CloneCommand
import org.eclipse.jgit.api.Git
import java.io.File
/**
@ -23,7 +22,10 @@ class CloneOperation(fileDir: File, callingActivity: Activity) : GitOperation(fi
* @return the current object
*/
fun setCommand(uri: String): CloneOperation {
this.command = Git.cloneRepository().setCloneAllBranches(true).setDirectory(repository.workTree).setURI(uri)
this.command = Git.cloneRepository()
.setCloneAllBranches(true)
.setDirectory(repository?.workTree)
.setURI(uri)
return this
}
@ -53,14 +55,12 @@ class CloneOperation(fileDir: File, callingActivity: Activity) : GitOperation(fi
}
override fun execute() {
if (this.provider != null) {
(this.command as CloneCommand).setCredentialsProvider(this.provider)
}
(this.command as? CloneCommand)?.setCredentialsProvider(this.provider)
GitAsyncTask(callingActivity, true, false, this).execute(this.command)
}
override fun onError(errorMessage: String) {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occured during the clone operation, "
+ callingActivity.resources.getString(R.string.jgit_error_dialog_text)

View file

@ -1,726 +0,0 @@
package com.zeapo.pwdstore.git;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.zeapo.pwdstore.R;
import com.zeapo.pwdstore.UserPreference;
import com.zeapo.pwdstore.git.config.SshApiSessionFactory;
import com.zeapo.pwdstore.utils.PasswordRepository;
import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GitActivity extends AppCompatActivity {
public static final int REQUEST_PULL = 101;
public static final int REQUEST_PUSH = 102;
public static final int REQUEST_CLONE = 103;
public static final int REQUEST_INIT = 104;
public static final int EDIT_SERVER = 105;
public static final int REQUEST_SYNC = 106;
public static final int REQUEST_CREATE = 107;
public static final int EDIT_GIT_CONFIG = 108;
public static final int BREAK_OUT_OF_DETACHED = 109;
private static final String TAG = "GitAct";
private static final String emailPattern = "^[^@]+@[^@]+$";
private Activity activity;
private Context context;
private String protocol;
private String connectionMode;
private String hostname;
private SharedPreferences settings;
private SshApiSessionFactory.IdentityBuilder identityBuilder;
private SshApiSessionFactory.ApiIdentity identity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
context = getApplicationContext();
activity = this;
settings = PreferenceManager.getDefaultSharedPreferences(this.context);
protocol = settings.getString("git_remote_protocol", "ssh://");
connectionMode = settings.getString("git_remote_auth", "ssh-key");
int operationCode = getIntent().getExtras().getInt("Operation");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
switch (operationCode) {
case REQUEST_CLONE:
case EDIT_SERVER:
setContentView(R.layout.activity_git_clone);
setTitle(R.string.title_activity_git_clone);
final Spinner protcol_spinner = findViewById(R.id.clone_protocol);
final Spinner connection_mode_spinner = findViewById(R.id.connection_mode);
// init the spinner for connection modes
final ArrayAdapter<CharSequence> connection_mode_adapter = ArrayAdapter.createFromResource(this,
R.array.connection_modes, android.R.layout.simple_spinner_item);
connection_mode_adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
connection_mode_spinner.setAdapter(connection_mode_adapter);
connection_mode_spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
String selection = ((Spinner) findViewById(R.id.connection_mode)).getSelectedItem().toString();
connectionMode = selection;
settings.edit().putString("git_remote_auth", selection).apply();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
// init the spinner for protocols
ArrayAdapter<CharSequence> protocol_adapter = ArrayAdapter.createFromResource(this,
R.array.clone_protocols, android.R.layout.simple_spinner_item);
protocol_adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
protcol_spinner.setAdapter(protocol_adapter);
protcol_spinner.setOnItemSelectedListener(
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
protocol = ((Spinner) findViewById(R.id.clone_protocol)).getSelectedItem().toString();
if (protocol.equals("ssh://")) {
((EditText) findViewById(R.id.clone_uri)).setHint("user@hostname:path");
((EditText) findViewById(R.id.server_port)).setHint(R.string.default_ssh_port);
// select ssh-key auth mode as default and enable the spinner in case it was disabled
connection_mode_spinner.setSelection(0);
connection_mode_spinner.setEnabled(true);
// however, if we have some saved that, that's more important!
if (connectionMode.equalsIgnoreCase("ssh-key")) {
connection_mode_spinner.setSelection(0);
} else if (connectionMode.equalsIgnoreCase("OpenKeychain")) {
connection_mode_spinner.setSelection(2);
} else {
connection_mode_spinner.setSelection(1);
}
} else {
((EditText) findViewById(R.id.clone_uri)).setHint("hostname/path");
((EditText) findViewById(R.id.server_port)).setHint(R.string.default_https_port);
// select user/pwd auth-mode and disable the spinner
connection_mode_spinner.setSelection(1);
connection_mode_spinner.setEnabled(false);
}
updateURI();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
}
);
if (protocol.equals("ssh://")) {
protcol_spinner.setSelection(0);
} else {
protcol_spinner.setSelection(1);
}
// init the server information
final EditText server_url = findViewById(R.id.server_url);
final EditText server_port = findViewById(R.id.server_port);
final EditText server_path = findViewById(R.id.server_path);
final EditText server_user = findViewById(R.id.server_user);
final EditText server_uri = findViewById(R.id.clone_uri);
server_url.setText(settings.getString("git_remote_server", ""));
server_port.setText(settings.getString("git_remote_port", ""));
server_user.setText(settings.getString("git_remote_username", ""));
server_path.setText(settings.getString("git_remote_location", ""));
server_url.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
if (server_url.isFocused())
updateURI();
}
@Override
public void afterTextChanged(Editable editable) {
}
});
server_port.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
if (server_port.isFocused())
updateURI();
}
@Override
public void afterTextChanged(Editable editable) {
}
});
server_user.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
if (server_user.isFocused())
updateURI();
}
@Override
public void afterTextChanged(Editable editable) {
}
});
server_path.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
if (server_path.isFocused())
updateURI();
}
@Override
public void afterTextChanged(Editable editable) {
}
});
server_uri.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
if (server_uri.isFocused())
splitURI();
}
@Override
public void afterTextChanged(Editable editable) {
}
});
if (operationCode == EDIT_SERVER) {
findViewById(R.id.clone_button).setVisibility(View.INVISIBLE);
findViewById(R.id.save_button).setVisibility(View.VISIBLE);
} else {
findViewById(R.id.clone_button).setVisibility(View.VISIBLE);
findViewById(R.id.save_button).setVisibility(View.INVISIBLE);
}
updateURI();
break;
case EDIT_GIT_CONFIG:
setContentView(R.layout.activity_git_config);
setTitle(R.string.title_activity_git_config);
showGitConfig();
break;
case REQUEST_PULL:
syncRepository(REQUEST_PULL);
break;
case REQUEST_PUSH:
syncRepository(REQUEST_PUSH);
break;
case REQUEST_SYNC:
syncRepository(REQUEST_SYNC);
break;
}
}
/**
* Fills in the server_uri field with the information coming from other fields
*/
private void updateURI() {
EditText uri = findViewById(R.id.clone_uri);
EditText server_url = findViewById(R.id.server_url);
EditText server_port = findViewById(R.id.server_port);
EditText server_path = findViewById(R.id.server_path);
EditText server_user = findViewById(R.id.server_user);
if (uri != null) {
switch (protocol) {
case "ssh://": {
String hostname =
server_user.getText()
+ "@" +
server_url.getText().toString().trim()
+ ":";
if (server_port.getText().toString().equals("22")) {
hostname += server_path.getText().toString();
findViewById(R.id.warn_url).setVisibility(View.GONE);
} else {
TextView warn_url = findViewById(R.id.warn_url);
if (!server_path.getText().toString().matches("/.*") && !server_port.getText().toString().isEmpty()) {
warn_url.setText(R.string.warn_malformed_url_port);
warn_url.setVisibility(View.VISIBLE);
} else {
warn_url.setVisibility(View.GONE);
}
hostname += server_port.getText().toString() + server_path.getText().toString();
}
if (!hostname.equals("@:")) uri.setText(hostname);
}
break;
case "https://": {
StringBuilder hostname = new StringBuilder();
hostname.append(server_url.getText().toString().trim());
if (server_port.getText().toString().equals("443")) {
hostname.append(server_path.getText().toString());
findViewById(R.id.warn_url).setVisibility(View.GONE);
} else {
hostname.append("/");
hostname.append(server_port.getText().toString())
.append(server_path.getText().toString());
}
if (!hostname.toString().equals("@/")) uri.setText(hostname);
}
break;
default:
break;
}
}
}
/**
* Splits the information in server_uri into the other fields
*/
private void splitURI() {
EditText server_uri = findViewById(R.id.clone_uri);
EditText server_url = findViewById(R.id.server_url);
EditText server_port = findViewById(R.id.server_port);
EditText server_path = findViewById(R.id.server_path);
EditText server_user = findViewById(R.id.server_user);
String uri = server_uri.getText().toString();
Pattern pattern = Pattern.compile("(.+)@([\\w\\d.]+):([\\d]+)*(.*)");
Matcher matcher = pattern.matcher(uri);
if (matcher.find()) {
int count = matcher.groupCount();
if (count > 1) {
server_user.setText(matcher.group(1));
server_url.setText(matcher.group(2));
}
if (count == 4) {
server_port.setText(matcher.group(3));
server_path.setText(matcher.group(4));
TextView warn_url = findViewById(R.id.warn_url);
if (!server_path.getText().toString().matches("/.*") && !server_port.getText().toString().isEmpty()) {
warn_url.setText(R.string.warn_malformed_url_port);
warn_url.setVisibility(View.VISIBLE);
} else {
warn_url.setVisibility(View.GONE);
}
}
}
}
@Override
public void onResume() {
super.onResume();
updateURI();
}
@Override
protected void onDestroy() {
// Do not leak the service connection
if (identityBuilder != null) {
identityBuilder.close();
identityBuilder = null;
}
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.git_clone, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.user_pref) {
try {
Intent intent = new Intent(this, UserPreference.class);
startActivity(intent);
} catch (Exception e) {
System.out.println("Exception caught :(");
e.printStackTrace();
}
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Saves the configuration found in the form
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean saveConfiguration() {
// remember the settings
SharedPreferences.Editor editor = settings.edit();
editor.putString("git_remote_server", ((EditText) findViewById(R.id.server_url)).getText().toString());
editor.putString("git_remote_location", ((EditText) findViewById(R.id.server_path)).getText().toString());
editor.putString("git_remote_username", ((EditText) findViewById(R.id.server_user)).getText().toString());
editor.putString("git_remote_protocol", protocol);
editor.putString("git_remote_auth", connectionMode);
editor.putString("git_remote_port", ((EditText) findViewById(R.id.server_port)).getText().toString());
editor.putString("git_remote_uri", ((EditText) findViewById(R.id.clone_uri)).getText().toString());
// 'save' hostname variable for use by addRemote() either here or later
// in syncRepository()
hostname = ((EditText) findViewById(R.id.clone_uri)).getText().toString();
String port = ((EditText) findViewById(R.id.server_port)).getText().toString();
// don't ask the user, take off the protocol that he puts in
hostname = hostname.replaceFirst("^.+://", "");
((TextView) findViewById(R.id.clone_uri)).setText(hostname);
if (!protocol.equals("ssh://")) {
hostname = protocol + hostname;
} else {
// if the port is explicitly given, jgit requires the ssh://
if (!port.isEmpty() && !port.equals("22"))
hostname = protocol + hostname;
// did he forget the username?
if (!hostname.matches("^.+@.+")) {
new AlertDialog.Builder(this).
setMessage(activity.getResources().getString(R.string.forget_username_dialog_text)).
setPositiveButton(activity.getResources().getString(R.string.dialog_oops), null).
show();
return false;
}
}
if (PasswordRepository.isInitialized() && settings.getBoolean("repository_initialized", false)) {
// don't just use the clone_uri text, need to use hostname which has
// had the proper protocol prepended
PasswordRepository.addRemote("origin", hostname, true);
}
editor.apply();
return true;
}
/**
* Save the repository information to the shared preferences settings
*/
public void saveConfiguration(View view) {
if (!saveConfiguration())
return;
finish();
}
private void showGitConfig() {
// init the server information
final EditText git_user_name = findViewById(R.id.git_user_name);
final EditText git_user_email = findViewById(R.id.git_user_email);
final Button abort = findViewById(R.id.git_abort_rebase);
git_user_name.setText(settings.getString("git_config_user_name", ""));
git_user_email.setText(settings.getString("git_config_user_email", ""));
// git status
Repository repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(activity.getApplicationContext()));
if (repo != null) {
final TextView git_commit_hash = findViewById(R.id.git_commit_hash);
try {
ObjectId objectId = repo.resolve(Constants.HEAD);
Ref ref = repo.getRef("refs/heads/master");
String head = ref.getObjectId().equals(objectId) ? ref.getName() : "DETACHED";
git_commit_hash.setText(String.format("%s (%s)", objectId.abbreviate(8).name(), head));
// enable the abort button only if we're rebasing
abort.setEnabled(repo.getRepositoryState().isRebasing());
} catch (Exception e) {
// ignore
}
}
}
private boolean saveGitConfigs() {
// remember the settings
SharedPreferences.Editor editor = settings.edit();
String email = ((EditText) findViewById(R.id.git_user_email)).getText().toString();
editor.putString("git_config_user_email", email);
editor.putString("git_config_user_name", ((EditText) findViewById(R.id.git_user_name)).getText().toString());
if (!email.matches(emailPattern)) {
new AlertDialog.Builder(this).
setMessage(activity.getResources().getString(R.string.invalid_email_dialog_text)).
setPositiveButton(activity.getResources().getString(R.string.dialog_oops), null).
show();
return false;
}
editor.apply();
return true;
}
public void applyGitConfigs(View view) {
if (!saveGitConfigs())
return;
String git_user_name = settings.getString("git_config_user_name", "");
String git_user_email = settings.getString("git_config_user_email", "");
PasswordRepository.setUserName(git_user_name);
PasswordRepository.setUserEmail(git_user_email);
finish();
}
public void abortRebase(View view) {
launchGitOperation(BREAK_OUT_OF_DETACHED);
}
/**
* Clones the repository, the directory exists, deletes it
*/
public void cloneRepository(View view) {
if (PasswordRepository.getRepository(null) == null) {
PasswordRepository.initialize(this);
}
File localDir = PasswordRepository.getRepositoryDirectory(context);
if (!saveConfiguration())
return;
// Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
if (localDir.exists() && localDir.listFiles().length != 0
&& !(localDir.listFiles().length == 1 && localDir.listFiles()[0].getName().equals(".git"))) {
new AlertDialog.Builder(this).
setTitle(R.string.dialog_delete_title).
setMessage(getResources().getString(R.string.dialog_delete_msg) + " " + localDir.toString()).
setCancelable(false).
setPositiveButton(R.string.dialog_delete,
(dialog, id) -> {
try {
FileUtils.deleteDirectory(localDir);
launchGitOperation(REQUEST_CLONE);
} catch (IOException e) {
//TODO Handle the exception correctly if we are unable to delete the directory...
e.printStackTrace();
new AlertDialog.Builder(GitActivity.this).setMessage(e.getMessage()).show();
}
dialog.cancel();
}
).
setNegativeButton(R.string.dialog_do_not_delete,
(dialog, id) -> dialog.cancel()
).
show();
} else {
try {
// Silently delete & replace the lone .git folder if it exists
if (localDir.exists() && localDir.listFiles().length == 1 && localDir.listFiles()[0].getName().equals(".git")) {
try {
FileUtils.deleteDirectory(localDir);
} catch (IOException e) {
e.printStackTrace();
new AlertDialog.Builder(GitActivity.this).setMessage(e.getMessage()).show();
}
}
} catch (Exception e) {
//This is what happens when jgit fails :(
//TODO Handle the diffent cases of exceptions
e.printStackTrace();
new AlertDialog.Builder(this).setMessage(e.getMessage()).show();
}
launchGitOperation(REQUEST_CLONE);
}
}
/**
* Syncs the local repository with the remote one (either pull or push)
*
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
*/
private void syncRepository(int operation) {
if (settings.getString("git_remote_username", "").isEmpty() ||
settings.getString("git_remote_server", "").isEmpty() ||
settings.getString("git_remote_location", "").isEmpty())
new AlertDialog.Builder(this)
.setMessage(activity.getResources().getString(R.string.set_information_dialog_text))
.setPositiveButton(activity.getResources().getString(R.string.dialog_positive), (dialogInterface, i) -> {
Intent intent = new Intent(activity, UserPreference.class);
startActivityForResult(intent, REQUEST_PULL);
})
.setNegativeButton(activity.getResources().getString(R.string.dialog_negative), (dialogInterface, i) -> {
// do nothing :(
setResult(RESULT_OK);
finish();
})
.show();
else {
// check that the remote origin is here, else add it
PasswordRepository.addRemote("origin", hostname, false);
launchGitOperation(operation);
}
}
/**
* Attempt to launch the requested GIT operation. Depending on the configured auth, it may not
* be possible to launch the operation immediately. In that case, this function may launch an
* intermediate activity instead, which will gather necessary information and post it back via
* onActivityResult, which will then re-call this function. This may happen multiple times,
* until either an error is encountered or the operation is successfully launched.
*
* @param operation The type of GIT operation to launch
*/
protected void launchGitOperation(int operation) {
GitOperation op;
File localDir = PasswordRepository.getRepositoryDirectory(context);
try {
// Before launching the operation with OpenKeychain auth, we need to issue several requests
// to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
// we just need to keep calling it until it returns a completed ApiIdentity.
if (connectionMode.equalsIgnoreCase("OpenKeychain") && identity == null) {
// Lazy initialization of the IdentityBuilder
if (identityBuilder == null) {
identityBuilder = new SshApiSessionFactory.IdentityBuilder(this);
}
// Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure
// that onActivityResult is called with operation again, which will re-invoke us here
identity = identityBuilder.tryBuild(operation);
if (identity == null)
return;
}
switch (operation) {
case REQUEST_CLONE:
case GitOperation.GET_SSH_KEY_FROM_CLONE:
op = new CloneOperation(localDir, activity).setCommand(hostname);
break;
case REQUEST_PULL:
op = new PullOperation(localDir, activity).setCommand();
break;
case REQUEST_PUSH:
op = new PushOperation(localDir, activity).setCommand();
break;
case REQUEST_SYNC:
op = new SyncOperation(localDir, activity).setCommands();
break;
case BREAK_OUT_OF_DETACHED:
op = new BreakOutOfDetached(localDir, activity).setCommands();
break;
case SshApiSessionFactory.POST_SIGNATURE:
return;
default:
Log.e(TAG, "Operation not recognized : " + operation);
setResult(RESULT_CANCELED);
finish();
return;
}
op.executeAfterAuthentication(connectionMode,
settings.getString("git_remote_username", "git"),
new File(getFilesDir() + "/.ssh_key"),
identity);
} catch (Exception e) {
e.printStackTrace();
new AlertDialog.Builder(this).setMessage(e.getMessage()).show();
}
}
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
// In addition to the pre-operation-launch series of intents for OpenKeychain auth
// that will pass through here and back to launchGitOperation, there is one
// synchronous operation that happens /after/ the operation has been launched in the
// background thread - the actual signing of the SSH challenge. We pass through the
// completed signature to the ApiIdentity, which will be blocked in the other thread
// waiting for it.
if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null)
identity.postSignature(data);
if (resultCode == RESULT_CANCELED) {
setResult(RESULT_CANCELED);
finish();
} else if (resultCode == RESULT_OK) {
// If an operation has been re-queued via this mechanism, let the
// IdentityBuilder attempt to extract some updated state from the intent before
// trying to re-launch the operation.
if (identityBuilder != null) {
identityBuilder.consume(data);
}
launchGitOperation(requestCode);
}
super.onActivityResult(requestCode, resultCode, data);
}
}

View file

@ -0,0 +1,660 @@
package com.zeapo.pwdstore.git
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView
import androidx.preference.PreferenceManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.utils.PasswordRepository
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.lib.Constants
import java.io.File
import java.io.IOException
import java.util.regex.Pattern
open class GitActivity : AppCompatActivity() {
private lateinit var context: Context
private lateinit var settings: SharedPreferences
private lateinit var protocol: String
private lateinit var connectionMode: String
private lateinit var hostname: String
private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null
private var identity: SshApiSessionFactory.ApiIdentity? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context = requireNotNull(this)
settings = PreferenceManager.getDefaultSharedPreferences(this)
protocol = settings.getString("git_remote_protocol", null) ?: "ssh://"
connectionMode = settings.getString("git_remote_auth", null) ?: "ssh-key"
hostname = settings.getString("git_remote_location", null) ?: ""
val operationCode = intent.extras!!.getInt("Operation")
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
when (operationCode) {
REQUEST_CLONE, EDIT_SERVER -> {
setContentView(R.layout.activity_git_clone)
setTitle(R.string.title_activity_git_clone)
val protcolSpinner = findViewById<Spinner>(R.id.clone_protocol)
val connectionModeSpinner = findViewById<Spinner>(R.id.connection_mode)
// init the spinner for connection modes
val connectionModeAdapter = ArrayAdapter.createFromResource(this,
R.array.connection_modes, android.R.layout.simple_spinner_item)
connectionModeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
connectionModeSpinner.adapter = connectionModeAdapter
connectionModeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
val selection = (findViewById<View>(R.id.connection_mode) as Spinner).selectedItem.toString()
connectionMode = selection
settings.edit().putString("git_remote_auth", selection).apply()
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
}
}
// init the spinner for protocols
val protocolAdapter = ArrayAdapter.createFromResource(this,
R.array.clone_protocols, android.R.layout.simple_spinner_item)
protocolAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
protcolSpinner.adapter = protocolAdapter
protcolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
protocol = (findViewById<View>(R.id.clone_protocol) as Spinner).selectedItem.toString()
if (protocol == "ssh://") {
// select ssh-key auth mode as default and enable the spinner in case it was disabled
connectionModeSpinner.setSelection(0)
connectionModeSpinner.isEnabled = true
// however, if we have some saved that, that's more important!
when {
connectionMode.equals("ssh-key", ignoreCase = true) -> connectionModeSpinner.setSelection(0)
connectionMode.equals("OpenKeychain", ignoreCase = true) -> connectionModeSpinner.setSelection(2)
else -> connectionModeSpinner.setSelection(1)
}
} else {
// select user/pwd auth-mode and disable the spinner
connectionModeSpinner.setSelection(1)
connectionModeSpinner.isEnabled = false
}
updateURI()
}
override fun onNothingSelected(adapterView: AdapterView<*>) {
}
}
if (protocol == "ssh://") {
protcolSpinner.setSelection(0)
} else {
protcolSpinner.setSelection(1)
}
// init the server information
val serverUrl = findViewById<TextInputEditText>(R.id.server_url)
val serverPort = findViewById<TextInputEditText>(R.id.server_port)
val serverPath = findViewById<TextInputEditText>(R.id.server_path)
val serverUser = findViewById<TextInputEditText>(R.id.server_user)
val serverUri = findViewById<TextInputEditText>(R.id.clone_uri)
serverUrl.setText(settings.getString("git_remote_server", ""))
serverPort.setText(settings.getString("git_remote_port", ""))
serverUser.setText(settings.getString("git_remote_username", ""))
serverPath.setText(settings.getString("git_remote_location", ""))
serverUrl.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
if (serverUrl.isFocused)
updateURI()
}
override fun afterTextChanged(editable: Editable) {}
})
serverPort.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
if (serverPort.isFocused)
updateURI()
}
override fun afterTextChanged(editable: Editable) {}
})
serverUser.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
if (serverUser.isFocused)
updateURI()
}
override fun afterTextChanged(editable: Editable) {}
})
serverPath.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
if (serverPath.isFocused)
updateURI()
}
override fun afterTextChanged(editable: Editable) {}
})
serverUri.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i2: Int, i3: Int) {
if (serverUri.isFocused)
splitURI()
}
override fun afterTextChanged(editable: Editable) {}
})
if (operationCode == EDIT_SERVER) {
findViewById<View>(R.id.clone_button).visibility = View.INVISIBLE
findViewById<View>(R.id.save_button).visibility = View.VISIBLE
} else {
findViewById<View>(R.id.clone_button).visibility = View.VISIBLE
findViewById<View>(R.id.save_button).visibility = View.INVISIBLE
}
updateURI()
}
EDIT_GIT_CONFIG -> {
setContentView(R.layout.activity_git_config)
setTitle(R.string.title_activity_git_config)
showGitConfig()
}
REQUEST_PULL -> syncRepository(REQUEST_PULL)
REQUEST_PUSH -> syncRepository(REQUEST_PUSH)
REQUEST_SYNC -> syncRepository(REQUEST_SYNC)
}
}
/**
* Fills in the server_uri field with the information coming from other fields
*/
private fun updateURI() {
val uri = findViewById<TextInputEditText>(R.id.clone_uri)
val serverUrl = findViewById<TextInputEditText>(R.id.server_url)
val serverPort = findViewById<TextInputEditText>(R.id.server_port)
val serverPath = findViewById<TextInputEditText>(R.id.server_path)
val serverUser = findViewById<TextInputEditText>(R.id.server_user)
if (uri != null) {
when (protocol) {
"ssh://" -> {
var hostname = (serverUser.text.toString()
+ "@" +
serverUrl.text.toString().trim { it <= ' ' }
+ ":")
if (serverPort.text.toString() == "22") {
hostname += serverPath.text.toString()
findViewById<View>(R.id.warn_url).visibility = View.GONE
} else {
val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url)
if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) {
warnUrl.setText(R.string.warn_malformed_url_port)
warnUrl.visibility = View.VISIBLE
} else {
warnUrl.visibility = View.GONE
}
hostname += serverPort.text.toString() + serverPath.text.toString()
}
if (hostname != "@:") uri.setText(hostname)
}
"https://" -> {
val hostname = StringBuilder()
hostname.append(serverUrl.text.toString().trim { it <= ' ' })
if (serverPort.text.toString() == "443") {
hostname.append(serverPath.text.toString())
findViewById<View>(R.id.warn_url).visibility = View.GONE
} else {
hostname.append("/")
hostname.append(serverPort.text.toString())
.append(serverPath.text.toString())
}
if (hostname.toString() != "@/") uri.setText(hostname)
}
else -> {
}
}
}
}
/**
* Splits the information in server_uri into the other fields
*/
private fun splitURI() {
val serverUri = findViewById<TextInputEditText>(R.id.clone_uri)
val serverUrl = findViewById<TextInputEditText>(R.id.server_url)
val serverPort = findViewById<TextInputEditText>(R.id.server_port)
val serverPath = findViewById<TextInputEditText>(R.id.server_path)
val serverUser = findViewById<TextInputEditText>(R.id.server_user)
val uri = serverUri.text.toString()
val pattern = Pattern.compile("(.+)@([\\w\\d.]+):([\\d]+)*(.*)")
val matcher = pattern.matcher(uri)
if (matcher.find()) {
val count = matcher.groupCount()
if (count > 1) {
serverUser.setText(matcher.group(1))
serverUrl.setText(matcher.group(2))
}
if (count == 4) {
serverPort.setText(matcher.group(3))
serverPath.setText(matcher.group(4))
val warnUrl = findViewById<AppCompatTextView>(R.id.warn_url)
if (!serverPath.text.toString().matches("/.*".toRegex()) && serverPort.text.toString().isNotEmpty()) {
warnUrl.setText(R.string.warn_malformed_url_port)
warnUrl.visibility = View.VISIBLE
} else {
warnUrl.visibility = View.GONE
}
}
}
}
public override fun onResume() {
super.onResume()
updateURI()
}
override fun onDestroy() {
// Do not leak the service connection
if (identityBuilder != null) {
identityBuilder!!.close()
identityBuilder = null
}
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.git_clone, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.user_pref -> try {
val intent = Intent(this, UserPreference::class.java)
startActivity(intent)
return true
} catch (e: Exception) {
println("Exception caught :(")
e.printStackTrace()
}
android.R.id.home -> {
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
/**
* Saves the configuration found in the form
*/
private fun saveConfiguration(): Boolean {
// remember the settings
val editor = settings.edit()
editor.putString("git_remote_server", (findViewById<View>(R.id.server_url) as TextInputEditText).text.toString())
editor.putString("git_remote_location", (findViewById<View>(R.id.server_path) as TextInputEditText).text.toString())
editor.putString("git_remote_username", (findViewById<View>(R.id.server_user) as TextInputEditText).text.toString())
editor.putString("git_remote_protocol", protocol)
editor.putString("git_remote_auth", connectionMode)
editor.putString("git_remote_port", (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString())
editor.putString("git_remote_uri", (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString())
// 'save' hostname variable for use by addRemote() either here or later
// in syncRepository()
hostname = (findViewById<View>(R.id.clone_uri) as TextInputEditText).text.toString()
val port = (findViewById<View>(R.id.server_port) as TextInputEditText).text.toString()
// don't ask the user, take off the protocol that he puts in
hostname = hostname.replaceFirst("^.+://".toRegex(), "")
(findViewById<View>(R.id.clone_uri) as TextInputEditText).setText(hostname)
if (protocol != "ssh://") {
hostname = protocol + hostname
} else {
// if the port is explicitly given, jgit requires the ssh://
if (port.isNotEmpty() && port != "22")
hostname = protocol + hostname
// did he forget the username?
if (!hostname.matches("^.+@.+".toRegex())) {
MaterialAlertDialogBuilder(this)
.setMessage(context.getString(R.string.forget_username_dialog_text))
.setPositiveButton(context.getString(R.string.dialog_oops), null)
.show()
return false
}
}
if (PasswordRepository.isInitialized && settings.getBoolean("repository_initialized", false)) {
// don't just use the clone_uri text, need to use hostname which has
// had the proper protocol prepended
PasswordRepository.addRemote("origin", hostname, true)
}
editor.apply()
return true
}
/**
* Save the repository information to the shared preferences settings
*/
@Suppress("UNUSED_PARAMETER")
fun saveConfiguration(view: View) {
if (!saveConfiguration())
return
finish()
}
private fun showGitConfig() {
// init the server information
val username = findViewById<TextInputEditText>(R.id.git_user_name)
val email = findViewById<TextInputEditText>(R.id.git_user_email)
val abort = findViewById<MaterialButton>(R.id.git_abort_rebase)
username.setText(settings.getString("git_config_user_name", ""))
email.setText(settings.getString("git_config_user_email", ""))
// git status
val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory(context))
if (repo != null) {
val commitHash = findViewById<AppCompatTextView>(R.id.git_commit_hash)
try {
val objectId = repo.resolve(Constants.HEAD)
val ref = repo.getRef("refs/heads/master")
val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED"
commitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head)
// enable the abort button only if we're rebasing
val isRebasing = repo.repositoryState.isRebasing
abort.isEnabled = isRebasing
abort.alpha = if (isRebasing) 1.0f else 0.5f
} catch (e: Exception) {
// ignore
}
}
}
private fun saveGitConfigs(): Boolean {
// remember the settings
val editor = settings.edit()
val email = (findViewById<View>(R.id.git_user_email) as TextInputEditText).text!!.toString()
editor.putString("git_config_user_email", email)
editor.putString("git_config_user_name", (findViewById<View>(R.id.git_user_name) as TextInputEditText).text.toString())
if (!email.matches(emailPattern.toRegex())) {
MaterialAlertDialogBuilder(this)
.setMessage(context.getString(R.string.invalid_email_dialog_text))
.setPositiveButton(context.getString(R.string.dialog_oops), null)
.show()
return false
}
editor.apply()
return true
}
@Suppress("UNUSED_PARAMETER")
fun applyGitConfigs(view: View) {
if (!saveGitConfigs())
return
PasswordRepository.setUserName(settings.getString("git_config_user_name", null) ?: "")
PasswordRepository.setUserEmail(settings.getString("git_config_user_email", null) ?: "")
finish()
}
@Suppress("UNUSED_PARAMETER")
fun abortRebase(view: View) {
launchGitOperation(BREAK_OUT_OF_DETACHED)
}
@Suppress("UNUSED_PARAMETER")
fun resetToRemote(view: View) {
launchGitOperation(REQUEST_RESET)
}
/**
* Clones the repository, the directory exists, deletes it
*/
@Suppress("UNUSED_PARAMETER")
fun cloneRepository(view: View) {
if (PasswordRepository.getRepository(null) == null) {
PasswordRepository.initialize(this)
}
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context))
if (!saveConfiguration())
return
// Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
if (localDir.exists() && localDir.listFiles()!!.isNotEmpty()
&& !(localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git")) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_delete_title)
.setMessage(resources.getString(R.string.dialog_delete_msg) + " " + localDir.toString())
.setCancelable(false)
.setPositiveButton(R.string.dialog_delete
) { dialog, _ ->
try {
FileUtils.deleteDirectory(localDir)
launchGitOperation(REQUEST_CLONE)
} catch (e: IOException) {
//TODO Handle the exception correctly if we are unable to delete the directory...
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
dialog.cancel()
}
.setNegativeButton(R.string.dialog_do_not_delete
) { dialog, _ -> dialog.cancel() }
.show()
} else {
try {
// Silently delete & replace the lone .git folder if it exists
if (localDir.exists() && localDir.listFiles()!!.size == 1 && localDir.listFiles()!![0].name == ".git") {
try {
FileUtils.deleteDirectory(localDir)
} catch (e: IOException) {
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
}
} catch (e: Exception) {
//This is what happens when jgit fails :(
//TODO Handle the diffent cases of exceptions
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
launchGitOperation(REQUEST_CLONE)
}
}
/**
* Syncs the local repository with the remote one (either pull or push)
*
* @param operation the operation to execute can be REQUEST_PULL or REQUEST_PUSH
*/
private fun syncRepository(operation: Int) {
if (settings.getString("git_remote_username", "")!!.isEmpty() ||
settings.getString("git_remote_server", "")!!.isEmpty() ||
settings.getString("git_remote_location", "")!!.isEmpty())
MaterialAlertDialogBuilder(this)
.setMessage(context.getString(R.string.set_information_dialog_text))
.setPositiveButton(context.getString(R.string.dialog_positive)) { _, _ ->
val intent = Intent(context, UserPreference::class.java)
startActivityForResult(intent, REQUEST_PULL)
}
.setNegativeButton(context.getString(R.string.dialog_negative)) { _, _ ->
// do nothing :(
setResult(AppCompatActivity.RESULT_OK)
finish()
}
.show()
else {
// check that the remote origin is here, else add it
PasswordRepository.addRemote("origin", hostname, false)
launchGitOperation(operation)
}
}
/**
* Attempt to launch the requested GIT operation. Depending on the configured auth, it may not
* be possible to launch the operation immediately. In that case, this function may launch an
* intermediate activity instead, which will gather necessary information and post it back via
* onActivityResult, which will then re-call this function. This may happen multiple times,
* until either an error is encountered or the operation is successfully launched.
*
* @param operation The type of GIT operation to launch
*/
private fun launchGitOperation(operation: Int) {
val op: GitOperation
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory(context))
try {
// Before launching the operation with OpenKeychain auth, we need to issue several requests
// to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
// we just need to keep calling it until it returns a completed ApiIdentity.
if (connectionMode.equals("OpenKeychain", ignoreCase = true) && identity == null) {
// Lazy initialization of the IdentityBuilder
if (identityBuilder == null) {
identityBuilder = SshApiSessionFactory.IdentityBuilder(this)
}
// Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure
// that onActivityResult is called with operation again, which will re-invoke us here
identity = identityBuilder!!.tryBuild(operation)
if (identity == null)
return
}
when (operation) {
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> op = CloneOperation(localDir, this).setCommand(hostname)
REQUEST_PULL -> op = PullOperation(localDir, this).setCommand()
REQUEST_PUSH -> op = PushOperation(localDir, this).setCommand()
REQUEST_SYNC -> op = SyncOperation(localDir, this).setCommands()
BREAK_OUT_OF_DETACHED -> op = BreakOutOfDetached(localDir, this).setCommands()
REQUEST_RESET -> op = ResetToRemoteOperation(localDir, this).setCommands()
SshApiSessionFactory.POST_SIGNATURE -> return
else -> {
Log.e(TAG, "Operation not recognized : $operation")
setResult(AppCompatActivity.RESULT_CANCELED)
finish()
return
}
}
op.executeAfterAuthentication(connectionMode,
settings.getString("git_remote_username", "git")!!,
File("$filesDir/.ssh_key"),
identity)
} catch (e: Exception) {
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int,
data: Intent?) {
// In addition to the pre-operation-launch series of intents for OpenKeychain auth
// that will pass through here and back to launchGitOperation, there is one
// synchronous operation that happens /after/ the operation has been launched in the
// background thread - the actual signing of the SSH challenge. We pass through the
// completed signature to the ApiIdentity, which will be blocked in the other thread
// waiting for it.
if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null)
identity!!.postSignature(data)
if (resultCode == AppCompatActivity.RESULT_CANCELED) {
setResult(AppCompatActivity.RESULT_CANCELED)
finish()
} else if (resultCode == AppCompatActivity.RESULT_OK) {
// If an operation has been re-queued via this mechanism, let the
// IdentityBuilder attempt to extract some updated state from the intent before
// trying to re-launch the operation.
if (identityBuilder != null) {
identityBuilder!!.consume(data)
}
launchGitOperation(requestCode)
}
super.onActivityResult(requestCode, resultCode, data)
}
companion object {
const val REQUEST_PULL = 101
const val REQUEST_PUSH = 102
const val REQUEST_CLONE = 103
const val REQUEST_INIT = 104
const val EDIT_SERVER = 105
const val REQUEST_SYNC = 106
@Suppress("Unused")
const val REQUEST_CREATE = 107
const val EDIT_GIT_CONFIG = 108
const val BREAK_OUT_OF_DETACHED = 109
const val REQUEST_RESET = 110
private const val TAG = "GitAct"
private const val emailPattern = "^[^@]+@[^@]+$"
}
}

View file

@ -3,15 +3,14 @@ package com.zeapo.pwdstore.git
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.preference.PreferenceManager
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.widget.CheckBox
import android.widget.EditText
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jcraft.jsch.JSch
import com.jcraft.jsch.JSchException
import com.jcraft.jsch.KeyPair
@ -21,12 +20,10 @@ import com.zeapo.pwdstore.git.config.GitConfigSessionFactory
import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.config.SshConfigSessionFactory
import com.zeapo.pwdstore.utils.PasswordRepository
import org.eclipse.jgit.api.GitCommand
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.SshSessionFactory
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
import java.io.File
/**
@ -37,7 +34,7 @@ import java.io.File
*/
abstract class GitOperation(fileDir: File, internal val callingActivity: Activity) {
protected val repository: Repository = PasswordRepository.getRepository(fileDir)
protected val repository: Repository? = PasswordRepository.getRepository(fileDir)
internal var provider: UsernamePasswordCredentialsProvider? = null
internal var command: GitCommand<*>? = null
@ -117,7 +114,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
showError: Boolean) {
if (connectionMode.equals("ssh-key", ignoreCase = true)) {
if (sshKey == null || !sshKey.exists()) {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
.setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ ->
@ -170,7 +167,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
executeAfterAuthentication(connectionMode, username, sshKey, identity, true)
}
} else {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title))
.setMessage(callingActivity.resources.getString(R.string.passphrase_dialog_text))
.setView(dialogView)
@ -195,10 +192,11 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
setAuthentication(sshKey, username, "").execute()
}
} catch (e: JSchException) {
AlertDialog.Builder(callingActivity)
e.printStackTrace()
MaterialAlertDialogBuilder(callingActivity)
.setTitle("Unable to open the ssh-key")
.setMessage("Please check that it was imported.")
.setPositiveButton("Ok") { _, _ -> }
.setPositiveButton("Ok") { _, _ -> callingActivity.finish() }
.show()
}
@ -211,7 +209,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
password.width = LinearLayout.LayoutParams.MATCH_PARENT
password.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.passphrase_dialog_title))
.setMessage(callingActivity.resources.getString(R.string.password_dialog_text))
.setView(password)
@ -231,7 +229,7 @@ abstract class GitOperation(fileDir: File, internal val callingActivity: Activit
* Action to execute on error
*/
open fun onError(errorMessage: String) {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage(callingActivity.resources.getString(R.string.jgit_error_dialog_text) + errorMessage)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->

View file

@ -1,11 +1,10 @@
package com.zeapo.pwdstore.git
import android.app.Activity
import android.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.PullCommand
import java.io.File
/**
@ -30,14 +29,12 @@ class PullOperation(fileDir: File, callingActivity: Activity) : GitOperation(fil
}
override fun execute() {
if (this.provider != null) {
(this.command as PullCommand).setCredentialsProvider(this.provider)
}
(this.command as? PullCommand)?.setCredentialsProvider(this.provider)
GitAsyncTask(callingActivity, true, false, this).execute(this.command)
}
override fun onError(errorMessage: String) {
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occured during the pull operation, "
+ callingActivity.resources.getString(R.string.jgit_error_dialog_text)

View file

@ -1,11 +1,10 @@
package com.zeapo.pwdstore.git
import android.app.Activity
import android.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.PushCommand
import java.io.File
/**
@ -30,15 +29,13 @@ class PushOperation(fileDir: File, callingActivity: Activity) : GitOperation(fil
}
override fun execute() {
if (this.provider != null) {
(this.command as PushCommand).setCredentialsProvider(this.provider)
}
(this.command as? PushCommand)?.setCredentialsProvider(this.provider)
GitAsyncTask(callingActivity, true, false, this).execute(this.command)
}
override fun onError(errorMessage: String) {
// TODO handle the "Nothing to push" case
AlertDialog.Builder(callingActivity)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage(callingActivity.getString(R.string.jgit_error_push_dialog_text) + errorMessage)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }

View file

@ -0,0 +1,52 @@
package com.zeapo.pwdstore.git
import android.app.Activity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import org.eclipse.jgit.api.AddCommand
import org.eclipse.jgit.api.FetchCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ResetCommand
import java.io.File
/**
* Creates a new git operation
*
* @param fileDir the git working tree directory
* @param callingActivity the calling activity
*/
class ResetToRemoteOperation(fileDir: File, callingActivity: Activity) : GitOperation(fileDir, callingActivity) {
private var addCommand: AddCommand? = null
private var fetchCommand: FetchCommand? = null
private var resetCommand: ResetCommand? = null
/**
* Sets the command
*
* @return the current object
*/
fun setCommands(): ResetToRemoteOperation {
val git = Git(repository)
this.addCommand = git.add().addFilepattern(".")
this.fetchCommand = git.fetch().setRemote("origin")
this.resetCommand = git.reset().setRef("origin/master").setMode(ResetCommand.ResetType.HARD)
return this
}
override fun execute() {
this.fetchCommand?.setCredentialsProvider(this.provider)
GitAsyncTask(callingActivity, true, false, this)
.execute(this.addCommand, this.fetchCommand, this.resetCommand)
}
override fun onError(errorMessage: String) {
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occured during the sync operation, "
+ "\nPlease check the FAQ for possible reasons why this error might occur."
+ callingActivity.resources.getString(R.string.jgit_error_dialog_text)
+ errorMessage)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> }
.show()
}
}

View file

@ -1,7 +1,7 @@
package com.zeapo.pwdstore.git
import android.app.Activity
import android.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import org.eclipse.jgit.api.AddCommand
import org.eclipse.jgit.api.CommitCommand
@ -9,7 +9,6 @@ import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.PullCommand
import org.eclipse.jgit.api.PushCommand
import org.eclipse.jgit.api.StatusCommand
import java.io.File
/**
@ -42,14 +41,15 @@ class SyncOperation(fileDir: File, callingActivity: Activity) : GitOperation(fil
override fun execute() {
if (this.provider != null) {
this.pullCommand!!.setCredentialsProvider(this.provider)
this.pushCommand!!.setCredentialsProvider(this.provider)
this.pullCommand?.setCredentialsProvider(this.provider)
this.pushCommand?.setCredentialsProvider(this.provider)
}
GitAsyncTask(callingActivity, true, false, this).execute(this.addCommand, this.statusCommand, this.commitCommand, this.pullCommand, this.pushCommand)
}
override fun onError(errorMessage: String) {
AlertDialog.Builder(callingActivity).setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
.setMessage("Error occured during the sync operation, "
+ "\nPlease check the FAQ for possible reasons why this error might occur."
+ callingActivity.resources.getString(R.string.jgit_error_dialog_text)

View file

@ -5,8 +5,9 @@ import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender;
import androidx.appcompat.app.AlertDialog;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.jcraft.jsch.Identity;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
@ -49,9 +50,9 @@ public class SshApiSessionFactory extends GitConfigSessionFactory {
this.identity = identity;
}
@NonNull
@Override
protected JSch
getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException {
protected JSch getJSch(@NonNull final OpenSshConfig.Host hc, @NonNull FS fs) throws JSchException {
JSch jsch = super.getJSch(hc, fs);
jsch.removeAllIdentity();
jsch.addIdentity(identity, null);
@ -59,7 +60,7 @@ public class SshApiSessionFactory extends GitConfigSessionFactory {
}
@Override
protected void configure(OpenSshConfig.Host hc, Session session) {
protected void configure(@NonNull OpenSshConfig.Host hc, Session session) {
session.setConfig("StrictHostKeyChecking", "no");
session.setConfig("PreferredAuthentications", "publickey");
@ -204,8 +205,9 @@ public class SshApiSessionFactory extends GitConfigSessionFactory {
@Override
public void onError() {
new AlertDialog.Builder(callingActivity).setMessage(callingActivity.getString(
R.string.openkeychain_ssh_api_connect_fail)).show();
new MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.getString(
R.string.openkeychain_ssh_api_connect_fail)).show();
}
});

View file

@ -1,155 +0,0 @@
package com.zeapo.pwdstore.utils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.zeapo.pwdstore.R;
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;
public abstract class EntryRecyclerAdapter extends RecyclerView.Adapter<EntryRecyclerAdapter.ViewHolder> {
final Set<Integer> selectedItems = new TreeSet<>();
private final Activity activity;
private final ArrayList<PasswordItem> values;
EntryRecyclerAdapter(Activity activity, ArrayList<PasswordItem> values) {
this.activity = activity;
this.values = values;
}
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return values.size();
}
public ArrayList<PasswordItem> getValues() {
return this.values;
}
public void clear() {
this.values.clear();
this.notifyDataSetChanged();
}
public void addAll(ArrayList<PasswordItem> list) {
this.values.addAll(list);
this.notifyDataSetChanged();
}
public void add(PasswordItem item) {
this.values.add(item);
this.notifyItemInserted(getItemCount());
}
void toggleSelection(int position) {
if (!selectedItems.remove(position)) {
selectedItems.add(position);
}
}
// use this after an item is removed to update the positions of items in set
// that followed the removed position
public void updateSelectedItems(int position, Set<Integer> selectedItems) {
Set<Integer> temp = new TreeSet<>();
for (int selected : selectedItems) {
if (selected > position) {
temp.add(selected - 1);
} else {
temp.add(selected);
}
}
selectedItems.clear();
selectedItems.addAll(temp);
}
public void remove(int position) {
this.values.remove(position);
this.notifyItemRemoved(position);
// keep selectedItems updated so we know what to notifyItemChanged
// (instead of just using notifyDataSetChanged)
updateSelectedItems(position, selectedItems);
}
@NonNull
View.OnLongClickListener getOnLongClickListener(ViewHolder holder, PasswordItem pass) {
return v -> false;
}
// Replace the contents of a view (invoked by the layout manager)
@SuppressLint("SetTextI18n")
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
final PasswordItem pass = getValues().get(position);
holder.name.setText(pass.toString());
if (pass.getType() == PasswordItem.TYPE_CATEGORY) {
holder.typeImage.setImageResource(R.drawable.ic_folder_grey600_24dp);
} else {
holder.typeImage.setImageResource(R.drawable.ic_action_secure);
holder.name.setText(pass.toString());
}
holder.type.setText(pass.getFullPathToParent().replaceAll("(^/)|(/$)", ""));
holder.view.setOnClickListener(getOnClickListener(holder, pass));
holder.view.setOnLongClickListener(getOnLongClickListener(holder, pass));
// after removal, everything is rebound for some reason; views are shuffled?
boolean selected = selectedItems.contains(position);
holder.view.setSelected(selected);
if (selected) {
holder.itemView.setBackgroundResource(R.color.deep_orange_200);
holder.type.setTextColor(Color.BLACK);
} else {
holder.itemView.setBackgroundColor(Color.alpha(1));
holder.type.setTextColor(ContextCompat.getColor(activity, R.color.grey_500));
}
}
@NonNull
protected abstract View.OnClickListener getOnClickListener(ViewHolder holder, PasswordItem pass);
// Create new views (invoked by the layout manager)
@Override
@NonNull
public PasswordRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_row_layout, parent, false);
return new ViewHolder(v);
}
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
static class ViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public final View view;
public final TextView name;
final TextView type;
final ImageView typeImage;
ViewHolder(View v) {
super(v);
view = v;
name = view.findViewById(R.id.label);
type = view.findViewById(R.id.type);
typeImage = view.findViewById(R.id.type_image);
}
}
}

View file

@ -0,0 +1,118 @@
package com.zeapo.pwdstore.utils
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.recyclerview.widget.RecyclerView
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.widget.MultiselectableLinearLayout
import java.util.ArrayList
import java.util.TreeSet
abstract class EntryRecyclerAdapter internal constructor(val values: ArrayList<PasswordItem>) : RecyclerView.Adapter<EntryRecyclerAdapter.ViewHolder>() {
internal val selectedItems: MutableSet<Int> = TreeSet()
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount(): Int {
return values.size
}
fun clear() {
this.values.clear()
this.notifyDataSetChanged()
}
fun addAll(list: ArrayList<PasswordItem>) {
this.values.addAll(list)
this.notifyDataSetChanged()
}
fun add(item: PasswordItem) {
this.values.add(item)
this.notifyItemInserted(itemCount)
}
internal fun toggleSelection(position: Int) {
if (!selectedItems.remove(position)) {
selectedItems.add(position)
}
}
// use this after an item is removed to update the positions of items in set
// that followed the removed position
fun updateSelectedItems(position: Int, selectedItems: MutableSet<Int>) {
val temp = TreeSet<Int>()
for (selected in selectedItems) {
if (selected > position) {
temp.add(selected - 1)
} else {
temp.add(selected)
}
}
selectedItems.clear()
selectedItems.addAll(temp)
}
fun remove(position: Int) {
this.values.removeAt(position)
this.notifyItemRemoved(position)
// keep selectedItems updated so we know what to notifyItemChanged
// (instead of just using notifyDataSetChanged)
updateSelectedItems(position, selectedItems)
}
internal open fun getOnLongClickListener(holder: ViewHolder, pass: PasswordItem): View.OnLongClickListener {
return View.OnLongClickListener { false }
}
// Replace the contents of a view (invoked by the layout manager)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val pass = values[position]
holder.name.text = pass.toString()
if (pass.type == PasswordItem.TYPE_CATEGORY) {
holder.typeImage.setImageResource(R.drawable.ic_folder_tinted_24dp)
} else {
holder.typeImage.setImageResource(R.drawable.ic_action_secure)
holder.name.text = pass.toString()
}
holder.type.text = pass.fullPathToParent.replace("(^/)|(/$)".toRegex(), "")
holder.view.setOnClickListener(getOnClickListener(holder, pass))
holder.view.setOnLongClickListener(getOnLongClickListener(holder, pass))
// after removal, everything is rebound for some reason; views are shuffled?
val selected = selectedItems.contains(position)
holder.view.isSelected = selected
(holder.itemView as MultiselectableLinearLayout).setMultiSelected(selected)
}
protected abstract fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener
// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): ViewHolder {
// create a new view
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.password_row_layout, parent, false)
return ViewHolder(v)
}
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
class ViewHolder(// each data item is just a string in this case
val view: View) : RecyclerView.ViewHolder(view) {
val name: AppCompatTextView = view.findViewById(R.id.label)
val type: AppCompatTextView = view.findViewById(R.id.type)
val typeImage: AppCompatImageView = view.findViewById(R.id.type_image)
}
}

View file

@ -1,5 +1,14 @@
package com.zeapo.pwdstore.utils
import android.content.Context
import android.util.TypedValue
fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
fun Context.resolveAttribute(attr: Int): Int {
val typedValue = TypedValue()
this.theme.resolveAttribute(attr, typedValue, true)
return typedValue.data
}

View file

@ -1,29 +0,0 @@
package com.zeapo.pwdstore.utils;
import android.view.View;
import androidx.annotation.NonNull;
import com.zeapo.pwdstore.SelectFolderActivity;
import com.zeapo.pwdstore.SelectFolderFragment;
import java.util.ArrayList;
public class FolderRecyclerAdapter extends EntryRecyclerAdapter {
private final SelectFolderFragment.OnFragmentInteractionListener listener;
// Provide a suitable constructor (depends on the kind of dataset)
public FolderRecyclerAdapter(SelectFolderActivity activity, SelectFolderFragment.OnFragmentInteractionListener listener, ArrayList<PasswordItem> values) {
super(activity, values);
this.listener = listener;
}
@NonNull
protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) {
return v -> {
listener.onFragmentInteraction(pass);
notifyItemChanged(holder.getAdapterPosition());
};
}
}

View file

@ -0,0 +1,20 @@
package com.zeapo.pwdstore.utils
import android.view.View
import com.zeapo.pwdstore.SelectFolderFragment
import java.util.ArrayList
class FolderRecyclerAdapter(private val listener: SelectFolderFragment.OnFragmentInteractionListener,
values: ArrayList<PasswordItem>
) : EntryRecyclerAdapter(values) {
override fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener {
return View.OnClickListener {
listener.onFragmentInteraction(pass)
notifyItemChanged(holder.adapterPosition)
}
}
}

View file

@ -1,110 +0,0 @@
package com.zeapo.pwdstore.utils;
import androidx.annotation.NonNull;
import com.zeapo.pwdstore.crypto.PgpActivity;
import java.io.File;
public class PasswordItem implements Comparable {
public final static char TYPE_CATEGORY = 'c';
public final static char TYPE_PASSWORD = 'p';
private final char type;
private final String name;
private final PasswordItem parent;
private final File file;
private final String fullPathToParent;
private final String longName;
/**
* Create a password item
* <p>
* Make it protected so that we use a builder
*/
private PasswordItem(String name, PasswordItem parent, char type, File file, File rootDir) {
this.name = name;
this.parent = parent;
this.type = type;
this.file = file;
fullPathToParent = file.getAbsolutePath()
.replace(rootDir.getAbsolutePath(), "")
.replace(file.getName(), "");
longName = PgpActivity.getLongName(fullPathToParent, rootDir.getAbsolutePath(), toString());
}
/**
* Create a new Category item
*/
public static PasswordItem newCategory(String name, File file, PasswordItem parent, File rootDir) {
return new PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir);
}
/**
* Create a new parentless category item
*/
public static PasswordItem newCategory(String name, File file, File rootDir) {
return new PasswordItem(name, null, TYPE_CATEGORY, file, rootDir);
}
/**
* Create a new password item
*/
public static PasswordItem newPassword(String name, File file, PasswordItem parent, File rootDir) {
return new PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir);
}
/**
* Create a new parentless password item
*/
public static PasswordItem newPassword(String name, File file, File rootDir) {
return new PasswordItem(name, null, TYPE_PASSWORD, file, rootDir);
}
public char getType() {
return this.type;
}
String getName() {
return this.name;
}
public PasswordItem getParent() {
return this.parent;
}
public File getFile() {
return this.file;
}
public String getFullPathToParent() {
return this.fullPathToParent;
}
public String getLongName() {
return longName;
}
@NonNull
@Override
public String toString() {
return this.getName().replace(".gpg", "");
}
@Override
public boolean equals(Object o) {
// Makes it possible to have a category and a password with the same name
return o != null
&& o.getClass() == PasswordItem.class
&& ((PasswordItem) o).getFile().equals(this.getFile());
}
@Override
public int compareTo(@NonNull Object o) {
PasswordItem other = (PasswordItem) o;
// Appending the type will make the sort type dependent
return (this.getType() + this.getName())
.compareToIgnoreCase(other.getType() + other.getName());
}
}

View file

@ -0,0 +1,81 @@
package com.zeapo.pwdstore.utils
import com.zeapo.pwdstore.crypto.PgpActivity
import java.io.File
data class PasswordItem(
val name: String,
val parent: PasswordItem? = null,
val type: Char,
val file: File,
val rootDir: File
) : Comparable<PasswordItem> {
val fullPathToParent = file.absolutePath
.replace(rootDir.absolutePath, "")
.replace(file.name, "")
val longName = PgpActivity.getLongName(
fullPathToParent,
rootDir.absolutePath,
toString())
override fun equals(other: Any?): Boolean {
return (other is PasswordItem) && (other.file == file)
}
override fun compareTo(other: PasswordItem): Int {
return (type + name).compareTo(other.type + other.name, ignoreCase = true)
}
override fun toString(): String {
return name.replace(".gpg", "")
}
override fun hashCode(): Int {
return 0
}
companion object {
const val TYPE_CATEGORY = 'c'
const val TYPE_PASSWORD = 'p'
@JvmStatic
fun newCategory(
name: String,
file: File,
parent: PasswordItem,
rootDir: File
): PasswordItem {
return PasswordItem(name, parent, TYPE_CATEGORY, file, rootDir)
}
@JvmStatic
fun newCategory(
name: String,
file: File,
rootDir: File
): PasswordItem {
return PasswordItem(name, null, TYPE_CATEGORY, file, rootDir)
}
@JvmStatic
fun newPassword(
name: String,
file: File,
parent: PasswordItem,
rootDir: File
): PasswordItem {
return PasswordItem(name, parent, TYPE_PASSWORD, file, rootDir)
}
@JvmStatic
fun newPassword(
name: String,
file: File,
rootDir: File
): PasswordItem {
return PasswordItem(name, null, TYPE_PASSWORD, file, rootDir)
}
}
}

View file

@ -1,133 +0,0 @@
package com.zeapo.pwdstore.utils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.view.ActionMode;
import com.zeapo.pwdstore.PasswordFragment;
import com.zeapo.pwdstore.PasswordStore;
import com.zeapo.pwdstore.R;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.TreeSet;
public class PasswordRecyclerAdapter extends EntryRecyclerAdapter {
private final PasswordStore activity;
private final PasswordFragment.OnFragmentInteractionListener listener;
public ActionMode mActionMode;
private Boolean canEdit;
private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
// Called when the action mode is created; startActionMode() was called
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
mode.getMenuInflater().inflate(R.menu.context_pass, menu);
// hide the fab
activity.findViewById(R.id.fab).setVisibility(View.GONE);
return true;
}
// Called each time the action mode is shown. Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (canEdit) {
menu.findItem(R.id.menu_edit_password).setVisible(true);
} else {
menu.findItem(R.id.menu_edit_password).setVisible(false);
}
return true; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_delete_password:
activity.deletePasswords(PasswordRecyclerAdapter.this, new TreeSet<>(selectedItems));
mode.finish(); // Action picked, so close the CAB
return true;
case R.id.menu_edit_password:
activity.editPassword(getValues().get(selectedItems.iterator().next()));
mode.finish();
return true;
case R.id.menu_move_password:
ArrayList<PasswordItem> selectedPasswords = new ArrayList<>();
for (Integer id : selectedItems) {
selectedPasswords.add(getValues().get(id));
}
activity.movePasswords(selectedPasswords);
default:
return false;
}
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
for (Iterator<Integer> it = selectedItems.iterator(); it.hasNext(); ) {
// need the setSelected line in onBind
notifyItemChanged(it.next());
it.remove();
}
mActionMode = null;
// show the fab
activity.findViewById(R.id.fab).setVisibility(View.VISIBLE);
}
};
// Provide a suitable constructor (depends on the kind of dataset)
public PasswordRecyclerAdapter(PasswordStore activity, PasswordFragment.OnFragmentInteractionListener listener, ArrayList<PasswordItem> values) {
super(activity, values);
this.activity = activity;
this.listener = listener;
}
@Override
@NonNull
protected View.OnLongClickListener getOnLongClickListener(final ViewHolder holder, final PasswordItem pass) {
return v -> {
if (mActionMode != null) {
return false;
}
toggleSelection(holder.getAdapterPosition());
canEdit = pass.getType() == PasswordItem.TYPE_PASSWORD;
// Start the CAB using the ActionMode.Callback
mActionMode = activity.startSupportActionMode(mActionModeCallback);
mActionMode.setTitle("" + selectedItems.size());
mActionMode.invalidate();
notifyItemChanged(holder.getAdapterPosition());
return true;
};
}
@Override
@NonNull
protected View.OnClickListener getOnClickListener(final ViewHolder holder, final PasswordItem pass) {
return v -> {
if (mActionMode != null) {
toggleSelection(holder.getAdapterPosition());
mActionMode.setTitle("" + selectedItems.size());
if (selectedItems.isEmpty()) {
mActionMode.finish();
} else if (selectedItems.size() == 1 && !canEdit) {
if (getValues().get(selectedItems.iterator().next()).getType() == PasswordItem.TYPE_PASSWORD) {
canEdit = true;
mActionMode.invalidate();
}
} else if (selectedItems.size() >= 1 && canEdit) {
canEdit = false;
mActionMode.invalidate();
}
} else {
listener.onFragmentInteraction(pass);
}
notifyItemChanged(holder.getAdapterPosition());
};
}
}

View file

@ -0,0 +1,116 @@
package com.zeapo.pwdstore.utils
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import com.zeapo.pwdstore.PasswordFragment
import com.zeapo.pwdstore.PasswordStore
import com.zeapo.pwdstore.R
import java.util.ArrayList
import java.util.TreeSet
class PasswordRecyclerAdapter(private val activity: PasswordStore,
private val listener: PasswordFragment.OnFragmentInteractionListener,
values: ArrayList<PasswordItem>
) : EntryRecyclerAdapter(values) {
var actionMode: ActionMode? = null
private var canEdit: Boolean = false
private val actionModeCallback = object : ActionMode.Callback {
// Called when the action mode is created; startActionMode() was called
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
// Inflate a menu resource providing context menu items
mode.menuInflater.inflate(R.menu.context_pass, menu)
// hide the fab
activity.findViewById<View>(R.id.fab).visibility = View.GONE
return true
}
// Called each time the action mode is shown. Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
menu.findItem(R.id.menu_edit_password).isVisible = canEdit
return true // Return false if nothing is done
}
// Called when the user selects a contextual menu item
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_delete_password -> {
activity.deletePasswords(this@PasswordRecyclerAdapter, TreeSet(selectedItems))
mode.finish() // Action picked, so close the CAB
return true
}
R.id.menu_edit_password -> {
activity.editPassword(values[selectedItems.iterator().next()])
mode.finish()
return true
}
R.id.menu_move_password -> {
val selectedPasswords = ArrayList<PasswordItem>()
for (id in selectedItems) {
selectedPasswords.add(values[id])
}
activity.movePasswords(selectedPasswords)
return false
}
else -> return false
}
}
// Called when the user exits the action mode
override fun onDestroyActionMode(mode: ActionMode) {
val it = selectedItems.iterator()
while (it.hasNext()) {
// need the setSelected line in onBind
notifyItemChanged(it.next())
it.remove()
}
actionMode = null
// show the fab
activity.findViewById<View>(R.id.fab).visibility = View.VISIBLE
}
}
override fun getOnLongClickListener(holder: ViewHolder, pass: PasswordItem): View.OnLongClickListener {
return View.OnLongClickListener {
if (actionMode != null) {
return@OnLongClickListener false
}
toggleSelection(holder.adapterPosition)
canEdit = pass.type == PasswordItem.TYPE_PASSWORD
// Start the CAB using the ActionMode.Callback
actionMode = activity.startSupportActionMode(actionModeCallback)
actionMode?.title = "" + selectedItems.size
actionMode?.invalidate()
notifyItemChanged(holder.adapterPosition)
true
}
}
override fun getOnClickListener(holder: ViewHolder, pass: PasswordItem): View.OnClickListener {
return View.OnClickListener {
if (actionMode != null) {
toggleSelection(holder.adapterPosition)
actionMode?.title = "" + selectedItems.size
if (selectedItems.isEmpty()) {
actionMode?.finish()
} else if (selectedItems.size == 1 && (canEdit.not())) {
if (values[selectedItems.iterator().next()].type == PasswordItem.TYPE_PASSWORD) {
canEdit = true
actionMode?.invalidate()
}
} else if (selectedItems.size >= 1 && canEdit) {
canEdit = false
actionMode?.invalidate()
}
} else {
listener.onFragmentInteraction(pass)
}
notifyItemChanged(holder.adapterPosition)
}
}
}

View file

@ -1,284 +0,0 @@
package com.zeapo.pwdstore.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import static java.util.Collections.sort;
public class PasswordRepository {
private static Repository repository;
protected PasswordRepository() {
}
/**
* Returns the git repository
*
* @param localDir needed only on the creation
* @return the git repository
*/
public static Repository getRepository(File localDir) {
if (repository == null && localDir != null) {
FileRepositoryBuilder builder = new FileRepositoryBuilder();
try {
repository = builder.setGitDir(localDir)
.readEnvironment()
.build();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return repository;
}
public static boolean isInitialized() {
return repository != null;
}
public static void createRepository(File localDir) throws Exception {
localDir.delete();
Git.init().setDirectory(localDir).call();
getRepository(localDir);
}
// TODO add multiple remotes support for pull/push
public static void addRemote(String name, String url, Boolean replace) {
StoredConfig storedConfig = repository.getConfig();
Set<String> remotes = storedConfig.getSubsections("remote");
if (!remotes.contains(name)) {
try {
URIish uri = new URIish(url);
RefSpec refSpec = new RefSpec("+refs/head/*:refs/remotes/" + name + "/*");
RemoteConfig remoteConfig = new RemoteConfig(storedConfig, name);
remoteConfig.addFetchRefSpec(refSpec);
remoteConfig.addPushRefSpec(refSpec);
remoteConfig.addURI(uri);
remoteConfig.addPushURI(uri);
remoteConfig.update(storedConfig);
storedConfig.save();
} catch (Exception e) {
e.printStackTrace();
}
} else if (replace) {
try {
URIish uri = new URIish(url);
RemoteConfig remoteConfig = new RemoteConfig(storedConfig, name);
// remove the first and eventually the only uri
if (remoteConfig.getURIs().size() > 0) {
remoteConfig.removeURI(remoteConfig.getURIs().get(0));
}
if (remoteConfig.getPushURIs().size() > 0) {
remoteConfig.removePushURI(remoteConfig.getPushURIs().get(0));
}
remoteConfig.addURI(uri);
remoteConfig.addPushURI(uri);
remoteConfig.update(storedConfig);
storedConfig.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void closeRepository() {
if (repository != null) repository.close();
repository = null;
}
public static File getRepositoryDirectory(Context context) {
File dir = null;
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
if (settings.getBoolean("git_external", false)) {
String external_repo = settings.getString("git_external_repo", null);
if (external_repo != null) {
dir = new File(external_repo);
}
} else {
dir = new File(context.getFilesDir() + "/store");
}
return dir;
}
public static Repository initialize(Context context) {
File dir = getRepositoryDirectory(context);
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
if (dir == null) {
return null;
}
// uninitialize the repo if the dir does not exist or is absolutely empty
if (!dir.exists() || !dir.isDirectory() || dir.listFiles().length == 0) {
settings.edit().putBoolean("repository_initialized", false).apply();
} else {
settings.edit().putBoolean("repository_initialized", true).apply();
}
// create the repository static variable in PasswordRepository
return PasswordRepository.getRepository(new File(dir.getAbsolutePath() + "/.git"));
}
/**
* Gets the password items in the root directory
*
* @return a list of passwords in the root direcotyr
*/
public static ArrayList<PasswordItem> getPasswords(File rootDir, PasswordSortOrder sortOrder) {
return getPasswords(rootDir, rootDir, sortOrder);
}
/**
* Gets the .gpg files in a directory
*
* @param path the directory path
* @return the list of gpg files in that directory
*/
public static ArrayList<File> getFilesList(File path) {
if (path == null || !path.exists()) return new ArrayList<>();
Log.d("REPO", "current path: " + path.getPath());
List<File> directories = Arrays.asList(path.listFiles((FileFilter) FileFilterUtils.directoryFileFilter()));
List<File> files = Arrays.asList(path.listFiles((FileFilter) FileFilterUtils.suffixFileFilter(".gpg")));
ArrayList<File> items = new ArrayList<>();
items.addAll(directories);
items.addAll(files);
return items;
}
/**
* Gets the passwords (PasswordItem) in a directory
*
* @param path the directory path
* @return a list of password items
*/
public static ArrayList<PasswordItem> getPasswords(File path, File rootDir, PasswordSortOrder sortOrder) {
//We need to recover the passwords then parse the files
ArrayList<File> passList = getFilesList(path);
if (passList.size() == 0) return new ArrayList<>();
ArrayList<PasswordItem> passwordList = new ArrayList<>();
for (File file : passList) {
if (file.isFile()) {
if (!file.isHidden()) {
passwordList.add(PasswordItem.newPassword(file.getName(), file, rootDir));
}
} else {
if (!file.isHidden()) {
passwordList.add(PasswordItem.newCategory(file.getName(), file, rootDir));
}
}
}
sort(passwordList, sortOrder.comparator);
return passwordList;
}
/**
* Sets the git user name
*
* @param username username
*/
public static void setUserName(String username) {
setStringConfig("user", null, "name", username);
}
/**
* Sets the git user email
*
* @param email email
*/
public static void setUserEmail(String email) {
setStringConfig("user", null, "email", email);
}
/**
* Sets a git config value
*
* @param section config section name
* @param subsection config subsection name
* @param name config name
* @param value the value to be set
*/
private static void setStringConfig(String section, String subsection, String name, String value) {
if (isInitialized()) {
StoredConfig config = repository.getConfig();
config.setString(section, subsection, name, value);
try {
config.save();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public enum PasswordSortOrder {
FOLDER_FIRST(new Comparator<PasswordItem>() {
@Override
public int compare(PasswordItem p1, PasswordItem p2) {
return (p1.getType() + p1.getName())
.compareToIgnoreCase(p2.getType() + p2.getName());
}
}),
INDEPENDENT(new Comparator<PasswordItem>() {
@Override
public int compare(PasswordItem p1, PasswordItem p2) {
return p1.getName().compareToIgnoreCase(p2.getName());
}
}),
FILE_FIRST(new Comparator<PasswordItem>() {
@Override
public int compare(PasswordItem p1, PasswordItem p2) {
return (p2.getType() + p1.getName())
.compareToIgnoreCase(p1.getType() + p2.getName());
}
});
private Comparator<PasswordItem> comparator;
PasswordSortOrder(Comparator<PasswordItem> comparator) {
this.comparator = comparator;
}
public static PasswordSortOrder getSortOrder(SharedPreferences settings) {
return valueOf(settings.getString("sort_order", FOLDER_FIRST.name()));
}
}
}

View file

@ -0,0 +1,282 @@
package com.zeapo.pwdstore.utils
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import org.apache.commons.io.filefilter.FileFilterUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.transport.RemoteConfig
import org.eclipse.jgit.transport.URIish
import java.io.File
import java.io.FileFilter
import java.util.Comparator
open class PasswordRepository protected constructor() {
@Suppress("Unused")
enum class PasswordSortOrder(val comparator: Comparator<PasswordItem>) {
FOLDER_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem ->
(p1.type + p1.name)
.compareTo(p2.type + p2.name, ignoreCase = true)
}),
INDEPENDENT(Comparator { p1: PasswordItem, p2: PasswordItem ->
p1.name.compareTo(p2.name, ignoreCase = true)
}),
FILE_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem ->
(p2.type + p1.name).compareTo(p1.type + p2.name, ignoreCase = true)
});
companion object {
@JvmStatic
fun getSortOrder(settings: SharedPreferences): PasswordSortOrder {
return valueOf(settings.getString("sort_order", null) ?: FOLDER_FIRST.name)
}
}
}
companion object {
private var repository: Repository? = null
/**
* Returns the git repository
*
* @param localDir needed only on the creation
* @return the git repository
*/
@JvmStatic
fun getRepository(localDir: File?): Repository? {
if (repository == null && localDir != null) {
val builder = FileRepositoryBuilder()
try {
repository = builder.setGitDir(localDir)
.readEnvironment()
.build()
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
return repository
}
@JvmStatic
val isInitialized: Boolean
get() = repository != null
@JvmStatic
@Throws(Exception::class)
fun createRepository(localDir: File) {
localDir.delete()
Git.init().setDirectory(localDir).call()
getRepository(localDir)
}
// TODO add multiple remotes support for pull/push
@JvmStatic
fun addRemote(name: String, url: String, replace: Boolean?) {
val storedConfig = repository!!.config
val remotes = storedConfig.getSubsections("remote")
if (!remotes.contains(name)) {
try {
val uri = URIish(url)
val refSpec = RefSpec("+refs/head/*:refs/remotes/$name/*")
val remoteConfig = RemoteConfig(storedConfig, name)
remoteConfig.addFetchRefSpec(refSpec)
remoteConfig.addPushRefSpec(refSpec)
remoteConfig.addURI(uri)
remoteConfig.addPushURI(uri)
remoteConfig.update(storedConfig)
storedConfig.save()
} catch (e: Exception) {
e.printStackTrace()
}
} else if (replace!!) {
try {
val uri = URIish(url)
val remoteConfig = RemoteConfig(storedConfig, name)
// remove the first and eventually the only uri
if (remoteConfig.urIs.size > 0) {
remoteConfig.removeURI(remoteConfig.urIs[0])
}
if (remoteConfig.pushURIs.size > 0) {
remoteConfig.removePushURI(remoteConfig.pushURIs[0])
}
remoteConfig.addURI(uri)
remoteConfig.addPushURI(uri)
remoteConfig.update(storedConfig)
storedConfig.save()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@JvmStatic
fun closeRepository() {
if (repository != null) repository!!.close()
repository = null
}
@JvmStatic
fun getRepositoryDirectory(context: Context): File? {
var dir: File? = null
val settings = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
if (settings.getBoolean("git_external", false)) {
val externalRepo = settings.getString("git_external_repo", null)
if (externalRepo != null) {
dir = File(externalRepo)
}
} else {
dir = File(context.filesDir.toString() + "/store")
}
return dir
}
@JvmStatic
fun initialize(context: Context): Repository? {
val dir = getRepositoryDirectory(context)
val settings = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
if (dir == null) {
return null
}
// uninitialize the repo if the dir does not exist or is absolutely empty
if (!dir.exists() || !dir.isDirectory || dir.listFiles()!!.isEmpty()) {
settings.edit().putBoolean("repository_initialized", false).apply()
} else {
settings.edit().putBoolean("repository_initialized", true).apply()
}
// create the repository static variable in PasswordRepository
return getRepository(File(dir.absolutePath + "/.git"))
}
/**
* Gets the password items in the root directory
*
* @return a list of passwords in the root direcotyr
*/
@JvmStatic
fun getPasswords(rootDir: File, sortOrder: PasswordSortOrder): ArrayList<PasswordItem> {
return getPasswords(rootDir, rootDir, sortOrder)
}
/**
* Gets the .gpg files in a directory
*
* @param path the directory path
* @return the list of gpg files in that directory
*/
@JvmStatic
fun getFilesList(path: File?): ArrayList<File> {
if (path == null || !path.exists()) return ArrayList()
val directories = (path.listFiles(FileFilterUtils.directoryFileFilter() as FileFilter)
?: emptyArray()).toList()
val files = (path.listFiles(FileFilterUtils.suffixFileFilter(".gpg") as FileFilter)
?: emptyArray()).toList()
val items = ArrayList<File>()
items.addAll(directories)
items.addAll(files)
return items
}
/**
* Gets the passwords (PasswordItem) in a directory
*
* @param path the directory path
* @return a list of password items
*/
@JvmStatic
fun getPasswords(path: File, rootDir: File, sortOrder: PasswordSortOrder): ArrayList<PasswordItem> {
//We need to recover the passwords then parse the files
val passList = getFilesList(path)
if (passList.size == 0) return ArrayList()
val passwordList = ArrayList<PasswordItem>()
for (file in passList) {
if (file.isFile) {
if (!file.isHidden) {
passwordList.add(PasswordItem.newPassword(file.name, file, rootDir))
}
} else {
if (!file.isHidden) {
passwordList.add(PasswordItem.newCategory(file.name, file, rootDir))
}
}
}
passwordList.sortWith(sortOrder.comparator)
return passwordList
}
/**
* Sets the git user name
*
* @param username username
*/
@JvmStatic
fun setUserName(username: String) {
setStringConfig("user", null, "name", username)
}
/**
* Sets the git user email
*
* @param email email
*/
@JvmStatic
fun setUserEmail(email: String) {
setStringConfig("user", null, "email", email)
}
/**
* Sets a git config value
*
* @param section config section name
* @param subsection config subsection name
* @param name config name
* @param value the value to be set
*/
@JvmStatic
@Suppress("SameParameterValue")
private fun setStringConfig(section: String, subsection: String?, name: String, value: String) {
if (isInitialized) {
val config = repository!!.config
config.setString(section, subsection, name, value)
try {
config.save()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright © 2017-2018 WireGuard LLC.
* Copyright © 2018-2019 Harsh Shandilya <msfjarvis@gmail.com>. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.zeapo.pwdstore.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import com.zeapo.pwdstore.R
class MultiselectableLinearLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) {
private var multiselected: Boolean = false
override fun onCreateDrawableState(extraSpace: Int): IntArray {
if (multiselected) {
val drawableState = super.onCreateDrawableState(extraSpace + 1)
View.mergeDrawableStates(drawableState, STATE_MULTISELECTED)
return drawableState
}
return super.onCreateDrawableState(extraSpace)
}
fun setMultiSelected(on: Boolean) {
if (!multiselected) {
multiselected = true
refreshDrawableState()
}
isActivated = on
}
fun setSingleSelected(on: Boolean) {
if (multiselected) {
multiselected = false
refreshDrawableState()
}
isActivated = on
}
companion object {
private val STATE_MULTISELECTED = intArrayOf(R.attr.state_multiselected)
}
}

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_enabled="false" android:color="@color/grey_500"/>
<item android:color="@color/blue_grey_900"/>
</selector>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@color/blue_grey_200" />
<item android:drawable="@color/grey_white_1000" />
</selector>

View file

@ -2,13 +2,13 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/blue_grey_500"/>
<solid android:color="?android:attr/textColor"/>
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<solid android:color="?android:attr/windowBackground" />
</shape>
</item>
</layer-list>

View file

@ -2,5 +2,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="@color/grey_300" />
<solid android:color="?attr/colorPrimaryDark" />
</shape>

View file

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/grey_600">
android:tint="?attr/passwordIconColor">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>

View file

@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/grey_600">
android:tint="?attr/passwordIconColor">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>

View file

@ -1,9 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="110.34687"
android:viewportHeight="110.34687">
android:width="108dp"
android:height="108dp"
android:viewportWidth="110.34687"
android:viewportHeight="110.34687">
<group android:translateX="24.828047"
android:translateY="24.828047">
<path

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item>
<selector>
<item app:state_multiselected="true" android:state_activated="true">
<color android:color="@color/list_multiselect_background" />
</item>
</selector>
</item>
<item android:drawable="?attr/selectableItemBackground" />
</layer-list>

View file

@ -13,7 +13,7 @@
<item>
<shape android:shape="rectangle" android:dither="true">
<corners android:radius="2dp" />
<solid android:color="@android:color/holo_red_light" />
<solid android:color="#FF0000" />
<padding android:bottom="8dp"
android:left="8dp"

View file

@ -1,211 +1,187 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="@dimen/activity_horizontal_margin"
tools:context="com.zeapo.pwdstore.git.GitActivity"
android:background="@android:color/white">
android:background="?android:attr/windowBackground">
<LinearLayout
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.MaterialComponents.Headline5"
android:id="@+id/server_label"
android:textStyle="bold"
android:textSize="24sp"
android:text="@string/server_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_margin="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/server_name"
android:textStyle="bold"
style="@android:style/TextAppearance.Large"
android:gravity="start"
android:paddingBottom="6dp"
android:textColor="@color/blue_grey_500"
android:background="@drawable/bottom_line"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/label_server_protocol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_protocol"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/server_label"
app:layout_constraintStart_toStartOf="parent" />
<RelativeLayout
<Spinner
android:id="@+id/clone_protocol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/server_label"
app:layout_constraintStart_toEndOf="@id/label_server_protocol" />
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/server_user_layout"
android:hint="@string/server_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:hintEnabled="true"
app:layout_constraintTop_toBottomOf="@id/label_server_protocol">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_user"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
android:inputType="textWebEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/label_server_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_url"
app:hintEnabled="true"
app:layout_constraintTop_toBottomOf="@id/server_user_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/label_server_port">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_protocol"
android:id="@+id/label_server_protocol"
android:layout_centerVertical="true"
android:layout_alignParentStart="true" />
<Spinner
android:id="@+id/clone_protocol"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/label_server_protocol" />
</RelativeLayout>
<RelativeLayout
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
android:id="@+id/server_url"
android:inputType="textWebEmailAddress" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_user"
android:id="@+id/label_server_user"
android:layout_centerVertical="true"
android:paddingBottom="8dp"
android:layout_alignParentStart="true" />
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/server_user_hint"
android:id="@+id/server_user"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/label_server_user"
android:layout_alignParentEnd="true"
android:inputType="textWebEmailAddress"/>
</RelativeLayout>
<RelativeLayout
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/label_server_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_port_hint"
app:hintEnabled="true"
app:layout_constraintStart_toEndOf="@id/label_server_url"
app:layout_constraintTop_toBottomOf="@id/server_user_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="1:0.8">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
android:inputType="number" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_url"
android:paddingBottom="8dp"
android:id="@+id/label_server_url"
android:layout_centerVertical="true"
android:layout_alignParentStart="true" />
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/server_url_hint"
android:id="@+id/server_url"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/label_server_url"
android:layout_toStartOf="@+id/label_server_port"
android:inputType="textWebEmailAddress"
tools:ignore="TextFields" />
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/label_server_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_path"
app:hintEnabled="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_url">
<TextView
android:id="@+id/label_server_port"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/server_port"
android:paddingBottom="8dp"
android:text=":"
tools:ignore="HardcodedText" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/server_port_hint"
android:id="@+id/server_port"
android:layout_alignParentEnd="true"
android:inputType="number"/>
</RelativeLayout>
<RelativeLayout
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
android:inputType="textWebEmailAddress"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_path"
android:paddingBottom="8dp"
android:id="@+id/label_server_path"
android:layout_centerVertical="true"
android:layout_alignParentStart="true" />
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/server_path_hint"
android:layout_marginStart="8dp"
android:id="@+id/server_path"
android:layout_toEndOf="@+id/label_server_path"
android:layout_alignParentEnd="true"
android:inputType="textWebEmailAddress"/>
</RelativeLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/label_clone_uri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/repository_uri"
android:editable="false"
android:layout_margin="8dp"
app:hintEnabled="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_path">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/server_resulting_url"
android:textStyle="bold"
style="@android:style/TextAppearance.Large"
android:gravity="start"
android:paddingBottom="6dp"
android:textColor="@color/blue_grey_500"
android:background="@drawable/bottom_line"/>
<EditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/clone_uri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/repository_uri"
android:inputType="textWebEmailAddress"
tools:ignore="TextFields" />
android:inputType="textWebEmailAddress"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/red_rectangle"
android:textColor="@android:color/white"
android:visibility="gone"
android:id="@+id/warn_url"/>
</com.google.android.material.textfield.TextInputLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/red_rectangle"
android:textColor="@android:color/white"
android:visibility="gone"
android:id="@+id/warn_url"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_clone_uri"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/label_connection_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode"
android:layout_margin="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/warn_url" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode"
android:id="@+id/label_connection_mode"
android:layout_centerVertical="true"
android:layout_alignParentStart="true" />
<Spinner
android:id="@+id/connection_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintTop_toBottomOf="@id/warn_url"
app:layout_constraintStart_toEndOf="@id/label_connection_mode" />
<Spinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/connection_mode"
android:layout_toEndOf="@+id/label_connection_mode" />
</RelativeLayout>
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/clone_button"
android:text="@string/clone_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="cloneRepository"
android:textColor="?android:attr/windowBackground"
android:layout_marginTop="8dp"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/label_connection_mode"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/clone_button"
android:text="@string/clone_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="cloneRepository"/>
<Button
android:id="@+id/save_button"
android:text="@string/crypto_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="saveConfiguration"/>
</LinearLayout>
</ScrollView>
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/save_button"
android:text="@string/crypto_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="saveConfiguration"
android:textColor="?android:attr/windowBackground"
android:layout_marginTop="8dp"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/label_connection_mode"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,143 +1,119 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollView2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:padding="@dimen/activity_horizontal_margin"
android:background="?android:attr/windowBackground"
tools:context="com.zeapo.pwdstore.git.GitActivity"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="81dp">
<TextView
android:id="@+id/textView5"
style="@android:style/TextAppearance.Large"
android:layout_width="344dp"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/username_input_layout"
style="@style/TextInputLayoutBase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:background="@drawable/bottom_line"
android:gravity="start"
android:paddingBottom="6dp"
android:text="@string/git_config"
android:textColor="@color/blue_grey_500"
android:textStyle="bold"
android:layout_margin="8dp"
android:hint="@string/git_user_name_hint"
app:hintEnabled="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
tools:layout_editor_absoluteY="64dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/git_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions|textVisiblePassword" />
<TextView
android:id="@+id/label_git_user_name"
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/email_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/git_user_email"
app:hintEnabled="true"
app:layout_constraintTop_toBottomOf="@id/username_input_layout"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/git_user_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/save_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:paddingBottom="8dp"
android:text="@string/git_user_name"
app:layout_constraintBaseline_toBaselineOf="@+id/git_user_name"
android:layout_margin="8dp"
android:text="@string/crypto_save"
android:onClick="applyGitConfigs"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/email_input_layout"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/TextAppearance.MaterialComponents.Headline5"
android:textStyle="bold"
android:textSize="24sp"
android:id="@+id/git_tools_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/hackish_tools"
app:layout_constraintTop_toBottomOf="@id/save_button"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/git_user_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:hint="@string/git_user_name_hint"
android:inputType="textWebEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/label_git_user_name"
app:layout_constraintTop_toBottomOf="@+id/textView5" />
<TextView
android:id="@+id/label_git_user_email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:text="@string/git_user_email"
app:layout_constraintBaseline_toBaselineOf="@+id/git_user_email"
app:layout_constraintEnd_toEndOf="@+id/label_git_user_name"
app:layout_constraintStart_toStartOf="@+id/label_git_user_name" />
<EditText
android:id="@+id/git_user_email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:hint="@string/git_user_email_hint"
android:inputType="textWebEmailAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/git_user_name"
app:layout_constraintTop_toBottomOf="@+id/git_user_name" />
<Button
android:id="@+id/save_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:onClick="applyGitConfigs"
android:text="@string/crypto_save"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/git_user_email" />
<TextView
android:id="@+id/textView6"
style="@android:style/TextAppearance.Large"
android:layout_width="344dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/bottom_line"
android:gravity="start"
android:paddingBottom="6dp"
android:text="@string/hackish_tools"
android:textColor="@color/blue_grey_500"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/save_button" />
<Button
android:id="@+id/git_abort_rebase"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:onClick="abortRebase"
android:text="@string/abort_rebase"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView7" />
<TextView
android:id="@+id/textView7"
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/commit_hash_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_margin="8dp"
android:text="@string/commit_hash"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView6" />
app:layout_constraintTop_toBottomOf="@id/git_tools_title"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/git_commit_hash"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="HASH"
android:layout_margin="8dp"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@+id/textView7"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toEndOf="@+id/textView7"
tools:ignore="HardcodedText" />
app:layout_constraintTop_toBottomOf="@id/git_tools_title"
app:layout_constraintStart_toEndOf="@id/commit_hash_label"
tools:text="HASH"/>
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/git_abort_rebase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/abort_rebase"
android:onClick="abortRebase"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/commit_hash_label" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/git_reset_to_remote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/reset_to_remote"
android:onClick="resetToRemote"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/git_abort_rebase" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,4 +1,4 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -11,4 +11,4 @@
android:layout_height="match_parent"
android:orientation="vertical"/>
</RelativeLayout>
</LinearLayout>

View file

@ -12,14 +12,13 @@
android:paddingRight="24dp"
android:paddingTop="20dp">
<TextView
android:id="@+id/textView"
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pref_autofill_enable_msg"
android:textSize="16sp" />
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -29,8 +28,7 @@
android:src="@drawable/autofill_ins_1"
android:contentDescription="@string/autofill_ins_1_hint" />
<ImageView
android:id="@+id/imageView2"
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
@ -38,15 +36,13 @@
android:src="@drawable/autofill_ins_2"
android:contentDescription="@string/autofill_ins_2_hint" />
<TextView
android:id="@+id/textView3"
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pref_autofill_enable_msg2"
android:textSize="16sp" />
<ImageView
android:id="@+id/imageView3"
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="match_parent"
android:layout_height="114dp"
android:layout_marginBottom="8dp"
@ -54,8 +50,7 @@
android:src="@drawable/autofill_ins_3"
android:contentDescription="@string/autofill_ins_3_hint" />
<TextView
android:id="@+id/textView4"
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pref_autofill_enable_msg3"

View file

@ -27,8 +27,8 @@
android:layout_gravity="bottom|end"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:backgroundTint="@color/blue_grey_500"
app:rippleColor="@color/blue_grey_50"
app:backgroundTint="?attr/colorSecondary"
app:rippleColor="?attr/colorSecondary"
app:borderWidth="0dp"
android:layout_margin="@dimen/fab_compat_margin"
android:layout_alignParentBottom="true"

View file

@ -1,21 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
<LinearLayout android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@drawable/autofill_row_background"
android:orientation="vertical">
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin">
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
@ -24,24 +17,21 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:gravity="center_vertical"
android:orientation="vertical"
tools:ignore="RtlHardcoded">
android:orientation="vertical" >
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/secondary_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/grey_600" />
android:textColor="?android:attr/textColor" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View file

@ -3,228 +3,251 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?android:attr/windowBackground"
android:orientation="vertical"
tools:context="com.zeapo.pwdstore.crypto.PgpActivity">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_password_category_decrypt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:textColor="?android:attr/textColor"
android:textIsSelectable="false"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="CATEGORY HERE" />
<TextView
android:id="@+id/crypto_password_category_decrypt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:textColor="@color/grey_500"
android:textIsSelectable="false"
android:textSize="18sp"
tools:text="CATEGORY HERE" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_password_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
android:textColor="?attr/colorSecondary"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/crypto_password_category_decrypt"
tools:text="PASSWORD FILE NAME HERE" />
<TextView
android:id="@+id/crypto_password_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
android:textColor="@color/accent"
android:textSize="24sp"
android:textStyle="bold"
tools:text="PASSWORD FILE NAME HERE" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_password_last_changed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:textColor="?android:attr/textColor"
android:textIsSelectable="false"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/crypto_password_file"
tools:text="LAST CHANGED HERE" />
<TextView
android:id="@+id/crypto_password_last_changed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:textColor="@color/grey_500"
android:textIsSelectable="false"
android:textSize="18sp"
tools:text="LAST CHANGED HERE" />
</LinearLayout>
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/divider"
app:layout_constraintTop_toBottomOf="@id/crypto_password_last_changed"
tools:ignore="ContentDescription" />
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/crypto_container_decrypt"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
android:visibility="invisible">
android:visibility="invisible"
tools:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/divider">
<GridLayout
android:id="@+id/crypto_password_show_layout"
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_password_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/password"
android:textColor="?android:attr/textColor"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_password_show"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="fill"
android:gravity="bottom"
android:textColor="?android:attr/textColor"
app:layout_constraintStart_toEndOf="@id/crypto_password_show_label"
app:layout_constraintBaseline_toBaselineOf="@id/crypto_password_show_label"
android:typeface="monospace" />
<ProgressBar
android:id="@+id/pbLoading"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="8dp"
android:layout_marginBottom="8dp"
android:visibility="invisible"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/crypto_password_show_label"/>
<TextView
android:id="@+id/crypto_password_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_column="0"
android:layout_row="0"
android:text="@string/password"
android:textColor="@android:color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/crypto_password_show"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_column="2"
android:layout_gravity="fill"
android:gravity="bottom"
android:layout_row="0"
android:textColor="@android:color/black"
android:typeface="monospace" />
<ProgressBar
android:id="@+id/pbLoading"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_column="0"
android:layout_columnSpan="3"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:layout_row="1" />
<Button
android:id="@+id/crypto_password_toggle_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_column="0"
android:layout_columnSpan="3"
android:layout_row="2"
android:text="@string/show_password" />
</GridLayout>
<RelativeLayout
android:id="@+id/crypto_extra_show_layout"
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/crypto_password_toggle_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible">
android:textColor="?android:attr/windowBackground"
android:text="@string/show_password"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/crypto_password_show_label"
app:backgroundTint="?attr/colorSecondary"/>
<ImageButton
android:id="@+id/crypto_copy_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:background="@color/background"
android:contentDescription="@string/copy_username"
android:src="@drawable/ic_content_copy"
android:visibility="invisible" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/crypto_username_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_toStartOf="@id/crypto_copy_username"
android:text="@string/username"
android:textColor="@android:color/black"
android:textStyle="bold"
android:visibility="invisible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/crypto_extra_show_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintTop_toBottomOf="@id/crypto_container_decrypt"
tools:visibility="visible">
<TextView
android:id="@+id/crypto_username_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_username_show_label"
android:layout_toStartOf="@id/crypto_copy_username"
android:textColor="@android:color/black"
android:textIsSelectable="true"
android:typeface="monospace"
android:visibility="invisible" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/crypto_copy_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:background="?android:attr/windowBackground"
android:contentDescription="@string/copy_username"
android:src="@drawable/ic_content_copy"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible"/>
<ImageButton
android:id="@+id/crypto_copy_otp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/crypto_username_show"
android:background="@color/background"
android:contentDescription="@string/copy_otp"
android:src="@drawable/ic_content_copy"
android:visibility="invisible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_username_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_toStartOf="@id/crypto_copy_username"
android:text="@string/username"
android:textColor="?android:attr/textColor"
android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"/>
<TextView
android:id="@+id/crypto_otp_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_username_show"
android:layout_toStartOf="@id/crypto_copy_otp"
android:text="@string/otp"
android:textColor="@android:color/black"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_username_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_username_show_label"
android:layout_toStartOf="@id/crypto_copy_username"
android:textColor="?android:attr/textColor"
android:textIsSelectable="true"
android:typeface="monospace"
android:visibility="invisible"
app:layout_constraintTop_toBottomOf="@id/crypto_username_show_label"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible"/>
<TextView
android:id="@+id/crypto_otp_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_otp_show_label"
android:layout_toStartOf="@id/crypto_copy_otp"
android:textColor="@android:color/black"
android:textIsSelectable="true"
android:typeface="monospace" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/crypto_copy_otp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/crypto_username_show"
android:background="?android:attr/windowBackground"
android:contentDescription="@string/copy_otp"
android:src="@drawable/ic_content_copy"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="@id/crypto_otp_show_label"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible"/>
<TextView
android:id="@+id/crypto_extra_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_otp_show"
android:text="@string/extra_content"
android:textColor="@android:color/black"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_otp_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_username_show"
android:layout_toStartOf="@id/crypto_copy_otp"
android:text="@string/otp"
android:textColor="?android:attr/textColor"
app:layout_constraintTop_toBottomOf="@id/crypto_username_show"
app:layout_constraintStart_toStartOf="parent"
android:textStyle="bold" />
<TextView
android:id="@+id/crypto_extra_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_extra_show_label"
android:textColor="@android:color/black"
android:textIsSelectable="true"
android:typeface="monospace" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_otp_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_otp_show_label"
android:layout_toStartOf="@id/crypto_copy_otp"
android:textColor="?android:attr/textColor"
android:textIsSelectable="true"
app:layout_constraintTop_toBottomOf="@id/crypto_otp_show_label"
android:typeface="monospace" />
<ToggleButton
android:id="@+id/crypto_extra_toggle_show"
style="@style/Widget.AppCompat.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/crypto_extra_show"
android:layout_alignParentStart="true"
android:checked="false"
android:paddingTop="8dp"
android:textOff="@string/show_extra"
android:textOn="@string/hide_extra" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_extra_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_otp_show"
android:text="@string/extra_content"
android:textColor="?android:attr/textColor"
app:layout_constraintTop_toBottomOf="@id/crypto_otp_show"
app:layout_constraintStart_toStartOf="parent"
android:textStyle="bold" />
</RelativeLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_extra_show"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/crypto_extra_show_label"
android:textColor="?android:attr/textColor"
android:textIsSelectable="true"
app:layout_constraintTop_toBottomOf="@id/crypto_extra_show_label"
android:typeface="monospace" />
</LinearLayout>
<ToggleButton
android:id="@+id/crypto_extra_toggle_show"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/crypto_extra_show"
android:layout_alignParentStart="true"
android:checked="false"
android:paddingTop="8dp"
android:textColor="?android:attr/windowBackground"
android:textOff="@string/show_extra"
android:textOn="@string/hide_extra"
app:layout_constraintTop_toBottomOf="@id/crypto_extra_show"
android:backgroundTint="?attr/colorSecondary"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -1,89 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?android:attr/windowBackground"
android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin"
tools:context="com.zeapo.pwdstore.crypto.PgpActivity">
<ScrollView
android:id="@+id/crypto_scroll_view"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/crypto_password_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:textColor="?android:attr/textColor"
android:textIsSelectable="false"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="CATEGORY HERE" />
<LinearLayout
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/name_input_layout"
android:layout_gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/crypto_name_hint"
app:hintEnabled="true"
app:layout_constraintTop_toBottomOf="@id/crypto_password_category"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/crypto_password_file_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/crypto_password_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:textColor="@color/grey_500"
android:textIsSelectable="false"
android:textSize="18sp"
tools:text="CATEGORY HERE" />
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/password_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/crypto_pass_label"
app:hintEnabled="true"
app:layout_constraintTop_toBottomOf="@id/name_input_layout"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/crypto_password_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/crypto_password_file_edit"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:hint="@string/crypto_name_hint"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
android:textColor="@color/accent"
android:textSize="24sp"
tools:ignore="TextFields" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/generate_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/pwd_generate_button"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
app:layout_constraintTop_toBottomOf="@id/password_input_layout"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/crypto_pass_label"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:id="@+id/extra_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/crypto_extra_label"
app:hintEnabled="true"
app:layout_constraintTop_toBottomOf="@id/generate_password"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/crypto_password_edit"
android:layout_width="match_parent"
android:hint="@string/crypto_password_edit_hint"
android:layout_height="0dp"
android:layout_weight="1"
android:inputType="textVisiblePassword"
android:typeface="monospace" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/crypto_extra_edit"
android:inputType="textMultiLine|textVisiblePassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/generate_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/pwd_generate_button" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/crypto_extra_label"
android:textStyle="bold" />
<EditText
android:id="@+id/crypto_extra_edit"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:hint="@string/crypto_extra_edit_hint"
android:enabled="true"
android:inputType="textMultiLine|textVisiblePassword"
android:typeface="monospace"
android:visibility="visible" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -14,15 +14,13 @@
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintTextAppearance="@style/TextAppearance.AppCompat">
<EditText
android:hint="URL"
app:hintEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/webURL"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="URL"
android:inputType="textUri"
tools:ignore="HardcodedText" />
android:inputType="textUri"/>
</com.google.android.material.textfield.TextInputLayout>
<RadioGroup
@ -61,13 +59,15 @@
android:layout_gravity="center_horizontal"
android:layout_weight="1"/>
<Button
style="?android:attr/buttonStyleSmall"
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:id="@+id/matchButton"
android:layout_gravity="center_horizontal"
android:textColor="?android:attr/windowBackground"
app:backgroundTint="?attr/colorSecondary"
tools:ignore="HardcodedText" />
<RadioButton

View file

@ -13,7 +13,7 @@
android:paddingTop="20dp"
tools:context=".MainActivityFragment">
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/passwordText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -34,8 +34,7 @@
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/include"
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
@ -75,7 +74,7 @@
android:layout_weight="1"
android:orientation="vertical">
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/length"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -12,14 +12,13 @@
android:paddingRight="24dp"
android:paddingTop="20dp">
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/public_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true" />
<TextView
android:id="@+id/public_key_tip"
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ssh_keygen_tip"

View file

@ -11,8 +11,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin">
<TextView
android:id="@+id/label_length"
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="48dp"
android:gravity="center_vertical"
@ -25,16 +24,17 @@
android:spinnerMode="dropdown" />
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:hintTextAppearance="@style/TextAppearance.AppCompat">
android:hint="@string/ssh_keygen_passphrase"
app:hintEnabled="true">
<EditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passphrase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ssh_keygen_passphrase"
android:importantForAccessibility="no"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
@ -48,21 +48,21 @@
android:text="@string/ssh_keygen_show_passphrase" />
<com.google.android.material.textfield.TextInputLayout
style="@style/TextInputLayoutBase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:hintTextAppearance="@style/TextAppearance.AppCompat">
android:hint="@string/ssh_keygen_comment"
app:hintEnabled="true">
<EditText
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/ssh_keygen_comment"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/generate_ssh_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"

View file

@ -2,7 +2,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blue_grey_500"
android:background="?attr/colorPrimary"
android:orientation="vertical"
tools:context="com.zeapo.pwdstore.PasswordStore">
@ -11,7 +11,7 @@
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView"
@ -21,7 +21,7 @@
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
@ -32,7 +32,6 @@
android:textStyle="bold"/>
<Button
android:id="@+id/main_settings_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="openSettings"
@ -48,20 +47,19 @@
</RelativeLayout>
<RelativeLayout
android:id="@+id/myRectangleView"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:background="@android:color/white">
android:background="?android:attr/windowBackground">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:attr/borderlessButtonStyle"
android:backgroundTint="?android:attr/windowBackground"
android:onClick="createNewRepository"
android:text="@string/initialize"
android:id="@+id/button"
android:layout_alignTop="@+id/main_clone_button"
android:layout_alignParentStart="true"
android:textSize="12sp" />
@ -70,7 +68,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:attr/borderlessButtonStyle"
android:textColor="@color/teal_A700"
android:backgroundTint="?android:attr/windowBackground"
android:onClick="cloneExistingRepository"
android:text="@string/clone"
android:layout_alignParentBottom="true"

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

View file

@ -21,8 +21,8 @@
android:layout_gravity="bottom|end"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:backgroundTint="@color/accent"
app:rippleColor="@color/blue_grey_50"
app:backgroundTint="?attr/colorSecondary"
app:rippleColor="?attr/colorSecondary"
app:borderWidth="0dp"
android:layout_margin="@dimen/fab_compat_margin"
android:layout_alignParentBottom="true"

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.zeapo.pwdstore.widget.MultiselectableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator">
android:background="@drawable/password_row_background">
<LinearLayout
android:layout_width="match_parent"
@ -15,7 +15,7 @@
android:paddingRight="16dp"
android:gravity="start">
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/type_image"
android:layout_width="80dp"
android:layout_height="32dp"
@ -24,7 +24,7 @@
android:alpha="0.5"
android:contentDescription="@string/folder_icon_hint"
android:paddingEnd="8dp"
android:src="@drawable/ic_folder_grey600_24dp"
android:src="@drawable/ic_folder_tinted_24dp"
tools:ignore="RtlSymmetry" />
<LinearLayout
@ -34,24 +34,24 @@
android:layout_gravity="bottom"
android:orientation="vertical">
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="start"
android:singleLine="true"
android:text="TYPE"
android:textColor="@color/grey_500"
android:textColor="?android:attr/textColor"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="FILE_NAME"
android:textColor="@android:color/black"
android:textColor="?android:attr/textColor"
android:textSize="18sp"
tools:ignore="HardcodedText" />
@ -59,4 +59,4 @@
</LinearLayout>
</LinearLayout>
</com.zeapo.pwdstore.widget.MultiselectableLinearLayout>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base palette -->
<color name="primary_color">#FF111111</color>
<color name="primary_light_color">#FF373737</color>
<color name="primary_dark_color">#FF000000</color>
<color name="secondary_color">#FFFF7539</color>
<color name="secondary_light_color">#FFFFa667</color>
<color name="secondary_dark_color">#FFC54506</color>
<color name="primary_text_color">#FFFFFFFF</color>
<color name="secondary_text_color">#FFFFFFFF</color>
<!-- Theme variables -->
<color name="window_background">@color/primary_color</color>
<color name="password_icon_color">#FAFAFA</color>
<color name="color_surface">#FF111111</color>
<color name="list_multiselect_background">#1AEEEEEE</color>
</resources>

View file

@ -0,0 +1,7 @@
<resources>
<style name="TextInputLayoutBase" parent="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense">
<item name="boxBackgroundColor">?attr/colorPrimaryVariant</item>
<item name="boxStrokeColor">?attr/colorSecondary</item>
<item name="hintTextColor">?attr/colorOnPrimary</item>
</style>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PasswordIconStyle">
<attr name="passwordIconColor" format="reference|color" />
</declare-styleable>
<declare-styleable name="Multiselected">
<attr name="state_multiselected" format="boolean"/>
</declare-styleable>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="light_status_bar">false</bool>
<bool name="light_navigation_bar">false</bool>
</resources>

View file

@ -1,19 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="accent">#ff7043</color>
<color name="background">#eee</color>
<color name="blue_grey_50">#eceff1</color>
<color name="blue_grey_500">#607d8b</color>
<color name="blue_grey_200">#b0bec5</color>
<color name="blue_grey_700">#455a64</color>
<color name="blue_grey_900">#263238</color>
<color name="deep_orange_200">#ffab91</color>
<color name="grey_300">#e0e0e0</color>
<color name="grey_500">#9e9e9e</color>
<color name="grey_600">#757575</color>
<color name="grey_white_1000">#ffffff</color>
<color name="grey_black_1000">#000000</color>
<color name="teal_900">#004d40</color>
<color name="teal_A700">#00bfa5</color>
<!-- Base palette -->
<color name="primary_color">#607d8b</color>
<color name="primary_light_color">#8eacbb</color>
<color name="primary_dark_color">#34515e</color>
<color name="secondary_color">#ff7043</color>
<color name="secondary_light_color">#ffa270</color>
<color name="secondary_dark_color">#c63f17</color>
<color name="primary_text_color">#212121</color>
<color name="secondary_text_color">#ffffff</color>
<!-- Theme variables -->
<color name="window_background">#eceff1</color>
<color name="ic_launcher_background">#D4F1EA</color>
<color name="password_icon_color">#757575</color>
<color name="color_control_normal">@color/primary_text_color</color>
<color name="color_surface">#FFFFFF</color>
<color name="list_multiselect_background">#EEEEEE</color>
</resources>

View file

@ -74,7 +74,7 @@
<string name="server_protocol">Protocol</string>
<string name="server_url">Server URL</string>
<string name="server_url_hint" translatable="false">server.com</string>
<string name="server_port_hint">22</string>
<string name="server_port_hint">Port</string>
<string name="default_ssh_port">22</string>
<string name="default_https_port">443</string>
<string name="server_path">Repo path</string>
@ -90,16 +90,16 @@
<!-- Git Config fragment -->
<string name="git_config" translatable="false">Git config</string>
<string name="git_user_name" translatable="false">Username</string>
<string name="git_user_name_hint">User name</string>
<string name="git_user_name_hint">Username</string>
<string name="git_user_email">Email</string>
<string name="git_user_email_hint">email</string>
<string name="invalid_email_dialog_text">Please enter a valid email address</string>
<string name="clone_button">Clone!</string>
<!-- PGP Handler -->
<string name="crypto_name_hint">name</string>
<string name="crypto_name_hint">Name</string>
<string name="crypto_pass_label">Password</string>
<string name="crypto_extra_label">Extra</string>
<string name="crypto_extra_label">Extra content</string>
<string name="crypto_select">Select</string>
<string name="crypto_cancel">Cancel</string>
<string name="crypto_save">Save</string>
@ -138,8 +138,8 @@
<string name="pref_no_key_selected">No key selected</string>
<string name="pref_general_title">General</string>
<string name="pref_password_title">Password Show Time</string>
<string name="pref_password_dialog_title">Set the time you want the password to be in clipboard. 0 means forever.</string>
<string name="pref_copy_title">Automatically Copy Password</string>
<string name="pref_password_dialog_title">Set the time (in seconds) you want the password to be in clipboard. 0 means forever.</string>
<string name="pref_copy_title">Automatically copy Password</string>
<string name="pref_copy_dialog_title">Automatically copy the password to the clipboard after decryption was successful.</string>
<string name="ssh_key_success_dialog_title">SSH-key imported</string>
<string name="ssh_key_error_dialog_title">Error while trying to import the ssh-key</string>
@ -251,8 +251,8 @@
<string name="remember_the_passphrase">Remember the passphrase in the app configuration (insecure)</string>
<string name="hackish_tools">Hackish tools</string>
<string name="abort_rebase">Abort rebase and push new branch</string>
<string name="reset_to_remote">Hard reset to remote branch</string>
<string name="commit_hash">Commit hash</string>
<string name="crypto_password_edit_hint" translatable="false">p@ssw0rd!</string>
<string name="crypto_extra_edit_hint">username: something other extra content</string>
<string name="get_last_changed_failed">Failed to get last changed date</string>
<string name="hotp_pending">Tap copy to calculate HOTP</string>

View file

@ -1,22 +1,46 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/blue_grey_500</item>
<item name="colorPrimaryDark">@color/blue_grey_700</item>
<item name="android:windowBackground">@color/blue_grey_50</item>
<item name="android:textColorPrimary">@color/teal_900</item>
<item name="android:textColor">@color/text_color</item>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/primary_color</item>
<item name="colorOnPrimary">@color/color_control_normal</item>
<item name="colorPrimaryDark">@color/primary_color</item>
<item name="colorPrimaryVariant">@color/primary_light_color</item>
<item name="colorSecondary">@color/secondary_color</item>
<item name="colorOnSecondary">@android:color/white</item>
<item name="colorSurface">@color/color_surface</item>
<item name="colorOnSurface">@color/color_control_normal</item>
<item name="colorControlNormal">@color/color_control_normal</item>
<item name="passwordIconColor">@color/password_icon_color</item>
<item name="android:colorBackground">@color/window_background</item>
<item name="colorBackgroundFloating">@color/primary_dark_color</item>
<item name="android:windowBackground">@color/window_background</item>
<item name="android:textColorPrimary">@color/primary_text_color</item>
<item name="android:textColor">@color/primary_text_color</item>
<item name="android:statusBarColor">@color/primary_dark_color</item>
<item name="android:navigationBarColor">@android:color/black</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">@bool/light_status_bar</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">@bool/light_navigation_bar</item>
<item name="actionModeStyle">@style/ActionMode</item>
<item name="alertDialogTheme">@style/AppTheme.Dialog</item>
<item name="materialAlertDialogTheme">@style/AppTheme.Dialog</item>
<item name="actionBarPopupTheme">@style/ThemeOverlay.MaterialComponents.ActionBar</item>
</style>
<style name="AppTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert">
<item name="colorPrimary">@color/secondary_color</item>
<item name="colorSecondary">@color/secondary_color</item>
<item name="android:windowBackground">@color/window_background</item>
</style>
<style name="ActionMode" parent="@style/Widget.AppCompat.ActionMode">
<item name="background">@color/blue_grey_700</item>
<item name="background">@color/primary_color</item>
</style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
<item name="colorPrimary">@color/blue_grey_500</item>
<item name="colorPrimaryDark">@color/blue_grey_700</item>
<item name="colorAccent">@color/teal_A700</item>
<style name="TextInputLayoutBase" parent="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense">
<item name="boxBackgroundColor">?android:attr/windowBackground</item>
<item name="boxStrokeColor">?attr/colorSecondary</item>
<item name="hintTextColor">?attr/colorOnPrimary</item>
</style>
</resources>

View file

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/pref_git_title">
<Preference
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.preference.PreferenceCategory android:title="@string/pref_git_title">
<androidx.preference.Preference
android:key="git_server_info"
android:title="@string/pref_edit_server_info" />
<Preference
<androidx.preference.Preference
android:key="git_config"
android:title="@string/pref_edit_git_config" />
<Preference
<androidx.preference.Preference
android:key="ssh_key"
android:title="@string/pref_ssh_title" />
<Preference
<androidx.preference.Preference
android:key="ssh_keygen"
android:title="@string/pref_ssh_keygen_title" />
<Preference
<androidx.preference.Preference
android:key="ssh_key_clear_passphrase"
android:title="@string/ssh_key_clear_passphrase" />
<Preference
<androidx.preference.Preference
android:key="hotp_remember_clear_choice"
android:title="@string/hotp_remember_clear_choice" />
<Preference
<androidx.preference.Preference
android:key="ssh_see_key"
android:title="@string/pref_ssh_see_key_title" />
<Preference
<androidx.preference.Preference
android:key="git_delete_repo"
android:summary="@string/pref_git_delete_repo_summary"
android:title="@string/pref_git_delete_repo" />
@ -30,106 +30,102 @@
android:key="git_external"
android:summary="@string/pref_external_repository_summary"
android:title="@string/pref_external_repository" />
<Preference
<androidx.preference.Preference
android:dependency="git_external"
android:key="pref_select_external"
android:title="@string/pref_select_external_repository" />
</PreferenceCategory>
</androidx.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/pref_crypto_title">
<androidx.preference.PreferenceCategory android:title="@string/pref_crypto_title">
<!-- TODO(msf): Update the damn library and re-enable this
<org.openintents.openpgp.util.OpenPgpAppPreference
android:key="openpgp_provider_list"
android:title="@string/pref_provider_title" />
<Preference
-->
<androidx.preference.Preference
android:key="openpgp_key_id_pref"
android:title="@string/pref_key_title" />
</PreferenceCategory>
</androidx.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/pref_general_title">
<EditTextPreference
<androidx.preference.PreferenceCategory android:title="@string/pref_general_title">
<androidx.preference.EditTextPreference
android:defaultValue="45"
android:dialogTitle="@string/pref_password_dialog_title"
android:inputType="number"
android:key="general_show_time"
android:summary="@string/pref_password_dialog_title"
android:title="@string/pref_password_title" />
<CheckBoxPreference
<androidx.preference.CheckBoxPreference
android:defaultValue="true"
android:title="@string/show_password_pref_title"
android:summary="@string/show_password_pref_summary"
android:key="show_password" />
<CheckBoxPreference
<androidx.preference.CheckBoxPreference
android:defaultValue="true"
android:title="@string/show_extra_content_pref_title"
android:summary="@string/show_extra_content_pref_summary"
android:key="show_extra_content" />
<CheckBoxPreference
<androidx.preference.CheckBoxPreference
android:defaultValue="true"
android:dialogTitle="@string/pref_copy_dialog_title"
android:key="copy_on_decrypt"
android:summary="@string/pref_copy_dialog_title"
android:title="@string/pref_copy_title" />
<CheckBoxPreference
<androidx.preference.CheckBoxPreference
android:defaultValue="true"
android:key="clear_after_copy"
android:summary="@string/prefs_clear_after_copy_summary"
android:title="@string/prefs_clear_after_copy_title" />
<CheckBoxPreference
<androidx.preference.CheckBoxPreference
android:defaultValue="true"
android:key="filter_recursively"
android:summary="@string/pref_recursive_filter_hint"
android:title="@string/pref_recursive_filter" />
<ListPreference
<androidx.preference.ListPreference
android:title="@string/pref_sort_order_title"
android:defaultValue="FOLDER_FIRST"
android:key="sort_order"
android:entries="@array/sort_order_entries"
android:entryValues="@array/sort_order_values"
android:persistent="true"
/>
</PreferenceCategory>
android:persistent="true" />
</androidx.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/pref_autofill_title">
<CheckBoxPreference
<androidx.preference.PreferenceCategory android:title="@string/pref_autofill_title">
<androidx.preference.CheckBoxPreference
android:defaultValue="true"
android:key="autofill_enable"
android:title="@string/pref_autofill_enable_title"/>
<Preference
android:dependency="autofill_enable"
<androidx.preference.Preference
android:key="autofill_apps"
android:title="@string/pref_autofill_apps_title"/>
<CheckBoxPreference
android:dependency="autofill_enable"
<androidx.preference.CheckBoxPreference
android:defaultValue="true"
android:key="autofill_default"
android:summary="@string/pref_autofill_default_hint"
android:title="@string/pref_autofill_default_title"/>
<CheckBoxPreference
android:dependency="autofill_enable"
<androidx.preference.CheckBoxPreference
android:defaultValue="false"
android:key="autofill_always"
android:title="@string/pref_autofill_always_title"/>
</PreferenceCategory>
</androidx.preference.PreferenceCategory>
<PreferenceCategory android:title="@string/pref_misc_title">
<Preference
<androidx.preference.PreferenceCategory android:title="@string/pref_misc_title">
<androidx.preference.Preference
android:key="export_passwords"
android:title="@string/prefs_export_passwords_title"
android:summary="@string/prefs_export_passwords_summary"/>
<CheckBoxPreference
<androidx.preference.CheckBoxPreference
android:defaultValue="false"
android:key="clear_clipboard_20x"
android:summary="@string/pref_clear_clipboard_hint"
android:title="@string/pref_clear_clipboard_title" />
</androidx.preference.PreferenceCategory>
<CheckBoxPreference
android:defaultValue="false"
android:key="use_android_file_picker"
android:title="@string/prefs_use_default_file_picker" />
</PreferenceCategory>
<Preference
android:key="app_version"
android:title="@string/prefs_version" />
</PreferenceScreen>
<androidx.preference.PreferenceCategory>
<androidx.preference.Preference
android:icon="@mipmap/ic_launcher_round"
android:key="app_version"
android:title="@string/prefs_version" />
</androidx.preference.PreferenceCategory>
</androidx.preference.PreferenceScreen>