diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 00ceb581b..a4d9439f1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -82,7 +82,6 @@ cache: key: ${CI_COMMIT_REF_SLUG} paths: - .gradle - - '**/build' detekt analysis: stage: analyze diff --git a/app/build.gradle b/app/build.gradle index ee4a26c48..0115a61b8 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -524,7 +524,6 @@ dependencies { detektPlugins 'pm.algirdas.detekt:codeanalysis:0.3.2' detektPlugins 'io.gitlab.arturbosch.detekt:detekt-formatting:1.9.1' - implementation(name:'govpn', ext:'aar') implementation 'androidx.test.espresso:espresso-idling-resource:3.3.0' // 3.10 of commons-lang3 causes NoClassDefFoundError on start (to be investigated) implementation 'org.apache.commons:commons-lang3:3.9' @@ -588,6 +587,7 @@ dependencies { kapt 'com.github.bumptech.glide:compiler:4.12.0' // Proton Core libs + implementation 'me.proton.vpn:go-vpn-lib:0.1.5' implementation 'me.proton.core:domain:1.0.3' implementation 'me.proton.core:network:1.1.3' implementation "me.proton.core:util-kotlin:0.2.5" diff --git a/app/config/GuestHoleServers.json b/app/config/GuestHoleServers.json index 165ac6eec..9a03f9f95 100644 --- a/app/config/GuestHoleServers.json +++ b/app/config/GuestHoleServers.json @@ -1,322 +1,332 @@ [ { - "Name": "LT#6", - "EntryCountry": "LT", - "ExitCountry": "LT", - "Domain": "85.206.163.144", + "Name": "SK#7", + "EntryCountry": "SK", + "ExitCountry": "SK", + "Domain": "sk-07.protonvpn.com", "Tier": 2, "Features": 0, "Region": null, - "City": "Siauliai", - "Score": 1.20026203, + "City": "Bratislava", + "Score": 1.25228717, "HostCountry": null, - "ID": "4zzEVouX_i77J4x3fGAssTJ1xYp20LBgCNK49D2K65UvKULmo4TvyHdk7SI8N-alWEeUqu_GLNH1m_XlkwoD8g==", + "ID": "nIHWdoaK-DZj1OGK0c1KjxujZQr_kFJVQAq1D2evIa8jWG1gm4bNFnOX8JJL7EAc3BU5o3SOWXLXYaxSpO_naQ==", "Location": { - "Lat": 55.93, - "Long": 23.32 + "Lat": 48.140000000000001, + "Long": 17.109999999999999 }, "Status": 1, "Servers": [ { - "EntryIP": "85.206.163.144", - "ExitIP": "85.206.163.150", - "Domain": "node-lt-01.protonvpn.net", - "ID": "VrU6Q2JNh73TmbIbM01l14hgdlU6vNyo8bP6piXdD9-cYZxgUBloWFDrG61QbIWHl25I_mdVhNF0Sa2T6s3-bQ==", - "Label": "2", - "X25519PublicKey": "t84vWcN20dsay78OFAopB9Vk/4GjL9IbK7SiHhtmug8=", + "EntryIP": "196.245.151.210", + "ExitIP": "196.245.151.217", + "Domain": "lxc-sk-01.protonvpn.com", + "ID": "Km6D_JIeNGXswoXcbmbADUgFdCsaQJelOhiTFFXWoaljJ9OAbRj6B2vNVMzKQ2PWkNdViEAoAuPxdT27hQ5G-Q==", + "Label": "3", + "X25519PublicKey": "kxtiQsbblJPBJcrw8p2SdtJ8auswUd5PxHIGXNxkvFw=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 30 + "Load": 74 }, { - "Name": "DE#3", - "EntryCountry": "DE", - "ExitCountry": "DE", - "Domain": "node-de-01.protonvpn.net", + "Name": "CA#8", + "EntryCountry": "CA", + "ExitCountry": "CA", + "Domain": "172.83.40.66", "Tier": 2, - "Features": 12, + "Features": 8, "Region": null, - "City": "Frankfurt", - "Score": 1.1859171799999999, + "City": "Vancouver", + "Score": 1000.7705370899999, "HostCountry": null, - "ID": "7J7smwDoOZD537x3sohypBmu8phtWjoc7NmddefXLbHy76M8iTpcU9Zn0QsZhN9tRpJ8ILZ2GZVhaeCbku4IPQ==", + "ID": "ayHRvHKO2CJP1wdCmopFM31BMtrt9f4tCf-gfHRZ3572i2LvzdtRLeZwYaeZf4mCenDXWzA7rXk7C6uG25CJBQ==", "Location": { - "Lat": 51, - "Long": 9 + "Lat": 49.25, + "Long": -123.133 }, "Status": 1, "Servers": [ { - "EntryIP": "46.165.221.35", - "ExitIP": "37.58.58.231", - "Domain": "node-de-01.protonvpn.net", - "ID": "7J7smwDoOZD537x3sohypBmu8phtWjoc7NmddefXLbHy76M8iTpcU9Zn0QsZhN9tRpJ8ILZ2GZVhaeCbku4IPQ==", - "Label": "0", - "X25519PublicKey": "hi0OnyLL8AHVv5SGlyVa5FxruisqP7gCrQQwUHMXm0k=", + "EntryIP": "172.83.40.66", + "ExitIP": "172.83.40.70", + "Domain": "lxc-ca-02.protonvpn.com", + "ID": "dQXEyOftKl9eC3Bct6I8t9MOTglcvZtSDUuqFWUy70w8Kp-xkrqrOdlM1uJ892F9ebvTRNBDZb8gUUIh3WHoGQ==", + "Label": "2", + "X25519PublicKey": "VUp/Ro7hB3T5d5IvmSkamNM+zNP2Mb1M/zEZh4GHYFU=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 65 + "Load": 100 }, { - "Name": "US-CA#33", - "EntryCountry": "US", - "ExitCountry": "US", - "Domain": "us-ca-33.protonvpn.net", + "Name": "SI#1", + "EntryCountry": "SI", + "ExitCountry": "SI", + "Domain": "si-01.protonvpn.net", "Tier": 1, - "Features": 4, + "Features": 0, "Region": null, - "City": "Los Angeles", - "Score": 2.5061782799999999, + "City": "Ljubljana", + "Score": 2.1127012199999999, "HostCountry": null, - "ID": "7ZEsSm9JHoeMums0sAzeE9IRZMH9U4fs55xmgdF5BGJm7TDBUfAOwFMASlqgjZmPGnP3A2I0FCTNsrdm7zJyXQ==", + "ID": "o9c4wu9NrcNEPoPmlP4ve-VFBi3VLMNmrqNxYWuZb0V-Pe0qt34TScbzkoRoKh6ISrMmfReESf3NDz7kmCeYcA==", "Location": { - "Lat": 34.049999999999997, - "Long": -118.26000000000001 + "Lat": 46.049999999999997, + "Long": 14.51 }, "Status": 1, "Servers": [ { - "EntryIP": "185.230.126.19", - "ExitIP": "185.230.126.19", - "Domain": "us-ca-33.protonvpn.net", - "ID": "NHK7gMw8XcVds7OhOCnhu-U7vmj_o_e-a_6728_vW_UMlj60ezJfZqFGz3gvZLOuGNMcAx6tAgtGigBGRTGFGg==", + "EntryIP": "195.80.150.227", + "ExitIP": "195.80.150.227", + "Domain": "si-01.protonvpn.net", + "ID": "2DEfQMVPWg2DGWBC0nF-viyDo57n0jCuxQZmI1Jqrio9oQGjLa1g7-b60ec2iqHw5eqNhS9ydoptMqp4FpHwDw==", "Label": "0", - "X25519PublicKey": "02VE3OuUf61AlT6CVndSyGfQtM1ed1U1ShsLF/xdzGk=", + "X25519PublicKey": "XoZejEWO9AguoeWUXQGJU/N//2aVKOY6Eh0gfZE2/T4=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 20 + "Load": 11 }, { - "Name": "UA#16", - "EntryCountry": "UA", - "ExitCountry": "UA", - "Domain": "node-ua-02.protonvpn.net", + "Name": "US-NJ#14", + "EntryCountry": "US", + "ExitCountry": "US", + "Domain": "us-nj-14.protonvpn.com", "Tier": 2, - "Features": 0, + "Features": 8, "Region": null, - "City": "Kyiv", - "Score": 1.23236061, + "City": "Secaucus", + "Score": 1.4578082999999999, "HostCountry": null, - "ID": "1NlUNuCHSwxs9EStt82xZFtoViKd6gPw_Mqbcv50ZbFIayUKKUO7Z-ja0LDRqRWdqoZoRBoNdFFNQt-K6VfXCQ==", + "ID": "CR8NFC3v5qrQ6uJyU12YIuPCkPH99Crnxv9JgTFiePqWQRSVzc7tbaYoRJTG_MJNcj_8zUgpl07J77HtX_wxBg==", "Location": { - "Lat": 50.450000000000003, - "Long": 30.52 + "Lat": 40.780000000000001, + "Long": -74.099999999999994 }, "Status": 1, "Servers": [ { - "EntryIP": "156.146.50.5", - "ExitIP": "156.146.50.9", - "Domain": "node-ua-02.protonvpn.net", - "ID": "_TgqmwyCLU8pMHg8KiZ1JNgaBupvoUz0a6lIY3V-a_6qvLxxesg6dS6AQzXGDBX8THBr9OwH2XM_ZTH7KgJnDA==", - "Label": "4", - "X25519PublicKey": "eqjhoqO6K1nLiej026+RkpSTHloVrOHLlMQaB0Tl5GM=", + "EntryIP": "107.152.101.210", + "ExitIP": "107.152.101.212", + "Domain": "lxc-us-83.protonvpn.com", + "ID": "SIjbFPKOXRBR0yF-vFmfmUSf2hLkTJfliHASJ_zckNEm99VwskMZf1SwH5mzb2iU0FPJKDinV52t79v99E_YXQ==", + "Label": "1", + "X25519PublicKey": "r387cxQIlFcpqSDAEf695i9+3K5zGNQjLaafOHY7A1o=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 43 + "Load": 54 }, { - "Name": "US-CO#8", - "EntryCountry": "US", - "ExitCountry": "US", - "Domain": "node-us-58.protonvpn.net", + "Name": "NL#39", + "EntryCountry": "NL", + "ExitCountry": "NL", + "Domain": "190.2.132.139", "Tier": 2, - "Features": 8, + "Features": 0, "Region": null, - "City": "Denver", - "Score": 1.47804998, + "City": "Amsterdam", + "Score": 1.2397255300000001, "HostCountry": null, - "ID": "9vv4sMOJegrTiDnJ9mxvXC6mXCmjpifyUVJn5iiFK6Rt7lXSZG7jVHIe52hAy6eAPOQE6orGoaoPkLRFwWfJvQ==", + "ID": "50UcXdfLD9mv7EkB8rjQqo6hkT61yquX0VJqOFj6BIivVjVh4NXjltfmWBIHuM-966vIWRZy8zD0IfItoODEoA==", "Location": { - "Lat": 39.740000000000002, - "Long": -104.98999999999999 + "Lat": 52.369999999999997, + "Long": 4.8899999999999997 }, "Status": 1, "Servers": [ { - "EntryIP": "84.17.63.8", - "ExitIP": "84.17.63.16", - "Domain": "node-us-58.protonvpn.net", - "ID": "khXXigiKQ1lRgN4B0KfYpSdGl3CbKnTbx48sqOG_HJtqduvB3c6lYAYM_q_J4NfRwOzlOjtaxO1lIGg7ABOVuA==", - "Label": "4", - "X25519PublicKey": "Yu2fgynXUAASCkkrXWj76LRriFxKMTQq+zjTzyOKG1Q=", + "EntryIP": "190.2.132.139", + "ExitIP": "190.2.132.155", + "Domain": "lxc-nl-28.protonvpn.com", + "ID": "6b_OORGgt2TBrTKwr668v8eu0G0NKNpyyvp4CgB407hoZCiGwbaQJA4fwU6gK1ts3vJMIcsUeVeHqOa8JNslkg==", + "Label": "0", + "X25519PublicKey": "oVHQPMeCCfPpGhPjEKAFG94wrSmn5MR/kGOThxcefVU=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 29 + "Load": 72 }, { - "Name": "US-FL#28", + "Name": "US-CO#13", "EntryCountry": "US", "ExitCountry": "US", - "Domain": "node-us-64.protonvpn.net", - "Tier": 2, - "Features": 12, + "Domain": "us-co-13.protonvpn.net", + "Tier": 1, + "Features": 0, "Region": null, - "City": "Miami", - "Score": 1.53215572, + "City": "Denver", + "Score": 2.4825244799999999, "HostCountry": null, - "ID": "IFIhm_XLFyW-B65rz5gDouB8g4DSwoFhBj4bLIkldvaJM-pEHVTgMFSWOzw_nSf-FUlgjixJqoeYTK_1jQr_JA==", + "ID": "y2_ZOWsGf7Rx64kR0Pw5FzAXPrRoxt7JHSN-RdISnb4aUxWbYUCwpRVwwqeSo6hSMakEQCcs3uoRtTIgHDCuGA==", "Location": { - "Lat": 25.77, - "Long": -80.189999999999998 + "Lat": 39.740000000000002, + "Long": -104.98999999999999 }, "Status": 1, "Servers": [ { - "EntryIP": "45.87.214.210", - "ExitIP": "45.87.214.214", - "Domain": "node-us-64.protonvpn.net", - "ID": "-U7jRsK11OFdAjOZ1R4EyzZIP2ABQA2oaXlPkE0QixVC4x7qfuLY2QTVw8ru7UJJ1O7uzuYDdpxlEUzos_wvHw==", + "EntryIP": "212.102.44.162", + "ExitIP": "212.102.44.162", + "Domain": "us-co-13.protonvpn.net", + "ID": "gRTZQvMugC8-lwnzbgl-dKvrm7ihIK2G5RoKyZ4uMGbBhNdRLJiHhQeViXZTb233dNMEoQ0aLVW6OFXY6p2VXg==", "Label": "0", - "X25519PublicKey": "7/eRyWTEpZBO8ete4Ur39sUyrR8RVZpsU4D/v8NM7F0=", + "X25519PublicKey": "bmAgkOihCQ0bJWIet+LeqoCqU41nk+yjsl6t057e6iM=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 63 + "Load": 34 }, { - "Name": "NZ#9", - "EntryCountry": "NZ", - "ExitCountry": "NZ", - "Domain": "node-nz-02.protonvpn.net", - "Tier": 2, + "Name": "UK#18", + "EntryCountry": "UK", + "ExitCountry": "UK", + "Domain": "uk-18.protonvpn.com", + "Tier": 1, "Features": 0, "Region": null, - "City": "Auckland", - "Score": 999.70318736000002, + "City": "Manchester", + "Score": 2.3425959700000001, "HostCountry": null, - "ID": "AMhNBA1JukVBd9Fhu93p1RVcglsmZXmgYAI9En7RbwEiNIYH-W3kov1P-CHZt9MMuj59FQyEA8htG2Cmfd683Q==", + "ID": "sId6XkzULCEzDPTuidkwWOgPInKjmzYJrw4nUYKnZHIwnlkiFqQjg_uHvzGrByCB99th0dfcVW-K5lK0E5tZmg==", "Location": { - "Lat": -36.848999999999997, - "Long": 174.77500000000001 + "Lat": 53.479999999999997, + "Long": -2.2400000000000002 }, "Status": 1, "Servers": [ { - "EntryIP": "116.90.74.242", - "ExitIP": "116.90.74.247", - "Domain": "node-nz-02.protonvpn.net", - "ID": "SxQUBkOGzUUyVHl5Rabp_qAMY-ciZ3hNVLVbtQRLlmQq2sWz1dtDx7d0j0yTlJz3zIjojdRvIE9crOSFMUW73Q==", + "EntryIP": "217.138.196.19", + "ExitIP": "217.138.196.20", + "Domain": "uk-17.protonvpn.com", + "ID": "f466k2Ptg59XIGn5m5xYfrPRRCjIKHCUcmSJdCyuyRrsEsNjMvJFk7V8Q29JUelidH_zTgLhXNoZaBVn4enVaA==", "Label": "1", - "X25519PublicKey": "ErCIBur8/FyI+SpFAXyfJTDOF1rvi3nlYvHnPBz3K3c=", + "X25519PublicKey": "r+Y2iuqrADz1bPxzPhAE0/EpC754IWn/N58dDVjs82g=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 22 + "Load": 84 }, { - "Name": "AU#29", - "EntryCountry": "AU", - "ExitCountry": "AU", - "Domain": "au-29.protonvpn.net", - "Tier": 1, + "Name": "NL#42", + "EntryCountry": "NL", + "ExitCountry": "NL", + "Domain": "nl-42.protonvpn.com", + "Tier": 2, "Features": 0, "Region": null, - "City": "Perth", - "Score": 2.6150790100000001, + "City": "Amsterdam", + "Score": 1.12805113, "HostCountry": null, - "ID": "hgcLm_yZkwMKUoToa2p3XGA60FGwaD3U4GIkQ3VAE6ef59hhm5pJ5fshBZXJQITMMxqeB_k40uHyAYBgU2oA9w==", + "ID": "mHGhoTDiym71ug5Fp_zrIQSzmoMqcHAOm1h3qs-JjkcMFRQGjH9D1LwKsIMydUWMqKvGfAuJKgQDpsp0n1hmUQ==", "Location": { - "Lat": -32.039999999999999, - "Long": 115.68000000000001 + "Lat": 52.369999999999997, + "Long": 4.8899999999999997 }, "Status": 1, "Servers": [ { - "EntryIP": "103.107.197.3", - "ExitIP": "103.107.197.3", - "Domain": "au-29.protonvpn.net", - "ID": "7mg-8Dw1wRhLhBRhPEPIo2-mtUcb_hvu_kjetpCwFN48gdO_zd2NHEzBYADZ8SqrMyKZuBOzN_msfgla3FUyrQ==", - "Label": "0", - "X25519PublicKey": "mM2CdcIn8d64ITQEZqN6mWJ09qFIkAuhWDZWcQqR7Gs=", + "EntryIP": "190.2.132.124", + "ExitIP": "190.2.132.127", + "Domain": "lxc-nl-29.protonvpn.com", + "ID": "59KEHNCsXSALCYt5_YHgGx6s94mcfE1H8D1HPvRs4TaceEfZr_YQ4JXX_7JzQDk6EHnA43cESfeJP0UDD05UUw==", + "Label": "1", + "X25519PublicKey": "9jkx1qC/7bNkn5rlOfxblxoN11MGZFYobwbWXZ7Sql8=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 31 + "Load": 10 }, { - "Name": "IL#2", - "EntryCountry": "IL", - "ExitCountry": "IL", - "Domain": "node-il-01c.protonvpn.net", + "Name": "HU#7", + "EntryCountry": "HU", + "ExitCountry": "HU", + "Domain": "node-hu-01.protonvpn.net", "Tier": 2, - "Features": 0, + "Features": 4, "Region": null, - "City": "Tel Aviv", - "Score": 999.27922446000002, + "City": "Budapest", + "Score": 1.3707309700000001, "HostCountry": null, - "ID": "r7-20s78k3ejJuW6NV1Hj0tJ6DE079Szw32pmSwpI67HUO6zk14Z3XY43zQhLbIBLei27chonwiXr7DBA4_dFw==", + "ID": "ULA5Hi9bTksyv55qXeKpgvqh1DXxS1tJtbswLwQI_Bw1QxERlZEWC4b2HVSVfWettz5xr7kxR2eQ-0O4Qltr1g==", "Location": { - "Lat": 32.079999999999998, - "Long": 34.799999999999997 + "Lat": 47.490000000000002, + "Long": 19.050000000000001 }, "Status": 1, "Servers": [ { - "EntryIP": "87.239.255.100", - "ExitIP": "87.239.255.102", - "Domain": "node-il-01c.protonvpn.net", - "ID": "H4ZiPDHqj8-uRdzmNAIUrFSWJuBHfthiyin_B1kboo9rJl7aHHnTCZhtYYf7Br2zIQ-14MWs6beSzrbrk6UBuQ==", - "Label": "0", - "X25519PublicKey": "YY1lfYuNfNkBOSfFhzuixH+lzNXgCI4GKIo2T0IsOj0=", + "EntryIP": "185.128.26.210", + "ExitIP": "185.128.26.217", + "Domain": "node-hu-01.protonvpn.net", + "ID": "AINFyh3drzRNU3djmjjymu838WYLBNjpWn2VeSSixG5zgACrblfItQzM7VDn0HFdEKw7sZlEv2mjFn_zVLtxJw==", + "Label": "3", + "X25519PublicKey": "mphAr+LCeChgTxphYhAURPdxUyn1KOVINg68fG2s3HM=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 28 + "Load": 91 }, { - "Name": "US-IL#28", + "Name": "US-NY#54", "EntryCountry": "US", "ExitCountry": "US", - "Domain": "us-il-28.protonvpn.com", + "Domain": "37.120.244.98", "Tier": 2, "Features": 8, "Region": null, - "City": "Chicago", - "Score": 1.50397526, + "City": "New York City", + "Score": 1.50465563, "HostCountry": null, - "ID": "tbzT86Ytpvj_NkChyn_fgdXYLW7bF_3B3nVV3UdciBAvtO3xSCyVSOqHKrMAYVnC52XKAIEwWEKJAa494mbB5w==", + "ID": "PSFTautHDRb3I_rvky8jXGz2YyXVdgCogqqTLTbenDT-74-x1wEU_aw3fS0RDgsh3dSyfI64xcbkFhtyyYq-Aw==", "Location": { - "Lat": 41.850000000000001, - "Long": -87.650000000000006 + "Lat": 40.729999999999997, + "Long": -73.939999999999998 }, "Status": 1, "Servers": [ { - "EntryIP": "173.0.77.11", - "ExitIP": "173.0.77.15", - "Domain": "lxc-us-53.protonvpn.com", - "ID": "iewws3fsZHkb3oFAT-2KTriNdPIEux5X9sU5oV-Qcr9b9Y9Z7VjGVyDZCC4oQX1DyJcdkyV6Cdpsf0Dcq0FdjA==", + "EntryIP": "37.120.244.98", + "ExitIP": "37.120.244.100", + "Domain": "node-us-107.protonvpn.net", + "ID": "nKD1JizuuVX6CPjvR4gVebXLyU9myPrRTH3foL9Cvb8DUuTfnYx_sGNJUWSa-IJ7BjkOpD8p0valEyWSnNruwQ==", "Label": "0", - "X25519PublicKey": "GKzlkcd4hUxfmJpmsZi03km4S2k3R/snVXeIhA9NXlI=", + "X25519PublicKey": "CauDP8lrkv8WCgA33k4FT26kveLzc39IZ1RhVEWyMk4=", "Generation": 0, "Status": 1, + "ServicesDown": 0, "ServicesDownReason": null } ], - "Load": 61 + "Load": 68 } ] \ No newline at end of file diff --git a/app/libs/govpn.aar b/app/libs/govpn.aar deleted file mode 100644 index ba3f7c2fb..000000000 Binary files a/app/libs/govpn.aar and /dev/null differ diff --git a/app/src/androidTest/java/com/protonvpn/actions/ConnectionRobot.java b/app/src/androidTest/java/com/protonvpn/actions/ConnectionRobot.java index 4976af937..5ca451044 100644 --- a/app/src/androidTest/java/com/protonvpn/actions/ConnectionRobot.java +++ b/app/src/androidTest/java/com/protonvpn/actions/ConnectionRobot.java @@ -40,7 +40,7 @@ public ConnectionResult clickCancelConnectionButton() { } public ConnectionRobot checkIfNotReachableErrorAppears() { - String errorMessage = getContext().getString(R.string.error_unreachable); + String errorMessage = getContext().getString(R.string.error_server_unreachable); checkIfObjectWithIdAndTextIsDisplayed(R.id.textError, errorMessage); return this; } diff --git a/app/src/androidTest/java/com/protonvpn/di/MockApi.kt b/app/src/androidTest/java/com/protonvpn/di/MockApi.kt index a70a4cc20..8675829b8 100644 --- a/app/src/androidTest/java/com/protonvpn/di/MockApi.kt +++ b/app/src/androidTest/java/com/protonvpn/di/MockApi.kt @@ -27,7 +27,9 @@ import com.protonvpn.android.appconfig.ApiNotificationTypes import com.protonvpn.android.appconfig.ApiNotificationsResponse import com.protonvpn.android.appconfig.ForkedSessionResponse import com.protonvpn.android.appconfig.AppConfigResponse +import com.protonvpn.android.appconfig.DefaultPortsConfig import com.protonvpn.android.appconfig.FeatureFlags +import com.protonvpn.android.appconfig.SmartProtocolConfig import com.protonvpn.android.components.LoaderUI import com.protonvpn.android.models.config.UserData import com.protonvpn.android.models.login.GenericResponse @@ -47,11 +49,19 @@ import java.util.concurrent.TimeUnit class MockApi(scope: CoroutineScope, manager: ApiManager, val userData: UserData) : ProtonApiRetroFit(scope, manager) { override suspend fun getAppConfig(): ApiResult = - ApiResult.Success(AppConfigResponse(featureFlags = FeatureFlags( - maintenanceTrackerEnabled = true, - netShieldEnabled = true, - pollApiNotifications = true, - vpnAccelerator = true))) + ApiResult.Success(AppConfigResponse( + featureFlags = FeatureFlags( + maintenanceTrackerEnabled = true, + netShieldEnabled = true, + pollApiNotifications = true, + vpnAccelerator = true), + smartProtocolConfig = SmartProtocolConfig( + ikeV2Enabled = true, + openVPNEnabled = true, + wireguardEnabled = false + ), + defaultPortsConfig = DefaultPortsConfig.defaultConfig + )) override suspend fun getSession(): ApiResult = ApiResult.Success(SessionListResponse(0, listOf())) diff --git a/app/src/androidTest/java/com/protonvpn/di/MockAppModule.kt b/app/src/androidTest/java/com/protonvpn/di/MockAppModule.kt index 7df55bbd0..923f473a9 100644 --- a/app/src/androidTest/java/com/protonvpn/di/MockAppModule.kt +++ b/app/src/androidTest/java/com/protonvpn/di/MockAppModule.kt @@ -18,6 +18,8 @@ */ package com.protonvpn.di +import android.app.ActivityManager +import android.content.Context import android.os.SystemClock import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingResource @@ -91,6 +93,10 @@ class MockAppModule { fun provideServerManager(userData: UserData) = ServerManager(ProtonApplication.getAppContext(), userData) + @Provides + fun provideActivityManager(): ActivityManager = + ProtonApplication.getAppContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + @Singleton @Provides fun provideServerListUpdater( @@ -172,8 +178,8 @@ class MockAppModule { @Singleton @Provides - fun provideApiClient(userData: UserData, connectivityMonitor: ConnectivityMonitor): VpnApiClient = - VpnApiClient(scope, userData, connectivityMonitor) + fun provideApiClient(userData: UserData, vpnStateMonitor: VpnStateMonitor): VpnApiClient = + VpnApiClient(scope, userData, vpnStateMonitor) @Singleton @Provides @@ -254,7 +260,8 @@ class MockAppModule { vpnStateMonitor, notificationHelper, serverManager, - scope + scope, + System::currentTimeMillis ) @Singleton @@ -301,7 +308,9 @@ class MockAppModule { VpnProtocol.OpenVPN), wireGuard = MockVpnBackend(scope, networkManager, certificateRepository, userData, appConfig, VpnProtocol.WireGuard), - serverDeliver = serverManager) + serverDeliver = serverManager, + config = appConfig + ) @Singleton @Provides @@ -309,6 +318,7 @@ class MockAppModule { userData: UserData, networkManager: NetworkManager, appConfig: AppConfig, + dispatcherProvider: DispatcherProvider, certificateRepository: CertificateRepository, ) = WireguardBackend( ProtonApplication.getAppContext(), @@ -317,6 +327,7 @@ class MockAppModule { userData, appConfig, certificateRepository, + dispatcherProvider, scope ) diff --git a/app/src/androidTest/java/com/protonvpn/di/MockVpnStateMonitor.kt b/app/src/androidTest/java/com/protonvpn/di/MockVpnStateMonitor.kt index 5a3829630..50579de93 100644 --- a/app/src/androidTest/java/com/protonvpn/di/MockVpnStateMonitor.kt +++ b/app/src/androidTest/java/com/protonvpn/di/MockVpnStateMonitor.kt @@ -39,9 +39,10 @@ class MockVpnConnectionManager( vpnStateMonitor: VpnStateMonitor, notificationHelper: NotificationHelper, serverManager: ServerManager, - scope: CoroutineScope + scope: CoroutineScope, + now: () -> Long ) : VpnConnectionManager(ProtonApplication.getAppContext(), userData, vpnBackendProvider, networkManager, - vpnErrorHandler, vpnStateMonitor, notificationHelper, serverManager, scope) { + vpnErrorHandler, vpnStateMonitor, notificationHelper, serverManager, scope, now) { override fun prepare(context: Context): Intent? = null } diff --git a/app/src/androidTest/java/com/protonvpn/mocks/MockVpnBackend.kt b/app/src/androidTest/java/com/protonvpn/mocks/MockVpnBackend.kt index cda65ef11..13b3ee9bc 100644 --- a/app/src/androidTest/java/com/protonvpn/mocks/MockVpnBackend.kt +++ b/app/src/androidTest/java/com/protonvpn/mocks/MockVpnBackend.kt @@ -19,6 +19,7 @@ package com.protonvpn.mocks import com.protonvpn.android.appconfig.AppConfig +import com.protonvpn.android.concurrency.DefaultDispatcherProvider import com.protonvpn.android.models.config.UserData import com.protonvpn.android.models.config.VpnProtocol import com.protonvpn.android.models.profiles.Profile @@ -31,8 +32,8 @@ import com.protonvpn.android.vpn.RetryInfo import com.protonvpn.android.vpn.VpnBackend import com.protonvpn.android.vpn.VpnState import kotlinx.coroutines.CoroutineScope -import localAgent.NativeClient import me.proton.core.network.domain.NetworkManager +import me.proton.vpn.golib.localAgent.NativeClient typealias MockAgentProvider = ( certInfo: CertificateRepository.CertificateResult.Success, @@ -46,14 +47,15 @@ class MockVpnBackend( certificateRepository: CertificateRepository, userData: UserData, appConfig: AppConfig, - val protocol: VpnProtocol + val protocol: VpnProtocol, ) : VpnBackend( userData = userData, appConfig = appConfig, networkManager = networkManager, certificateRepository = certificateRepository, vpnProtocol = protocol, - mainScope = scope + mainScope = scope, + dispatcherProvider = DefaultDispatcherProvider() ) { private var agentProvider: MockAgentProvider? = null @@ -65,7 +67,8 @@ class MockVpnBackend( profile: Profile, server: Server, scan: Boolean, - numberOfPorts: Int + numberOfPorts: Int, + waitForAll: Boolean ): List = if (scan && failScanning) emptyList() diff --git a/app/src/androidTest/java/com/protonvpn/tests/login/LoginViewModelTest.kt b/app/src/androidTest/java/com/protonvpn/tests/login/LoginViewModelTest.kt index 81353b6b9..2385153a5 100644 --- a/app/src/androidTest/java/com/protonvpn/tests/login/LoginViewModelTest.kt +++ b/app/src/androidTest/java/com/protonvpn/tests/login/LoginViewModelTest.kt @@ -47,12 +47,12 @@ import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import me.proton.core.network.domain.ApiResult import me.proton.core.test.kotlin.CoroutinesTest +import me.proton.vpn.golib.srp.Proofs import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test -import srp.Proofs @OptIn(ExperimentalCoroutinesApi::class) @SdkSuppress(minSdkVersion = 28) // Mocking final classes doesn't work on older API levels. diff --git a/app/src/androidTest/java/com/protonvpn/tests/vpn/VpnConnectionTests.kt b/app/src/androidTest/java/com/protonvpn/tests/vpn/VpnConnectionTests.kt index f88d1f191..1cc6ce22e 100644 --- a/app/src/androidTest/java/com/protonvpn/tests/vpn/VpnConnectionTests.kt +++ b/app/src/androidTest/java/com/protonvpn/tests/vpn/VpnConnectionTests.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry import com.protonvpn.android.api.GuestHole import com.protonvpn.android.appconfig.AppConfig +import com.protonvpn.android.appconfig.SmartProtocolConfig import com.protonvpn.android.models.config.UserData import com.protonvpn.android.models.config.VpnProtocol import com.protonvpn.android.models.profiles.Profile @@ -61,6 +62,7 @@ import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.yield import me.proton.core.network.domain.NetworkStatus import me.proton.core.network.domain.session.SessionId +import me.proton.vpn.golib.localAgent.LocalAgent import org.junit.Assert import org.junit.Before import org.junit.Rule @@ -111,12 +113,13 @@ class VpnConnectionTests { private val switchServerFlow = MutableSharedFlow() - private val agentConsts = localAgent.LocalAgent.constants() + private val agentConsts = LocalAgent.constants() private val validCert = CertificateRepository.CertificateResult.Success("good_cert", "good_key") private val badCert = CertificateRepository.CertificateResult.Success("bad_cert", "bad_key") private lateinit var currentCert: CertificateRepository.CertificateResult.Success + private var time = 0L @Before fun setup() { @@ -127,6 +130,8 @@ class VpnConnectionTests { every { userData.sessionId } returns SessionId("1") networkManager = MockNetworkManager() + every { appConfig.getSmartProtocolConfig() } returns SmartProtocolConfig( + ikeV2Enabled = true, openVPNEnabled = true, wireguardEnabled = true) mockStrongSwan = spyk(MockVpnBackend( scope, networkManager, certificateRepository, userData, appConfig, VpnProtocol.IKEv2)) mockOpenVpn = spyk(MockVpnBackend( @@ -140,12 +145,13 @@ class VpnConnectionTests { strongSwan = mockStrongSwan, openVpn = mockOpenVpn, wireGuard = mockWireguard, - serverDeliver = serverManager + serverDeliver = serverManager, + config = appConfig ) monitor = VpnStateMonitor() manager = MockVpnConnectionManager(userData, backendProvider, networkManager, vpnErrorHandler, monitor, - mockk(relaxed = true), mockk(relaxed = true), scope) + mockk(relaxed = true), mockk(relaxed = true), scope, ::time) MockNetworkManager.currentStatus = NetworkStatus.Unmetered @@ -194,6 +200,7 @@ class VpnConnectionTests { @Test fun testSmartFallbackToOpenVPN() = runBlockingTest { + mockWireguard.failScanning = true mockStrongSwan.failScanning = true manager.connect(context, profileSmart) yield() @@ -204,6 +211,7 @@ class VpnConnectionTests { @Test fun testAllBlocked() = runBlockingTest { + mockWireguard.failScanning = true mockStrongSwan.failScanning = true mockOpenVpn.failScanning = true userData.manualProtocol = VpnProtocol.OpenVPN @@ -212,8 +220,8 @@ class VpnConnectionTests { // When scanning fails we'll fallback to attempt connecting with default manual protocol coVerify(exactly = 1) { - mockOpenVpn.prepareForConnection(any(), any(), false) - mockOpenVpn.connect(any()) + mockStrongSwan.prepareForConnection(any(), any(), false) + mockStrongSwan.connect(any()) } Assert.assertEquals(VpnState.Connected, monitor.state) @@ -222,16 +230,15 @@ class VpnConnectionTests { @Test fun smartNoInternet() = runBlockingTest { MockNetworkManager.currentStatus = NetworkStatus.Disconnected - userData.manualProtocol = VpnProtocol.OpenVPN manager.connect(context, profileSmart) yield() coVerify(exactly = 0) { - mockStrongSwan.prepareForConnection(any(), any(), any()) + mockOpenVpn.prepareForConnection(any(), any(), any()) } coVerify(exactly = 1) { - mockOpenVpn.prepareForConnection(any(), any(), false) - mockOpenVpn.connect(any()) + mockStrongSwan.prepareForConnection(any(), any(), false) + mockStrongSwan.connect(any()) } Assert.assertEquals(VpnState.Connected, monitor.state) @@ -291,7 +298,6 @@ class VpnConnectionTests { @Test fun guestHoleFail() = runBlockingTest { - mockStrongSwan.failScanning = true mockOpenVpn.failScanning = true mockOpenVpn.stateOnConnect = VpnState.Disabled @@ -459,6 +465,13 @@ class VpnConnectionTests { @Test fun testExpiredCert() = runBlockingTest { + coEvery { certificateRepository.getCertificate(any(), any()) } coAnswers { + if (currentCert == badCert) + certificateRepository.updateCertificate(userData.sessionId!!, false) + else + currentCert + } + currentCert = badCert manager.connect(context, profileWireguard) scope.advanceUntilIdle() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cfe3bf03..50e74996d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -139,6 +139,10 @@ + - - \ No newline at end of file + diff --git a/app/src/main/java/com/protonvpn/android/AppInfoService.kt b/app/src/main/java/com/protonvpn/android/AppInfoService.kt new file mode 100644 index 000000000..1a2ba6e6c --- /dev/null +++ b/app/src/main/java/com/protonvpn/android/AppInfoService.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2021. Proton Technologies AG + * + * This file is part of ProtonVPN. + * + * ProtonVPN is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonVPN is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonVPN. If not, see . + */ + +package com.protonvpn.android + +import android.app.IntentService +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.Log +import com.protonvpn.android.utils.Constants +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.ByteArrayOutputStream + +private const val WEBP_QUALITY = 70 + +@Suppress("Deprecation") +class AppInfoService : IntentService("AppInfoService") { + + override fun onHandleIntent(intent: Intent?) { + try { + val packageNames = intent?.getStringArrayExtra(EXTRA_PACKAGE_NAME_LIST) ?: return + val iconSizePx = intent.getIntExtra(EXTRA_ICON_SIZE, 48) + val requestCode = intent.getLongExtra(EXTRA_REQUEST_CODE, 0) + + val scope = CoroutineScope(Dispatchers.Default) + val appMetaDataChannel = Channel(10) + val processAndSendJob = scope.launch { + for (appMetaData in appMetaDataChannel) { + val resultIntent = createResultIntent(appMetaData, iconSizePx, requestCode) + sendBroadcast(resultIntent) + } + } + + packageNames.forEach { pkgName -> + appMetaDataChannel.sendBlocking(getAppMetaData(pkgName)) + } + + appMetaDataChannel.close() + + runBlocking { + processAndSendJob.join() + } + } catch (e: Throwable) { + Log.i(Constants.SECONDARY_PROCESS_TAG, "Exception while reading app metadata", e) + throw e + } + } + + private data class AppMetaData(val packageName: String, val label: String, val iconDrawable: Drawable?) + + private fun getAppMetaData(pkgName: String): AppMetaData = + try { + val appInfo = packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) + AppMetaData( + pkgName, + appInfo.loadLabel(packageManager).toString(), + // Don't extract process the default icon. + if (appInfo.icon > 0) appInfo.loadIcon(packageManager) else null + ) + } catch (e: PackageManager.NameNotFoundException) { + AppMetaData(pkgName, pkgName, null) + } + + private fun createResultIntent(appMetaData: AppMetaData, iconSizePx: Int, requestCode: Long): Intent = + Intent(RESULT_ACTION).apply { + setPackage(getPackageName()) + putExtra(EXTRA_REQUEST_CODE, requestCode) + putExtra(EXTRA_PACKAGE_NAME, appMetaData.packageName) + putExtra(EXTRA_APP_LABEL, appMetaData.label) + if (appMetaData.iconDrawable != null) { + putExtra(EXTRA_APP_ICON, compressIcon(appMetaData.iconDrawable, iconSizePx)) + } + } + + private fun compressIcon(iconDrawable: Drawable, sizePx: Int): ByteArray { + val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + with(iconDrawable) { + setBounds(0, 0, sizePx, sizePx) + draw(canvas) + } + + val bytes = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.WEBP, WEBP_QUALITY, bytes) + return bytes.toByteArray() + } + + companion object { + private const val EXTRA_PACKAGE_NAME_LIST = "package names" + private const val EXTRA_ICON_SIZE = "icon size" + const val RESULT_ACTION = "app info action" + const val EXTRA_PACKAGE_NAME = "package name" + const val EXTRA_APP_LABEL = "app label" + const val EXTRA_APP_ICON = "app icon" + const val EXTRA_REQUEST_CODE = "request code" + + fun createIntent(context: Context, packageNames: List, iconSizePx: Int, requestCode: Long) = + Intent(context, AppInfoService::class.java).apply { + putExtra(EXTRA_PACKAGE_NAME_LIST, packageNames.toTypedArray()) + putExtra(EXTRA_ICON_SIZE, iconSizePx) + putExtra(EXTRA_REQUEST_CODE, requestCode) + } + + fun createStopIntent(context: Context) = Intent(context, AppInfoService::class.java) + } +} diff --git a/app/src/main/java/com/protonvpn/android/ProtonApplication.java b/app/src/main/java/com/protonvpn/android/ProtonApplication.java index 03adbf59c..b2f39f6c0 100644 --- a/app/src/main/java/com/protonvpn/android/ProtonApplication.java +++ b/app/src/main/java/com/protonvpn/android/ProtonApplication.java @@ -19,8 +19,11 @@ package com.protonvpn.android; import android.app.Activity; +import android.app.ActivityManager; import android.content.Context; +import android.os.Build; import android.os.SystemClock; +import android.util.Log; import com.datatheorem.android.trustkit.TrustKit; import com.evernote.android.state.StateSaver; @@ -29,9 +32,10 @@ import com.protonvpn.android.components.NotificationHelper; import com.protonvpn.android.di.AppComponent; import com.protonvpn.android.di.DaggerAppComponent; -import com.protonvpn.android.migration.NewAppMigrator; import com.protonvpn.android.utils.AndroidUtils; +import com.protonvpn.android.utils.Constants; import com.protonvpn.android.utils.DefaultActivityLifecycleCallbacks; +import com.protonvpn.android.utils.ProtonExceptionHandler; import com.protonvpn.android.utils.ProtonLogger; import com.protonvpn.android.utils.ProtonPreferences; import com.protonvpn.android.utils.Storage; @@ -43,7 +47,7 @@ import org.jetbrains.annotations.NotNull; import org.strongswan.android.logic.StrongSwanApplication; -import javax.inject.Inject; +import java.util.List; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; @@ -64,8 +68,16 @@ public class ProtonApplication extends DaggerApplication { @Override public void onCreate() { super.onCreate(); - initActivityObserver(); + String processName = AndroidUtils.getMyProcessName(this); + boolean isMainProcess = processName.equals(getPackageName()); + initSentry(); + if (!isMainProcess) { + Log.i(Constants.SECONDARY_PROCESS_TAG, "Starting process: " + processName); + return; + } + + initActivityObserver(); initStrongSwan(); NotificationHelper.Companion.initNotificationChannel(this); JodaTimeAndroid.init(this); @@ -73,7 +85,6 @@ public void onCreate() { new ANRWatchDog(15000).start(); initPreferences(); - NewAppMigrator.INSTANCE.migrate(this); RxActivityResult.register(this); StateSaver.setEnabledForAllActivitiesAndSupportFragments(this, true); @@ -124,6 +135,9 @@ private void initPreferences() { private void initSentry() { String sentryDsn = BuildConfig.DEBUG ? null : BuildConfig.Sentry_DSN; Sentry.init(sentryDsn, new AndroidSentryClientFactory(this)); + + Thread.UncaughtExceptionHandler currentHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(new ProtonExceptionHandler(currentHandler)); } private void initLeakCanary() { diff --git a/app/src/main/java/com/protonvpn/android/api/ProtonApiRetroFit.kt b/app/src/main/java/com/protonvpn/android/api/ProtonApiRetroFit.kt index 1b4ddd111..dfc7e37fe 100644 --- a/app/src/main/java/com/protonvpn/android/api/ProtonApiRetroFit.kt +++ b/app/src/main/java/com/protonvpn/android/api/ProtonApiRetroFit.kt @@ -28,6 +28,7 @@ import com.protonvpn.android.models.login.SessionListResponse import com.protonvpn.android.models.login.VpnInfoResponse import com.protonvpn.android.models.vpn.CertificateRequestBody import com.protonvpn.android.models.vpn.CertificateResponse +import com.protonvpn.android.utils.NetUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import me.proton.core.network.data.protonApi.RefreshTokenRequest @@ -54,10 +55,10 @@ open class ProtonApiRetroFit(val scope: CoroutineScope, private val manager: Api ) = makeCall(callback, loader) { it.postBugReport(params) } open suspend fun getServerList(loader: LoaderUI?, ip: String?) = - makeCall(loader) { it.getServers(ip) } + makeCall(loader) { it.getServers(createNetZoneHeaders(ip)) } open suspend fun getLoads(ip: String?) = - manager { getLoads(ip) } + manager { getLoads(createNetZoneHeaders(ip)) } open suspend fun getStreamingServices() = manager { getStreamingServices() } @@ -135,4 +136,17 @@ open class ProtonApiRetroFit(val scope: CoroutineScope, private val manager: Api } } } + + // Used in routes that provide server information including a score of how good a server is + // for the particular user to connect to. + // To provide relevant scores even when connected to VPN, we send a truncated version of + // the user's public IP address. In keeping with our no-logs policy, this partial IP address + // is not stored on the server and is only used to fulfill this one-off API request. + private fun createNetZoneHeaders(ip: String?) = + if (!ip.isNullOrEmpty()) { + val netzone = NetUtils.stripIP(ip) + mapOf(ProtonVPNRetrofit.HEADER_NETZONE to netzone) + } else { + emptyMap() + } } diff --git a/app/src/main/java/com/protonvpn/android/api/ProtonVPNRetrofit.kt b/app/src/main/java/com/protonvpn/android/api/ProtonVPNRetrofit.kt index cd0bf5478..a21dd6732 100644 --- a/app/src/main/java/com/protonvpn/android/api/ProtonVPNRetrofit.kt +++ b/app/src/main/java/com/protonvpn/android/api/ProtonVPNRetrofit.kt @@ -41,6 +41,7 @@ import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.HeaderMap import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -48,11 +49,15 @@ import retrofit2.http.Query @Suppress("ComplexInterface") interface ProtonVPNRetrofit : BaseRetrofitApi { + companion object { + const val HEADER_NETZONE = "x-pm-netzone" + } + @GET("vpn/logicals") - suspend fun getServers(@Query("IP") ip: String?): ServerList + suspend fun getServers(@HeaderMap headers: Map): ServerList @GET("vpn/loads") - suspend fun getLoads(@Query("IP") ip: String?): LoadsResponse + suspend fun getLoads(@HeaderMap headers: Map): LoadsResponse @GET("vpn/streamingservices") suspend fun getStreamingServices(): StreamingServicesResponse @@ -88,7 +93,7 @@ interface ProtonVPNRetrofit : BaseRetrofitApi { @POST("reports/bug") suspend fun postBugReport(@Body params: RequestBody): GenericResponse - @GET("vpn/clientconfig") + @GET("vpn/v2/clientconfig") suspend fun getAppConfig(): AppConfigResponse @GET("core/v4/notifications") diff --git a/app/src/main/java/com/protonvpn/android/api/VpnApiClient.kt b/app/src/main/java/com/protonvpn/android/api/VpnApiClient.kt index fe3130f47..1ea1c9c17 100644 --- a/app/src/main/java/com/protonvpn/android/api/VpnApiClient.kt +++ b/app/src/main/java/com/protonvpn/android/api/VpnApiClient.kt @@ -22,17 +22,20 @@ import android.os.Build import com.protonvpn.android.BuildConfig import com.protonvpn.android.models.config.UserData import com.protonvpn.android.utils.Constants -import com.protonvpn.android.vpn.ConnectivityMonitor +import com.protonvpn.android.vpn.VpnState +import com.protonvpn.android.vpn.VpnStateMonitor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import me.proton.core.network.domain.ApiClient import java.util.Locale +private val NO_DOH_STATES = listOf(VpnState.Connected, VpnState.Connecting) + class VpnApiClient( val scope: CoroutineScope, val userData: UserData, - val connectivityMonitor: ConnectivityMonitor, + val vpnStateMonitor: VpnStateMonitor ) : ApiClient { val forceUpdateEvent = MutableSharedFlow() @@ -40,7 +43,7 @@ class VpnApiClient( override val appVersionHeader get() = "${Constants.CLIENT_ID}_" + BuildConfig.VERSION_NAME + BuildConfig.STORE_SUFFIX override val enableDebugLogging = BuildConfig.DEBUG - override val shouldUseDoh get() = userData.apiUseDoH && !connectivityMonitor.vpnActive + override val shouldUseDoh get() = userData.apiUseDoH && vpnStateMonitor.state !in NO_DOH_STATES override val userAgent: String get() = String.format(Locale.US, "ProtonVPN/%s (Android %s; %s %s)", diff --git a/app/src/main/java/com/protonvpn/android/appconfig/AppConfig.kt b/app/src/main/java/com/protonvpn/android/appconfig/AppConfig.kt index 9a1a940d8..d019f046a 100644 --- a/app/src/main/java/com/protonvpn/android/appconfig/AppConfig.kt +++ b/app/src/main/java/com/protonvpn/android/appconfig/AppConfig.kt @@ -74,17 +74,34 @@ class AppConfig(scope: CoroutineScope, val api: ProtonApiRetroFit, val userData: fun isMaintenanceTrackerEnabled(): Boolean = appConfigResponse.featureFlags.maintenanceTrackerEnabled - fun getOpenVPNPorts(): DefaultPorts = appConfigResponse.defaultPorts!! + fun getOpenVPNPorts(): DefaultPorts = getDefaultPortsConfig().getOpenVPNPorts() + + fun getWireguardPorts(): DefaultPorts = getDefaultPortsConfig().getWireguardPorts() + + private fun getDefaultPortsConfig() : DefaultPortsConfig = appConfigResponse.defaultPortsConfig ?: DefaultPortsConfig.defaultConfig + + fun getSmartProtocolConfig(): SmartProtocolConfig { + val smartConfig = appConfigResponse.smartProtocolConfig + return smartConfig ?: getDefaultConfig().smartProtocolConfig!! + } fun getFeatureFlags(): FeatureFlags = appConfigResponse.featureFlags fun getLiveConfig(): LiveData = appConfigResponseObservable private fun getDefaultConfig(): AppConfigResponse { - val defaultPorts = OpenVPNConfigResponse(DefaultPorts.defaults) + val defaultPorts = DefaultPortsConfig.defaultConfig val defaultFeatureFlags = FeatureFlags() - return AppConfigResponse(openVPNConfigResponse = defaultPorts, - featureFlags = defaultFeatureFlags) + val defaultSmartProtocolConfig = SmartProtocolConfig( + ikeV2Enabled = true, + openVPNEnabled = true, + wireguardEnabled = false + ) + return AppConfigResponse( + defaultPortsConfig = defaultPorts, + featureFlags = defaultFeatureFlags, + smartProtocolConfig = defaultSmartProtocolConfig + ) } companion object { diff --git a/app/src/main/java/com/protonvpn/android/appconfig/AppConfigResponse.kt b/app/src/main/java/com/protonvpn/android/appconfig/AppConfigResponse.kt index 6f8236590..b059bdc95 100644 --- a/app/src/main/java/com/protonvpn/android/appconfig/AppConfigResponse.kt +++ b/app/src/main/java/com/protonvpn/android/appconfig/AppConfigResponse.kt @@ -26,8 +26,7 @@ import kotlinx.serialization.Serializable class AppConfigResponse( @SerialName(value = "ServerRefreshInterval") val underMaintenanceDetectionDelay: Long = Constants.DEFAULT_MAINTENANCE_CHECK_MINUTES, - @SerialName(value = "OpenVPNConfig") val openVPNConfigResponse: OpenVPNConfigResponse? = null, - @SerialName(value = "FeatureFlags") val featureFlags: FeatureFlags -) { - val defaultPorts: DefaultPorts? = openVPNConfigResponse?.defaultPorts -} + @SerialName(value = "DefaultPorts") val defaultPortsConfig: DefaultPortsConfig?, + @SerialName(value = "FeatureFlags") val featureFlags: FeatureFlags, + @SerialName(value = "SmartProtocol") val smartProtocolConfig: SmartProtocolConfig? +) diff --git a/app/src/main/java/com/protonvpn/android/appconfig/DefaultPorts.kt b/app/src/main/java/com/protonvpn/android/appconfig/DefaultPorts.kt index 75af46dcd..2e61dfa6e 100644 --- a/app/src/main/java/com/protonvpn/android/appconfig/DefaultPorts.kt +++ b/app/src/main/java/com/protonvpn/android/appconfig/DefaultPorts.kt @@ -23,18 +23,6 @@ import kotlinx.serialization.Serializable @Serializable class DefaultPorts( - @SerialName(value = "UDP") private val udpPorts: List, - @SerialName(value = "TCP") private val tcpPorts: List -) { - fun getUdpPorts(): List = - if (udpPorts.isEmpty()) DEFAULT_PORT_LIST else udpPorts - - fun getTcpPorts(): List = - if (tcpPorts.isEmpty()) DEFAULT_PORT_LIST else tcpPorts - - companion object { - private val DEFAULT_PORT_LIST = listOf(443) - val defaults: DefaultPorts - get() = DefaultPorts(DEFAULT_PORT_LIST, DEFAULT_PORT_LIST) - } -} + @SerialName(value = "UDP") val udpPorts: List, + @SerialName(value = "TCP") val tcpPorts: List = emptyList() +) diff --git a/app/src/main/java/com/protonvpn/android/appconfig/OpenVPNConfigResponse.kt b/app/src/main/java/com/protonvpn/android/appconfig/DefaultPortsConfig.kt similarity index 52% rename from app/src/main/java/com/protonvpn/android/appconfig/OpenVPNConfigResponse.kt rename to app/src/main/java/com/protonvpn/android/appconfig/DefaultPortsConfig.kt index f3cf7044a..440edd65e 100644 --- a/app/src/main/java/com/protonvpn/android/appconfig/OpenVPNConfigResponse.kt +++ b/app/src/main/java/com/protonvpn/android/appconfig/DefaultPortsConfig.kt @@ -22,6 +22,27 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -class OpenVPNConfigResponse( - @SerialName(value = "DefaultPorts") val defaultPorts: DefaultPorts -) +class DefaultPortsConfig( + @SerialName(value = "OpenVPN") private val openVpnPorts: DefaultPorts, + @SerialName(value = "WireGuard") private val wireguardPorts: DefaultPorts +) { + fun getOpenVPNPorts() = + if (openVpnPorts.tcpPorts.isEmpty() || openVpnPorts.udpPorts.isEmpty()) + openVPNDefaults + else + openVpnPorts + + fun getWireguardPorts() = + if (wireguardPorts.udpPorts.isEmpty()) + wireguardDefaults + else + wireguardPorts + + companion object { + private val wireguardDefaults = + DefaultPorts(listOf(51820), emptyList()) + private val openVPNDefaults = + DefaultPorts(listOf(443), listOf(443)) + val defaultConfig = DefaultPortsConfig(openVPNDefaults, wireguardDefaults) + } +} diff --git a/app/src/main/java/com/protonvpn/android/appconfig/SmartProtocolConfig.kt b/app/src/main/java/com/protonvpn/android/appconfig/SmartProtocolConfig.kt new file mode 100644 index 000000000..e38a07a04 --- /dev/null +++ b/app/src/main/java/com/protonvpn/android/appconfig/SmartProtocolConfig.kt @@ -0,0 +1,11 @@ +package com.protonvpn.android.appconfig + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class SmartProtocolConfig( + @SerialName(value = "IKEv2") val ikeV2Enabled: Boolean, + @SerialName(value = "OpenVPN") val openVPNEnabled: Boolean, + @SerialName(value = "WireGuard") val wireguardEnabled: Boolean +) diff --git a/app/src/main/java/com/protonvpn/android/components/ProtonLoader.java b/app/src/main/java/com/protonvpn/android/components/ProtonLoader.java index f06c9de81..aa00bb095 100644 --- a/app/src/main/java/com/protonvpn/android/components/ProtonLoader.java +++ b/app/src/main/java/com/protonvpn/android/components/ProtonLoader.java @@ -28,6 +28,7 @@ import com.protonvpn.android.R; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import butterknife.BindView; import butterknife.ButterKnife; @@ -36,6 +37,9 @@ public class ProtonLoader extends FrameLayout { @BindView(R.id.loaderCircle) View loaderCircle; @BindView(R.id.loaderCircle2) View loaderCircle2; + @Nullable + private AnimatorSet animations; + public ProtonLoader(@NonNull Context context) { super(context); } @@ -51,20 +55,39 @@ public ProtonLoader(@NonNull Context context, AttributeSet attrs, int defStyleAt private void init() { inflate(getContext(), R.layout.item_proton_loader, this); ButterKnife.bind(this); - animateView(loaderCircle); - animateView(loaderCircle2); } - private void animateView(View view) { + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + init(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + stopAnimations(); + animations = new AnimatorSet(); + animations.playTogether(animateView(loaderCircle), animateView(loaderCircle2)); + animations.start(); + } + + @Override + protected void onDetachedFromWindow() { + stopAnimations(); + super.onDetachedFromWindow(); + } + + private AnimatorSet animateView(View view) { AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(getContext(), R.animator.animation_rotate); set.setTarget(view); - set.start(); + return set; } - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - init(); + private void stopAnimations() { + if (animations != null) { + animations.cancel(); + } } } diff --git a/app/src/main/java/com/protonvpn/android/di/AppModule.kt b/app/src/main/java/com/protonvpn/android/di/AppModule.kt index 977556037..955642fa0 100644 --- a/app/src/main/java/com/protonvpn/android/di/AppModule.kt +++ b/app/src/main/java/com/protonvpn/android/di/AppModule.kt @@ -18,6 +18,8 @@ */ package com.protonvpn.android.di +import android.app.ActivityManager +import android.content.Context import android.os.SystemClock import com.google.gson.Gson import com.protonvpn.android.BuildConfig @@ -100,6 +102,10 @@ class AppModule { fun provideServerManager(userData: UserData) = ServerManager(ProtonApplication.getAppContext(), userData) + @Provides + fun provideActivityManager(): ActivityManager = + ProtonApplication.getAppContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + @Singleton @Provides fun provideServerListUpdater( @@ -182,8 +188,8 @@ class AppModule { @Singleton @Provides - fun provideApiClient(userData: UserData, connectivityMonitor: ConnectivityMonitor): VpnApiClient = - VpnApiClient(scope, userData, connectivityMonitor) + fun provideApiClient(userData: UserData, vpnStateMonitor: VpnStateMonitor): VpnApiClient = + VpnApiClient(scope, userData, vpnStateMonitor) @Singleton @Provides @@ -262,6 +268,7 @@ class AppModule { notificationHelper, serverManager, scope, + System::currentTimeMillis ) @Singleton @@ -301,17 +308,19 @@ class AppModule { appConfig: AppConfig, serverManager: ServerManager, certificateRepository: CertificateRepository, - wireguardBackend: WireguardBackend + wireguardBackend: WireguardBackend, + dispatcherProvider: DispatcherProvider ): VpnBackendProvider = ProtonVpnBackendProvider( + appConfig, StrongSwanBackend( random, networkManager, scope, - System::currentTimeMillis, userData, appConfig, - certificateRepository + certificateRepository, + dispatcherProvider ), OpenVpnBackend( random, @@ -320,7 +329,8 @@ class AppModule { appConfig, System::currentTimeMillis, certificateRepository, - scope + scope, + dispatcherProvider ), wireguardBackend, serverManager @@ -333,6 +343,7 @@ class AppModule { networkManager: NetworkManager, appConfig: AppConfig, certificateRepository: CertificateRepository, + dispatcherProvider: DispatcherProvider ) = WireguardBackend( ProtonApplication.getAppContext(), GoBackend(WireguardContextWrapper(ProtonApplication.getAppContext())), @@ -340,6 +351,7 @@ class AppModule { userData, appConfig, certificateRepository, + dispatcherProvider, scope ) diff --git a/app/src/main/java/com/protonvpn/android/migration/NewAppMigrator.kt b/app/src/main/java/com/protonvpn/android/migration/NewAppMigrator.kt deleted file mode 100644 index a91384c9e..000000000 --- a/app/src/main/java/com/protonvpn/android/migration/NewAppMigrator.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2019 Proton Technologies AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ -package com.protonvpn.android.migration - -import android.content.ContentResolver -import android.content.Context -import android.net.Uri -import com.google.gson.JsonSyntaxException -import com.protonvpn.android.models.config.UserData -import com.protonvpn.android.models.login.LoginResponse -import com.protonvpn.android.models.profiles.SavedProfilesV3 -import com.protonvpn.android.ui.onboarding.OnboardingPreferences -import com.protonvpn.android.utils.AndroidUtils.isPackageSignedWith -import com.protonvpn.android.utils.Log -import com.protonvpn.android.utils.Storage - -object NewAppMigrator { - - const val PREFS_MIGRATED_FROM_OLD = "ProtonApplication.PREFS_MIGRATED_FROM_OLD" - const val OLD_APP_ID = "com.protonvpn.android" - - private const val PREFS_MIGRATION_FINISHED = "ProtonApplication.PREFS_MIGRATION_FINISHED" - private const val CONTENT_URI_PREFIX = "content://$OLD_APP_ID.content.migration/" - private const val MIGRATION_PROVIDER_AUTHORITY = "$OLD_APP_ID.content.migration" - private const val OLD_PACKAGE_CERTIFICATE = - "308203233082020ba003020102020444fcbebf300d06092a864886f70d01010b050030423110300e060355040a13075465736f6e657431123010060355040b130950726f746f6e56504e311a301806035504031311416c6769726461732050756e647a697573301e170d3137303631363130343535305a170d3432303631303130343535305a30423110300e060355040a13075465736f6e657431123010060355040b130950726f746f6e56504e311a301806035504031311416c6769726461732050756e647a69757330820122300d06092a864886f70d01010105000382010f003082010a028201010086968df72b768bcc8f6f2e022587424f40e7be7d8d4e069e0a2997014a1baf1bc982ce3f4c0ccea8fbd2124a14eab29e26710514872de597c1c24f321634821cdefca2a57c03b0a26b28f857aecbfdfd4f90916f65780f5ac9ff098d6c0559633b139660bf02a970b058252d4d3cce33eda49e68f899336f72d20c4afa66233d7401e81b7e6c69e09edae7ef187d85564f39e25436c32ab781f403ad2d40f2517d20c4bd364b27639a24c88e3ff5c95cc810553da46f4a994256bf117f7c77beaad44d068c8e68adec72070d48eabf53beb94a0a27a363eba3731afaf2a8458cd8e7d0a10055b244fa5604d4de4c40da5d288f4afad435f5fce530f52d4f9eb10203010001a321301f301d0603551d0e0416041426cc2e63f2ff414b85d38f51bce78e2d58f09e82300d06092a864886f70d01010b0500038201010071d5517cfcf5b4cb265f0e3d7e92fc5f45f04a6717a86393b4e1b611a13493d43c11e0e9079b84fa6eee61ed1a75977bbbb36054c19fa05cc734b22286754f79461863ce1e85efaf18936f490212c82d65077e56e5b96d76e90ea28c0dabec2d737817b7129ef9e494f591f57e6e25e9cf8fe86dfbf4a072afeecaf37e8fd37b6a22e9495e1d7a3d4e87f43ff956fd5a04bcc259bfb1545bca5617df22e1664f0f41104ef98786703e025f143dedc520d7d455d7e5649f697de34724d970e710b363b14d9a6eefab60032c76eba32b411ae792c2c6060649545abb7f97bff40bb3b5025dd16ebc27edd7bec7ca8830d66eea71bd9e5bc975c29d125eed2bdbec" - - fun migrate(context: Context) = with(context) { - if (Storage.getBoolean(PREFS_MIGRATION_FINISHED)) - return - - val providerPackage = - packageManager.resolveContentProvider(MIGRATION_PROVIDER_AUTHORITY, 0)?.packageName - if (providerPackage == OLD_APP_ID && isPackageSignedWith(context, providerPackage, OLD_PACKAGE_CERTIFICATE)) { - Storage.saveBoolean(OnboardingPreferences.SLIDES_SHOWN, true) - migrateObject(contentResolver, SavedProfilesV3::class.java, "profiles") - migrateObject(contentResolver, UserData::class.java, "userData") - migrateObject(contentResolver, LoginResponse::class.java, "loginResponse") - Storage.saveBoolean(PREFS_MIGRATED_FROM_OLD, true) - } - - Storage.saveBoolean(PREFS_MIGRATION_FINISHED, true) - } - - private fun migrateObject(contentResolver: ContentResolver, objClass: Class, name: String) { - contentResolver.query( - Uri.parse(CONTENT_URI_PREFIX + name), null, null, null, null - )?.apply { - try { - if (moveToNext()) - Storage.save(Storage.toObject(objClass, getString(1))) - } catch (e: Exception) { - Log.exception(e) - } catch (e: JsonSyntaxException) { - Log.exception(e) - } finally { - close() - } - } - } -} diff --git a/app/src/main/java/com/protonvpn/android/models/config/VpnProtocol.kt b/app/src/main/java/com/protonvpn/android/models/config/VpnProtocol.kt index a8b7a052c..ae169620b 100644 --- a/app/src/main/java/com/protonvpn/android/models/config/VpnProtocol.kt +++ b/app/src/main/java/com/protonvpn/android/models/config/VpnProtocol.kt @@ -25,17 +25,5 @@ enum class VpnProtocol { Smart; fun localAgentEnabled(): Boolean = this == WireGuard - - // TODO remove this parameter after wireguard exits beta - fun displayName(wireguardWithBeta: Boolean = true): String { - return if (this == WireGuard) { - if (wireguardWithBeta) { - "WireGuard (beta)" - } else { - "WireGuard" - } - } else { - toString() - } - } + fun displayName() = toString() } diff --git a/app/src/main/java/com/protonvpn/android/models/vpn/ConnectionParamsWireguard.kt b/app/src/main/java/com/protonvpn/android/models/vpn/ConnectionParamsWireguard.kt index 2e29f2239..959ccaee8 100644 --- a/app/src/main/java/com/protonvpn/android/models/vpn/ConnectionParamsWireguard.kt +++ b/app/src/main/java/com/protonvpn/android/models/vpn/ConnectionParamsWireguard.kt @@ -34,6 +34,7 @@ import org.strongswan.android.utils.IPRangeSet class ConnectionParamsWireguard( profile: Profile, server: Server, + val port: Int, connectingDomain: ConnectingDomain ) : ConnectionParams( profile, @@ -60,7 +61,7 @@ class ConnectionParamsWireguard( val peerProxy = config.addPeer() peerProxy.publicKey = connectingDomain.publicKeyX25519 - peerProxy.endpoint = connectingDomain.entryIp + ":" + WIREGUARD_PORT + peerProxy.endpoint = connectingDomain.entryIp + ":" + port val excludedIPs = mutableListOf() if (userData.useSplitTunneling) { @@ -75,7 +76,8 @@ class ConnectionParamsWireguard( excludedIPs += NetworkUtils.getLocalNetworks(context, false).toList() val allowedIps = calculateAllowedIps(excludedIPs) - ProtonLogger.log("Allowed IPs: " + allowedIps) + ProtonLogger.log("Port: $port") + ProtonLogger.log("Allowed IPs: $allowedIps") peerProxy.allowedIps = allowedIps return config.resolve() @@ -94,8 +96,6 @@ class ConnectionParamsWireguard( return ipRangeSet.subnets().joinToString(", ") + ", 2000::/3" } - companion object { - - private const val WIREGUARD_PORT = "51820" - } + override fun hasSameProtocolParams(other: ConnectionParams) = + super.hasSameProtocolParams(other) && other is ConnectionParamsWireguard && other.port == port } diff --git a/app/src/main/java/com/protonvpn/android/tv/TvStatusFragment.kt b/app/src/main/java/com/protonvpn/android/tv/TvStatusFragment.kt index 122dce0f2..e017781cf 100644 --- a/app/src/main/java/com/protonvpn/android/tv/TvStatusFragment.kt +++ b/app/src/main/java/com/protonvpn/android/tv/TvStatusFragment.kt @@ -150,11 +150,8 @@ class TvStatusFragment : DaggerFragment() { private fun onError(error: ErrorType) = with(binding) { when (error) { - ErrorType.LOOKUP_FAILED -> - textStatus.setText(R.string.error_lookup_failed) ErrorType.UNREACHABLE -> - textStatus.setText(R.string.error_unreachable) - + textStatus.setText(R.string.error_server_unreachable) // dialog ErrorType.AUTH_FAILED -> showErrorDialog(R.string.error_auth_failed) diff --git a/app/src/main/java/com/protonvpn/android/ui/home/HomeActivity.java b/app/src/main/java/com/protonvpn/android/ui/home/HomeActivity.java index 06ad23f4f..a18527dfe 100644 --- a/app/src/main/java/com/protonvpn/android/ui/home/HomeActivity.java +++ b/app/src/main/java/com/protonvpn/android/ui/home/HomeActivity.java @@ -55,7 +55,6 @@ import com.protonvpn.android.components.SecureCoreCallback; import com.protonvpn.android.components.SwitchEx; import com.protonvpn.android.components.ViewPagerAdapter; -import com.protonvpn.android.migration.NewAppMigrator; import com.protonvpn.android.models.config.UserData; import com.protonvpn.android.models.profiles.Profile; import com.protonvpn.android.models.vpn.Server; @@ -73,7 +72,6 @@ import com.protonvpn.android.ui.login.LoginActivity; import com.protonvpn.android.ui.onboarding.OnboardingDialogs; import com.protonvpn.android.ui.onboarding.OnboardingPreferences; -import com.protonvpn.android.utils.AndroidUtils; import com.protonvpn.android.utils.AnimationTools; import com.protonvpn.android.utils.HtmlTools; import com.protonvpn.android.utils.ProtonLogger; @@ -197,13 +195,6 @@ protected void onCreate(Bundle savedInstanceState) { }); serverListUpdater.startSchedule(getLifecycle(), this); - - if (Storage.getBoolean(NewAppMigrator.PREFS_MIGRATED_FROM_OLD)) { - if (AndroidUtils.INSTANCE.isPackageInstalled(this, NewAppMigrator.OLD_APP_ID)) { - showMigrationDialog(); - } - Storage.saveBoolean(NewAppMigrator.PREFS_MIGRATED_FROM_OLD, false); - } } @Override @@ -226,16 +217,6 @@ public void onTrimMemory(int level) { ProtonLogger.INSTANCE.log("HomeActivity: onTrimMemory level " + level); } - private void showMigrationDialog() { - AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setMessage(R.string.successful_migration_message); - Intent oldAppIntent = AndroidUtils.INSTANCE.playMarketIntentFor(NewAppMigrator.OLD_APP_ID); - dialog.setPositiveButton(R.string.successful_migration_uninstall, - (dialogInterface, button) -> startActivity(oldAppIntent)); - dialog.setNegativeButton(R.string.ok, null); - dialog.create().show(); - } - private void checkForUpdate() { int versionCode = Storage.getInt("VERSION_CODE"); Storage.saveInt("VERSION_CODE", BuildConfig.VERSION_CODE); diff --git a/app/src/main/java/com/protonvpn/android/ui/home/ServerListUpdater.kt b/app/src/main/java/com/protonvpn/android/ui/home/ServerListUpdater.kt index 6da40c969..d4a221875 100644 --- a/app/src/main/java/com/protonvpn/android/ui/home/ServerListUpdater.kt +++ b/app/src/main/java/com/protonvpn/android/ui/home/ServerListUpdater.kt @@ -26,7 +26,6 @@ import com.protonvpn.android.api.NetworkLoader import com.protonvpn.android.api.ProtonApiRetroFit import com.protonvpn.android.models.config.UserData import com.protonvpn.android.models.vpn.ServerList -import com.protonvpn.android.utils.NetUtils import com.protonvpn.android.utils.ReschedulableTask import com.protonvpn.android.utils.ServerManager import com.protonvpn.android.utils.Storage @@ -102,9 +101,6 @@ class ServerListUpdater( } } - private val strippedIP - get() = ipAddress.value?.takeIf { it.isNotEmpty() }?.let { NetUtils.stripIP(it) } - private val lastLoadsUpdate get() = lastLoadsUpdateInternal.coerceAtLeast(lastServerListUpdate) @@ -147,7 +143,7 @@ class ServerListUpdater( } private suspend fun updateLoads(): Boolean { - val result = api.getLoads(strippedIP) + val result = api.getLoads(ipAddress.value) if (result is ApiResult.Success) { serverManager.updateLoads(result.value.loadsList) lastLoadsUpdateInternal = now() @@ -188,12 +184,7 @@ class ServerListUpdater( serverManager.setStreamingServices(it) } - // The following route is used to retrieve VPN server information, including scores for - // the best server to connect to depending on a user's proximity to a server and its load. - // To provide relevant scores even when connected to VPN, we send a truncated version of - // the user's public IP address. In keeping with our no-logs policy, this partial IP address - // is not stored on the server and is only used to fulfill this one-off API request. - val result = api.getServerList(loaderUI, strippedIP) + val result = api.getServerList(loaderUI, ipAddress.value) if (result is ApiResult.Success) { serverManager.setServers(result.value.serverList) } diff --git a/app/src/main/java/com/protonvpn/android/ui/home/vpn/VpnStateFragment.java b/app/src/main/java/com/protonvpn/android/ui/home/vpn/VpnStateFragment.java index 98f1a2732..b8cfb3b1b 100644 --- a/app/src/main/java/com/protonvpn/android/ui/home/vpn/VpnStateFragment.java +++ b/app/src/main/java/com/protonvpn/android/ui/home/vpn/VpnStateFragment.java @@ -123,7 +123,6 @@ public class VpnStateFragment extends BaseFragment { @BindView(R.id.textProtocol) TextView textProtocol; @BindView(R.id.textSessionTime) TextView textSessionTime; @BindView(R.id.textError) TextView textError; - @BindView(R.id.textSupport) TextView textSupport; @BindView(R.id.progressBarError) ProgressBar progressBarError; @BindView(R.id.textLoad) TextView textLoad; @BindView(R.id.imageLoad) CircleImageView imageLoad; @@ -166,11 +165,6 @@ public void buttonDisconnect() { buttonCancel(); } - @OnClick(R.id.textSupport) - public void textSupport() { - openProtonUrl(getActivity(), "https://protonvpn.com/support/solutions-android-vpn-app-issues/"); - } - @OnClick(R.id.buttonSaveToProfile) public void buttonSaveToProfile() { Profile currentProfile = stateMonitor.getConnectionProfile(); @@ -421,7 +415,7 @@ public void onGlobalLayout() { textServerName.setText(server.getServerName()); textServerIp.setText(stateMonitor.getExitIP()); - textProtocol.setText(stateMonitor.getConnectionProtocol().displayName(false)); + textProtocol.setText(stateMonitor.getConnectionProtocol().displayName()); int load = (int) server.getLoad(); textLoad.setText(textLoad.getContext().getString(R.string.serverLoad, String.valueOf(load))); imageLoad.setImageDrawable(new ColorDrawable( @@ -550,12 +544,8 @@ private boolean reportError(VpnState.Error error) { showErrorDialog(R.string.error_peer_auth_failed); Log.exception(new VPNException("Peer Auth: Verifying gateway authentication failed")); break; - case LOOKUP_FAILED: - showErrorDialog(R.string.error_lookup_failed); - Log.exception(new VPNException("Gateway address lookup failed")); - break; case UNREACHABLE: - showErrorDialog(R.string.error_unreachable); + showErrorDialog(R.string.error_server_unreachable); Log.exception(new VPNException("Gateway is unreachable")); break; case MAX_SESSIONS: @@ -617,9 +607,5 @@ private void showErrorDialog(@StringRes int textId) { else { textConnectingTo.setText(R.string.loaderReconnecting); } - boolean showSupportLink = textId == R.string.error_lookup_failed; - - textSupport.setPaintFlags(textSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); - textSupport.setVisibility(showSupportLink ? View.VISIBLE : View.GONE); } } diff --git a/app/src/main/java/com/protonvpn/android/ui/login/LoginActivity.kt b/app/src/main/java/com/protonvpn/android/ui/login/LoginActivity.kt index e8aee6d1d..38882bf91 100644 --- a/app/src/main/java/com/protonvpn/android/ui/login/LoginActivity.kt +++ b/app/src/main/java/com/protonvpn/android/ui/login/LoginActivity.kt @@ -30,6 +30,7 @@ import android.view.MotionEvent import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import android.view.WindowManager import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.TextView @@ -76,6 +77,9 @@ class LoginActivity : BaseActivityV2(), } override fun onCreate(savedInstanceState: Bundle?) { + // Prevent screen capture etc. to record user password + window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + super.onCreate(savedInstanceState) if (viewModel.userData.isLoggedIn) { launchActivity() diff --git a/app/src/main/java/com/protonvpn/android/ui/login/ProofsProvider.kt b/app/src/main/java/com/protonvpn/android/ui/login/ProofsProvider.kt index 22ae58b6d..309838461 100644 --- a/app/src/main/java/com/protonvpn/android/ui/login/ProofsProvider.kt +++ b/app/src/main/java/com/protonvpn/android/ui/login/ProofsProvider.kt @@ -22,8 +22,8 @@ package com.protonvpn.android.ui.login import com.protonvpn.android.models.login.LoginInfoResponse import kotlinx.coroutines.withContext import me.proton.core.util.kotlin.DispatcherProvider -import srp.Auth -import srp.Proofs +import me.proton.vpn.golib.srp.Auth +import me.proton.vpn.golib.srp.Proofs import java.util.Arrays import javax.inject.Inject diff --git a/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppViewHolder.kt b/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppViewHolder.kt new file mode 100644 index 000000000..41c24b996 --- /dev/null +++ b/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppViewHolder.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2021. Proton Technologies AG + * + * This file is part of ProtonVPN. + * + * ProtonVPN is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonVPN is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonVPN. If not, see . + */ + +package com.protonvpn.android.ui.splittunneling + +import android.view.View +import androidx.core.view.isVisible +import com.protonvpn.android.R +import com.protonvpn.android.databinding.ItemAppBinding +import com.protonvpn.android.databinding.ItemAppsHeaderBinding +import com.protonvpn.android.databinding.ItemAppsLoadSystemAppsBinding +import com.protonvpn.android.utils.BindableItemEx +import com.xwray.groupie.databinding.BindableItem + +class AppViewHolder( + private val item: SelectedApplicationEntry, + private val onAdd: (SelectedApplicationEntry) -> Unit, + private val onRemove: (SelectedApplicationEntry) -> Unit +) : BindableItemEx() { + + override fun bind(viewBinding: ItemAppBinding, position: Int) { + super.bind(viewBinding, position) + with(viewBinding) { + imageIcon.setImageDrawable(item.icon) + textName.text = item.toString() + textAdd.setOnClickListener { + onAdd(item) + toggleSelection() + } + clearIcon.setOnClickListener { + onRemove(item) + toggleSelection() + } + updateSelection() + } + } + + private fun toggleSelection() { + item.isSelected = !item.isSelected + updateSelection() + } + + private fun updateSelection() { + with(binding) { + clearIcon.isVisible = item.isSelected + textAdd.isVisible = !item.isSelected + } + } + + override fun getLayout(): Int = R.layout.item_app + + override fun clear() { + } +} + +class AppsHeaderViewHolder(private val titleRes: Int) + : BindableItem() { + override fun bind(viewBinding: ItemAppsHeaderBinding, position: Int) { + viewBinding.textHeader.setText(titleRes) + } + + override fun getLayout(): Int = R.layout.item_apps_header +} + +class LoadSystemAppsViewHolder( + private val onLoadClicked: (ProgressCallback) -> Unit +) : BindableItem() { + + override fun bind(viewBinding: ItemAppsLoadSystemAppsBinding, position: Int) { + with(viewBinding) { + buttonLoadSystemApps.setOnClickListener { + buttonLoadSystemApps.visibility = View.INVISIBLE + progressBar.isVisible = true + onLoadClicked { progress, total -> + progressBar.isIndeterminate = false + progressBar.progress = progress + progressBar.max = total + } + } + } + } + + override fun getLayout(): Int = R.layout.item_apps_load_system_apps +} diff --git a/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsAdapter.java b/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsAdapter.java deleted file mode 100644 index 361dc55a7..000000000 --- a/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsAdapter.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2018 Proton Technologies AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ -package com.protonvpn.android.ui.splittunneling; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import com.protonvpn.android.R; -import com.protonvpn.android.models.config.UserData; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; - -import static android.view.View.GONE; -import static android.view.View.VISIBLE; - -public class AppsAdapter extends RecyclerView.Adapter { - - private List data = new ArrayList<>(); - private UserData userData; - - public void setData(List data) { - if (data != null) { - this.data.addAll(data); - } - notifyDataSetChanged(); - } - - public AppsAdapter(UserData userData) { - this.userData = userData; - } - - @NonNull - @Override - public AppsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new AppsViewHolder( - LayoutInflater.from(parent.getContext()).inflate(R.layout.item_app, parent, false)) { - @Override - public void removeApp(String packageName) { - userData.removeAppFromSplitTunnel(packageName); - } - - @Override - public void addApp(String packageName) { - userData.addAppToSplitTunnel(packageName); - } - }; - } - - @Override - public void onBindViewHolder(@NonNull AppsViewHolder holder, int position) { - holder.bindData(data.get(position)); - } - - @Override - public long getItemId(int position) { - return data.get(position).toString().hashCode(); - } - - @Override - public int getItemCount() { - return data.size(); - } - -} - -abstract class AppsViewHolder extends RecyclerView.ViewHolder { - - @BindView(R.id.textName) TextView textName; - @BindView(R.id.imageIcon) ImageView imageIcon; - @BindView(R.id.clearIcon) ImageView clearIcon; - @BindView(R.id.textAdd) TextView textAdd; - private SelectedApplicationEntry item; - - public AppsViewHolder(View view) { - super(view); - ButterKnife.bind(this, view); - } - - public abstract void removeApp(String packageName); - - public abstract void addApp(String packageName); - - @OnClick(R.id.layoutAddRemove) - public void layoutAddRemove() { - item.setSelected(!item.isSelected()); - if (item.isSelected()) { - addApp(item.getInfo().packageName); - } - else { - removeApp(item.getInfo().packageName); - } - initSelection(); - } - - public void bindData(SelectedApplicationEntry object) { - this.item = object; - initSelection(); - imageIcon.setImageDrawable(object.getIcon()); - textName.setText(object.toString()); - } - - private void initSelection() { - clearIcon.setVisibility(item.isSelected() ? VISIBLE : GONE); - textAdd.setVisibility(item.isSelected() ? GONE : VISIBLE); - } - -} diff --git a/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsDialog.java b/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsDialog.java deleted file mode 100644 index 15b005db5..000000000 --- a/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsDialog.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2018 Proton Technologies AG - * - * This file is part of ProtonVPN. - * - * ProtonVPN is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonVPN is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonVPN. If not, see . - */ -package com.protonvpn.android.ui.splittunneling; - -import android.Manifest; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.view.View; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.protonvpn.android.R; -import com.protonvpn.android.components.BaseDialog; -import com.protonvpn.android.components.ContentLayout; -import com.protonvpn.android.models.config.UserData; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; - -import javax.inject.Inject; - -import androidx.core.util.Pair; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.AsyncTaskLoader; -import androidx.loader.content.Loader; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import butterknife.OnClick; - -@ContentLayout(R.layout.dialog_split_tunnel) -public class AppsDialog extends BaseDialog implements - LoaderManager.LoaderCallbacks, List>> { - - private AppsAdapter adapter; - private List selection; - @BindView(R.id.textTitle) TextView textTitle; - @BindView(R.id.textDescription) TextView textDescription; - @BindView(R.id.list) RecyclerView list; - @BindView(R.id.progressBar) ProgressBar progressBar; - @Inject UserData userData; - - @Override - public void onViewCreated() { - list.setLayoutManager(new LinearLayoutManager(getActivity())); - adapter = new AppsAdapter(userData); - selection = userData.getSplitTunnelApps(); - list.setAdapter(adapter); - getLoaderManager().initLoader(0, null, this); - textTitle.setText(R.string.excludeAppsTitle); - textDescription.setText(R.string.excludeAppsDescription); - } - - @OnClick(R.id.textDone) - public void textDone() { - dismiss(); - } - - @Override - public Loader, List>> onCreateLoader(int id, Bundle args) { - progressBar.setVisibility(View.VISIBLE); - return new InstalledPackagesLoader(getActivity(), selection); - } - - @Override - public void onLoadFinished(Loader, List>> loader, - Pair, List> data) { - adapter.setData(data.first); - selection.removeAll(data.second); - adapter.notifyDataSetChanged(); - progressBar.setVisibility(View.GONE); - } - - @Override - public void onLoaderReset(Loader, List>> loader) { - adapter.setData(null); - } - - public static class InstalledPackagesLoader extends - AsyncTaskLoader, List>> { - - private final PackageManager packageManager; - private final List selection; - private Pair, List> data; - - InstalledPackagesLoader(Context context, List selection) { - super(context); - packageManager = context.getPackageManager(); - this.selection = selection; - } - - @Override - public Pair, List> loadInBackground() { - List apps = new ArrayList<>(); - SortedSet seen = new TreeSet<>(); - for (ApplicationInfo info : packageManager.getInstalledApplications( - PackageManager.GET_META_DATA)) { - /* skip apps that can't access the network anyway */ - if (packageManager.checkPermission(Manifest.permission.INTERNET, info.packageName) - == PackageManager.PERMISSION_GRANTED) { - SelectedApplicationEntry entry = new SelectedApplicationEntry(packageManager, info); - entry.setSelected(selection.contains(info.packageName)); - apps.add(entry); - seen.add(info.packageName); - } - } - Collections.sort(apps); - /* check for selected packages that don't exist anymore */ - List missing = new ArrayList<>(); - for (String pkg : selection) { - if (!seen.contains(pkg)) { - missing.add(pkg); - } - } - return new Pair<>(apps, missing); - } - - @Override - protected void onStartLoading() { - if (data != null) { /* if we have data ready, deliver it directly */ - deliverResult(data); - } - if (takeContentChanged() || data == null) { - forceLoad(); - } - } - - @Override - public void deliverResult(Pair, List> data) { - if (isReset()) { - return; - } - this.data = data; - if (isStarted()) { - super.deliverResult(data); - } - } - - @Override - protected void onReset() { - data = null; - super.onReset(); - } - } -} diff --git a/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsDialog.kt b/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsDialog.kt new file mode 100644 index 000000000..6016ebdb8 --- /dev/null +++ b/app/src/main/java/com/protonvpn/android/ui/splittunneling/AppsDialog.kt @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2018 Proton Technologies AG + * + * This file is part of ProtonVPN. + * + * ProtonVPN is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonVPN is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonVPN. If not, see . + */ +package com.protonvpn.android.ui.splittunneling + +import android.Manifest +import android.app.ActivityManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.os.SystemClock +import android.view.View +import android.widget.ProgressBar +import android.widget.TextView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import butterknife.OnClick +import com.protonvpn.android.AppInfoService +import com.protonvpn.android.R +import com.protonvpn.android.components.BaseDialog +import com.protonvpn.android.components.ContentLayout +import com.protonvpn.android.models.config.UserData +import com.protonvpn.android.utils.ViewUtils.toPx +import com.protonvpn.android.utils.sortedByLocaleAware +import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.GroupieViewHolder +import com.xwray.groupie.Section +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.receiveOrNull +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import me.proton.core.util.kotlin.DispatcherProvider +import javax.inject.Inject +import kotlin.coroutines.coroutineContext + +private const val APP_INFO_RESULT_TIMEOUT_MS = 5_000L +private const val APP_ICON_SIZE_DP = 28 +typealias ProgressCallback = (progress: Int, total: Int) -> Unit + +@ContentLayout(R.layout.dialog_split_tunnel) +class AppsDialog : BaseDialog() { + @BindView(R.id.textTitle) + lateinit var textTitle: TextView + + @BindView(R.id.textDescription) + lateinit var textDescription: TextView + + @BindView(R.id.list) + lateinit var list: RecyclerView + + @BindView(R.id.progressBar) + lateinit var progressBar: ProgressBar + + @Inject + lateinit var userData: UserData + @Inject + lateinit var activityManager: ActivityManager + @Inject + lateinit var mainScope: CoroutineScope + @Inject + lateinit var dispatcherProvider: DispatcherProvider + + override fun onViewCreated() { + val layoutManager = LinearLayoutManager(activity) + list.layoutManager = layoutManager + + val adapter = GroupAdapter() + val regularAppsSection = Section(AppsHeaderViewHolder(R.string.excludeAppsRegularSectionTitle)) + val systemAppsSection = Section(AppsHeaderViewHolder(R.string.excludeAppsSystemSectionTitle)) + systemAppsSection.add(LoadSystemAppsViewHolder { progressCallback -> + loadSystemApps(layoutManager, adapter, systemAppsSection, progressCallback) + }) + + adapter.add(regularAppsSection) + adapter.add(systemAppsSection) + + textTitle.setText(R.string.excludeAppsTitle) + textDescription.setText(R.string.excludeAppsDescription) + progressBar.visibility = View.VISIBLE + + val selection = userData.splitTunnelApps.toSet() + viewLifecycleOwner.lifecycleScope.launch { + regularAppsSection.addAll( + getSortedAppViewHolders(requireContext(), true, selection) { progress, total -> + progressBar.isIndeterminate = false + progressBar.progress = progress + progressBar.max = total + } + ) + list.adapter = adapter + progressBar.visibility = View.GONE + } + mainScope.launch { + removeUninstalledApps(requireContext().packageManager, userData) + } + } + + @OnClick(R.id.textDone) + fun textDone() { + dismiss() + } + + private fun loadSystemApps( + layoutManager: LinearLayoutManager, + adapter: GroupAdapter, + systemAppsSection: Section, + progressCallback: ProgressCallback + ) { + viewLifecycleOwner.lifecycleScope.launch { + val selection = userData.splitTunnelApps.toSet() + systemAppsSection.addAll( + getSortedAppViewHolders(requireContext(), false, selection, progressCallback) + ) + systemAppsSection.remove(systemAppsSection.getItem(1)) + val headerPosition = adapter.getAdapterPosition(systemAppsSection.getItem(0)) + layoutManager.scrollToPositionWithOffset(headerPosition, 0) + } + } + + private suspend fun removeUninstalledApps(packageManager: PackageManager, userData: UserData) { + val installedPackages = withContext(dispatcherProvider.Io) { + packageManager.getInstalledApplications(0).mapTo(mutableSetOf()) { it.packageName } + } + val userDataAppPackages = userData.splitTunnelApps + userDataAppPackages + .filterNot { installedPackages.contains(it) } + .forEach { userData.removeAppFromSplitTunnel(it) } + } + + private suspend fun getSortedAppViewHolders( + context: Context, + withLaunchIntent: Boolean, + selection: Set, + onProgress: ProgressCallback + ): List { + val regularApps = getInstalledInternetApps(context, withLaunchIntent, onProgress) + val sortedRegularApps = withContext(dispatcherProvider.Comp) { + regularApps.forEach { app -> + if (selection.contains(app.packageName)) { + app.isSelected = true + } + } + regularApps.sortedByLocaleAware { it.toString() } + } + return sortedRegularApps.map { + AppViewHolder( + it, + onAdd = { userData.addAppToSplitTunnel(it.packageName) }, + onRemove = { userData.removeAppFromSplitTunnel(it.packageName) } + ) + } + } + + + private suspend fun getInstalledInternetApps( + context: Context, + withLaunchIntent: Boolean, + onProgress: ProgressCallback + ): List { + val pm = context.packageManager + val apps = withContext(Dispatchers.IO) { + pm.getInstalledApplications(0) + .map { it.packageName } + .filter { packageName -> + val hasInternet = (pm.checkPermission(Manifest.permission.INTERNET, packageName) + == PackageManager.PERMISSION_GRANTED) + val hasLaunchIntent = pm.getLaunchIntentForPackage(packageName) != null + hasInternet && hasLaunchIntent == withLaunchIntent + } + } + return getAppInfos(context, apps, onProgress) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun getAppInfos( + context: Context, + packages: List, + onProgress: ProgressCallback + ): List { + val channel = getAppInfosChannel(context, packages) + val results = ArrayList(packages.size) + try { + do { + val appInfo = withTimeoutOrNull(APP_INFO_RESULT_TIMEOUT_MS) { + channel.receiveOrNull() + } + if (appInfo != null) { + onProgress(results.size, packages.size) + results.add(appInfo) + } + } while (appInfo != null) + } catch (cancellation : CancellationException) { + channel.close() + } + if (results.size < packages.size) { + coroutineContext.ensureActive() + // Something went wrong, add missing items with no icon nor label. + val defaultIcon = context.packageManager.defaultActivityIcon + packages.subList(results.size, packages.size).forEach { packageName -> + results.add(SelectedApplicationEntry(packageName, packageName, defaultIcon)) + } + } + return results + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun getAppInfosChannel( + context: Context, + packages: List + ): Channel { + val requestCode = SystemClock.elapsedRealtime() // Unique value. + val resultsChannel = Channel() + var resultCount = 0 + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val resultRequestCode = intent.getLongExtra(AppInfoService.EXTRA_REQUEST_CODE, 0) + if (resultRequestCode != requestCode) + return + + val packageName = intent.getStringExtra(AppInfoService.EXTRA_PACKAGE_NAME) ?: return + val name = intent.getStringExtra(AppInfoService.EXTRA_APP_LABEL) ?: packageName + val iconBytes = intent.getByteArrayExtra(AppInfoService.EXTRA_APP_ICON) + val iconDrawable = + if (iconBytes != null) { + val iconBitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size) + BitmapDrawable(context.resources, iconBitmap) + } else { + context.packageManager.defaultActivityIcon + } + resultsChannel.sendBlocking(SelectedApplicationEntry(packageName, name, iconDrawable)) + if (++resultCount == packages.size) + resultsChannel.close() + } + } + + context.registerReceiver(receiver, IntentFilter(AppInfoService.RESULT_ACTION)) + resultsChannel.invokeOnClose { + context.unregisterReceiver(receiver) + context.stopService(AppInfoService.createStopIntent(context)) + } + val iconSizePx = APP_ICON_SIZE_DP.toPx() + context.startService(AppInfoService.createIntent(context, packages, iconSizePx, requestCode)) + return resultsChannel + } +} diff --git a/app/src/main/java/com/protonvpn/android/ui/splittunneling/SelectedApplicationEntry.java b/app/src/main/java/com/protonvpn/android/ui/splittunneling/SelectedApplicationEntry.java index 96551fa35..6a19773fc 100644 --- a/app/src/main/java/com/protonvpn/android/ui/splittunneling/SelectedApplicationEntry.java +++ b/app/src/main/java/com/protonvpn/android/ui/splittunneling/SelectedApplicationEntry.java @@ -18,8 +18,6 @@ */ package com.protonvpn.android.ui.splittunneling; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import java.text.Collator; @@ -28,16 +26,15 @@ public class SelectedApplicationEntry implements Comparable { - private final ApplicationInfo mInfo; + private final String mPackageName; private final Drawable mIcon; private final String mName; private boolean mSelected; - public SelectedApplicationEntry(PackageManager packageManager, ApplicationInfo info) { - mInfo = info; - CharSequence name = info.loadLabel(packageManager); - mName = name == null ? info.packageName : name.toString(); - mIcon = info.loadIcon(packageManager); + public SelectedApplicationEntry(@NonNull String packageName, @NonNull String label, @NonNull Drawable icon) { + mPackageName = packageName; + mName = label; + mIcon = icon; } public void setSelected(boolean selected) { @@ -48,8 +45,9 @@ public boolean isSelected() { return mSelected; } - public ApplicationInfo getInfo() { - return mInfo; + @NonNull + public String getPackageName() { + return mPackageName; } public Drawable getIcon() { @@ -65,4 +63,4 @@ public String toString() { public int compareTo(@NonNull SelectedApplicationEntry another) { return Collator.getInstance().compare(toString(), another.toString()); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/protonvpn/android/utils/AndroidUtils.kt b/app/src/main/java/com/protonvpn/android/utils/AndroidUtils.kt index 344809a41..197a9524d 100644 --- a/app/src/main/java/com/protonvpn/android/utils/AndroidUtils.kt +++ b/app/src/main/java/com/protonvpn/android/utils/AndroidUtils.kt @@ -19,6 +19,9 @@ package com.protonvpn.android.utils import android.app.Activity +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.app.Application import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context @@ -30,6 +33,7 @@ import android.content.res.Resources import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Process import android.text.Editable import android.text.TextUtils.getChars import android.util.DisplayMetrics @@ -159,6 +163,18 @@ object AndroidUtils { fun Context.isChromeOS() = packageManager.hasSystemFeature("org.chromium.arc.device_management") + + @JvmStatic + fun getMyProcessName(context: Context): String { + return if (Build.VERSION.SDK_INT >= 28) { + Application.getProcessName() + } else { + val pid = Process.myPid() + val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val allProcesses = manager.runningAppProcesses + allProcesses?.find { it.pid == pid }?.processName ?: "" + } + } } fun Context.openUrl(url: Uri) { diff --git a/app/src/main/java/com/protonvpn/android/utils/BindableItemEx.kt b/app/src/main/java/com/protonvpn/android/utils/BindableItemEx.kt index 1452041ed..e0ebe8612 100644 --- a/app/src/main/java/com/protonvpn/android/utils/BindableItemEx.kt +++ b/app/src/main/java/com/protonvpn/android/utils/BindableItemEx.kt @@ -50,6 +50,7 @@ abstract class BindableItemEx : BindableItem() { super.bind(viewHolder, position, payloads, onItemClickListener, onItemLongClickListener) } + @CallSuper override fun bind(viewBinding: T, position: Int) { // Sometimes we can get 2 binds in a row without unbind in between clear() diff --git a/app/src/main/java/com/protonvpn/android/utils/CollectionTools.kt b/app/src/main/java/com/protonvpn/android/utils/CollectionTools.kt index ea9bcc6a7..6feb0957d 100644 --- a/app/src/main/java/com/protonvpn/android/utils/CollectionTools.kt +++ b/app/src/main/java/com/protonvpn/android/utils/CollectionTools.kt @@ -18,6 +18,15 @@ */ package com.protonvpn.android.utils +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import me.proton.core.util.kotlin.mapNotNullAsync import java.text.Collator import java.util.Locale @@ -31,3 +40,37 @@ inline fun Iterable.sortedByLocaleAware(crossinline selector: (T) -> Stri c.compare(selector(s1), selector(s2)) }) } + +// Checks predicate in parallel (via scope.launch {}) for each item and returns element that first finished with true or +// null. +@OptIn(ExperimentalStdlibApi::class) +suspend fun List.parallelFirstOrNull(predicate: suspend (T) -> Boolean): T? = coroutineScope { + var count = size + if (count == 0) + null + else { + val responses = MutableSharedFlow(replay = count) + val workersScope = CoroutineScope(coroutineContext[CoroutineDispatcher.Key] ?: Dispatchers.IO) + try { + forEach { item -> + workersScope.launch { + responses.emit(item.takeIf { predicate(it) }) + } + } + responses.first { + count-- + it != null || count == 0 + } + } finally { + workersScope.cancel() + } + } +} + +// Search for elements satisfying [predicate] in parallel. [returnAll] = true will find all elements, otherwise only +// first (fastest) element is returned. +suspend fun List.parallelSearch(returnAll: Boolean, predicate: suspend (T) -> Boolean): List = + if (returnAll) + mapNotNullAsync { item -> item.takeIf { predicate(it) } } + else + listOfNotNull(parallelFirstOrNull { predicate(it) }) diff --git a/app/src/main/java/com/protonvpn/android/utils/Constants.kt b/app/src/main/java/com/protonvpn/android/utils/Constants.kt index 7962be59c..84a1b4bdf 100644 --- a/app/src/main/java/com/protonvpn/android/utils/Constants.kt +++ b/app/src/main/java/com/protonvpn/android/utils/Constants.kt @@ -49,6 +49,7 @@ object Constants { const val DEFAULT_MAINTENANCE_CHECK_MINUTES = 30L const val VPN_INFO_REFRESH_INTERVAL_MINUTES = 3 const val WIREGUARD_TUNNEL_NAME = "ProtonTunnel" + const val SECONDARY_PROCESS_TAG = "SecondaryProcess" val CLIENT_ID: String val VPN_USERNAME_PRODUCT_SUFFIX: String diff --git a/app/src/main/java/com/protonvpn/android/utils/CountryTools.kt b/app/src/main/java/com/protonvpn/android/utils/CountryTools.kt index e6e33daef..ed77b782c 100644 --- a/app/src/main/java/com/protonvpn/android/utils/CountryTools.kt +++ b/app/src/main/java/com/protonvpn/android/utils/CountryTools.kt @@ -132,9 +132,13 @@ object CountryTools { "TW" to CountryData(4135.0, 975.0, Continent.Asia), "TR" to CountryData(2779.0, 696.0, Continent.AfricaAndMiddleEast), "UA" to CountryData(2715.0, 517.0, Continent.Europe), + "NG" to CountryData(2385.0, 1235.0, Continent.AfricaAndMiddleEast), + "PH" to CountryData(4159.0, 1135.0, Continent.Asia), + "KH" to CountryData(3911.0, 1194.0, Continent.Asia), "AE" to CountryData(3103.0, 976.0, Continent.AfricaAndMiddleEast), "CO" to CountryData(1100.0, 1339.0, Continent.America), "PE" to CountryData(1056.0, 1589.0, Continent.America), + "PR" to CountryData(1216.0, 1076.0, Continent.America), "VN" to CountryData(3961.0, 1144.0, Continent.Asia)) val codeToMapCountryName = mapOf( diff --git a/app/src/main/java/com/protonvpn/android/migration/MigrateReceiver.kt b/app/src/main/java/com/protonvpn/android/utils/ProtonExceptionHandler.kt similarity index 58% rename from app/src/main/java/com/protonvpn/android/migration/MigrateReceiver.kt rename to app/src/main/java/com/protonvpn/android/utils/ProtonExceptionHandler.kt index 337ec3a48..4f1d74e5c 100644 --- a/app/src/main/java/com/protonvpn/android/migration/MigrateReceiver.kt +++ b/app/src/main/java/com/protonvpn/android/utils/ProtonExceptionHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Proton Technologies AG + * Copyright (c) 2021 Proton Technologies AG * * This file is part of ProtonVPN. * @@ -16,15 +16,17 @@ * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ -package com.protonvpn.android.migration +package com.protonvpn.android.utils -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent +import java.io.PrintWriter +import java.io.StringWriter -class MigrateReceiver : BroadcastReceiver() { +class ProtonExceptionHandler(val innerHandler: Thread.UncaughtExceptionHandler?) : Thread.UncaughtExceptionHandler { - override fun onReceive(context: Context?, intent: Intent?) { - // No need to do anything. ProtonApplication.onCreate will handle actual migration. + override fun uncaughtException(t: Thread, e: Throwable) { + val writer = StringWriter() + e.printStackTrace(PrintWriter(writer)) + ProtonLogger.logBlocking(writer.toString()) + innerHandler?.uncaughtException(t, e) } } diff --git a/app/src/main/java/com/protonvpn/android/utils/ProtonLoggerImpl.kt b/app/src/main/java/com/protonvpn/android/utils/ProtonLoggerImpl.kt index 935b28467..3dfdd16e7 100644 --- a/app/src/main/java/com/protonvpn/android/utils/ProtonLoggerImpl.kt +++ b/app/src/main/java/com/protonvpn/android/utils/ProtonLoggerImpl.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.callbackFlow @@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory import java.io.File @@ -191,8 +193,7 @@ open class ProtonLoggerImpl( private suspend fun processLogs() { messages.collect { message -> - logContext.putProperty("timeZone", timeZoneSuffix(GregorianCalendar())) - logger.debug(message) + logInternal(message) } } @@ -236,6 +237,16 @@ open class ProtonLoggerImpl( start() } + fun logInternal(msg: String) { + logContext.putProperty("timeZone", timeZoneSuffix(GregorianCalendar())) + logger.debug(msg) + } + + fun logBlocking(msg: String) = runBlocking(loggerDispatcher) { + logInternal(msg) + delay(100) + } + private class ChannelAdapter( private val channel: SendChannel, private val encoder: Encoder @@ -272,6 +283,10 @@ open class ProtonLoggerImpl( logMessageQueue.tryEmit(message) } + fun logBlocking(msg: String) { + backgroundLogger.logBlocking(msg) + } + fun getLogLines() = backgroundLogger.getLogLines() @Suppress("BlockingMethodInNonBlockingContext") diff --git a/app/src/main/java/com/protonvpn/android/utils/ServerManager.kt b/app/src/main/java/com/protonvpn/android/utils/ServerManager.kt index 604e08924..dc4f7abc2 100644 --- a/app/src/main/java/com/protonvpn/android/utils/ServerManager.kt +++ b/app/src/main/java/com/protonvpn/android/utils/ServerManager.kt @@ -20,6 +20,7 @@ package com.protonvpn.android.utils import android.content.Context import androidx.annotation.VisibleForTesting +import com.protonvpn.android.BuildConfig import com.protonvpn.android.models.config.UserData import com.protonvpn.android.models.profiles.Profile import com.protonvpn.android.models.profiles.SavedProfilesV3 @@ -41,6 +42,7 @@ class ServerManager( @Transient val userData: UserData ) : Serializable, ServerDeliver { + private var serverListAppVersionCode = 0 private val vpnCountries = mutableListOf() private val secureCoreEntryCountries = mutableListOf() private val secureCoreExitCountries = mutableListOf() @@ -68,7 +70,7 @@ class ServerManager( val isOutdated: Boolean get() = updatedAt == null || vpnCountries.isEmpty() || DateTime().millis - updatedAt!!.millis >= ServerListUpdater.LIST_CALL_DELAY || - !haveWireGuardSupport() + !haveWireGuardSupport() || serverListAppVersionCode < BuildConfig.VERSION_CODE private fun haveWireGuardSupport() = vpnCountries.any { country -> @@ -114,6 +116,7 @@ class ServerManager( secureCoreEntryCountries.addAll(oldManager.secureCoreEntryCountries) streamingServices = oldManager.streamingServices updatedAt = oldManager.updatedAt + serverListAppVersionCode = oldManager.serverListAppVersionCode } reInitProfiles() @@ -186,6 +189,7 @@ class ServerManager( secureCoreExitCountries.add(VpnCountry(country, servers, this)) } updatedAt = DateTime() + serverListAppVersionCode = BuildConfig.VERSION_CODE Storage.save(this) onServersUpdate() } diff --git a/app/src/main/java/com/protonvpn/android/vpn/CertificateRepository.kt b/app/src/main/java/com/protonvpn/android/vpn/CertificateRepository.kt index 02e481a37..f41fc650a 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/CertificateRepository.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/CertificateRepository.kt @@ -40,6 +40,7 @@ import me.proton.core.network.domain.ApiResult import me.proton.core.network.domain.session.SessionId import me.proton.core.util.kotlin.deserialize import me.proton.core.util.kotlin.serialize +import me.proton.vpn.golib.ed25519.KeyPair import java.util.Date import java.util.concurrent.TimeUnit @@ -80,7 +81,7 @@ class CertificateRepository( private val certRequests = mutableMapOf>() - private val guestX25519Key by lazy { ed25519.KeyPair().toX25519Base64() } + private val guestX25519Key by lazy { KeyPair().toX25519Base64() } private val refreshCertTask = ReschedulableTask(mainScope, wallClock) { updateCurrentCert(force = false) @@ -89,6 +90,11 @@ class CertificateRepository( init { refreshCertTask.scheduleIn(0) mainScope.launch { + userData.sessionId?.let { + val certInfo = getCertInfo(it) + ProtonLogger.log("Current cert: ${if (certInfo.certificatePem == null) + null else "expires ${Date(certInfo.expiresAt)} (refresh at ${Date(certInfo.refreshAt)})"}") + } userPlanManager.infoChangeFlow.collect { changes -> for (change in changes) when (change) { is UserPlanManager.InfoChange.PlanChange.Downgrade, @@ -117,7 +123,7 @@ class CertificateRepository( } suspend fun generateNewKey(sessionId: SessionId): CertInfo = withContext(mainScope.coroutineContext) { - val keyPair = ed25519.KeyPair() + val keyPair = KeyPair() val info = CertInfo(keyPair.privateKeyPKIXPem(), keyPair.publicKeyPKIXPem(), keyPair.toX25519Base64()) certRequests.remove(sessionId)?.cancel() @@ -135,7 +141,7 @@ class CertificateRepository( if (force || certInfo.certificatePem == null || wallClock() >= certInfo.refreshAt) updateCertificate(it, cancelOngoing = force) else - refreshCertTask.scheduleTo(certInfo.refreshAt) + rescheduleRefreshTo(certInfo.refreshAt) } } } @@ -144,48 +150,15 @@ class CertificateRepository( withContext(mainScope.coroutineContext) { if (cancelOngoing) certRequests.remove(sessionId)?.cancel() - val request = certRequests.getOrElse(sessionId) { - async { - val info = getCertInfo(sessionId) - val response = try { - api.getCertificate(info.publicKeyPem) - } finally { + val request = certRequests[sessionId] + ?: async { + updateCertificateInternal(sessionId).apply { certRequests.remove(sessionId) } - when (response) { - is ApiResult.Success -> { - val cert = response.value - val newInfo = info.copy( - expiresAt = cert.expirationTimeMs, - refreshAt = cert.refreshTimeMs, - certificatePem = cert.certificate, - refreshCount = 0) - setInfo(sessionId, newInfo) - ProtonLogger.log("New certificate expires at: " + Date(cert.expirationTimeMs)) - if (sessionId == userData.sessionId) - rescheduleRefreshTo(cert.refreshTimeMs) - CertificateResult.Success(cert.certificate, info.privateKeyPem) - } - is ApiResult.Error -> { - if (info.certificatePem == null) - ProtonLogger.log("Failed to get certificate (${info.refreshCount})") - else - ProtonLogger.log("Failed to refresh (${info.refreshCount}) certificate expiring at " + - Date(info.expiresAt)) - - if (info.refreshCount < MAX_REFRESH_COUNT) { - setInfo(sessionId, info.copy(refreshCount = info.refreshCount + 1)) - - val now = wallClock() - val newRefresh = ((now + info.expiresAt) / 2) - .coerceAtLeast(now + MIN_REFRESH_DELAY) - rescheduleRefreshTo(newRefresh) - } - CertificateResult.Error(response) - } - } + }.apply { + certRequests[sessionId] = this } - } + try { request.await() } catch (e: CancellationException) { @@ -193,7 +166,43 @@ class CertificateRepository( } } - suspend fun getCertInfo(sessionId: SessionId) = + private suspend fun updateCertificateInternal(sessionId: SessionId): CertificateResult { + val info = getCertInfo(sessionId) + return when (val response = api.getCertificate(info.publicKeyPem)) { + is ApiResult.Success -> { + val cert = response.value + val newInfo = info.copy( + expiresAt = cert.expirationTimeMs, + refreshAt = cert.refreshTimeMs, + certificatePem = cert.certificate, + refreshCount = 0) + setInfo(sessionId, newInfo) + ProtonLogger.log("New certificate expires at: " + Date(cert.expirationTimeMs)) + if (sessionId == userData.sessionId) + rescheduleRefreshTo(cert.refreshTimeMs) + CertificateResult.Success(cert.certificate, info.privateKeyPem) + } + is ApiResult.Error -> { + if (info.certificatePem == null) + ProtonLogger.log("Failed to get certificate (${info.refreshCount})") + else + ProtonLogger.log("Failed to refresh (${info.refreshCount}) certificate expiring at " + + Date(info.expiresAt)) + + if (info.refreshCount < MAX_REFRESH_COUNT) { + setInfo(sessionId, info.copy(refreshCount = info.refreshCount + 1)) + + val now = wallClock() + val newRefresh = ((now + info.expiresAt) / 2) + .coerceAtLeast(now + MIN_REFRESH_DELAY) + rescheduleRefreshTo(newRefresh) + } + CertificateResult.Error(response) + } + } + } + + private suspend fun getCertInfo(sessionId: SessionId) = certPrefs.getString(sessionId.id, null)?.deserialize() ?: run { generateNewKey(sessionId) } diff --git a/app/src/main/java/com/protonvpn/android/vpn/ConnectivityMonitor.kt b/app/src/main/java/com/protonvpn/android/vpn/ConnectivityMonitor.kt index 922bb2d7b..a0c9fb003 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/ConnectivityMonitor.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/ConnectivityMonitor.kt @@ -65,10 +65,6 @@ class ConnectivityMonitor( val networkCapabilitiesFlow = MutableSharedFlow>() - // This means current connection goes through VPN tunnel. It doesn't mean connection is fully functional (we can - // be hard-jailed for example) - for that use VpnStateMonitor - val vpnActive get() = currentCapabilities[NOT_VPN] == false - private val capabilitiesConstantMap = mutableMapOf( "MMS" to NET_CAPABILITY_MMS, "SUPL" to NET_CAPABILITY_SUPL, diff --git a/app/src/main/java/com/protonvpn/android/vpn/ProtonVpnBackendProvider.kt b/app/src/main/java/com/protonvpn/android/vpn/ProtonVpnBackendProvider.kt index 53dbd5943..7eea340cd 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/ProtonVpnBackendProvider.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/ProtonVpnBackendProvider.kt @@ -18,17 +18,21 @@ */ package com.protonvpn.android.vpn +import com.protonvpn.android.appconfig.AppConfig import com.protonvpn.android.models.config.VpnProtocol import com.protonvpn.android.models.profiles.Profile import com.protonvpn.android.models.profiles.ServerDeliver import com.protonvpn.android.models.vpn.Server import com.protonvpn.android.utils.AndroidUtils.whenNotNullNorEmpty -import kotlinx.coroutines.async +import com.protonvpn.android.utils.ProtonLogger import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import me.proton.core.util.kotlin.mapAsync -import com.protonvpn.android.utils.ProtonLogger class ProtonVpnBackendProvider( + val config: AppConfig, val strongSwan: VpnBackend, val openVpn: VpnBackend, val wireGuard: VpnBackend, @@ -38,17 +42,31 @@ class ProtonVpnBackendProvider( override suspend fun prepareConnection( protocol: VpnProtocol, profile: Profile, - server: Server + server: Server, + alwaysScan: Boolean ): PrepareResult? { ProtonLogger.log("Preparing connection with protocol: " + protocol.name) return when (protocol) { VpnProtocol.IKEv2 -> strongSwan.prepareForConnection(profile, server, scan = false) - VpnProtocol.OpenVPN -> openVpn.prepareForConnection(profile, server, scan = false) - VpnProtocol.WireGuard -> wireGuard.prepareForConnection(profile, server, scan = false) - VpnProtocol.Smart -> - strongSwan.prepareForConnection(profile, server, scan = true).takeIf { it.isNotEmpty() } - ?: openVpn.prepareForConnection(profile, server, scan = true) - }.firstOrNull() + VpnProtocol.OpenVPN -> openVpn.prepareForConnection(profile, server, scan = alwaysScan) + VpnProtocol.WireGuard -> wireGuard.prepareForConnection(profile, server, scan = alwaysScan) + VpnProtocol.Smart -> { + val backends = mutableListOf() + with(config.getSmartProtocolConfig()) { + if (wireguardEnabled && server.supportsProtocol(VpnProtocol.WireGuard)) + backends += wireGuard + if (ikeV2Enabled) + backends += strongSwan + if (openVPNEnabled) + backends += openVpn + } + backends.asFlow().map { + it.prepareForConnection(profile, server, scan = true) + }.firstOrNull { + it.isNotEmpty() + } + } + }?.firstOrNull() } override suspend fun pingAll( @@ -58,14 +76,12 @@ class ProtonVpnBackendProvider( val responses = coroutineScope { preferenceList.mapAsync { server -> val profile = Profile.getTempProfile(server.server, serverDeliver) - val portsLimit = if (server === fullScanServer) Int.MAX_VALUE else PING_ALL_MAX_PORTS - val strongSwanResponse = async { - strongSwan.prepareForConnection(profile, server.server, true, portsLimit) - } - val openVpnResponse = async { - openVpn.prepareForConnection(profile, server.server, true, portsLimit) - } - val responses = strongSwanResponse.await() + openVpnResponse.await() + val fullScan = server === fullScanServer + val portsLimit = if (fullScan) Int.MAX_VALUE else PING_ALL_MAX_PORTS + + val responses = listOf(wireGuard, strongSwan, openVpn).mapAsync { + it.prepareForConnection(profile, server.server, true, portsLimit, waitForAll = fullScan) + }.flatten() server to responses }.toMap() } diff --git a/app/src/main/java/com/protonvpn/android/vpn/VpnBackend.kt b/app/src/main/java/com/protonvpn/android/vpn/VpnBackend.kt index 2bc32939b..ccd35fcd4 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/VpnBackend.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/VpnBackend.kt @@ -26,29 +26,43 @@ import com.protonvpn.android.appconfig.AppConfig import com.protonvpn.android.models.config.UserData import com.protonvpn.android.models.config.VpnProtocol import com.protonvpn.android.models.profiles.Profile +import com.protonvpn.android.models.vpn.ConnectingDomain import com.protonvpn.android.models.vpn.ConnectionParams import com.protonvpn.android.models.vpn.Server import com.protonvpn.android.utils.Constants import com.protonvpn.android.utils.ProtonLogger +import com.protonvpn.android.utils.parallelSearch import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.yield -import localAgent.AgentConnection -import localAgent.Features -import localAgent.NativeClient import me.proton.core.network.domain.NetworkManager import me.proton.core.network.domain.NetworkStatus +import me.proton.core.util.kotlin.DispatcherProvider +import me.proton.vpn.golib.localAgent.AgentConnection +import me.proton.vpn.golib.localAgent.Features +import me.proton.vpn.golib.localAgent.LocalAgent +import me.proton.vpn.golib.localAgent.NativeClient +import me.proton.vpn.golib.localAgent.StatusMessage +import me.proton.vpn.golib.vpnPing.VpnPing + +private const val SCAN_TIMEOUT_MILLIS = 5000L data class RetryInfo(val timeoutSeconds: Int, val retryInSeconds: Int) data class PrepareResult(val backend: VpnBackend, val connectionParams: ConnectionParams) : java.io.Serializable interface VpnBackendProvider { - suspend fun prepareConnection(protocol: VpnProtocol, profile: Profile, server: Server): PrepareResult? + suspend fun prepareConnection( + protocol: VpnProtocol, + profile: Profile, + server: Server, + alwaysScan: Boolean = true + ): PrepareResult? // Returns first from [preferenceList] that responded in a given time frame or null // [fullScanServer] when set will have all ports scanned. @@ -58,6 +72,7 @@ interface VpnBackendProvider { interface AgentConnectionInterface { val state: String + val status: StatusMessage? fun setFeatures(features: Features) fun setConnectivity(connectivity: Boolean) fun close() @@ -69,14 +84,17 @@ abstract class VpnBackend( val certificateRepository: CertificateRepository, private val networkManager: NetworkManager, val vpnProtocol: VpnProtocol, - val mainScope: CoroutineScope + val mainScope: CoroutineScope, + val dispatcherProvider: DispatcherProvider, ) : VpnStateSource { abstract suspend fun prepareForConnection( profile: Profile, server: Server, scan: Boolean, - numberOfPorts: Int = Int.MAX_VALUE // Max number of ports to be scanned + numberOfPorts: Int = Int.MAX_VALUE, // Max number of ports to be scanned + waitForAll: Boolean = false // wait for all ports to respond if true, otherwise just wait for first successful + // response ): List protected var lastConnectionParams: ConnectionParams? = null @@ -116,6 +134,7 @@ abstract class VpnBackend( ) override val state: String get() = agent.state + override val status: StatusMessage? get() = agent.status override fun setFeatures(features: Features) { agent.setFeatures(features) @@ -156,7 +175,7 @@ abstract class VpnBackend( revokeCertificateAndReconnect() agentConstants.errorCodeCertificateExpired -> - refreshCertOnLocalAgent() + refreshCertOnLocalAgent(force = false) agentConstants.errorCodeKeyUsedMultipleTimes -> setLocalAgentError("Key used multiple times") @@ -174,6 +193,10 @@ abstract class VpnBackend( agentConstants.errorCodeRestrictedServer -> // Server should unblock eventually, but we need to keep track and provide watchdog if necessary. ProtonLogger.log("Local agent: Restricted server, waiting...") + else -> { + if (agent?.status?.reason?.final == true) + setLocalAgentError(description) + } } } @@ -182,7 +205,7 @@ abstract class VpnBackend( selfStateObservable.postValue(getGlobalVpnState(vpnProtocolState, state)) } - override fun onStatusUpdate(status: localAgent.StatusMessage) {} + override fun onStatusUpdate(status: StatusMessage) {} } private fun setAuthError(description: String? = null) = @@ -202,7 +225,7 @@ abstract class VpnBackend( private var agentConnectionJob: Job? = null private var reconnectionJob: Job? = null private val features: Features = Features() - private val agentConstants = localAgent.LocalAgent.constants() + private val agentConstants = LocalAgent.constants() init { mainScope.launch { @@ -262,9 +285,15 @@ abstract class VpnBackend( VpnState.Connected agentConstants.stateConnectionError, agentConstants.stateServerUnreachable -> + // When unreachable comes from local agent it means VPN tunnel is still active, set UNREACHABLE + // instead of UNREACHABLE_INETRNAL to skip recovery with pings, as those won't help in this situation. VpnState.Error(ErrorType.UNREACHABLE) - agentConstants.stateClientCertificateError -> { - refreshCertOnLocalAgent() + agentConstants.stateClientCertificateExpiredError -> { + refreshCertOnLocalAgent(force = false) + VpnState.Connecting + } + agentConstants.stateClientCertificateUnknownCA -> { + refreshCertOnLocalAgent(force = true) VpnState.Connecting } agentConstants.stateServerCertificateError -> @@ -280,11 +309,15 @@ abstract class VpnBackend( } } - private fun refreshCertOnLocalAgent() { + private fun refreshCertOnLocalAgent(force: Boolean) { selfStateObservable.postValue(VpnState.Connecting) closeAgentConnection() reconnectionJob = mainScope.launch { - when (certificateRepository.updateCertificate(userData.sessionId!!, true)) { + val result = if (force) + certificateRepository.updateCertificate(userData.sessionId!!, false) + else + certificateRepository.getCertificate(userData.sessionId!!, false) + when (result) { is CertificateRepository.CertificateResult.Success -> connectToLocalAgent() is CertificateRepository.CertificateResult.Error -> { @@ -353,6 +386,28 @@ abstract class VpnBackend( setSelfState(VpnState.Disabled) } + protected suspend fun scanUdpPorts( + connectingDomain: ConnectingDomain, + ports: List, + numberOfPorts: Int, + waitForAll: Boolean + ): List = withContext(dispatcherProvider.Io) { + if (connectingDomain.publicKeyX25519 == null) + emptyList() + else { + val candidatePorts = if (numberOfPorts < ports.size) + ports.shuffled().take(numberOfPorts) + else + ports.shuffled() + + ProtonLogger.log("${connectingDomain.entryDomain}/$vpnProtocol port scan: $candidatePorts") + candidatePorts.parallelSearch(waitForAll) { + VpnPing.pingSync(connectingDomain.entryIp, it.toLong(), + connectingDomain.publicKeyX25519, SCAN_TIMEOUT_MILLIS) + } + } + } + companion object { private const val DISCONNECT_WAIT_TIMEOUT = 3000L private const val FEATURES_NETSHIELD = "netshield-level" diff --git a/app/src/main/java/com/protonvpn/android/vpn/VpnConnectionManager.kt b/app/src/main/java/com/protonvpn/android/vpn/VpnConnectionManager.kt index e1c6d9b63..51f4ab932 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/VpnConnectionManager.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/VpnConnectionManager.kt @@ -51,12 +51,19 @@ import com.protonvpn.android.utils.Storage import com.protonvpn.android.utils.eagerMapNotNull import com.protonvpn.android.utils.implies import io.sentry.event.EventBuilder -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import me.proton.core.network.domain.NetworkManager +import java.util.concurrent.TimeUnit import javax.inject.Singleton import kotlin.coroutines.coroutineContext +private val FALLBACK_PROTOCOL = VpnProtocol.IKEv2 +private val UNREACHABLE_MIN_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1) + @Singleton open class VpnConnectionManager( private val appContext: Context, @@ -67,7 +74,8 @@ open class VpnConnectionManager( private val vpnStateMonitor: VpnStateMonitor, private val notificationHelper: NotificationHelper, private val serverManager: ServerManager, - private val scope: CoroutineScope + private val scope: CoroutineScope, + private val now: () -> Long ) : VpnStateSource { companion object { @@ -86,6 +94,7 @@ open class VpnConnectionManager( private var connectionParams: ConnectionParams? = null private var lastProfile: Profile? = null + private var lastUnreachable = Long.MIN_VALUE override val selfStateObservable = MutableLiveData(VpnState.Disabled) @@ -122,9 +131,11 @@ open class VpnConnectionManager( if (errorType != null && errorType in RECOVERABLE_ERRORS) { if (ongoingFallback?.isActive != true) { - ongoingFallback = scope.launch { - handleRecoverableError(errorType, connectionParams!!) - ongoingFallback = null + if (!skipFallback(errorType)) { + ongoingFallback = scope.launch { + handleRecoverableError(errorType, connectionParams!!) + ongoingFallback = null + } } } } else { @@ -163,6 +174,13 @@ open class VpnConnectionManager( initialized = true } + private fun skipFallback(errorType: ErrorType) = + errorType == ErrorType.UNREACHABLE_INTERNAL && + (lastUnreachable > now() - UNREACHABLE_MIN_INTERVAL_MS).also { skip -> + if (!skip) + lastUnreachable = now() + } + private fun activateBackend(newBackend: VpnBackend) { DebugUtils.debugAssert { activeBackend == null || activeBackend == newBackend @@ -272,20 +290,19 @@ open class VpnConnectionManager( ProtonLogger.log("Disconnected, start connecting to new server.") } - if (profile.getProtocol(userData) == VpnProtocol.Smart) - setSelfState(VpnState.ScanningPorts) + setSelfState(VpnState.ScanningPorts) var protocol = profile.getProtocol(userData) - if (!networkManager.isConnectedToNetwork() && protocol == VpnProtocol.Smart) - protocol = userData.manualProtocol - var preparedConnection = backendProvider.prepareConnection(protocol, profile, server) + val hasNetwork = networkManager.isConnectedToNetwork() + if (!hasNetwork && protocol == VpnProtocol.Smart) + protocol = FALLBACK_PROTOCOL + var preparedConnection = backendProvider.prepareConnection(protocol, profile, server, alwaysScan = hasNetwork) if (preparedConnection == null) { - ProtonLogger.log("Smart protocol: no protocol available for ${server.domain}, " + - "falling back to ${userData.manualProtocol}") + val fallbackProtocol = if (protocol == VpnProtocol.Smart) FALLBACK_PROTOCOL else protocol + ProtonLogger.log("No response for ${server.domain}, using fallback $fallbackProtocol") - // If port scanning fails (because e.g. some temporary network situation) just connect - // without smart protocol - preparedConnection = backendProvider.prepareConnection(userData.manualProtocol, profile, server)!! + // If port scanning fails (because e.g. some temporary network situation) just connect without pinging + preparedConnection = backendProvider.prepareConnection(fallbackProtocol, profile, server, false)!! } preparedConnect(preparedConnection) diff --git a/app/src/main/java/com/protonvpn/android/vpn/VpnLogCapture.kt b/app/src/main/java/com/protonvpn/android/vpn/VpnLogCapture.kt index 40c677ff9..61958e8aa 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/VpnLogCapture.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/VpnLogCapture.kt @@ -53,8 +53,9 @@ class VpnLogCapture(appComponent: AppComponent, val monoClock: () -> Long) { do { val start = monoClock() try { + val wireguardTag = "WireGuard/GoBackend/${Constants.WIREGUARD_TUNNEL_NAME}" val process = Runtime.getRuntime().exec( - "logcat -s WireGuard/GoBackend/${Constants.WIREGUARD_TUNNEL_NAME}:* charon:* -T 1 -v raw" + "logcat -s ${wireguardTag}:* charon:* ${Constants.SECONDARY_PROCESS_TAG}:* -T 1 -v raw" ) BufferedReader(InputStreamReader(process.inputStream)).useLines { lines -> lines.forEach { diff --git a/app/src/main/java/com/protonvpn/android/vpn/VpnState.kt b/app/src/main/java/com/protonvpn/android/vpn/VpnState.kt index 7be2e687c..56fb41cf4 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/VpnState.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/VpnState.kt @@ -51,7 +51,7 @@ sealed class VpnState(val isEstablishingConnection: Boolean) { } enum class ErrorType { - AUTH_FAILED_INTERNAL, AUTH_FAILED, PEER_AUTH_FAILED, LOOKUP_FAILED_INTERNAL, LOOKUP_FAILED, - UNREACHABLE, UNREACHABLE_INTERNAL, MAX_SESSIONS, UNPAID, GENERIC_ERROR, MULTI_USER_PERMISSION, + AUTH_FAILED_INTERNAL, AUTH_FAILED, PEER_AUTH_FAILED, LOOKUP_FAILED_INTERNAL, UNREACHABLE, + UNREACHABLE_INTERNAL, MAX_SESSIONS, UNPAID, GENERIC_ERROR, MULTI_USER_PERMISSION, LOCAL_AGENT_ERROR } diff --git a/app/src/main/java/com/protonvpn/android/vpn/ikev2/StrongSwanBackend.kt b/app/src/main/java/com/protonvpn/android/vpn/ikev2/StrongSwanBackend.kt index c30f4f5ae..cc7570598 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/ikev2/StrongSwanBackend.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/ikev2/StrongSwanBackend.kt @@ -31,7 +31,6 @@ import com.protonvpn.android.models.profiles.Profile import com.protonvpn.android.models.vpn.ConnectionParams import com.protonvpn.android.models.vpn.ConnectionParamsIKEv2 import com.protonvpn.android.models.vpn.Server -import com.protonvpn.android.utils.NetUtils import com.protonvpn.android.vpn.CertificateRepository import com.protonvpn.android.vpn.ErrorType import com.protonvpn.android.vpn.PrepareResult @@ -44,31 +43,30 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import me.proton.core.network.domain.NetworkManager import me.proton.core.network.domain.NetworkStatus +import me.proton.core.util.kotlin.DispatcherProvider import org.strongswan.android.logic.VpnStateService -import java.io.ByteArrayOutputStream import java.util.Random -import java.util.concurrent.TimeUnit class StrongSwanBackend( val random: Random, networkManager: NetworkManager, mainScope: CoroutineScope, - val now: () -> Long, userData: UserData, appConfig: AppConfig, - certificateRepository: CertificateRepository + certificateRepository: CertificateRepository, + dispatcherProvider: DispatcherProvider ) : VpnBackend( userData, appConfig, certificateRepository, networkManager, VpnProtocol.IKEv2, - mainScope + mainScope, + dispatcherProvider ), VpnStateService.VpnStateListener { private var vpnService: VpnStateService? = null private val serviceProvider = Channel() - private var lastUnreachable = 0L init { bindCharonMonitor() @@ -89,29 +87,23 @@ class StrongSwanBackend( profile: Profile, server: Server, scan: Boolean, - numberOfPorts: Int + numberOfPorts: Int, // unused, IKEv2 uses 2 ports and both need to be functional + waitForAll: Boolean // as above ): List { val connectingDomain = server.getRandomConnectingDomain() - if (!scan || isServerAvailable(connectingDomain.entryIp)) return listOf( - PrepareResult(this, ConnectionParamsIKEv2(profile, server, connectingDomain))) - return emptyList() + val result = listOf(PrepareResult(this, ConnectionParamsIKEv2(profile, server, connectingDomain))) + return if (!scan) + result + else { + val ports = STRONGSWAN_PORTS + val availablePorts = scanUdpPorts(connectingDomain, ports, ports.size, true) + if (availablePorts.toSet() == ports.toSet()) + result + else + emptyList() + } } - private suspend fun isServerAvailable(ip: String) = - NetUtils.ping(ip, STRONGSWAN_PORT, getPingData(), tcp = false, timeout = 5000) - - @Suppress("MagicNumber") - private fun getPingData() = ByteArrayOutputStream().apply { - repeat(8) { write(random.nextInt(256)) } // my SPI - repeat(8) { write(0) } // other SPI - write(0x21) // Security association - write(0x20) // Version 2 - write(0x22) // IKE_SA_INIT - write(0x08) // Initiator, no higher version, request - repeat(4) { write(0) } // Message id - repeat(4) { write(0) } // Length = 0 - }.toByteArray() - override suspend fun connect(connectionParams: ConnectionParams) { super.connect(connectionParams) getVpnService().connect(null, true) @@ -168,23 +160,11 @@ class StrongSwanBackend( override fun stateChanged() { vpnService?.let { - val newState = translateState(it.state, it.errorState) - if (newState.isUnreachable()) { - // Limit frequency of unreachable notifications - if (vpnProtocolState.isUnreachable() && now() - lastUnreachable < UNREACHABLE_MIN_INTERVAL_MS) - return - lastUnreachable = now() - } - vpnProtocolState = newState + vpnProtocolState = translateState(it.state, it.errorState) } } - private fun VpnState.isUnreachable() = (this as? VpnState.Error)?.type.let { - it == ErrorType.UNREACHABLE_INTERNAL || it == ErrorType.UNREACHABLE - } - companion object { - private const val STRONGSWAN_PORT = 500 - private val UNREACHABLE_MIN_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1) + private val STRONGSWAN_PORTS = listOf(500, 4500) } } diff --git a/app/src/main/java/com/protonvpn/android/vpn/openvpn/OpenVpnBackend.kt b/app/src/main/java/com/protonvpn/android/vpn/openvpn/OpenVpnBackend.kt index 546f2c389..af6360133 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/openvpn/OpenVpnBackend.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/openvpn/OpenVpnBackend.kt @@ -31,12 +31,10 @@ import com.protonvpn.android.models.vpn.ConnectionParams import com.protonvpn.android.models.vpn.ConnectionParamsOpenVpn import com.protonvpn.android.models.vpn.Server import com.protonvpn.android.utils.Constants -import com.protonvpn.android.utils.DebugUtils import com.protonvpn.android.utils.Log import com.protonvpn.android.utils.NetUtils import com.protonvpn.android.utils.ProtonLogger -import com.protonvpn.android.utils.implies -import com.protonvpn.android.utils.randomNullable +import com.protonvpn.android.utils.parallelSearch import com.protonvpn.android.vpn.CertificateRepository import com.protonvpn.android.vpn.ErrorType import com.protonvpn.android.vpn.PrepareResult @@ -50,6 +48,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import me.proton.core.network.domain.NetworkManager +import me.proton.core.util.kotlin.DispatcherProvider import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.digest.HmacAlgorithms import org.apache.commons.codec.digest.HmacUtils @@ -64,14 +63,16 @@ class OpenVpnBackend( appConfig: AppConfig, val unixTime: () -> Long, certificateRepository: CertificateRepository, - mainScope: CoroutineScope + mainScope: CoroutineScope, + dispatcherProvider: DispatcherProvider ) : VpnBackend( userData, appConfig, certificateRepository, networkManager, VpnProtocol.OpenVPN, - mainScope + mainScope, + dispatcherProvider ), VpnStatus.StateListener { init { @@ -87,17 +88,18 @@ class OpenVpnBackend( profile: Profile, server: Server, scan: Boolean, - numberOfPorts: Int + numberOfPorts: Int, + waitForAll: Boolean ): List { val connectingDomain = server.getRandomConnectingDomain() val openVpnPorts = appConfig.getOpenVPNPorts() val protocolInfo = if (!scan) { val transmissionProtocol = profile.getTransmissionProtocol(userData) val port = (if (transmissionProtocol == TransmissionProtocol.UDP) - openVpnPorts.getUdpPorts() else openVpnPorts.getTcpPorts()).random() + openVpnPorts.udpPorts else openVpnPorts.tcpPorts).random() listOf(ProtocolInfo(transmissionProtocol, port)) } else { - scanPorts(connectingDomain, numberOfPorts) + scanPorts(connectingDomain, numberOfPorts, waitForAll) } return protocolInfo.map { PrepareResult(this, ConnectionParamsOpenVpn( @@ -107,62 +109,36 @@ class OpenVpnBackend( private suspend fun scanPorts( connectingDomain: ConnectingDomain, - numberOfPorts: Int = Int.MAX_VALUE + numberOfPorts: Int = Int.MAX_VALUE, + waitForAll: Boolean ): List { val openVpnPorts = appConfig.getOpenVPNPorts() val result = mutableListOf() coroutineScope { - val udpPingData = getPingData(tcp = false) - val udpPort = async { - scanInParallel( - samplePorts(openVpnPorts.getUdpPorts(), numberOfPorts), - connectingDomain.entryIp, - udpPingData, - withTcp = false) + val udpPorts = async { + scanUdpPorts(connectingDomain, samplePorts(openVpnPorts.udpPorts, numberOfPorts), numberOfPorts, waitForAll) } val tcpPingData = getPingData(tcp = true) - val tcpPort = async { - scanInParallel( - samplePorts(openVpnPorts.getTcpPorts(), numberOfPorts), - connectingDomain.entryIp, - tcpPingData, - withTcp = true) + val tcpPorts = async { + val ports = samplePorts(openVpnPorts.tcpPorts, numberOfPorts) + ProtonLogger.log("${connectingDomain.entryDomain}/OpenVPN/TCP port scan: $ports") + ports.parallelSearch(waitForAll) { port -> + NetUtils.ping(connectingDomain.entryIp, port, tcpPingData, tcp = true) + } } - udpPort.await()?.let { - result += ProtocolInfo(TransmissionProtocol.UDP, it) - } - tcpPort.await()?.let { - result += ProtocolInfo(TransmissionProtocol.TCP, it) - } + result += udpPorts.await().map { ProtocolInfo(TransmissionProtocol.UDP, it) } + result += tcpPorts.await().map { ProtocolInfo(TransmissionProtocol.TCP, it) } } return result } private fun samplePorts(list: List, count: Int) = if (list.contains(PRIMARY_PORT)) - list.filter { it != PRIMARY_PORT }.shuffled().take(count - 1).toSet() + setOf(PRIMARY_PORT) + list.filter { it != PRIMARY_PORT }.shuffled().take(count - 1) + PRIMARY_PORT else - list.shuffled().take(count).toSet() - - private suspend fun scanInParallel( - ports: Set, - ip: String, - data: ByteArray, - withTcp: Boolean - ): Int? = coroutineScope { - val available = ports.map { port -> - async { - port.takeIf { - NetUtils.ping(ip, port, data, withTcp) - } - } - }.mapNotNull { - it.await() - } - available.randomNullable() - } + list.shuffled().take(count) private fun getPingData(tcp: Boolean): ByteArray { // P_CONTROL_HARD_RESET_CLIENT_V2 TLS message. diff --git a/app/src/main/java/com/protonvpn/android/vpn/wireguard/WireguardBackend.kt b/app/src/main/java/com/protonvpn/android/vpn/wireguard/WireguardBackend.kt index c4b192631..c8ffc6f41 100644 --- a/app/src/main/java/com/protonvpn/android/vpn/wireguard/WireguardBackend.kt +++ b/app/src/main/java/com/protonvpn/android/vpn/wireguard/WireguardBackend.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.proton.core.network.domain.NetworkManager +import me.proton.core.util.kotlin.DispatcherProvider class WireguardBackend( val context: Context, @@ -52,11 +53,12 @@ class WireguardBackend( userData: UserData, appConfig: AppConfig, certificateRepository: CertificateRepository, + dispatcherProvider: DispatcherProvider, mainScope: CoroutineScope -) : VpnBackend(userData, appConfig, certificateRepository, networkManager, VpnProtocol.WireGuard, mainScope) { +) : VpnBackend(userData, appConfig, certificateRepository, networkManager, VpnProtocol.WireGuard, mainScope, + dispatcherProvider) { private var service: WireguardWrapperService? = null - private val testTunnel = WireGuardTunnel( name = Constants.WIREGUARD_TUNNEL_NAME, config = null, @@ -79,18 +81,26 @@ class WireguardBackend( profile: Profile, server: Server, scan: Boolean, - numberOfPorts: Int + numberOfPorts: Int, + waitForAll: Boolean ): List { - return listOf( + val connectingDomain = server.getRandomConnectingDomain() + val ports = appConfig.getWireguardPorts().udpPorts + val selectedPorts = if (scan) + scanUdpPorts(connectingDomain, ports, numberOfPorts, waitForAll) + else + listOfNotNull(ports.first()) + return selectedPorts.map { port -> PrepareResult( this, ConnectionParamsWireguard( profile, server, - server.getRandomConnectingDomain() + port, + connectingDomain ) ) - ) + } } override suspend fun connect(connectionParams: ConnectionParams) { diff --git a/app/src/main/java/org/strongswan/android/logic/VpnStateService.java b/app/src/main/java/org/strongswan/android/logic/VpnStateService.java index f0abb3545..a030693c5 100644 --- a/app/src/main/java/org/strongswan/android/logic/VpnStateService.java +++ b/app/src/main/java/org/strongswan/android/logic/VpnStateService.java @@ -228,7 +228,7 @@ public int getErrorText() case LOOKUP_FAILED: return R.string.error_lookup_failed; case UNREACHABLE: - return R.string.error_unreachable; + return R.string.error_server_unreachable; case PASSWORD_MISSING: return R.string.error_password_missing; case CERTIFICATE_UNAVAILABLE: diff --git a/app/src/main/res/layout/dialog_split_tunnel.xml b/app/src/main/res/layout/dialog_split_tunnel.xml index feef7d53f..dade1ac8d 100644 --- a/app/src/main/res/layout/dialog_split_tunnel.xml +++ b/app/src/main/res/layout/dialog_split_tunnel.xml @@ -109,17 +109,19 @@ + app:layout_constraintTop_toTopOf="@+id/list" + app:layout_constraintWidth_percent="0.45" /> . --> - + xmlns:tools="http://schemas.android.com/tools"> - - - - - + android:gravity="center_vertical" + android:orientation="horizontal" + tools:background="@color/grey"> + android:id="@+id/imageIcon" + android:layout_width="28dp" + android:layout_height="28dp" + android:layout_marginBottom="8dp" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:duplicateParentState="true" + android:scaleType="centerInside" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + - + android:layout_marginBottom="8dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="8dp" + android:foregroundGravity="right|center_vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + + + + + - + - + diff --git a/app/src/main/res/layout/item_apps_header.xml b/app/src/main/res/layout/item_apps_header.xml new file mode 100644 index 000000000..0011ba2f0 --- /dev/null +++ b/app/src/main/res/layout/item_apps_header.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/layout/item_apps_load_system_apps.xml b/app/src/main/res/layout/item_apps_load_system_apps.xml new file mode 100644 index 000000000..d6172a79a --- /dev/null +++ b/app/src/main/res/layout/item_apps_load_system_apps.xml @@ -0,0 +1,57 @@ + + + + + + +