diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2019-10-01 22:14:28 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-01 22:14:28 +0530 |
commit | 073346c157ec34b750d175c8543f346e6eae94de (patch) | |
tree | a55d24ef43563f3a2d03d7938a1e96c1f5cd1caf /app | |
parent | c9dc4034f12b62f4470f92fb13ecd72871f4a5b7 (diff) |
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.
Diffstat (limited to 'app')
72 files changed, 2497 insertions, 2459 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89add6f8..c1ea485e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt index f0c502d9..448edc00 100644 --- a/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt +++ b/app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt @@ -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 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 androidx.test.ext.junit.runners.AndroidJUnit4 -import android.util.Log 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 diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt index 64e7e143..1b164c63 100644 --- a/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt +++ b/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt @@ -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 diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml index 30de0468..9ca26a38 100644 --- a/app/src/debug/res/drawable/ic_launcher_foreground.xml +++ b/app/src/debug/res/drawable/ic_launcher_foreground.xml @@ -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> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3bf13b9..abe12820 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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" diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt index 52ab93d0..6b751b5c 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt @@ -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 } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java index f2c54ab8..33424353 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java @@ -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(); } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java b/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java index 0f2d22cf..6ec0529e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordGeneratorDialogFragment.java @@ -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) -> { diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index 95eb5a9b..469dac32 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -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) -> { diff --git a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt index 2e894987..2eda0f73 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/SelectFolderActivity.kt @@ -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 diff --git a/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java b/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java index 144e8ab5..b17ed746 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/SelectFolderFragment.java @@ -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 diff --git a/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java b/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java index 593a9bbb..77fbcf27 100644 --- a/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java +++ b/app/src/main/java/com/zeapo/pwdstore/SshKeyGen.java @@ -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(); } - } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 349b5b1b..3384356f 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -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") + + + // 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}" - findPreference("openpgp_key_id_pref").onPreferenceClickListener = Preference.OnPreferenceClickListener { + 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") diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt index d01ccdc8..04b8d7ef 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt @@ -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) { diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt index f4c9357b..7cd7fcb7 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt @@ -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 diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt index e4d1d04b..d9f42b00 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt @@ -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) + } } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt index 665e8b7b..81fef413 100644 --- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt @@ -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) { diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt b/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt index 31c65704..8b23c38e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/BreakOutOfDetached.kt @@ -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" + diff --git a/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt index bf407838..8489eb1d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/CloneOperation.kt @@ -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) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java deleted file mode 100644 index 9ef05f60..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java +++ /dev/null @@ -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); - } - -} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt new file mode 100644 index 00000000..11cc4c1b --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.kt @@ -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 = "^[^@]+@[^@]+$" + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt index 5c945c30..4ab90e96 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.kt @@ -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)) { _, _ -> diff --git a/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt index fa6c5445..90c46878 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/PullOperation.kt @@ -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) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt index 9674e5b0..0015c4d1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/PushOperation.kt @@ -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() } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt new file mode 100644 index 00000000..1bab0981 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/ResetToRemoteOperation.kt @@ -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() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt index 8712a200..a0fd52b2 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/SyncOperation.kt @@ -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) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java index 085beadd..dbb5b7b1 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java @@ -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(); } }); diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java deleted file mode 100644 index eba6a02e..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt new file mode 100644 index 00000000..f52756f4 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/EntryRecyclerAdapter.kt @@ -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) + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index a2be3ddc..7c2a28c3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -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 +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java deleted file mode 100644 index 1754e0f9..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.java +++ /dev/null @@ -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()); - }; - } - -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt new file mode 100644 index 00000000..4f0ca4c8 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/FolderRecyclerAdapter.kt @@ -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) + } + } + +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java deleted file mode 100644 index 1a498721..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.java +++ /dev/null @@ -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()); - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt new file mode 100644 index 00000000..ddecd2da --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt @@ -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) + } + } + +}
\ No newline at end of file diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java deleted file mode 100644 index 9170e408..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.java +++ /dev/null @@ -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()); - }; - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt new file mode 100644 index 00000000..d7493a48 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRecyclerAdapter.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java deleted file mode 100644 index 9c27a448..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.java +++ /dev/null @@ -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())); - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt new file mode 100644 index 00000000..9e94e400 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt @@ -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() + } + + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/widget/MultiselectableLinearLayout.kt b/app/src/main/java/com/zeapo/pwdstore/widget/MultiselectableLinearLayout.kt new file mode 100644 index 00000000..7ceea1db --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/widget/MultiselectableLinearLayout.kt @@ -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) + } +}
\ No newline at end of file diff --git a/app/src/main/res/color/text_color.xml b/app/src/main/res/color/text_color.xml deleted file mode 100644 index 9e70185e..00000000 --- a/app/src/main/res/color/text_color.xml +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/drawable/autofill_row_background.xml b/app/src/main/res/drawable/autofill_row_background.xml deleted file mode 100644 index 05e887ca..00000000 --- a/app/src/main/res/drawable/autofill_row_background.xml +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/drawable/bottom_line.xml b/app/src/main/res/drawable/bottom_line.xml index 323b03e0..e67c96fa 100644 --- a/app/src/main/res/drawable/bottom_line.xml +++ b/app/src/main/res/drawable/bottom_line.xml @@ -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> diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml index cf2134ff..23a88317 100644 --- a/app/src/main/res/drawable/divider.xml +++ b/app/src/main/res/drawable/divider.xml @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/drawable/ic_action_secure.xml b/app/src/main/res/drawable/ic_action_secure.xml index 8f7b7539..af182711 100644 --- a/app/src/main/res/drawable/ic_action_secure.xml +++ b/app/src/main/res/drawable/ic_action_secure.xml @@ -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"/> diff --git a/app/src/main/res/drawable/ic_folder_grey600_24dp.xml b/app/src/main/res/drawable/ic_folder_tinted_24dp.xml index dbd73db5..51380a42 100644 --- a/app/src/main/res/drawable/ic_folder_grey600_24dp.xml +++ b/app/src/main/res/drawable/ic_folder_tinted_24dp.xml @@ -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"/> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 318de001..4072a948 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -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 diff --git a/app/src/main/res/drawable/password_row_background.xml b/app/src/main/res/drawable/password_row_background.xml new file mode 100644 index 00000000..6806b12f --- /dev/null +++ b/app/src/main/res/drawable/password_row_background.xml @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/drawable/red_rectangle.xml b/app/src/main/res/drawable/red_rectangle.xml index a6c48d0f..8213a7e3 100644 --- a/app/src/main/res/drawable/red_rectangle.xml +++ b/app/src/main/res/drawable/red_rectangle.xml @@ -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" diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml index c96a6201..c4612765 100644 --- a/app/src/main/res/layout/activity_git_clone.xml +++ b/app/src/main/res/layout/activity_git_clone.xml @@ -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"> - - <LinearLayout + android:background="?android:attr/windowBackground"> + + <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"/> - - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_gravity="center_vertical"> - - - <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 - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_gravity="center_vertical"> - - <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" /> - - <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 - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_gravity="center_vertical"> - - <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" /> - - <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" /> - - <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 - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_gravity="center_vertical"> - - <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" /> - - <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> - - - <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 - android:id="@+id/clone_uri" + <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" /> + + <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:hint="@string/repository_uri" - android:inputType="textWebEmailAddress" - tools:ignore="TextFields" /> + android:inputType="textWebEmailAddress" /> + </com.google.android.material.textfield.TextInputLayout> - <TextView + <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"> + + <com.google.android.material.textfield.TextInputEditText 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"/> + android:id="@+id/server_url" + android:inputType="textWebEmailAddress" /> - <RelativeLayout + </com.google.android.material.textfield.TextInputLayout> + + <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"> - - - <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:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/connection_mode" - android:layout_toEndOf="@+id/label_connection_mode" /> - </RelativeLayout> - - <Button - android:id="@+id/clone_button" - android:text="@string/clone_button" + android:inputType="number" /> + + </com.google.android.material.textfield.TextInputLayout> + + <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"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/server_path" 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:inputType="textWebEmailAddress"/> + + </com.google.android.material.textfield.TextInputLayout> + + <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"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/clone_uri" android:layout_width="match_parent" android:layout_height="wrap_content" - android:onClick="saveConfiguration"/> - </LinearLayout> + android:inputType="textWebEmailAddress"/> -</ScrollView> + </com.google.android.material.textfield.TextInputLayout> + + <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" /> + + <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" /> + + <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" /> + + <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> diff --git a/app/src/main/res/layout/activity_git_config.xml b/app/src/main/res/layout/activity_git_config.xml index 2283353b..bb2eac8b 100644 --- a/app/src/main/res/layout/activity_git_config.xml +++ b/app/src/main/res/layout/activity_git_config.xml @@ -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" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - - <TextView - android:id="@+id/label_git_user_name" - 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" - 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:layout_margin="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" /> + app:hintEnabled="true" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="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_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" /> + </com.google.android.material.textfield.TextInputLayout> - <EditText - android:id="@+id/git_user_email" - android:layout_width="0dp" + <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_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: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="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="8dp" - android:layout_marginTop="8dp" - android:onClick="applyGitConfigs" + android:layout_margin="8dp" android:text="@string/crypto_save" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintRight_toRightOf="parent" - app:layout_constraintTop_toBottomOf="@+id/git_user_email" /> + 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"/> - <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" + <androidx.appcompat.widget.AppCompatTextView + style="@style/TextAppearance.MaterialComponents.Headline5" 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:textSize="24sp" + android:id="@+id/git_tools_title" + android:layout_width="wrap_content" 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" /> + android:layout_margin="8dp" + android:text="@string/hackish_tools" + app:layout_constraintTop_toBottomOf="@id/save_button" + app:layout_constraintStart_toStartOf="parent" /> - <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> diff --git a/app/src/main/res/layout/activity_pwdstore.xml b/app/src/main/res/layout/activity_pwdstore.xml index dd30d3fc..e76b88a1 100644 --- a/app/src/main/res/layout/activity_pwdstore.xml +++ b/app/src/main/res/layout/activity_pwdstore.xml @@ -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> diff --git a/app/src/main/res/layout/autofill_instructions.xml b/app/src/main/res/layout/autofill_instructions.xml index 333cd5fa..d75320d3 100644 --- a/app/src/main/res/layout/autofill_instructions.xml +++ b/app/src/main/res/layout/autofill_instructions.xml @@ -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" diff --git a/app/src/main/res/layout/autofill_recycler_view.xml b/app/src/main/res/layout/autofill_recycler_view.xml index c0bbacdb..30b72c04 100644 --- a/app/src/main/res/layout/autofill_recycler_view.xml +++ b/app/src/main/res/layout/autofill_recycler_view.xml @@ -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" diff --git a/app/src/main/res/layout/autofill_row_layout.xml b/app/src/main/res/layout/autofill_row_layout.xml index e07a73d3..12354077 100644 --- a/app/src/main/res/layout/autofill_row_layout.xml +++ b/app/src/main/res/layout/autofill_row_layout.xml @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/layout/decrypt_layout.xml b/app/src/main/res/layout/decrypt_layout.xml index 4536042a..17ab55ed 100644 --- a/app/src/main/res/layout/decrypt_layout.xml +++ b/app/src/main/res/layout/decrypt_layout.xml @@ -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"> - - <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" /> - - <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" /> - - <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 + 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" /> + + <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" /> + + <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" /> + + + <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"> - - <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" + android:layout_height="8dp" + android:layout_marginBottom="8dp" + android:visibility="invisible" + android:layout_marginTop="8dp" + app:layout_constraintTop_toBottomOf="@id/crypto_password_show_label"/> + + <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: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"/> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <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"> + + <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"/> + + <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"/> + + <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"/> + + <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"/> + + <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" /> + + <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" /> + + <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" /> + + <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" /> + + <ToggleButton + android:id="@+id/crypto_extra_toggle_show" + style="@style/Widget.MaterialComponents.Button" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="invisible"> - - <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" /> - - <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" /> - - <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" /> - - <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" /> - - <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" /> - - <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" /> - - <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" /> - - <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" /> - - <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" /> - - </RelativeLayout> - </LinearLayout> - - </LinearLayout> + 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> diff --git a/app/src/main/res/layout/encrypt_layout.xml b/app/src/main/res/layout/encrypt_layout.xml index f84792e8..41fa78ee 100644 --- a/app/src/main/res/layout/encrypt_layout.xml +++ b/app/src/main/res/layout/encrypt_layout.xml @@ -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" /> - - <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" /> - - <TextView - android:id="@+id/textView2" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/crypto_pass_label" - android:textStyle="bold" /> - - <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" /> - - <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" /> + <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> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:text="@string/crypto_extra_label" - android:textStyle="bold" /> + <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"/> - <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> + <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"> - </ScrollView> + <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"/> -</LinearLayout>
\ No newline at end of file + </com.google.android.material.textfield.TextInputLayout> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_autofill.xml b/app/src/main/res/layout/fragment_autofill.xml index 7f09f0cb..d780e6f6 100644 --- a/app/src/main/res/layout/fragment_autofill.xml +++ b/app/src/main/res/layout/fragment_autofill.xml @@ -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 diff --git a/app/src/main/res/layout/fragment_pwgen.xml b/app/src/main/res/layout/fragment_pwgen.xml index eab23f7c..df01c62e 100644 --- a/app/src/main/res/layout/fragment_pwgen.xml +++ b/app/src/main/res/layout/fragment_pwgen.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_show_ssh_key.xml b/app/src/main/res/layout/fragment_show_ssh_key.xml index f9b3b469..78ece084 100644 --- a/app/src/main/res/layout/fragment_show_ssh_key.xml +++ b/app/src/main/res/layout/fragment_show_ssh_key.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_ssh_keygen.xml b/app/src/main/res/layout/fragment_ssh_keygen.xml index 12961292..adfeb5e0 100644 --- a/app/src/main/res/layout/fragment_ssh_keygen.xml +++ b/app/src/main/res/layout/fragment_ssh_keygen.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_to_clone_or_not.xml b/app/src/main/res/layout/fragment_to_clone_or_not.xml index 7d9341ba..cef35e1f 100644 --- a/app/src/main/res/layout/fragment_to_clone_or_not.xml +++ b/app/src/main/res/layout/fragment_to_clone_or_not.xml @@ -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" diff --git a/app/src/main/res/layout/git_passphrase_layout.xml b/app/src/main/res/layout/git_passphrase_layout.xml index fb0c0514..6fb4ee13 100644 --- a/app/src/main/res/layout/git_passphrase_layout.xml +++ b/app/src/main/res/layout/git_passphrase_layout.xml @@ -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"> diff --git a/app/src/main/res/layout/password_recycler_view.xml b/app/src/main/res/layout/password_recycler_view.xml index d10db5fc..5e87351b 100644 --- a/app/src/main/res/layout/password_recycler_view.xml +++ b/app/src/main/res/layout/password_recycler_view.xml @@ -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" diff --git a/app/src/main/res/layout/password_row_layout.xml b/app/src/main/res/layout/password_row_layout.xml index 66616cc3..71ad8cf4 100644 --- a/app/src/main/res/layout/password_row_layout.xml +++ b/app/src/main/res/layout/password_row_layout.xml @@ -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>
\ No newline at end of file +</com.zeapo.pwdstore.widget.MultiselectableLinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 00000000..55d52ca2 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -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> diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..7fda5217 --- /dev/null +++ b/app/src/main/res/values-night/styles.xml @@ -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> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..1a044c40 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 00000000..b02fcc05 --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -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> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ef263161..98234581 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -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> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e54490f..e39558b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 0b0bc4e6..ad6a61ff 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -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> diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml index 3e8f34a9..194053e6 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -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> |