Compare commits

...

167 Commits
v1.5 ... master

Author SHA1 Message Date
Corewala 326c20f5ff Removed "fix" for images because it breaks things. 2023-02-20 16:54:27 -05:00
Corewala 867688a075 Fixed bug where current uri would be changed by resolving long-taps and image dialogs 2023-02-20 16:46:04 -05:00
Corewala bedbc9fb98 Made preferences less vulnerable to accidentally changing certificates 2023-02-20 16:14:17 -05:00
Corewala df6c159c2c Removed unused string 2023-02-20 15:31:37 -05:00
Corewala 93ef22a4f8 Removed unnecessary reference to proxy in redirect fix 2023-02-20 15:28:57 -05:00
Corewala 0a47f391e5 Fixed issue with local redirects from links on another host 2023-02-04 20:38:57 -05:00
Corewala 73afc7c684 Fixed redirect drop bug 2023-02-04 19:46:26 -05:00
Corewala 0fdffee966 v1.12 2022-11-09 23:21:34 -05:00
Corewala da4375b832 Merge remote-tracking branch 'origin/master' 2022-11-09 23:02:32 -05:00
Corewala 9f4d1c8027 Fixed bugged back button
Been a while eh?
2022-11-09 23:02:15 -05:00
Corewala 0df1501fd4
Merge pull request #30 from arielcostas/master
Delete local.properties,which shouldn't be on VCS

Thanks @arielcostas :)
2022-09-02 14:44:00 -04:00
Ariel Costas 2bd546d7ed Delete local.properties,which shouldn't be on VCS
Signed-off-by: Ariel Costas <arielcostas@mailbox.org>
2022-09-01 15:02:19 +02:00
Corewala 2c3c1db96b
Merge pull request #29 from rudmannn/master
fix android:lineHeight
2022-08-11 09:39:53 -04:00
strooonger 9038f49d40 fix android:lineHeight 2022-08-06 22:42:08 +08:00
Corewala eac1adb0cb Removed unused transverse function 2022-08-04 12:18:47 -04:00
Corewala eee109bb28 Made relative query strings work in accordance with the URL spec 2022-08-04 12:17:20 -04:00
Corewala f89f41ae14 Removed broken null check on http proxy 2022-08-01 18:38:09 -04:00
Corewala 26093144dd Fixed apostrophes in French fastlane description 2022-07-31 14:08:46 -04:00
Corewala 47dd2722e1 v1.11 2022-07-29 19:11:45 -04:00
Corewala b85de17c88 v1.10 changelog fix 2022-07-29 18:55:55 -04:00
Corewala 53c1980fa6 Updated todo 2022-07-29 16:23:24 -04:00
Corewala 0ae42a214d Removed redundant ouri and fixed local redirects (again) 2022-07-29 16:21:00 -04:00
Corewala 319b0b4d14 Fixed sharing of relative links 2022-07-29 15:47:33 -04:00
Corewala 13f21bc09b Fixed relative link handling with proxy 2022-07-29 15:09:26 -04:00
Corewala 79a3564569 Fixed snackbar on proxied content 2022-07-26 12:31:04 -04:00
Corewala 4a451ef5ca Fixed attention guide crash with long non-alphanumeric words 2022-07-26 12:09:00 -04:00
Corewala 467e3fc0b7 Added HTTP proxy
It's a beautiful half-broken mess
2022-07-25 23:07:55 -04:00
Corewala affa99e8f2 Added HTTP proxy to settings
It doesn't do anything yet. Great feature I know.
2022-07-09 18:38:31 -04:00
Corewala 4c94bd97e4 Fixed previousPosition when not through onLink 2022-07-09 13:27:27 -04:00
Corewala 679c3bd0be Replaced screenshots 2022-07-08 13:11:49 -04:00
Corewala aa6dcdad91 Previous scroll position is cached to reduce unnecessary re-scrolling 2022-07-08 12:28:17 -04:00
Corewala 9d4386939a Added HTTPS gateway support to todo 2022-07-08 11:08:12 -04:00
Corewala 001e7d3ffe Fixed type in screenshots folder name
That's a bit embarrassing
2022-07-02 11:09:06 -04:00
Corewala 882617df29 Added correct link to f-droid button 2022-07-02 11:02:34 -04:00
Corewala ba901e49ba Added f-droid button 2022-07-02 10:58:51 -04:00
Corewala cf8efbc625 Addressbar is cleared when loading about page 2022-07-01 15:53:06 -04:00
Corewala 09fc2a480a Improved handling of broken addresses in settings 2022-06-30 18:54:06 -04:00
Corewala a47d003f59 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	metadata/android/en-US/changelogs/11.txt
2022-06-23 13:55:40 -04:00
Corewala f79d0bf5ff Changelog v1.10 2022-06-23 13:33:14 -04:00
Licaon_Kter 011eb7487c
Add changelog 2022-06-22 10:51:00 +00:00
Corewala bae12d17a8 v1.10 2022-06-21 17:28:48 -04:00
Corewala f98756bebb Made local home into about page 2022-06-21 15:06:19 -04:00
Corewala e4cc13bb03 Made local page default 2022-06-21 14:37:04 -04:00
Corewala 3dee099dcf Added search to default homepage 2022-06-21 14:35:30 -04:00
Corewala 132749016a Made home capsule setting look better when empty/invalid 2022-06-21 14:23:44 -04:00
Corewala a791521565 Fixed infinite refresh on default homepage 2022-06-20 18:03:35 -04:00
Corewala 8258c48bb0 Updated strings on no internet page 2022-06-20 18:02:22 -04:00
Corewala c12702ef2f Fixed broken check for valid URI 2022-06-20 17:53:26 -04:00
Corewala 28fa72f59e Made refresh stop on empty homepage 2022-06-20 17:44:09 -04:00
Corewala 13af2089d8 Added fastlane icon 2022-06-20 14:22:35 -04:00
Corewala 2c24fa0c77 Does not load a URI on first launch
And fixed bugs
2022-06-20 12:45:20 -04:00
Corewala 0a59ac08cb Fixed crash if whitespace present in addressbar 2022-06-20 12:07:40 -04:00
Corewala c23b3efb70 Only show self-update option if sideloaded 2022-06-20 11:37:33 -04:00
Corewala 6d4dbbda94 Made self-updating false by default 2022-06-18 12:45:00 -04:00
Corewala 95ed0b815b Removed statement about Ariane source
Things have stayed pretty quiet for a while now, so it feels a bit unnecessary to have such a big statement anymore.
2022-06-16 15:28:07 -04:00
Corewala 125c34c6d0 Replace Egsam gemini link with repo
Apparently Github doesn't like non-http links in the README
2022-06-16 15:03:09 -04:00
Corewala 1c3905b197 Added goal of passing the Egsam client test 2022-06-16 15:00:51 -04:00
Corewala ee8ff30210 Changed images 2022-06-09 15:00:45 -04:00
Corewala 45b50afd47 Remove unnecessary plural French 2022-06-09 14:25:54 -04:00
Corewala dd95af5e5b Made French spelling consistent 2022-06-09 14:24:06 -04:00
Corewala fbdb04bf31 Fixed punctuation 2022-06-09 13:22:17 -04:00
Corewala 98d9af2871 Fixed plural accordance issue 2022-06-09 13:21:36 -04:00
Corewala 79a0d32bf1 Made capitalisation consistent 2022-06-09 13:21:01 -04:00
Corewala 5ccdaeb816 Added screenshots 2022-06-09 13:19:09 -04:00
Corewala f2d633e7e2 Added fastlane metadata 2022-06-09 13:18:54 -04:00
Corewala 05e2f64ebd Updated about body text 2022-06-09 12:41:15 -04:00
Corewala 2db5aa74ca Removed canGoBack check on request cancellation 2022-06-09 11:37:27 -04:00
Corewala 7828f3ea7f Deleted unused variable 2022-06-09 11:22:15 -04:00
Corewala bd8bbbc903 Made clear runtime cache button actually do something 2022-06-09 11:21:41 -04:00
Corewala 8f8bb15455 Added page switching to todo 2022-06-09 10:52:45 -04:00
Corewala 918deb4cdf Forgot to disable loading when request is canceled 2022-06-07 16:38:36 -04:00
Corewala cdcbdcdb2c Only latest gemini request is handled
Also back button cancels current requests if used while loading
2022-06-07 15:36:16 -04:00
Corewala 35352fa6f3 Only latest gemini request is handled 2022-06-07 15:06:54 -04:00
Corewala 0505e5d5bf Fixed redirects without leading slash 2022-06-03 10:42:42 -04:00
Corewala 764cee3042 Fixed addressbar for right-to-left languages 2022-05-31 16:21:45 -04:00
Corewala c0be77ef41 Improved internet connection handling 2022-05-30 19:53:44 -04:00
Corewala 60673c2fb3 Fixed attention guide crash on non-alphanumeric words 2022-05-30 11:38:45 -04:00
Corewala d247b17999 v1.9 2022-05-28 16:13:02 -04:00
Corewala eef63cd2e7 Made header rendering compliant with gemini spec 2022-05-28 14:42:44 -04:00
Corewala 8bb8c4f8f1 Removed mystery error fix
The fix would've broken legitimate response errors if opened with intents. Also for some reason the bug isn't happening on any of my devices anymore, even though I still have v1.8 installed on my phone. Very mysterious.
2022-05-27 12:21:15 -04:00
Corewala 30dcb45661 Fixed mystery connection error (for real this time)
Again, kind of an ugly fix, but at least it works I think
2022-05-26 14:05:58 -04:00
Corewala 589ab24a7e Removed useless fix 2022-05-26 13:36:05 -04:00
Corewala 1d2be092f1 Fixed a bunch of warnings
Spring cleaning I guess
2022-05-26 12:49:30 -04:00
Corewala d504df9cd3 Enabled more optimisation
The APK is so tiny and runs even faster now. How did I not do this earlier?
2022-05-26 12:42:16 -04:00
Corewala 1ec24a38a5 Fixed connection error before initialised
The fix isn't the cleanest, but it stopped throwing a mystery error.
2022-05-26 12:12:06 -04:00
Corewala d5ccf72ccf Removed unused import 2022-05-25 17:28:56 -04:00
Corewala 3d32bc95d5 Improved "no internet" screen 2022-05-25 17:00:56 -04:00
Corewala 5bb5fa7dcb Attention guides can handle hyphens 2022-05-25 11:34:55 -04:00
Corewala 8cde96c971 Made element names consistent with their titles 2022-05-24 15:47:44 -04:00
Corewala 0b7e24e764 Attention guides for gemtext 2022-05-24 15:38:47 -04:00
Corewala a7877cfc41 Removed unused variables and imports in GemtextAdapter 2022-05-22 16:49:11 -04:00
Corewala 684239c066 Removed unused code blocks parameter 2022-05-22 12:35:46 -04:00
Corewala e10e78b9f5 Added toasts for certificate loading 2022-05-22 11:43:48 -04:00
Corewala 3df657d4d4 Fixed white flash on startup 2022-05-22 11:22:00 -04:00
Corewala da37d8d684 Client certificate works with redirects 2022-05-22 10:55:52 -04:00
Corewala 04e8cfc2c7 Fixed hard crash on home button 2022-05-22 10:46:21 -04:00
Corewala 84af7e4588 Removed redundant internet check 2022-05-21 19:41:35 -04:00
Corewala 980a1ac4d5 Fixed localisation for update checks 2022-05-21 13:29:38 -04:00
Corewala 0b97d239c4 Hide update checks if installed from repository 2022-05-21 13:28:41 -04:00
Corewala 76145f80d5 v1.8 2022-05-19 14:11:45 -04:00
Corewala 0d30671b4b Fixed link buttons being enabled on first install despite being disabled in settings 2022-05-19 14:08:58 -04:00
Corewala c2cedab881 combined createBiometricPrompt function versions 2022-05-19 13:39:10 -04:00
Corewala 815c6bdccf Cleaned up unused imports and such 2022-05-19 13:25:58 -04:00
Corewala 5387f6635f Update cert icon in request function 2022-05-19 13:24:14 -04:00
Corewala 5e1e580f68 Shrunk signature icon to prevent rescaling of addressbar 2022-05-19 13:15:09 -04:00
Corewala cf3402e91f Overflow menu option changes title when cert is (un)loaded 2022-05-19 13:13:17 -04:00
Corewala 506976b25e Certificate button in overflow menu works better
Also the client cert icon isn't broken and doesn't cause the address bar to do weird scaling stuff anymore
2022-05-18 19:13:29 -04:00
Corewala b2f10454db Fixed null pointer error 2022-05-18 18:50:56 -04:00
Corewala da4702b1a0 Added signature system for sites which do not require certs
Also broke a bunch of things, but we'll get there I swear.
2022-05-18 12:18:59 -04:00
Corewala 29fe06ba54 Fixed inconsistent client cert weirdness 2022-05-17 20:45:40 -04:00
Corewala 5210d8484e Removed broken PREF_KEY_CLIENT_CERT_ACTIVE value 2022-05-17 20:02:39 -04:00
Corewala b954e94d3b Removed debug print 2022-05-17 19:46:29 -04:00
Corewala 2007e64550 Fixed recursion error 2022-05-17 19:45:31 -04:00
Corewala 4808620743 Simplified request function 2022-05-17 19:12:42 -04:00
Corewala 20846dad47 Only require verification for cert if changing host
Also removed all direct references to model.request
2022-05-17 19:00:47 -04:00
Corewala a519a522cf Make biometric unlock only required once per session 2022-05-17 18:37:11 -04:00
Corewala e46ea379c3 Fixed order of actions for biometrics 2022-05-17 15:34:44 -04:00
Corewala 55940914c0 Removed unused and broken useClientCertPreference 2022-05-17 15:14:31 -04:00
Corewala 36952de39f Fixed broken check for active client certificate 2022-05-17 13:49:18 -04:00
Corewala fe957eb581 Updated todo 2022-05-17 13:46:47 -04:00
Corewala 0c1cb3a309 Biometric verification for client certificates 2022-05-17 13:45:54 -04:00
Corewala 55db823420 Removed unnecessary values in BiometricManager 2022-05-17 13:25:49 -04:00
Corewala 93191e67c4 Touched up client cert settings 2022-05-17 13:19:04 -04:00
Corewala 64f3308e1b Added biometric cert password encryption to settings
Does nothing useful right now and just makes your client cert unusable while enabled. Obviously I plan to change that in the near-ish future.
2022-05-16 20:37:15 -04:00
Corewala 88583d1f49 Reverted changes to GemActivity and OmniTerm which broke image rendering 2022-05-16 12:19:41 -04:00
Corewala 02e27c7868 Fixed issue with local redirect responses 2022-05-15 12:26:06 -04:00
Corewala 15cc2d9a31 Removed debug strings lmao 2022-05-10 18:49:59 -04:00
Corewala 75f42173c9 External non-http URLs are handled correctly 2022-05-10 12:14:17 -04:00
Corewala bfd2572cec Removed %2F substitution in onLink 2022-05-10 11:32:28 -04:00
Corewala 1542ec5539 Checks theme in preferences on startup 2022-05-10 10:34:04 -04:00
Corewala fbf6c80282 Localised missing host error message 2022-05-08 17:50:00 -04:00
Corewala 94074e4689 Removed unused transverse function in OmniTerm 2022-05-07 12:50:27 -04:00
Corewala 82688f02b6 Fixed error with navigation to URLs containing "%2F" 2022-05-07 12:49:28 -04:00
Corewala 494997c0ea Fixed inline images with query strings 2022-05-07 12:47:07 -04:00
Corewala 913bf12686 Fixed update system
Pretty embarrassed that I put this in a release without proper testing lol
2022-05-05 14:22:57 -04:00
Corewala 95d6e92a04 Made dialog buttons uppercase in line with Material Design 2022-05-05 10:19:03 -04:00
Corewala 1275136433 v1.7 2022-05-03 21:34:26 -04:00
Corewala 0981c23e37 Check for certificate before offering continue button 2022-05-03 21:24:01 -04:00
Corewala ee664058bc Localised "ok" in dialogs 2022-05-03 18:54:12 -04:00
Corewala e27526deaa Fixed missing buttons in input dialog 2022-05-03 18:50:27 -04:00
Corewala d60c1a3b20 Added dialog when client certificate is required
The biometric unlock feature is on the way, if you couldn't tell
2022-05-03 18:29:44 -04:00
Corewala b2ef138402 Rearranged client cert section in settings
Also removed unnecessary "if true" statement
2022-05-03 12:25:57 -04:00
Corewala 79056fe060 Relaunches if internet is found after unsuccessful launch 2022-05-02 14:49:35 -04:00
Corewala 01df294462 Made internet checks consistent, reduced spaghetticode 2022-05-02 12:04:29 -04:00
Corewala 8c7546f095 Check if internet is available before opening a history entry 2022-05-01 12:25:35 -04:00
Corewala f54add615a Check if internet is available before opening a bookmark 2022-05-01 12:21:22 -04:00
Corewala 539f17e9be Check if internet is available before refreshing 2022-05-01 12:17:28 -04:00
Corewala 676d64e4bc Check if internet is available before loading previous page 2022-05-01 11:59:08 -04:00
Corewala 508d1f39e6 Check if internet is available before attempting to load new capsule 2022-05-01 11:48:56 -04:00
Corewala 972ff95ecd Lock addressbar if launched with no internet 2022-05-01 11:44:42 -04:00
Corewala 791fabca74 Fixed instant crash when internet is unavailable 2022-05-01 11:31:45 -04:00
Corewala 3661c134c4 Search engine select 2022-04-20 15:28:11 -04:00
Corewala f0c85315a1 Updated todo 2022-03-14 09:23:49 -04:00
Corewala d999c683f0 Merge remote-tracking branch 'origin/master' 2022-03-13 17:04:09 -04:00
Corewala 6dab6918c6 v1.6 2022-03-13 17:03:58 -04:00
Corewala 70734e33d9 v1.7 2022-03-13 17:03:05 -04:00
Corewala d7a2cdeb9f Updater 2022-03-13 16:58:10 -04:00
Corewala 8d70218d23 Added update install function 2022-03-12 14:40:01 -05:00
Corewala 48c7339d57 Update checking function 2022-03-07 15:51:07 -05:00
Corewala 4cd0e1977b Set max lines in input query 2022-02-17 18:27:34 -05:00
Corewala a5ec6624c4 Made input dialog margins consistent 2022-02-16 20:28:41 -05:00
Corewala 8c0bfd04db Replaced full-screen query dialog with AlertDialog 2022-02-16 20:02:27 -05:00
Corewala a78d11bf5a Fixed keyboard glitching in address bar 2022-02-08 13:06:30 -05:00
Corewala 324d947330 Fixed bug where the intended page is replaced by homepage when opened with link 2022-02-08 11:04:34 -05:00
Corewala f6f7997c69 Changed homepage to tlgs.one 2022-02-08 10:22:57 -05:00
Corewala 102377d8d3 Localised app name
Realised too late for v1.5, guess it'll be in v1.6.
2022-01-30 13:51:23 -05:00
Corewala c550a37581 Updated link 2022-01-30 13:40:32 -05:00
56 changed files with 1405 additions and 642 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
.gradle
.idea
build
release
release
local.properties
app/local.properties

View File

@ -8,26 +8,32 @@
[![shields](https://img.shields.io/badge/Download-Here-orange?style=for-the-badge)](https://github.com/Corewala/Buran/releases/latest)
[![shields](https://img.shields.io/badge/license-GPL-blue?style=for-the-badge)](https://github.com/Corewala/Buran/blob/master/LICENSE)
<a href="https://f-droid.org/packages/corewala.gemini.buran">
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">
</a>
Buran is a simple Gemini protocol browser for Android.
### Todo list
- [ ] Utility for creating and managing client certificates
- [ ] Keystore generator and catalog
- [ ] Option to require password or biometric authentication
- [X] Option to require password or biometric authentication
- [ ] Color palette interface for picking background and accent colors
- [x] Option to render links as buttons
- [X] Option to render links as buttons
- [X] Inline rendering of images
- [ ] Page navigation feature
- [ ] Update notifier
## Statement about Ariane
> I've seen some pretty cringe discussion in geminispace complaining about the decision by Öppen to close-source the Ariane browser, sometimes referencing this project as a more "stable" or "principled" project. Just to be 100% clear, I think that the license that Öppen used for Ariane 4 was actually much better than the one that Ariane 3 used, and I would have made the same decision if the EUPL was compatible with copyfarleft. Although I wish Ariane could continue to grow as an open-source project, I respect his decision to make Seren closed source. I only made this fork out of a legitimate desire to keep some fragment of this project public and an interest in learning Kotlin. Even though this fork exists, you should still consider buying a copy of Seren. He's definitely a better developer than me, and I don't want this project to be used as a rhetorical tool against him.
- [X] Update notifier
- [X] Attention guide mode
- [ ] Simple A/B page switching system
- [ ] Pass [Egsam test](https://github.com/pitr/egsam)
- [X] Option to define an HTTPS gateway
## Credits
Buran is based on the [Ariane source code](https://web.archive.org/web/20210920212507/https://codeberg.org/oppenlab/Ariane) (now [Seren](https://orllewin.uk/)), created by ÖLAB.
Buran is based on the [Ariane source code](https://web.archive.org/web/20210920212507/https://codeberg.org/oppenlab/Ariane) (now [Seren](https://orllewin.neocities.org/seren/)), created by ÖLAB.
The font used in code blocks is [JetBrains Mono](https://www.jetbrains.com/lp/mono/), created by JetBrains.

View File

@ -11,8 +11,8 @@ android {
applicationId "corewala.gemini.buran"
minSdkVersion 21
targetSdkVersion 30
versionCode 6
versionName "v1.5"
versionCode 13
versionName "v1.12"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -21,6 +21,7 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@ -56,6 +57,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.3.0-rc01'
implementation "androidx.biometric:biometric:1.1.0"
//ROOM DB
def room_version = "2.2.6"

View File

@ -1,4 +0,0 @@
sdk.dir=/home/vagrant/android-sdk
sdk-location=/home/vagrant/android-sdk
ndk.dir=/home/vagrant/android-ndk/r12b
ndk-location=/home/vagrant/android-ndk/r12b

View File

@ -3,13 +3,15 @@
package="corewala.buran">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name="corewala.buran.Buran"
android:allowBackup="true"
android:icon="@drawable/launcher"
android:label="Buran"
android:label="@string/app_name"
android:roundIcon="@drawable/launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme"

View File

@ -5,14 +5,13 @@ import android.app.Application
class Buran: Application() {
companion object{
const val DEFAULT_HOME_CAPSULE = "gemini://rawtext.club/~sloum/spacewalk.gmi"
const val FEATURE_CLIENT_CERTS = true
const val DEFAULT_HOME_CAPSULE = ""
const val DEFAULT_SEARCH_BASE = "gemini://tlgs.one/search?"
const val PREF_KEY_CLIENT_CERT_URI = "client_cert_uri"
const val PREF_KEY_CLIENT_CERT_HUMAN_READABLE = "client_cert_uri_human_readable"
const val PREF_KEY_CLIENT_CERT_ACTIVE = "client_cert_active"
const val PREF_KEY_CLIENT_CERT_PASSWORD = "client_cert_password"
const val CLIENT_CERT_PASSWORD_SECRET_KEY_NAME = "client_cert_secret_key_name"
const val PREF_KEY_USE_CUSTOM_TAB = "use_custom_tabs"
}
}

View File

@ -4,7 +4,6 @@ import android.net.Uri
import java.util.*
const val GEM_SCHEME = "gemini://"
const val SEARCH_BASE = "gemini://tlgs.one/search?"
class OmniTerm(private val listener: Listener) {
val history = ArrayList<OppenURI>()
@ -15,8 +14,12 @@ class OmniTerm(private val listener: Listener) {
* User input to the 'omni bar' - could be an address or a search term
* @param term - User-inputted term
*/
fun input(term: String){
fun input(term: String, searchbase: String?){
when {
term.contains(" ") -> {
val encoded = Uri.encode(term)
listener.request("$searchbase$encoded")
}
term.startsWith(GEM_SCHEME) && term != GEM_SCHEME -> {
listener.request(term)
return
@ -26,14 +29,14 @@ class OmniTerm(private val listener: Listener) {
}
else -> {
val encoded = Uri.encode(term)
listener.request("$SEARCH_BASE$encoded")
listener.request("$searchbase$encoded")
}
}
}
fun search(term: String){
fun search(term: String, searchbase: String?){
val encoded = Uri.encode(term)
listener.request("$SEARCH_BASE$encoded")
listener.request("$searchbase$encoded")
}
@ -51,21 +54,27 @@ class OmniTerm(private val listener: Listener) {
*/
private fun navigation(link: String, invokeListener: Boolean) {
when {
link.startsWith("http") -> listener.openBrowser(link)
link.startsWith(GEM_SCHEME) -> uri.set(link)
link.startsWith("//") -> uri.set("gemini:$link")
link.startsWith("http://") or link.startsWith("https://") -> {
uri.set(link)
}
link.contains(":") -> listener.openExternal(link)
else -> uri.resolve(link)
}
//todo - fix this, the double slash fix breaks the scheme, so this hack puts it back... uggh
val address = uri.toString().replace("%2F", "/").replace("//", "/").replace("gemini:/", "gemini://")
println("OmniTerm resolved address: $address")
val address = uri.toString().replace("//", "/").replace(":/", "://")
if(invokeListener) listener.request(address)
println("OmniTerm resolved address: $address")
}
fun traverse(address: String): String {
return OppenURI(address).traverse().toString()
fun getGlobalUri(reference: String): String {
return when {
reference.contains(":") -> reference
reference.startsWith("//") -> "gemini:$reference"
else -> uri.resolve(reference, false)
}
}
fun reset(){
@ -81,7 +90,11 @@ class OmniTerm(private val listener: Listener) {
}
fun getCurrent(): String {
return history.last().toString()
return if(history.size > 0){
history.last().toString()
}else{
""
}
}
fun canGoBack(): Boolean {
@ -93,8 +106,12 @@ class OmniTerm(private val listener: Listener) {
return history.last().toString()
}
fun clearCache() {
history.clear()
}
interface Listener{
fun request(address: String)
fun openBrowser(address: String)
fun openExternal(address: String)
}
}

View File

@ -1,9 +1,12 @@
package corewala.buran
const val SCHEME = "gemini://"
import corewala.toURI
const val GEMSCHEME = "gemini://"
const val TRAVERSE = "../"
const val SOLIDUS = "/"
const val DIREND = "/"
const val QUERY = "?"
/**
*
@ -15,68 +18,65 @@ class OppenURI constructor(private var ouri: String) {
constructor(): this("")
var host: String = ""
var scheme: String = ""
init {
extractHost()
if(ouri.isNotEmpty()){
host = ouri.toURI().host
scheme = ouri.toURI().scheme
}
}
fun set(ouri: String){
this.ouri = ouri
extractHost()
if(ouri.isNotEmpty()){
host = ouri.toURI().host
scheme = ouri.toURI().scheme
}
}
fun resolve(reference: String) {
if(ouri == "$SCHEME$host") ouri = "$ouri/"
return when {
reference.startsWith(SCHEME) -> set(reference)
reference.startsWith(SOLIDUS) -> ouri = "$SCHEME$host$reference"
fun resolve(reference: String, persistent: Boolean): String{
if(ouri == "$GEMSCHEME$host") ouri = "$ouri/"
var resolvedUri = ""
when {
reference.startsWith(GEMSCHEME) -> set(reference)
reference.startsWith(SOLIDUS) -> resolvedUri = "$scheme://$host$reference"
reference.startsWith(TRAVERSE) -> {
if(!ouri.endsWith(DIREND)) ouri = ouri.removeFile()
if(!ouri.endsWith(DIREND)) resolvedUri = ouri.removeFile()
val traversalCount = reference.split(TRAVERSE).size - 1
ouri = traverse(traversalCount) + reference.replace(TRAVERSE, "")
resolvedUri = traverse(traversalCount) + reference.replace(TRAVERSE, "")
}
reference.startsWith(QUERY) -> {
resolvedUri = if(reference.contains(QUERY)){
ouri.substringBefore(QUERY) + reference
}else{
ouri + reference
}
}
else -> {
ouri = when {
ouri.endsWith(DIREND) -> "${ouri}$reference"
resolvedUri = when {
ouri.endsWith(DIREND) -> {
"${ouri}$reference"
}
else -> "${ouri.substring(0, ouri.lastIndexOf("/"))}/$reference"
}
}
}
if(persistent){
ouri = resolvedUri
}
return resolvedUri
}
fun traverse(): OppenURI{
val path = ouri.removePrefix("$SCHEME$host")
val segments = path.split(SOLIDUS).filter { it.isNotEmpty() }
var nouri = "$SCHEME$host"
when (ouri) {
"" -> {
}
SCHEME -> ouri = ""
"$nouri/" -> ouri = SCHEME
else -> {
when {
segments.isNotEmpty() -> {
val remaining = segments.dropLast(1)
remaining.forEach { segment ->
nouri += "/$segment"
}
ouri = "$nouri/"
}
else -> ouri = "$nouri/"
}
}
}
return this
fun resolve(reference: String): String{
return resolve(reference, true)
}
private fun traverse(count: Int): String{
val path = ouri.removePrefix("$SCHEME$host")
val path = ouri.removePrefix("$GEMSCHEME$host")
val segments = path.split(SOLIDUS).filter { it.isNotEmpty() }
val segmentCount = segments.size
var nouri = "$SCHEME$host"
var nouri = "$GEMSCHEME$host"
segments.forEachIndexed{ index, segment ->
if(index < segmentCount - count){
@ -88,15 +88,6 @@ class OppenURI constructor(private var ouri: String) {
}
private fun extractHost(){
if(ouri.isEmpty()) return
val urn = ouri.removePrefix(SCHEME)
host = when {
urn.contains(SOLIDUS) -> urn.substring(0, urn.indexOf(SOLIDUS))
else -> urn
}
}
fun copy(): OppenURI = OppenURI(ouri)
override fun toString(): String = ouri

View File

@ -8,17 +8,16 @@ sealed class GemState {
data class AppQuery(val uri: URI): GemState()
data class Requesting(val uri: URI): GemState()
data class NotGeminiRequest(val uri: URI) : GemState()
data class ResponseGemtext(val uri: URI, val header: GeminiResponse.Header, val lines: List<String>) : GemState()
data class ResponseInput(val uri: URI, val header: GeminiResponse.Header) : GemState()
data class Redirect(val uri: URI, val header: GeminiResponse.Header) : GemState()
data class ResponseText(val uri: URI, val header: GeminiResponse.Header, val content: String) : GemState()
data class ResponseImage(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : GemState()
data class ResponseBinary(val uri: URI, val header: GeminiResponse.Header, val cacheUri: Uri) : GemState()
data class ResponseUnknownMime(val uri: URI, val header: GeminiResponse.Header) : GemState()
data class ResponseError(val header: GeminiResponse.Header): GemState()
data class ResponseUnknownHost(val uri: URI): GemState()
data class ClientCertRequired(val uri: URI, val header: GeminiResponse.Header): GemState()
data class ClientCertError(val header: GeminiResponse.Header): GemState()
object Blank: GemState()
}

View File

@ -40,7 +40,6 @@ class BuranHistory(private val db: BuranAbstractDatabase): HistoryDatasource {
override fun add(uri: Uri, onAdded: () -> Unit) {
if(!uri.toString().startsWith("gemini://")){
onAdded
return
}
GlobalScope.launch(Dispatchers.IO){

View File

@ -6,11 +6,10 @@ import corewala.buran.io.database.history.BuranHistory
import java.net.URI
interface Datasource {
fun request(address: String, onUpdate: (state: GemState) -> Unit)
fun request(address: String, forceDownload: Boolean, onUpdate: (state: GemState) -> Unit)
fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, alternativeRequest: String?, onUpdate: (state: GemState) -> Unit)
fun isRequesting(): Boolean
fun cancel()
fun canGoBack(): Boolean
fun goBack(onUpdate: (state: GemState) -> Unit)
companion object{
fun factory(context: Context, history: BuranHistory): Datasource {
return GeminiDatasource(context, history)

View File

@ -2,16 +2,15 @@ package corewala.buran.io.gemini
import android.content.Context
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import corewala.buran.Buran
import corewala.buran.OppenURI
import corewala.buran.io.GemState
import corewala.buran.io.database.history.BuranHistory
import corewala.buran.io.keymanager.BuranKeyManager
import corewala.toURI
import corewala.toUri
import java.io.*
import java.lang.IllegalStateException
import java.net.ConnectException
import java.net.URI
import java.net.UnknownHostException
@ -20,7 +19,6 @@ import javax.net.ssl.*
class GeminiDatasource(private val context: Context, val history: BuranHistory): Datasource {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val runtimeHistory = mutableListOf<URI>()
private var forceDownload = false
@ -32,61 +30,78 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
private var socketFactory: SSLSocketFactory? = null
override fun request(address: String, forceDownload: Boolean, onUpdate: (state: GemState) -> Unit) {
this.forceDownload = forceDownload
request(address, onUpdate)
}
private var currentRequestAddress: String? = null
override fun request(address: String, forceDownload: Boolean, clientCertPassword: String?, alternativeRequest: String?, onUpdate: (state: GemState) -> Unit){
this.forceDownload = forceDownload
override fun request(address: String, onUpdate: (state: GemState) -> Unit) {
this.onUpdate = onUpdate
val uri = URI.create(address)
onUpdate(GemState.Requesting(uri))
if(address.startsWith("gemini://")){
currentRequestAddress = address
}
GlobalScope.launch {
geminiRequest(uri, onUpdate)
geminiRequest(uri, onUpdate, clientCertPassword, alternativeRequest)
}
}
private fun initSSLFactory(protocol: String){
override fun isRequesting(): Boolean{
return !currentRequestAddress.isNullOrEmpty()
}
override fun cancel(){
currentRequestAddress = null
}
private fun initSSLFactory(protocol: String, clientCertPassword: String?){
val sslContext = when (protocol) {
"TLS_ALL" -> SSLContext.getInstance("TLS")
else -> SSLContext.getInstance(protocol)
}
sslContext.init(buranKeyManager.getFactory()?.keyManagers, DummyTrustManager.get(), null)
sslContext.init(buranKeyManager.getFactory(clientCertPassword)?.keyManagers, DummyTrustManager.get(), null)
socketFactory = sslContext.socketFactory
}
private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> Unit){
private fun geminiRequest(uri: URI, onUpdate: (state: GemState) -> Unit, clientCertPassword: String?, alternativeRequest: String?){
val protocol = "TLS"
val useClientCert = prefs.getBoolean(Buran.PREF_KEY_CLIENT_CERT_ACTIVE, false)
//Update factory if operating mode has changed
when {
socketFactory == null -> initSSLFactory(protocol!!)
useClientCert && !buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!)
!useClientCert && buranKeyManager.lastCallUsedKey -> initSSLFactory(protocol!!)
initSSLFactory(protocol, clientCertPassword)
val port = if(uri.port != -1){
uri.port
}else{
1965
}
val socket: SSLSocket?
try {
socket = socketFactory?.createSocket(uri.host, 1965) as SSLSocket
socket = socketFactory?.createSocket(uri.host, port) as SSLSocket
println("Buran socket handshake with ${uri.host}")
socket.startHandshake()
}catch (uhe: UnknownHostException){
println("Buran socket error, unknown host: $uhe")
onUpdate(GemState.ResponseUnknownHost(uri))
if(currentRequestAddress == uri.toString()) {
println("Buran socket error, unknown host: $uhe")
onUpdate(GemState.ResponseUnknownHost(uri))
}
return
}catch (ce: ConnectException){
println("Buran socket error, connect exception: $ce")
onUpdate(GemState.ResponseError(GeminiResponse.Header(-1, ce.message ?: ce.toString())))
if(currentRequestAddress == uri.toString()) {
println("Buran socket error, connect exception: $ce")
onUpdate(GemState.ResponseError(GeminiResponse.Header(-1, ce.message ?: ce.toString())))
}
return
}catch (she: SSLHandshakeException){
println("Buran socket error, ssl handshake exception: $she")
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, she.message ?: she.toString())))
if(currentRequestAddress == uri.toString()) {
println("Buran socket error, ssl handshake exception: $she")
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, she.message ?: she.toString())))
}
return
}
@ -95,7 +110,12 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
val bufferedWriter = BufferedWriter(outputStreamWriter)
val outWriter = PrintWriter(bufferedWriter)
val requestEntity = uri.toString() + "\r\n"
val requestEntity = if(alternativeRequest.isNullOrEmpty()){
uri.toString()
}else{
alternativeRequest
} + "\r\n"
println("Buran socket requesting $requestEntity")
outWriter.print(requestEntity)
outWriter.flush()
@ -113,30 +133,38 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
val bufferedReader = BufferedReader(headerInputReader)
val headerLine = bufferedReader.readLine()
println("Buran: response header: $headerLine")
println("Buran response header: $headerLine")
if(headerLine == null){
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, "Server did not respond with a Gemini header: $uri")))
if(currentRequestAddress == uri.toString()){
onUpdate(GemState.ResponseError(GeminiResponse.Header(-2, "Server did not respond with a Gemini header: $uri")))
}
return
}
val header = GeminiResponse.parseHeader(headerLine)
when {
header.code == GeminiResponse.INPUT -> onUpdate(GemState.ResponseInput(uri, header))
header.code == GeminiResponse.REDIRECT -> request(URI.create(header.meta).toString(), onUpdate)
header.code != GeminiResponse.SUCCESS -> onUpdate(GemState.ResponseError(header))
header.meta.startsWith("text/gemini") -> getGemtext(bufferedReader, uri, header, onUpdate)
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
else -> {
//File served over Gemini but not handled in-app, eg .pdf
if(forceDownload){
getBinary(socket, uri, header, onUpdate)
}else{
onUpdate(GemState.ResponseUnknownMime(uri, header))
if(currentRequestAddress == uri.toString()){
currentRequestAddress = null
when {
header.code == GeminiResponse.INPUT -> onUpdate(GemState.ResponseInput(uri, header))
header.code == GeminiResponse.REDIRECT -> onUpdate(GemState.Redirect(uri, header))
header.code == GeminiResponse.CLIENT_CERTIFICATE_REQUIRED -> onUpdate(GemState.ClientCertRequired(uri, header))
header.code != GeminiResponse.SUCCESS -> onUpdate(GemState.ResponseError(header))
header.meta.startsWith("text/gemini") -> getGemtext(bufferedReader, requestEntity.trim().toURI(), header, onUpdate)
header.meta.startsWith("text/") -> getString(socket, uri, header, onUpdate)
header.meta.startsWith("image/") -> getBinary(socket, uri, header, onUpdate)
else -> {
//File served over Gemini but not handled in-app, eg .pdf
if(forceDownload){
getBinary(socket, uri, header, onUpdate)
}else{
onUpdate(GemState.ResponseUnknownMime(uri, header))
}
}
}
}else{
println("Buran dropped response from $uri: request cancelled or superseded")
}
//Close input
@ -159,10 +187,6 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
val processed = GemtextHelper.findCodeBlocks(lines)
when {
!uri.toString().startsWith("gemini://") -> throw IllegalStateException("Not a Gemini Uri")
}
updateHistory(uri)
onUpdate(GemState.ResponseGemtext(uri, header, processed))
}
@ -186,12 +210,10 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
private fun getBinary(socket: SSLSocket?, uri: URI, header: GeminiResponse.Header, onUpdate: (state: GemState) -> Unit){
var filename: String? = null
val fileSegmentIndex: Int = uri.path.lastIndexOf('/')
when {
fileSegmentIndex != -1 -> filename = uri.path.substring(fileSegmentIndex + 1)
}
val charset = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789"
val filename = (1..12)
.map{charset.random()}
.joinToString("")
val host = uri.host.replace(".", "_")
val cacheName = "${host}_$filename"
@ -223,11 +245,6 @@ class GeminiDatasource(private val context: Context, val history: BuranHistory):
override fun canGoBack(): Boolean = runtimeHistory.isEmpty() || runtimeHistory.size > 1
override fun goBack(onUpdate: (state: GemState) -> Unit) {
runtimeHistory.removeLast()
request(runtimeHistory.last().toString(), onUpdate)
}
//This just forces the factory to rebuild before the next request
fun invalidate() {
socketFactory = null

View File

@ -0,0 +1,107 @@
package corewala.buran.io.keymanager
import android.content.Context
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import corewala.buran.Buran
import corewala.buran.R
import java.nio.charset.Charset
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
data class EncryptedData(val ciphertext: ByteArray, val initializationVector: ByteArray)
@RequiresApi(Build.VERSION_CODES.P)
class BuranBiometricManager {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
fun createBiometricPrompt(context: Context, fragment: Fragment?, activity: FragmentActivity?, callback: BiometricPrompt.AuthenticationCallback){
val executor = ContextCompat.getMainExecutor(context)
if(fragment != null){
biometricPrompt = BiometricPrompt(fragment, executor, callback)
}else if(activity != null){
biometricPrompt = BiometricPrompt(activity, executor, callback)
}
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setConfirmationRequired(false)
.setTitle(context.getString(R.string.confirm_your_identity))
.setSubtitle(context.getString(R.string.use_biometric_unlock))
.setNegativeButtonText(context.getString(R.string.cancel).toUpperCase())
.build()
}
fun authenticateToEncryptData() {
val cipher = getCipher()
val secretKey = getSecretKey(Buran.CLIENT_CERT_PASSWORD_SECRET_KEY_NAME)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
fun authenticateToDecryptData(initializationVector: ByteArray) {
val cipher = getCipher()
val secretKey = getSecretKey(Buran.CLIENT_CERT_PASSWORD_SECRET_KEY_NAME)
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, initializationVector))
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
// Allows ByteArrays to be stored in prefs as strings. Possibly the most horrifying function I've ever written.
fun decodeByteArray(encodedByteArray: String): ByteArray{
val byteList = encodedByteArray.substring(1, encodedByteArray.length - 1).split(", ")
var decodedByteArray = byteArrayOf()
for(byte in byteList){
decodedByteArray += byte.toInt().toByte()
}
println(decodedByteArray.contentToString())
return decodedByteArray
}
fun encryptData(plaintext: String, cipher: Cipher): EncryptedData {
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charset.forName("UTF-8")))
return EncryptedData(ciphertext,cipher.iv)
}
fun decryptData(ciphertext: ByteArray, cipher: Cipher): String {
val plaintext = cipher.doFinal(ciphertext)
return String(plaintext, Charset.forName("UTF-8"))
}
private fun getCipher(): Cipher {
val transformation = "${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/${KeyProperties.ENCRYPTION_PADDING_NONE}"
return Cipher.getInstance(transformation)
}
private fun getSecretKey(keyName: String): SecretKey {
val androidKeystore = "AndroidKeyStore"
val keyStore = KeyStore.getInstance(androidKeystore)
keyStore.load(null)
keyStore.getKey(keyName, null)?.let { return it as SecretKey }
val keyGenParams = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
setKeySize(256)
setUserAuthenticationRequired(true)
}.build()
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
androidKeystore
)
keyGenerator.init(keyGenParams)
return keyGenerator.generateKey()
}
}

View File

@ -5,7 +5,6 @@ import android.content.SharedPreferences
import android.net.Uri
import androidx.preference.PreferenceManager
import corewala.buran.Buran
import corewala.buran.R
import java.io.FileNotFoundException
import java.io.IOException
import java.security.KeyStore
@ -19,23 +18,20 @@ class BuranKeyManager(val context: Context, val onKeyError: (error: String) -> U
var lastCallUsedKey = false
//If the user has a key loaded load it here - or else return null
fun getFactory(): KeyManagerFactory? {
val isClientCertActive = prefs.getBoolean(Buran.PREF_KEY_CLIENT_CERT_ACTIVE, false)
return when {
isClientCertActive -> {
fun getFactory(clientCertPassword: String?): KeyManagerFactory? {
return when { !clientCertPassword.isNullOrEmpty() -> {
lastCallUsedKey = true
val keyStore: KeyStore = KeyStore.getInstance("pkcs12")
val uriStr = prefs.getString(Buran.PREF_KEY_CLIENT_CERT_URI, "")
val password = prefs.getString(Buran.PREF_KEY_CLIENT_CERT_PASSWORD, "")
val uri = Uri.parse(uriStr)
try {
context.contentResolver?.openInputStream(uri)?.use {
try {
keyStore.load(it, password?.toCharArray())
keyStore.load(it, clientCertPassword.toCharArray())
val keyManagerFactory: KeyManagerFactory =
KeyManagerFactory.getInstance("X509")
keyManagerFactory.init(keyStore, password?.toCharArray())
keyManagerFactory.init(keyStore, clientCertPassword.toCharArray())
return@use keyManagerFactory
} catch (ioe: IOException) {
onKeyError("${ioe.message}")
@ -53,15 +49,4 @@ class BuranKeyManager(val context: Context, val onKeyError: (error: String) -> U
}
}
}
//Working example with cert packaged with app
fun getFactoryDemo(context: Context): KeyManagerFactory? {
val keyStore: KeyStore = KeyStore.getInstance("pkcs12")
keyStore.load(context.resources.openRawResource(R.raw.cert), "PASSWORD".toCharArray())
val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("X509")
keyManagerFactory.init(keyStore, "PASSWORD".toCharArray())
return keyManagerFactory
}
}

View File

@ -0,0 +1,91 @@
package corewala.buran.io.update
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Context.DOWNLOAD_SERVICE
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.content.FileProvider
import corewala.buran.BuildConfig
import corewala.buran.R
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
class BuranUpdates {
fun getLatestVersion(): String {
var latestVersion = BuildConfig.VERSION_NAME
val updateCheckThread = Thread {
val url = "https://github.com/Corewala/Buran/releases/latest"
val con = URL(url).openConnection() as HttpURLConnection
con.connect()
con.getInputStream()
latestVersion = con.getURL().toString().drop(47)
}
updateCheckThread.start()
updateCheckThread.join()
println("Latest version: $latestVersion")
return latestVersion
}
fun installUpdate(context: Context, latestVersion: String){
val updateUrl = "https://github.com/Corewala/Buran/releases/download/$latestVersion/Buran-$latestVersion.apk"
var updateDestination = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/Buran.apk"
val fileUri = Uri.parse("file://$updateDestination")
val packageFile = File(updateDestination)
if(packageFile.exists()){
packageFile.delete()
}
val downloadManager = context.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
val downloadUri = Uri.parse(updateUrl)
val request = DownloadManager.Request(downloadUri)
request.setTitle(context.getString(R.string.app_name))
request.setDescription("")
request.setDestinationUri(fileUri)
request.setMimeType("application/vnd.android.package-archive")
val contentUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
packageFile
)
val updateDownloadReceiver: BroadcastReceiver = object : BroadcastReceiver(){
override fun onReceive(context: Context, intent: Intent){
val intent = Intent(Intent.ACTION_VIEW)
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
intent.data = contentUri
} else {
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.setDataAndType(
fileUri,
"application/vnd.android.package-archive"
)
}
println("Installing update")
context.startActivity(intent)
context.unregisterReceiver(this)
}
}
context.registerReceiver(updateDownloadReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
downloadManager.enqueue(request)
}
}

View File

@ -6,19 +6,30 @@ import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.biometric.BiometricPrompt
import androidx.browser.customtabs.CustomTabsIntent
import androidx.databinding.DataBindingUtil
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import corewala.*
import corewala.buran.BuildConfig
import corewala.buran.Buran
import corewala.buran.OmniTerm
import corewala.buran.R
@ -28,14 +39,14 @@ import corewala.buran.io.database.BuranDatabase
import corewala.buran.io.database.bookmarks.BookmarksDatasource
import corewala.buran.io.gemini.Datasource
import corewala.buran.io.gemini.GeminiResponse
import corewala.buran.io.keymanager.BuranBiometricManager
import corewala.buran.io.update.BuranUpdates
import corewala.buran.ui.bookmarks.BookmarkDialog
import corewala.buran.ui.bookmarks.BookmarksDialog
import corewala.buran.ui.content_image.ImageDialog
import corewala.buran.ui.content_text.TextDialog
import corewala.buran.ui.gemtext_adapter.*
import corewala.buran.ui.modals_menus.about.AboutDialog
import corewala.buran.ui.gemtext_adapter.AbstractGemtextAdapter
import corewala.buran.ui.modals_menus.history.HistoryDialog
import corewala.buran.ui.modals_menus.input.InputDialog
import corewala.buran.ui.modals_menus.overflow.OverflowPopup
import corewala.buran.ui.settings.SettingsActivity
import java.io.File
@ -43,6 +54,7 @@ import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.URI
const val CREATE_IMAGE_FILE_REQ = 628
const val CREATE_BINARY_FILE_REQ = 630
const val CREATE_BOOKMARK_EXPORT_FILE_REQ = 631
@ -53,6 +65,7 @@ class GemActivity : AppCompatActivity() {
lateinit var prefs: SharedPreferences
private var inSearch = false
private lateinit var bookmarkDatasource: BookmarksDatasource
private lateinit var db: BuranDatabase
private var bookmarksDialog: BookmarksDialog? = null
private val model by viewModels<GemViewModel>()
@ -60,23 +73,31 @@ class GemActivity : AppCompatActivity() {
private val omniTerm = OmniTerm(object : OmniTerm.Listener {
override fun request(address: String) {
loadingView(true)
model.request(address)
gemRequest(address)
}
override fun openBrowser(address: String) = openWebLink(address)
override fun openExternal(address: String) = openExternalLink(address)
})
lateinit var adapter: AbstractGemtextAdapter
private var certPassword: String? = null
private val onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit = { uri, longTap, position: Int ->
private var proxiedAddress: String? = null
private var previousPosition: Int = 0
private var initialised: Boolean = false
private var goingBack: Boolean = false
private lateinit var adapter: AbstractGemtextAdapter
private lateinit var home: String
private lateinit var searchBase: String
private val onLink: (link: URI, longTap: Boolean, adapterPosition: Int) -> Unit = { uri, longTap, _: Int ->
if(longTap){
var globalURI: String
if(!uri.toString().contains("//")){
globalURI = (omniTerm.getCurrent() + uri.toString()).replace("%2F", "/").replace("//", "/").replace("gemini:/", "gemini://")
} else {
globalURI = uri.toString()
}
val globalURI = omniTerm.getGlobalUri(uri.toString())
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, globalURI)
@ -89,19 +110,26 @@ class GemActivity : AppCompatActivity() {
binding.addressEdit.hint = getString(R.string.main_input_hint)
inSearch = false
}
omniTerm.navigation(uri.toString())
}
}
private val inlineImage: (link: URI, adapterPosition: Int) -> Unit = { uri, position: Int ->
omniTerm.imageAddress(uri.toString())
omniTerm.uri.let{
model.requestInlineImage(URI.create(it.toString())){ imageUri ->
imageUri?.let{
runOnUiThread {
loadImage(position, imageUri)
loadingView(false)
if(getInternetStatus()){
omniTerm.imageAddress(uri.toString())
val clientCertPassword = if(isHostSigned(uri)){
certPassword
}else{
null
}
omniTerm.uri.let{
model.requestInlineImage(URI.create(it.toString()), clientCertPassword){ imageUri ->
imageUri?.let{
runOnUiThread {
loadImage(position, imageUri)
loadingView(false)
}
}
}
}
@ -115,9 +143,6 @@ class GemActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val db = BuranDatabase(applicationContext)
bookmarkDatasource = db.bookmarks()
binding = DataBindingUtil.setContentView(this, R.layout.activity_gem)
binding.viewmodel = model
binding.lifecycleOwner = this
@ -128,25 +153,62 @@ class GemActivity : AppCompatActivity() {
prefs = PreferenceManager.getDefaultSharedPreferences(this)
when (prefs.getString("theme", "theme_FollowSystem")) {
"theme_FollowSystem" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"theme_Light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"theme_Dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
adapter = AbstractGemtextAdapter.getAdapter(onLink, inlineImage)
binding.gemtextRecycler.adapter = adapter
model.initialise(
home = prefs.getString(
home = prefs.getString(
"home_capsule",
Buran.DEFAULT_HOME_CAPSULE
) ?: Buran.DEFAULT_HOME_CAPSULE
if(
!home.startsWith("gemini://")
or home.contains(" ")
or !home.contains(".")
){
home = ""
}
searchBase = prefs.getString(
"search_base",
Buran.DEFAULT_SEARCH_BASE
) ?: Buran.DEFAULT_SEARCH_BASE
if(
!searchBase.startsWith("gemini://")
or searchBase.contains(" ")
or !searchBase.contains(".")
or !searchBase.endsWith("?")
){
searchBase = Buran.DEFAULT_SEARCH_BASE
}
if(getInternetStatus()){
initialise()
}else{
loadingView(false)
val home = PreferenceManager.getDefaultSharedPreferences(this).getString(
"home_capsule",
Buran.DEFAULT_HOME_CAPSULE
) ?: Buran.DEFAULT_HOME_CAPSULE,
gemini = Datasource.factory(this, db.history()),
db = db,
onState = this::handleState
)
)
val title = "# ${this.getString(R.string.no_internet)}"
val text = this.getString(R.string.retry)
omniTerm.set(home!!)
adapter.render(listOf(title, text))
binding.addressEdit.inputType = InputType.TYPE_NULL
}
binding.addressEdit.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_GO -> {
omniTerm.input(binding.addressEdit.text.toString().trim())
binding.addressEdit.hideKeyboard()
omniTerm.input(binding.addressEdit.text.toString().trim(), searchBase)
binding.addressEdit.clearFocus()
return@setOnEditorActionListener true
}
@ -155,14 +217,12 @@ class GemActivity : AppCompatActivity() {
}
binding.addressEdit.setOnClickListener {
binding.addressEdit.clearFocus()
binding.addressEdit.showKeyboard()
binding.addressEdit.requestFocus()
}
binding.addressEdit.setOnFocusChangeListener { v, hasFocus ->
binding.addressEdit.setOnFocusChangeListener { _, hasFocus ->
var addressPaddingRight = resources.getDimensionPixelSize(R.dimen.def_address_right_margin)
val addressPaddingRight = resources.getDimensionPixelSize(R.dimen.def_address_right_margin)
if(!hasFocus) {
binding.addressEdit.hideKeyboard()
@ -185,6 +245,25 @@ class GemActivity : AppCompatActivity() {
binding.addressEdit.requestFocus()
inSearch = true
}
R.id.overflow_menu_sign -> {
if (prefs.getBoolean("use_biometrics", false) and certPassword.isNullOrEmpty()) {
biometricSecureRequest(binding.addressEdit.text.toString())
}else if(certPassword.isNullOrEmpty()){
if (certPassword.isNullOrEmpty()) {
certPassword = prefs.getString(
Buran.PREF_KEY_CLIENT_CERT_PASSWORD,
null
)
}
refresh()
Toast.makeText(this, this.getString(R.string.cert_loaded), Toast.LENGTH_SHORT).show()
}else{
certPassword = null
refresh()
Toast.makeText(this, this.getString(R.string.cert_unloaded), Toast.LENGTH_SHORT).show()
}
}
R.id.overflow_menu_bookmark -> {
val name = adapter.inferTitle()
BookmarkDialog(
@ -198,7 +277,7 @@ class GemActivity : AppCompatActivity() {
}
R.id.overflow_menu_bookmarks -> {
bookmarksDialog = BookmarksDialog(this, bookmarkDatasource) { bookmark ->
model.request(bookmark.uri.toString())
gemRequest(bookmark.uri.toString())
}
bookmarksDialog?.show()
}
@ -212,29 +291,41 @@ class GemActivity : AppCompatActivity() {
}
R.id.overflow_menu_history -> HistoryDialog.show(
this,
db.history()
db.history(),
omniTerm
) { historyAddress ->
model.request(historyAddress)
gemRequest(historyAddress)
}
R.id.overflow_menu_about -> AboutDialog.show(this)
R.id.overflow_menu_about -> gemRequest("")
R.id.overflow_menu_settings -> {
startActivity(Intent(this, SettingsActivity::class.java))
}
}
}
if(!prefs.getString(Buran.PREF_KEY_CLIENT_CERT_URI, null).isNullOrEmpty()){
OverflowPopup.setItemVisibility(R.id.overflow_menu_sign, true)
if(certPassword.isNullOrEmpty()){
OverflowPopup.setItemTitle(R.id.overflow_menu_sign, getString(R.string.load_cert))
}else{
OverflowPopup.setItemTitle(R.id.overflow_menu_sign, getString(R.string.unload_cert))
}
}else{
OverflowPopup.setItemVisibility(R.id.overflow_menu_sign, false)
}
}
binding.home.setOnClickListener {
val home = PreferenceManager.getDefaultSharedPreferences(this).getString(
"home_capsule",
Buran.DEFAULT_HOME_CAPSULE
)
omniTerm.history.clear()
model.request(home!!)
gemRequest(home, false)
}
binding.pullToRefresh.setOnRefreshListener {
refresh()
if(getInternetStatus()){
refresh()
}else{
binding.pullToRefresh.isRefreshing = false
Snackbar.make(binding.root, getString(R.string.no_internet), Snackbar.LENGTH_LONG).show()
}
}
checkIntentExtras(intent)
@ -244,13 +335,40 @@ class GemActivity : AppCompatActivity() {
omniTerm.getCurrent().run{
binding.addressEdit.setText(this)
focusEnd()
model.request(this)
gemRequest(this)
}
}
override fun onResume() {
super.onResume()
home = prefs.getString(
"home_capsule",
Buran.DEFAULT_HOME_CAPSULE
) ?: Buran.DEFAULT_HOME_CAPSULE
if(
!home.startsWith("gemini://")
or home.contains(" ")
or !home.contains(".")
){
home = ""
}
searchBase = prefs.getString(
"search_base",
Buran.DEFAULT_SEARCH_BASE
) ?: Buran.DEFAULT_SEARCH_BASE
if(
!searchBase.startsWith("gemini://")
or searchBase.contains(" ")
or !searchBase.contains(".")
or !searchBase.endsWith("?")
){
searchBase = Buran.DEFAULT_SEARCH_BASE
}
when {
prefs.contains("background_colour") -> {
when (val backgroundColor = prefs.getString("background_colour", "#XXXXXX")) {
@ -260,22 +378,6 @@ class GemActivity : AppCompatActivity() {
}
}
when {
prefs.getBoolean(
Buran.PREF_KEY_CLIENT_CERT_ACTIVE,
false
) -> {
binding.addressEdit.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.vector_client_cert,
0,
0,
0
)
binding.addressEdit.compoundDrawablePadding = 6.toPx().toInt()
}
else -> hideClientCertShield()
}
val showInlineIcons = prefs.getBoolean(
"show_inline_icons",
true
@ -284,10 +386,16 @@ class GemActivity : AppCompatActivity() {
val showLinkButtons = prefs.getBoolean(
"show_link_buttons",
true
false
)
adapter.linkButtons(showLinkButtons)
val useAttentionGuides = prefs.getBoolean(
"use_attention_guides",
false
)
adapter.attentionGuides(useAttentionGuides)
val showInlineImages = prefs.getBoolean(
"show_inline_images",
@ -295,11 +403,32 @@ class GemActivity : AppCompatActivity() {
)
adapter.inlineImages(showInlineImages)
model.invalidateDatasource()
if(getInternetStatus()){
model.invalidateDatasource()
}
}
private fun hideClientCertShield(){
binding.addressEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
private fun updateClientCertIcon(){
if (!prefs.getString(
Buran.PREF_KEY_CLIENT_CERT_URI,
null
).isNullOrEmpty()){
if(certPassword.isNullOrEmpty()){
binding.addressEdit.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
0,
0
)
}else{
binding.addressEdit.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.vector_client_cert,
0,
0,
0
)
}
}
}
private fun handleState(state: GemState) {
@ -307,23 +436,88 @@ class GemActivity : AppCompatActivity() {
when (state) {
is GemState.AppQuery -> runOnUiThread { showAlert("App backdoor/query not implemented yet") }
is GemState.ResponseInput -> runOnUiThread {
val builder = AlertDialog.Builder(this, R.style.AppDialogTheme)
val inflater: LayoutInflater = layoutInflater
val dialogLayout: View = inflater.inflate(R.layout.dialog_input_query, null)
val editText: EditText = dialogLayout.findViewById(R.id.query_input)
editText.requestFocus()
editText.showKeyboard()
loadingView(false)
InputDialog.show(this, state) { queryAddress ->
model.request(queryAddress)
builder
.setTitle(state.header.meta)
.setPositiveButton(getString(R.string.confirm).toUpperCase()){ _, _ ->
gemRequest("${state.uri}?${Uri.encode(editText.text.toString())}")
editText.hideKeyboard()
}
.setNegativeButton(getString(R.string.cancel).toUpperCase()){ _, _ ->
editText.hideKeyboard()
}
.setView(dialogLayout)
.show()
}
is GemState.Redirect -> {
omniTerm.set(state.uri.toString())
gemRequest(omniTerm.getGlobalUri(state.header.meta))
}
is GemState.ClientCertRequired -> runOnUiThread {
loadingView(false)
val builder = AlertDialog.Builder(this, R.style.AppDialogTheme)
builder
.setTitle(getString(R.string.client_certificate_required))
.setMessage(state.header.meta)
if(!prefs.getString(Buran.PREF_KEY_CLIENT_CERT_URI, null).isNullOrEmpty()){
builder
.setPositiveButton(getString(R.string.use_client_certificate).toUpperCase()) { _, _ ->
if(prefs.getBoolean("use_biometrics", false) and certPassword.isNullOrEmpty()){
biometricSecureRequest(state.uri.toString())
}else{
if(certPassword.isNullOrEmpty()){
certPassword = prefs.getString(
Buran.PREF_KEY_CLIENT_CERT_PASSWORD,
null
)
}
gemRequest(state.uri.toString(), true)
Toast.makeText(this, this.getString(R.string.cert_loaded), Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton(getString(R.string.cancel).toUpperCase()) { _, _ -> }
.show()
}else{
builder
.setNegativeButton(getString(R.string.close).toUpperCase()) { _, _ -> }
.show()
}
}
is GemState.Requesting -> loadingView(true)
is GemState.NotGeminiRequest -> externalProtocol(state)
is GemState.Requesting -> {
loadingView(true)
}
is GemState.NotGeminiRequest -> {
externalProtocol(state.uri)
}
is GemState.ResponseError -> {
omniTerm.reset()
showAlert("${GeminiResponse.getCodeString(state.header.code)}:\n\n${state.header.meta}")
}
is GemState.ClientCertError -> {
hideClientCertShield()
certPassword = null
updateClientCertIcon()
showAlert("${GeminiResponse.getCodeString(state.header.code)}:\n\n${state.header.meta}")
}
is GemState.ResponseGemtext -> renderGemtext(state)
is GemState.ResponseGemtext -> {
if(state.uri.scheme != "gemini"){
Snackbar.make(binding.root, getString(R.string.proxied_content), Snackbar.LENGTH_LONG).setAction(getString(R.string.open_original)) {
externalProtocol(state.uri)
}.show()
}
renderGemtext(state)
}
is GemState.ResponseText -> renderText(state)
is GemState.ResponseImage -> renderImage(state)
is GemState.ResponseBinary -> renderBinary(state)
@ -337,28 +531,38 @@ class GemActivity : AppCompatActivity() {
val download = getString(R.string.download)
AlertDialog.Builder(this, R.style.AppDialogTheme)
.setTitle("$download: ${state.header.meta}")
.setMessage("${state.uri}")
.setPositiveButton(getString(R.string.download)) { _, _ ->
loadingView(true)
model.requestBinaryDownload(state.uri)
if(getInternetStatus()) {
val clientCertPassword = if(isHostSigned(state.uri)){
certPassword
}else{
null
}
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
.show()
AlertDialog.Builder(this, R.style.AppDialogTheme)
.setTitle("$download: ${state.header.meta}")
.setMessage("${state.uri}")
.setPositiveButton(getString(R.string.download).toUpperCase()) { _, _ ->
loadingView(true)
model.requestBinaryDownload(state.uri, clientCertPassword, null)
}
.setNegativeButton(getString(R.string.cancel).toUpperCase()) { _, _ -> }
.show()
}else{
Snackbar.make(binding.root, getString(R.string.no_internet), Snackbar.LENGTH_LONG).show()
}
}
}
is GemState.ResponseUnknownHost -> {
omniTerm.reset()
runOnUiThread {
loadingView(false)
AlertDialog.Builder(this, R.style.AppDialogTheme)
.setTitle(R.string.unknown_host_dialog_title)
.setMessage("Host not found: ${state.uri}\n\nSearch with TLGS instead?")
.setPositiveButton(getString(R.string.search)) { _, _ ->
.setTitle(getString(R.string.unknown_host))
.setMessage("${getString(R.string.unknown_host)}: ${state.uri}\n\n${getString(R.string.search_instead)}")
.setPositiveButton(getString(R.string.search).toUpperCase()) { _, _ ->
loadingView(true)
omniTerm.search(state.uri.toString())
omniTerm.search(state.uri.toString(), searchBase)
}
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
.setNegativeButton(getString(R.string.cancel).toUpperCase()) { _, _ -> }
.show()
}
}
@ -384,11 +588,51 @@ class GemActivity : AppCompatActivity() {
val uri = intent.data
if(uri != null){
binding.addressEdit.setText(uri.toString())
model.request(uri.toString())
gemRequest(uri.toString())
return
}
}
private fun biometricSecureRequest(address: String){
val biometricManager = BuranBiometricManager()
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
println("Authentication error: $errorCode: $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
println("Authentication failed")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
println("Authentication succeeded")
val ciphertext = biometricManager.decodeByteArray(
prefs.getString(
"password_ciphertext",
null
)!!
)
certPassword = biometricManager.decryptData(ciphertext, result.cryptoObject?.cipher!!)
gemRequest(address, true)
Toast.makeText(applicationContext, applicationContext.getString(R.string.cert_loaded), Toast.LENGTH_SHORT).show()
}
}
val initializationVector = biometricManager.decodeByteArray(
prefs.getString(
"password_init_vector",
null
)!!
)
biometricManager.createBiometricPrompt(this, null, this, callback)
biometricManager.authenticateToDecryptData(initializationVector)
}
private fun showAlert(message: String) = runOnUiThread{
loadingView(false)
@ -396,7 +640,7 @@ class GemActivity : AppCompatActivity() {
AlertDialog.Builder(this)
.setTitle(getString(R.string.error))
.setMessage(message)
.setPositiveButton("OK"){ _, _ ->
.setPositiveButton(getString(R.string.close).toUpperCase()){ _, _ ->
}
.show()
@ -405,15 +649,15 @@ class GemActivity : AppCompatActivity() {
}
}
private fun externalProtocol(state: GemState.NotGeminiRequest) = runOnUiThread {
private fun externalProtocol(uri: URI) = runOnUiThread {
loadingView(false)
val uri = state.uri.toString()
val uri = uri.toString()
when {
(uri.startsWith("http://") || uri.startsWith("https://")) -> openWebLink(uri)
(uri.startsWith("http://") || uri.startsWith("https://")) -> openExternalLink(uri)
else -> {
val viewIntent = Intent(Intent.ACTION_VIEW)
viewIntent.data = Uri.parse(state.uri.toString())
viewIntent.data = Uri.parse(uri.toString())
try {
startActivity(viewIntent)
@ -421,7 +665,7 @@ class GemActivity : AppCompatActivity() {
showAlert(
String.format(
getString(R.string.no_app_installed_that_can_open),
state.uri
uri
)
)
}
@ -429,14 +673,24 @@ class GemActivity : AppCompatActivity() {
}
}
private fun openWebLink(address: String){
private fun openExternalLink(address: String){
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
Buran.PREF_KEY_USE_CUSTOM_TAB,
true
)) {
)or !address.startsWith("http")) {
val builder = CustomTabsIntent.Builder()
val intent = builder.build()
intent.launchUrl(this, Uri.parse(address))
try {
intent.launchUrl(this, Uri.parse(address))
}catch (e: ActivityNotFoundException){
showAlert(
String.format(
getString(R.string.no_app_installed_that_can_open),
address
)
)
}
}else{
val viewIntent = Intent(Intent.ACTION_VIEW)
viewIntent.data = Uri.parse(address)
@ -447,18 +701,29 @@ class GemActivity : AppCompatActivity() {
private fun renderGemtext(state: GemState.ResponseGemtext) = runOnUiThread {
loadingView(false)
omniTerm.set(state.uri.toString())
omniTerm.set(proxiedAddress ?: state.uri.toString())
//todo - colours didn't change when switching themes, so disabled for now
//val addressSpan = SpannableString(state.uri.toString())
//addressSpan.set(0, 9, ForegroundColorSpan(resources.getColor(R.color.protocol_address)))
binding.addressEdit.setText(state.uri.toString())
if(!goingBack){
previousPosition = (binding.gemtextRecycler.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
}
adapter.render(state.lines)
//Scroll to top
binding.gemtextRecycler.post {
binding.gemtextRecycler.scrollToPosition(0)
//Scroll to correct position
if(goingBack){
println("Returning to previous position: $previousPosition")
binding.gemtextRecycler.scrollToPosition(previousPosition)
previousPosition = 0
goingBack = false
}else{
binding.gemtextRecycler.post {
binding.gemtextRecycler.scrollToPosition(0)
}
}
focusEnd()
@ -499,7 +764,7 @@ class GemActivity : AppCompatActivity() {
if(resultCode == RESULT_OK && (requestCode == CREATE_IMAGE_FILE_REQ || requestCode == CREATE_BINARY_FILE_REQ)){
//todo - tidy this mess up... refactor - none of this should be here
if(imageState == null && binaryState == null) return
data?.data?.let{ uri ->
data?.data?.let{ uri ->
val cachedFile = when {
imageState != null -> File(imageState!!.cacheUri.path ?: "")
binaryState != null -> File(binaryState!!.cacheUri.path ?: "")
@ -553,9 +818,151 @@ class GemActivity : AppCompatActivity() {
if(visible) binding.appBar.setExpanded(true)
}
private fun getInternetStatus(): Boolean {
val connectivityManager = this.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
if(capabilities != null){
when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { return true }
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> { return true }
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> { return true }
}
}
}else{
val activeNetworkInfo = connectivityManager.activeNetworkInfo
if(activeNetworkInfo != null && activeNetworkInfo.isConnected){
return true
}
}
return false
}
private fun initialise(){
db = BuranDatabase(applicationContext)
bookmarkDatasource = db.bookmarks()
if(intent.data == null){
model.initialise(
home = home,
gemini = Datasource.factory(this, db.history()),
db = db,
onState = this::handleState
)
if(home.isEmpty()){
loadLocalHome()
}
}else{
model.initialise(
home = intent.data.toString(),
gemini = Datasource.factory(this, db.history()),
db = db,
onState = this::handleState
)
}
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
"check_for_updates",
false
)){
val updates = BuranUpdates()
val latestVersion = updates.getLatestVersion()
if (latestVersion == BuildConfig.VERSION_NAME){
println("No new version available")
} else {
println("New version available")
Snackbar.make(binding.root, getString(R.string.new_version_available), Snackbar.LENGTH_LONG).setAction(getString(R.string.update)) {
updates.installUpdate(this, latestVersion)
}.show()
}
}
initialised = true
}
private fun loadLocalHome(){
loadingView(false)
binding.pullToRefresh.isRefreshing = false
binding.addressEdit.text?.clear()
val title = "# ${getString(R.string.app_name)}"
val sourceLink = "=> https://github.com/Corewala/Buran ${getString(R.string.source)}"
adapter.render(listOf(
title,
"",
getString(R.string.about_body),
"",
getString(R.string.about_ariane_source),
"",
getString(R.string.about_font),
"",
getString(R.string.about_glyphs),
"",
sourceLink,
getString(R.string.copyright)
))
omniTerm.set("")
}
private fun isHostSigned(uri: URI): Boolean{
if((uri.host != omniTerm.getCurrent().toURI().host) && !certPassword.isNullOrEmpty()) {
return false
}
return true
}
private fun gemRequest(address: String, sign: Boolean?){
if(sign == null){
if(!isHostSigned(address.toURI())) certPassword = null
}else if(!sign){
certPassword = null
}
updateClientCertIcon()
if(address.startsWith("http://") or address.startsWith("https://")){
val httpProxy = prefs.getString("http_proxy", null) ?: ""
if(
httpProxy.isNullOrEmpty()
or !httpProxy.startsWith("gemini://")
or httpProxy.contains(" ")
or !httpProxy.contains(".")
){
openExternalLink(address)
}else{
model.request(httpProxy, certPassword, address)
}
}
if(getInternetStatus()){
if(initialised){
if(address.isEmpty()){
loadLocalHome()
}else{
model.request(address, certPassword, null)
}
}else{
initialise()
}
}else{
Snackbar.make(binding.root, getString(R.string.no_internet), Snackbar.LENGTH_LONG).show()
loadingView(false)
}
}
private fun gemRequest(address: String){
gemRequest(address, null)
}
override fun onBackPressed() {
if (omniTerm.canGoBack()){
model.request(omniTerm.goBack())
if(model.isRequesting()){
model.cancel()
loadingView(false)
}else if(omniTerm.canGoBack()){
goingBack = true
gemRequest(omniTerm.goBack())
}else{
println("Buran history is empty - exiting")
super.onBackPressed()
@ -578,7 +985,7 @@ class GemActivity : AppCompatActivity() {
savedInstanceState.getString("uri")?.run {
omniTerm.set(this)
binding.addressEdit.setText(this)
model.request(this)
gemRequest(this)
}
}
}

View File

@ -19,24 +19,34 @@ class GemViewModel: ViewModel() {
this.db = db
this.onState = onState
request(home)
if(home.startsWith("gemini://") and !home.contains(" ")){
request(home, null, null)
}
}
fun request(address: String) {
gemini.request(address){ state ->
fun request(address: String, clientCertPassword: String?, alternativeRequest: String?) {
gemini.request(address, false, clientCertPassword, alternativeRequest){ state ->
onState(state)
}
}
fun requestBinaryDownload(uri: URI) {
gemini.request(uri.toString(), true){ state ->
fun isRequesting(): Boolean{
return gemini.isRequesting()
}
fun cancel(){
gemini.cancel()
}
fun requestBinaryDownload(uri: URI, clientCertPassword: String?, alternativeRequest: String?) {
gemini.request(uri.toString(), true, clientCertPassword, alternativeRequest){ state ->
onState(state)
}
}
//todo - same action as above... refactor
fun requestInlineImage(uri: URI, onImageReady: (cacheUri: Uri?) -> Unit){
gemini.request(uri.toString()){ state ->
fun requestInlineImage(uri: URI, clientCertPassword: String?, onImageReady: (cacheUri: Uri?) -> Unit){
gemini.request(uri.toString(), false, clientCertPassword, null){ state ->
when (state) {
is GemState.ResponseImage -> onImageReady(state.cacheUri)
else -> onState(state)

View File

@ -10,8 +10,8 @@ abstract class AbstractGemtextAdapter(
): RecyclerView.Adapter<GmiViewHolder>() {
var showInlineIcons: Boolean = false
var hideCodeBlocks: Boolean = false
var showLinkButtons: Boolean = false
var useAttentionGuides: Boolean = false
var showInlineImages: Boolean = false
abstract fun render(lines: List<String>)
@ -19,7 +19,7 @@ abstract class AbstractGemtextAdapter(
abstract fun inlineIcons(visible: Boolean)
abstract fun inlineImages(visible: Boolean)
abstract fun linkButtons(visible: Boolean)
abstract fun attentionGuides(enabled: Boolean)
abstract fun inferTitle(): String?
companion object{

View File

@ -2,17 +2,19 @@ package corewala.buran.ui.gemtext_adapter
import android.annotation.SuppressLint
import android.net.Uri
import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.gemtext_code_block.view.*
import kotlinx.android.synthetic.main.gemtext_image_link.view.*
import kotlinx.android.synthetic.main.gemtext_link.view.gemtext_text_link
import kotlinx.android.synthetic.main.gemtext_text.view.*
import androidx.core.text.bold
import corewala.buran.R
import corewala.endsWithImage
import corewala.visible
import kotlinx.android.synthetic.main.gemtext_code_block.view.*
import kotlinx.android.synthetic.main.gemtext_image_link.view.*
import kotlinx.android.synthetic.main.gemtext_link.view.gemtext_text_link
import kotlinx.android.synthetic.main.gemtext_quote.view.*
import kotlinx.android.synthetic.main.gemtext_text.view.*
import java.net.URI
class GemtextAdapter(
@ -81,39 +83,49 @@ class GemtextAdapter(
val line = lines[position]
when(holder){
is GmiViewHolder.Text -> holder.itemView.gemtext_text_textview.text = line
is GmiViewHolder.Text -> {
when {
useAttentionGuides -> holder.itemView.gemtext_text_textview.text = getAttentionGuideText(line)
else -> holder.itemView.gemtext_text_textview.text = line
}
}
is GmiViewHolder.Code -> {
var altText: String? = null
if(line.startsWith("```<|ALT|>")){
//there's alt text: "```<|ALT|>$alt</|ALT>"
altText = line.substring(10, line.indexOf("</|ALT>"))
holder.itemView.gemtext_text_monospace_textview.text = line.substring(line.indexOf("</|ALT>") + 7)
}else{
holder.itemView.gemtext_text_monospace_textview.text = line.substring(3)
}
}
is GmiViewHolder.Quote -> holder.itemView.gemtext_text_monospace_textview.text = line.substring(1).trim()
is GmiViewHolder.Quote -> {
when {
useAttentionGuides -> holder.itemView.gemtext_quote_textview.text = getAttentionGuideText(line.substring(1).trim())
else -> holder.itemView.gemtext_quote_textview.text = line.substring(1).trim()
}
}
is GmiViewHolder.H1 -> {
when {
line.length > 2 -> holder.itemView.gemtext_text_textview.text = line.substring(2).trim()
line.length > 2 -> holder.itemView.gemtext_text_textview.text = line.substring(1).trim()
else -> holder.itemView.gemtext_text_textview.text = ""
}
}
is GmiViewHolder.H2 -> {
when {
line.length > 3 -> holder.itemView.gemtext_text_textview.text = line.substring(3).trim()
line.length > 3 -> holder.itemView.gemtext_text_textview.text = line.substring(2).trim()
else -> holder.itemView.gemtext_text_textview.text = ""
}
}
is GmiViewHolder.H3 -> {
when {
line.length > 4 -> holder.itemView.gemtext_text_textview.text = line.substring(4).trim()
line.length > 4 -> holder.itemView.gemtext_text_textview.text = line.substring(3).trim()
else -> holder.itemView.gemtext_text_textview.text = ""
}
}
is GmiViewHolder.ListItem -> holder.itemView.gemtext_text_textview.text = "${line.substring(1)}".trim()
is GmiViewHolder.ListItem -> {
when {
useAttentionGuides -> holder.itemView.gemtext_text_textview.text = getAttentionGuideText("${line.substring(1)}".trim())
else -> holder.itemView.gemtext_text_textview.text = "${line.substring(1)}".trim()
}
}
is GmiViewHolder.Link -> {
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
var linkName = linkParts[0]
@ -189,11 +201,11 @@ class GemtextAdapter(
holder.itemView.gemtext_link_button.visible(true)
holder.itemView.gemtext_link_button.text = displayText
} else -> {
holder.itemView.gemtext_link_button.visible(false)
holder.itemView.gemtext_text_link.visible(true)
holder.itemView.gemtext_text_link.text = displayText
holder.itemView.gemtext_text_link.paint.isUnderlineText = true
}
holder.itemView.gemtext_link_button.visible(false)
holder.itemView.gemtext_text_link.visible(true)
holder.itemView.gemtext_text_link.text = displayText
holder.itemView.gemtext_text_link.paint.isUnderlineText = true
}
}
holder.itemView.gemtext_text_link.setOnClickListener {
@ -270,6 +282,49 @@ class GemtextAdapter(
return URI.create(linkParts.first())
}
private fun getAttentionGuideText(text: String): SpannableStringBuilder {
val wordList = text.split(" ")
val attentionGuideText = SpannableStringBuilder()
for(word in wordList){
val wordComponents = word.split("-")
for(component in wordComponents) {
val joiner = if((wordComponents.size > 1) and (wordComponents.indexOf(component) != wordComponents.size - 1)){
"-"
}else{
" "
}
if (component.length > 1) {
if (component.first().isLetterOrDigit()) {
val index = component.length / 2
attentionGuideText
.bold { append(component.substring(0, index)) }
.append("${component.substring(index)}$joiner")
} else {
var offset = 1
if (component.length - offset > 1) {
while ((component.length - offset > 1) and !component.substring(offset).first().isLetterOrDigit()) {
offset += 1
}
val index = (component.length - offset) / 2
attentionGuideText
.append(component.substring(0, offset))
.bold { append(component.substring(offset, index + offset)) }
.append("${component.substring(index + offset)}$joiner")
}else{
attentionGuideText.append("$component$joiner")
}
}
} else {
attentionGuideText.append("$component$joiner")
}
}
}
return attentionGuideText
}
override fun inferTitle(): String? {
lines.forEach { line ->
if(line.startsWith("#")) return line.replace("#", "").trim()
@ -293,6 +348,11 @@ class GemtextAdapter(
notifyDataSetChanged()
}
override fun attentionGuides(enabled: Boolean){
this.useAttentionGuides = enabled
notifyDataSetChanged()
}
override fun inlineImages(visible: Boolean){
this.showInlineImages = visible
notifyDataSetChanged()

View File

@ -1,45 +0,0 @@
package corewala.buran.ui.modals_menus.about
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.widget.AppCompatTextView
import kotlinx.android.synthetic.main.dialog_about.view.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import corewala.buran.R
import kotlinx.android.synthetic.main.dialog_content_text.view.*
import java.lang.StringBuilder
import java.security.SecureRandom
import java.security.Security
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
object AboutDialog {
fun show(context: Context){
val dialog = AppCompatDialog(context, R.style.AppTheme)
val view = View.inflate(context, R.layout.dialog_about, null)
dialog.setContentView(view)
view.about_toolbar.setNavigationIcon(R.drawable.vector_close)
view.about_toolbar.setNavigationOnClickListener {
dialog.dismiss()
}
view.source_button.setOnClickListener {
context.startActivity(Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("https://github.com/Corewala/Buran")
})
}
dialog.show()
}
}

View File

@ -8,13 +8,13 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatDialog
import androidx.core.view.forEach
import androidx.recyclerview.widget.LinearLayoutManager
import corewala.buran.OmniTerm
import kotlinx.android.synthetic.main.dialog_history.view.*
import corewala.buran.R
import corewala.buran.io.database.history.BuranHistory
import kotlinx.android.synthetic.main.dialog_bookmarks.view.*
object HistoryDialog {
fun show(context: Context, history: BuranHistory, onHistoryItem: (address: String) -> Unit){
fun show(context: Context, history: BuranHistory, omniTerm: OmniTerm, onHistoryItem: (address: String) -> Unit){
val dialog = AppCompatDialog(context, R.style.AppTheme)
@ -37,6 +37,7 @@ object HistoryDialog {
}
}
R.id.menu_action_clear_runtime_cache -> {
omniTerm.clearCache()
Toast.makeText(context, context.getString(R.string.runtime_cache_cleared), Toast.LENGTH_SHORT).show()
}
else -> {

View File

@ -1,33 +0,0 @@
package corewala.buran.ui.modals_menus.input
import android.content.Context
import android.view.View
import androidx.appcompat.app.AppCompatDialog
import kotlinx.android.synthetic.main.dialog_input_query.view.*
import corewala.buran.R
import corewala.buran.io.GemState
import java.net.URLEncoder
object InputDialog {
fun show(context: Context, state: GemState.ResponseInput, onQuery: (queryAddress: String) -> Unit) {
val dialog = AppCompatDialog(context, R.style.AppTheme)
val view = View.inflate(context, R.layout.dialog_input_query, null)
dialog.setContentView(view)
view.close_input_query_dialog.setOnClickListener {
dialog.dismiss()
}
view.query_text.text = state.header.meta
view.query_submit_button.setOnClickListener {
val encoded = URLEncoder.encode(view.query_input.text.toString(), "UTF-8")
onQuery("${state.uri}?$encoded")
dialog.dismiss()
}
dialog.show()
}
}

View File

@ -16,9 +16,11 @@ import corewala.buran.R
object OverflowPopup {
lateinit var popup: PopupMenu
fun show(view: View?, onMenuOption: (menuId: Int) -> Unit){
if(view != null) {
val popup = PopupMenu(view.context, view)
popup = PopupMenu(view.context, view)
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(R.menu.overflow_menu, popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
@ -31,6 +33,14 @@ object OverflowPopup {
}
}
fun setItemTitle(id: Int, title: String){
popup.menu.findItem(id).title = title
}
fun setItemVisibility(id: Int, visible: Boolean){
popup.menu.findItem(id).isVisible = visible
}
fun insertMenuItemIcons(context: Context, popupMenu: PopupMenu) {
val menu: Menu = popupMenu.menu
if (hasIcon(menu)) {

View File

@ -4,17 +4,20 @@ import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.view.inputmethod.EditorInfo
import androidx.appcompat.app.AppCompatDelegate
import androidx.biometric.BiometricPrompt
import androidx.preference.*
import corewala.buran.Buran
import corewala.buran.R
import corewala.buran.io.keymanager.BuranBiometricManager
const val PREFS_SET_CLIENT_CERT_REQ = 20
@ -23,7 +26,6 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
lateinit var prefs: SharedPreferences
private lateinit var clientCertPref: Preference
private lateinit var useClientCertPreference: SwitchPreferenceCompat
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -49,14 +51,35 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
val homecapsule = preferenceManager.sharedPreferences.getString(
"home_capsule",
Buran.DEFAULT_HOME_CAPSULE
)
)?.trim()
homePreference.summary = if(homecapsule.isNullOrEmpty()){
context.getString(R.string.no_home_capsule_set)
}else if(
!homecapsule.startsWith("gemini://")
or homecapsule.contains(" ")
or !homecapsule.contains(".")
){
context.getString(R.string.not_valid_address)
}else{
homecapsule
}
homePreference.summary = homecapsule
homePreference.positiveButtonText = getString(R.string.update)
homePreference.negativeButtonText = getString(R.string.cancel)
homePreference.title = getString(R.string.home_capsule)
homePreference.setOnPreferenceChangeListener { _, newValue ->
homePreference.summary = newValue.toString()
val newHomecapsule = newValue.toString().trim()
homePreference.summary = if(newHomecapsule.isNullOrEmpty()){
context.getString(R.string.no_home_capsule_set)
}else if(
!newHomecapsule.startsWith("gemini://")
or newHomecapsule.contains(" ")
or !newHomecapsule.contains(".")
){
context.getString(R.string.not_valid_address)
}else{
newHomecapsule
}
true
}
homePreference.setOnBindEditTextListener{ editText ->
@ -65,8 +88,72 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
}
appCategory.addPreference(homePreference)
//Home - Certificates
buildClientCertificateSection(context, appCategory)
//Search ---------------------------------------------
val searchPreference = EditTextPreference(context)
searchPreference.title = getString(R.string.search_engine)
searchPreference.key = "search_base"
searchPreference.dialogTitle = getString(R.string.search_base)
val searchengine = preferenceManager.sharedPreferences.getString(
"search_base",
Buran.DEFAULT_SEARCH_BASE
)?.trim()
searchPreference.summary = if(searchengine.isNullOrEmpty()){
Buran.DEFAULT_SEARCH_BASE
}else if(
!searchengine.startsWith("gemini://")
or searchengine.contains(" ")
or !searchengine.contains(".")
){
context.getString(R.string.not_valid_address)
}else if(!searchengine.endsWith("?")){
context.getString(R.string.not_valid_search_string)
}else{
searchengine
}
searchPreference.positiveButtonText = getString(R.string.update)
searchPreference.negativeButtonText = getString(R.string.cancel)
searchPreference.setOnPreferenceChangeListener { _, newValue ->
val newSearchBase = newValue.toString().trim()
searchPreference.summary = if(newSearchBase.isNullOrEmpty()){
Buran.DEFAULT_SEARCH_BASE
}else if(
!newSearchBase.startsWith("gemini://")
or newSearchBase.contains(" ")
or !newSearchBase.contains(".")
){
context.getString(R.string.not_valid_address)
}else if(!newSearchBase.endsWith("?")){
context.getString(R.string.not_valid_search_string)
}else{
newSearchBase
}
true
}
searchPreference.setOnBindEditTextListener{ editText ->
editText.imeOptions = EditorInfo.IME_ACTION_DONE
editText.setSelection(editText.text.toString().length)//Set caret position to end
}
appCategory.addPreference(searchPreference)
//Updates ---------------------------------------------
val sideloadedHashCode = -899861527
val isSideloaded = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
).signatures[0].hashCode() == sideloadedHashCode
val checkForUpdates = SwitchPreferenceCompat(context)
checkForUpdates.setDefaultValue(false)
checkForUpdates.key = "check_for_updates"
checkForUpdates.title = getString(R.string.check_for_updates)
checkForUpdates.isVisible = isSideloaded
appCategory.addPreference(checkForUpdates)
//Certificates
buildClientCertificateSection(context, screen)
//Appearance --------------------------------------------
buildAppearanceSection(context, appCategory)
@ -104,6 +191,50 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
showInlineImages.title = getString(R.string.show_inline_images)
webCategory.addPreference(showInlineImages)
val httpGeminiProxy = EditTextPreference(context)
httpGeminiProxy.title = getString(R.string.http_proxy)
httpGeminiProxy.key = "http_proxy"
httpGeminiProxy.dialogTitle = getString(R.string.http_proxy)
val httpProxy = preferenceManager.sharedPreferences.getString(
"http_proxy",
null
)?.trim()
httpGeminiProxy.summary = if(httpProxy.isNullOrEmpty()){
getString(R.string.no_http_proxy_set)
}else if(
!httpProxy.startsWith("gemini://")
or httpProxy.contains(" ")
or !httpProxy.contains(".")
){
getString(R.string.not_valid_address)
}else{
httpProxy
}
httpGeminiProxy.positiveButtonText = getString(R.string.update)
httpGeminiProxy.negativeButtonText = getString(R.string.cancel)
httpGeminiProxy.setOnPreferenceChangeListener { _, newValue ->
val newHomecapsule = newValue.toString().trim()
httpGeminiProxy.summary = if(newHomecapsule.isNullOrEmpty()){
getString(R.string.no_http_proxy_set)
}else if(
!newHomecapsule.startsWith("gemini://")
or newHomecapsule.contains(" ")
or !newHomecapsule.contains(".")
){
getString(R.string.not_valid_address)
}else{
newHomecapsule
}
true
}
httpGeminiProxy.setOnBindEditTextListener{ editText ->
editText.imeOptions = EditorInfo.IME_ACTION_DONE
editText.setSelection(editText.text.toString().length)//Set caret position to end
}
webCategory.addPreference(httpGeminiProxy)
}
private fun buildAppearanceSection(context: Context?, appCategory: PreferenceCategory) {
@ -192,82 +323,154 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
showLinkButtonsPreference.key = "show_link_buttons"
showLinkButtonsPreference.title = getString(R.string.show_link_buttons)
accessibilityCategory.addPreference(showLinkButtonsPreference)
//Accessibility - gemtext attention guides
val attentionGuidingText = SwitchPreferenceCompat(context)
attentionGuidingText.setDefaultValue(false)
attentionGuidingText.key = "use_attention_guides"
attentionGuidingText.title = getString(R.string.use_attention_guides)
accessibilityCategory.addPreference(attentionGuidingText)
}
private fun buildClientCertificateSection(context: Context?, appCategory: PreferenceCategory) {
if (Buran.FEATURE_CLIENT_CERTS) {
private fun buildClientCertificateSection(context: Context?, screen: PreferenceScreen) {
val aboutPref = Preference(context)
aboutPref.key = "unused_pref"
aboutPref.summary = getString(R.string.pkcs_notice)
aboutPref.isPersistent = false
aboutPref.isSelectable = false
appCategory.addPreference(aboutPref)
val certificateCategory = PreferenceCategory(context)
certificateCategory.key = "certificate_category"
certificateCategory.title = getString(R.string.client_certificate)
screen.addPreference(certificateCategory)
clientCertPref = Preference(context)
clientCertPref.title = getString(R.string.client_certificate)
clientCertPref.key = Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE
val aboutPref = Preference(context)
aboutPref.summary = getString(R.string.pkcs_notice)
aboutPref.isPersistent = false
aboutPref.isSelectable = false
certificateCategory.addPreference(aboutPref)
val clientCertUriHumanReadable = preferenceManager.sharedPreferences.getString(
Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE,
null
)
clientCertPref = Preference(context)
clientCertPref.title = getString(R.string.client_certificate)
clientCertPref.key = Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE
val hasCert = clientCertUriHumanReadable != null
if (!hasCert) {
clientCertPref.summary = getString(R.string.tap_to_select_client_certificate)
} else {
clientCertPref.summary = clientCertUriHumanReadable
val clientCertUriHumanReadable = preferenceManager.sharedPreferences.getString(
Buran.PREF_KEY_CLIENT_CERT_HUMAN_READABLE,
null
)
val hasCert = clientCertUriHumanReadable != null
if (!hasCert) {
clientCertPref.summary = getString(R.string.tap_to_select_client_certificate)
} else {
clientCertPref.summary = clientCertUriHumanReadable
}
clientCertPref.setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = "application/x-pkcs12"
}
startActivityForResult(intent, PREFS_SET_CLIENT_CERT_REQ)
true
}
clientCertPref.setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = "*/*"
certificateCategory.addPreference(clientCertPref)
val clientCertPassword = EditTextPreference(context)
clientCertPassword.key = Buran.PREF_KEY_CLIENT_CERT_PASSWORD
clientCertPassword.title = getString(R.string.client_certificate_password)
var certPassword = preferenceManager.sharedPreferences.getString(
Buran.PREF_KEY_CLIENT_CERT_PASSWORD,
null
)
clientCertPassword.dialogTitle = getString(R.string.client_certificate_password)
if (certPassword != null && certPassword.isNotEmpty()) {
clientCertPassword.summary = getDots(certPassword)
} else {
clientCertPassword.summary = getString(R.string.no_password)
}
clientCertPassword.isVisible = !preferenceManager.sharedPreferences.getBoolean("use_biometrics", false)
certificateCategory.addPreference(clientCertPassword)
val useBiometrics = SwitchPreferenceCompat(context)
useBiometrics.setDefaultValue(false)
useBiometrics.key = "use_biometrics"
useBiometrics.title = getString(R.string.biometric_cert_verification)
useBiometrics.isVisible = false
certificateCategory.addPreference(useBiometrics)
val passwordCiphertext = EditTextPreference(context)
passwordCiphertext.key = "password_ciphertext"
passwordCiphertext.isVisible = false
certificateCategory.addPreference(passwordCiphertext)
val passwordInitVector = EditTextPreference(context)
passwordInitVector.key = "password_init_vector"
passwordInitVector.isVisible = false
certificateCategory.addPreference(passwordInitVector)
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
useBiometrics.isVisible = (certPassword?.isNotEmpty() ?: false) or useBiometrics.isChecked
useBiometrics.setOnPreferenceChangeListener { _, newValue ->
val biometricManager = BuranBiometricManager()
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
println("Authentication error: $errorCode: $errString")
useBiometrics.isChecked = !(newValue as Boolean)
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
println("Authentication failed")
useBiometrics.isChecked = !(newValue as Boolean)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
println("Authentication succeeded")
if(newValue as Boolean){
println(certPassword)
val encryptedData = biometricManager.encryptData(certPassword!!, result.cryptoObject?.cipher!!)
val ciphertext = encryptedData.ciphertext
val initializationVector = encryptedData.initializationVector
passwordInitVector.text = initializationVector.contentToString()
passwordCiphertext.text = ciphertext.contentToString()
clientCertPassword.text = null
}else{
val ciphertext = biometricManager.decodeByteArray(passwordCiphertext.text)
clientCertPassword.text = biometricManager.decryptData(ciphertext, result.cryptoObject?.cipher!!)
clientCertPassword.summary = getDots(clientCertPassword.text)
}
clientCertPassword.isVisible = !(newValue as Boolean)
clientCertPref.isEnabled = !(newValue as Boolean)
}
}
startActivityForResult(intent, PREFS_SET_CLIENT_CERT_REQ)
biometricManager.createBiometricPrompt(requireContext(), this, null, callback)
if(newValue as Boolean){
biometricManager.authenticateToEncryptData()
}else{
val initializationVector = biometricManager.decodeByteArray(passwordInitVector.text)
biometricManager.authenticateToDecryptData(initializationVector)
}
true
}
}
appCategory.addPreference(clientCertPref)
val clientCertPassword = EditTextPreference(context)
clientCertPassword.key = Buran.PREF_KEY_CLIENT_CERT_PASSWORD
clientCertPassword.title = getString(R.string.client_certificate_password)
val certPasword = preferenceManager.sharedPreferences.getString(
Buran.PREF_KEY_CLIENT_CERT_PASSWORD,
null
)
if (certPasword != null && certPasword.isNotEmpty()) {
clientCertPassword.summary = getDots(certPasword)
} else {
clientCertPassword.setOnPreferenceChangeListener { _, newValue ->
val passphrase = "$newValue"
if (passphrase.isEmpty()) {
clientCertPassword.summary = getString(R.string.no_password)
useBiometrics.isVisible = false
} else {
clientCertPassword.summary = getDots(passphrase)
useBiometrics.isVisible = true
}
clientCertPassword.dialogTitle = getString(R.string.client_certificate_password)
clientCertPassword.setOnPreferenceChangeListener { _, newValue ->
val passphrase = "$newValue"
if (passphrase.isEmpty()) {
clientCertPassword.summary = getString(R.string.no_password)
} else {
clientCertPassword.summary = getDots(passphrase)
}
true//update the value
}
appCategory.addPreference(clientCertPassword)
useClientCertPreference = SwitchPreferenceCompat(context)
useClientCertPreference.key = Buran.PREF_KEY_CLIENT_CERT_ACTIVE
useClientCertPreference.title = getString(R.string.use_client_certificate)
appCategory.addPreference(useClientCertPreference)
if (!hasCert) {
useClientCertPreference.isVisible = false
}
certPassword = passphrase
true//update the value
}
}
@ -293,7 +496,6 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
persistPermissions(uri)
findFilename(uri)
}
}
super.onActivityResult(requestCode, resultCode, data)
}
@ -321,6 +523,5 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
readableReference
).apply()
clientCertPref.summary = readableReference
useClientCertPreference.isChecked = true
}
}

View File

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">

View File

@ -45,6 +45,7 @@
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_margin="@dimen/button_margin"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/vector_home"/>
@ -80,7 +81,7 @@
android:id="@+id/more"
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_margin="@dimen/button_margin"
android:background="?android:attr/selectableItemBackgroundBorderless"

View File

@ -1,103 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.Toolbar
android:id="@+id/about_toolbar"
android:layout_width="match_parent"
android:layout_marginTop="@dimen/default_margin"
android:layout_height="@dimen/bar_height"
app:title="@string/about"/>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/about_toolbar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/default_margin_big"
android:paddingRight="@dimen/default_margin_big"
android:paddingBottom="@dimen/default_margin_big"
android:orientation="vertical">
<!-- Description -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/default_margin_big"
android:textColor="@color/stroke"
android:text="@string/about_body"/>
<!-- Version -->
<TextView
android:id="@+id/version_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/stroke"
tools:text="1.0.0 alpha delta"/>
<!-- Copyright -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/default_margin"
android:textColor="@color/stroke"
android:text="@string/copyright"/>
<!-- Source button -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/source_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Source"/>
<!-- DIV -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="@dimen/default_margin"
android:layout_marginTop="@dimen/default_margin"
android:alpha="0.5"
android:background="?attr/colorOnSurface" />
<!-- Ariane source attribution-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/default_margin"
android:textColor="@color/stroke"
android:text="@string/about_ariane_source"/>
<!-- Font Attribution -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/default_margin"
android:textColor="@color/stroke"
android:text="@string/about_font"/>
<!-- Glyph Attribution -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/default_margin"
android:textColor="@color/stroke"
android:text="@string/about_glyphs"/>
<!-- DIV -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="@dimen/default_margin"
android:layout_marginTop="@dimen/default_margin"
android:alpha="0.5"
android:background="?attr/colorOnSurface" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>

View File

@ -2,61 +2,14 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<RelativeLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/default_margin">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/close_input_query_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_margin="@dimen/button_margin"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/vector_close" />
</RelativeLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/header">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/default_margin_big"
android:paddingRight="@dimen/default_margin_big"
android:paddingBottom="@dimen/default_margin_big"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/query_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/default_margin"
tools:text="Type your query"/>
android:paddingHorizontal="@dimen/screen_margin">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/query_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/default_margin"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/query_submit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/submit"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
android:inputType="text"
android:lines="1"
android:maxLines="1"/>
</RelativeLayout>

View File

@ -21,7 +21,7 @@
android:textSize="@dimen/code_text_size"
android:typeface="monospace"
android:textColor="@color/stroke"
android:id="@id/gemtext_text_monospace_textview"
android:id="@+id/gemtext_text_monospace_textview"
android:paddingLeft="@dimen/default_margin_big"
android:paddingTop="@dimen/default_margin_big"
android:paddingRight="@dimen/default_margin_big"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/gemtext_text_monospace_textview"
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/gemtext_quote_textview"
android:textSize="@dimen/default_text_size"
android:textColor="@color/stroke"
android:layout_marginLeft="@dimen/screen_margin"
@ -15,4 +15,4 @@
android:textIsSelectable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineHeight="@dimen/default_line_height"/>
app:lineHeight="@dimen/default_line_height" />

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/gemtext_text_textview"
android:textSize="@dimen/default_text_size"
android:layout_marginLeft="@dimen/screen_margin"
@ -9,4 +9,4 @@
android:textIsSelectable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineHeight="@dimen/default_line_height"/>
app:lineHeight="@dimen/default_line_height" />

View File

@ -4,6 +4,9 @@
<item
android:id="@+id/overflow_menu_search"
android:title="@string/search"/>
<item
android:id="@+id/overflow_menu_sign"
android:title="@string/load_cert"/>
<item
android:id="@+id/overflow_menu_bookmark"
android:title="@string/add_bookmark"/>

Binary file not shown.

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict" />

View File

@ -1,21 +1,21 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name">Buran</string>
<string name="app_name">Bourane</string>
<string name="gemini_protocol">gemini://</string>
<string name="main_input_hint">Entrez l\'adresse gemini://</string>
<string name="main_input_search_hint">Entrez un terme de recherche</string>
<string name="copy_address">Partager l\'adresse</string>
<string name="load_image">Afficher en ligne</string>
<string name="about">À propos</string>
<string name="address_copied_to_clipboard">Adresse copiée dans le presse-papiers</string>
<string name="gemini_address">Adresse Gemini</string>
<string name="share">Partager</string>
<string name="set_home">Choisir comme Accueil</string>
<string name="settings">Paramètres</string>
<string name="about_body">Buran: Un client pour le protocole Gemini par Corewala</string>
<string name="about_body">Un navigateur minimaliste pour le protocole Gemini par Corewala</string>
<string name="copyright">Copyright © 2022 Corewala</string>
<string name="about_ariane_source">Buran est basé sur le navigateur Ariane d\'ÖLAB sous la Licence Publique de l\'Union Européenne</string>
<string name="about_ariane_source">Bourane est basé sur le navigateur Ariane d\'ÖLAB sous la Licence Publique de l\'Union Européenne</string>
<string name="about_font">Les blocs de code sont rendus avec JetBrains Mono de JetBrains</string>
<string name="about_glyphs">Les glyphes utilisés proviennent de Material Icons par Google</string>
<string name="source">Source</string>
<string name="clear_cache">Vider le cache d\'exécution</string>
<string name="history">Historique</string>
<string name="clear_history">Vider l\'historique</string>
@ -37,15 +37,23 @@
<string name="move_down">Déplacer vers le bas</string>
<string name="move_up">Déplacer vers le haut</string>
<string name="unknown_mime_dialog_title">Type Mime inconnu</string>
<string name="unknown_host_dialog_title">Hôte inconnu</string>
<string name="unknown_host">Hôte inconnu</string>
<string name="search_instead">Rechercher au lieu?</string>
<string name="download">Télécharger</string>
<string name="cancel">Annuler</string>
<string name="close">Fermer</string>
<string name="confirm">Confirmer</string>
<string name="error">Erreur</string>
<string name="no_app_installed_that_can_open">Aucune app installée qui puisse ouvrir %s</string>
<string name="no_state_object_exists">Erreur de téléchargement de fichier - aucun état d\'objet n\'existe</string>
<string name="file_saved_to_device">Fichier sauvegardé dans l\'appareil</string>
<string name="configure_buran">Configurer Buran</string>
<string name="home_capsule">Capsule d\'accueil</string>
<string name="not_valid_search_string">Ceci n\'est pas une base de recherche valide</string>
<string name="not_valid_address">Ceci n\'est pas une adresse valide</string>
<string name="no_home_capsule_set">Pas de capsule d\'accueil</string>
<string name="search_engine">Moteur de recherche</string>
<string name="search_base">Base de recherche</string>
<string name="update">Mettre à jour</string>
<string name="appearance">Apparence</string>
<string name="theme">Thème</string>
@ -57,18 +65,35 @@
<string name="web_content">Contenu Web</string>
<string name="web_content_label">Ouvrir les sites web en interne en utilisant des \'Onglets Personnalisés\', plutôt que d\'utiliser le navigateur par défaut. Cela pourrait vous aider à rester dans le Geminispace plutôt que d\'être distrait·e par le vaste web. Cela requiert un navigateur par défaut compatible.</string>
<string name="web_content_switch_label">Ouvrir en interne</string>
<string name="http_proxy">Mandataire HTTP</string>
<string name="no_http_proxy_set">Pas de mandataire HTTP</string>
<string name="proxied_content">Ce contenu est visualisé via un mandataire</string>
<string name="open_original">Ouvrir l\'original</string>
<string name="show_inline_images">Images locales en ligne</string>
<string name="pkcs_notice">Seuls les magasins de clés client PKCS12 sont actuellement supportés</string>
<string name="pkcs_notice">Seuls les magasins de clés client PKCS12 sont actuellement supportés.</string>
<string name="client_certificate">Certificat Client</string>
<string name="tap_to_select_client_certificate">Cliquez pour sélectionner un certificat client</string>
<string name="client_certificate_password">Mot de passe du Certificat Client</string>
<string name="no_password">Pas de mot de passe</string>
<string name="use_client_certificate">Utiliser un Certificat Client</string>
<string name="use_client_certificate">Utiliser Certificat Client</string>
<string name="client_certificate_required">Certificat Client Requis</string>
<string name="confirm_your_identity">Confirmez votre identité</string>
<string name="use_biometric_unlock">Utilisez vos informations biométriques pour continuer</string>
<string name="biometric_cert_verification">Certificat Client biométrique</string>
<string name="load_cert">Chargez Certificat</string>
<string name="unload_cert">Déchargez Certificat</string>
<string name="cert_loaded">Certificat chargé</string>
<string name="cert_unloaded">Certificat dechargé</string>
<string name="set_home_capsule">Choisir comme capsule d\'accueil</string>
<string name="check_for_updates">Vérifier pour des mises à jour</string>
<string name="new_version_available">Nouvelle version disponible</string>
<string name="no_internet">Aucun accès internet</string>
<string name="retry">Rafraichissez cette page pour réessayer</string>
<string name="history_cleared">Historique vidé</string>
<string name="runtime_cache_cleared">Cache d\'exécution vidé</string>
<string name="show_inline_icons">Icônes de lien en ligne</string>
<string name="show_link_buttons">Boutons de lien</string>
<string name="use_attention_guides">Utiliser des guides d\'attention</string>
<string name="bookmarks_empty">Vous n\'avez encore aucun marque-pages</string>
<string name="import_bookmarks">Importer des marque-pages</string>
<string name="export_bookmarks">Exporter des marque-pages</string>

View File

@ -4,18 +4,18 @@
<string name="main_input_hint">Enter gemini:// address</string>
<string name="main_input_search_hint">Enter search term</string>
<string name="copy_address">Share address</string>
<string name="load_image">Display inline</string>
<string name="about">About</string>
<string name="address_copied_to_clipboard">Address copied to clipboard</string>
<string name="gemini_address">Gemini address</string>
<string name="share">Share</string>
<string name="set_home">Set Home</string>
<string name="settings">Settings</string>
<string name="about_body">Buran: A Gemini protocol browser from Corewala</string>
<string name="about_body">A simple Gemini protocol browser from Corewala</string>
<string name="copyright">Copyright © 2022 Corewala</string>
<string name="about_ariane_source">Buran is based on the Ariane browser by ÖLAB under the European Union Public Licence</string>
<string name="about_font">Code blocks are rendered using JetBrains Mono by JetBrains</string>
<string name="about_glyphs">Glyphs used are from Material Icons by Google</string>
<string name="source">Source</string>
<string name="clear_cache">Clear runtime cache</string>
<string name="history">History</string>
<string name="clear_history">Clear history</string>
@ -37,15 +37,23 @@
<string name="move_down">Move down</string>
<string name="move_up">Move up</string>
<string name="unknown_mime_dialog_title">Unknown Mime Type</string>
<string name="unknown_host_dialog_title" tools:ignore="MissingTranslation">Unknown Host</string>
<string name="unknown_host">Unknown Host</string>
<string name="search_instead">Search instead?</string>
<string name="download">Download</string>
<string name="cancel">Cancel</string>
<string name="close">Close</string>
<string name="confirm">Ok</string>
<string name="error">Error</string>
<string name="no_app_installed_that_can_open">No app installed that can open %s</string>
<string name="no_state_object_exists">File download error - no state object exists</string>
<string name="file_saved_to_device">File saved to device</string>
<string name="configure_buran">Configure Buran</string>
<string name="home_capsule">Home Capsule</string>
<string name="not_valid_search_string">This is not a valid search base</string>
<string name="not_valid_address">This is not a valid address</string>
<string name="no_home_capsule_set">No home capsule set</string>
<string name="search_engine">Search Engine</string>
<string name="search_base">Search Base</string>
<string name="update">Update</string>
<string name="appearance">Appearance</string>
<string name="theme">Theme</string>
@ -57,18 +65,35 @@
<string name="web_content">Web Content</string>
<string name="web_content_label">Open websites internally using \'Custom Tabs\', instead of using the default browser. This might help you stay in Geminispace instead of being distracted by the wider web. Requires compatible default browser.</string>
<string name="web_content_switch_label">Open internally</string>
<string name="http_proxy">HTTP proxy</string>
<string name="no_http_proxy_set">No HTTP proxy set</string>
<string name="proxied_content">This content is rendered through a proxy</string>
<string name="open_original">Open original</string>
<string name="show_inline_images">Inline local images</string>
<string name="pkcs_notice">Only PKCS12 client keystores are currently supported</string>
<string name="pkcs_notice">Only PKCS12 client keystores are currently supported.</string>
<string name="client_certificate">Client Certificate</string>
<string name="tap_to_select_client_certificate">Tap to select client certificate</string>
<string name="client_certificate_password">Client Certificate Password</string>
<string name="no_password">No Password</string>
<string name="use_client_certificate">Use Client Certificate</string>
<string name="client_certificate_required">Client Certificate Required</string>
<string name="confirm_your_identity">Confirm your identity</string>
<string name="use_biometric_unlock">Verify your biometric credentials to continue</string>
<string name="biometric_cert_verification">Client Certificate biometrics</string>
<string name="load_cert">Load Certificate</string>
<string name="unload_cert">Unload Certificate</string>
<string name="cert_loaded">Certificate loaded</string>
<string name="cert_unloaded">Certificate unloaded</string>
<string name="set_home_capsule">Set home capsule</string>
<string name="check_for_updates">Check for updates</string>
<string name="new_version_available">New version available</string>
<string name="no_internet">No internet access</string>
<string name="retry">Refresh this page to try again</string>
<string name="history_cleared">History cleared</string>
<string name="runtime_cache_cleared">Runtime cache cleared</string>
<string name="show_inline_icons">Inline link icons</string>
<string name="show_link_buttons">Show link buttons</string>
<string name="use_attention_guides">Use attention guides</string>
<string name="bookmarks_empty">You don\'t have any bookmarks yet</string>
<string name="import_bookmarks">Import bookmarks</string>
<string name="export_bookmarks">Export bookmarks</string>

View File

@ -10,6 +10,7 @@
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
<item name="android:navigationBarColor">@color/navigation_bar_background</item>
<item name="android:windowDisablePreview">true</item>
</style>
<style name="SettingsTheme" parent="@style/AppTheme">

View File

@ -22,3 +22,5 @@ kotlin.code.style=official
# Kapt workaround
kapt.use.worker.api=false
kapt.incremental.apt=false
# R8 optimisation
android.enableR8.fullMode=true

View File

@ -1,11 +0,0 @@
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Mon Nov 29 22:38:09 EST 2021
sdk.dir=/home/corewala/.android_sdk
sdk-location=/home/vagrant/android-sdk
ndk.dir=/home/vagrant/android-ndk/r12b
ndk-location=/home/vagrant/android-ndk/r12b

View File

@ -0,0 +1,9 @@
Changelog:
- Default homepage is stored locally
- Only the latest request is rendered
- Back button cancels current request
- Lots of bugfixes

View File

@ -0,0 +1,9 @@
Changelog:
- HTTP proxy support
- Previous scroll position is stored
- Local links and redirects are fixed
- Minor tweaks and bugfixes

View File

@ -0,0 +1,9 @@
Changelog:
- Fixed broken unproxied HTTP links
- Un-bugged the back button
- This is a really tiny release
- Actual features coming at some point in the nebulous future

View File

@ -0,0 +1,5 @@
Buran is a simple Gemini protocol browser for Android which allows users to explore geminispace in style.
This application has no external dependencies and does not require any nonfree services, using only default system libraries. It has been fully localized in English and French, and supports an array of accessibility and quality-of-life features.
Buran is a fork of Ariane by ÖLAB allowed under the terms of the European Union Public Licence.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1 @@
Simple Gemini browser for Android

View File

@ -0,0 +1 @@
Buran

View File

@ -0,0 +1,5 @@
Bourane est un navigateur minimaliste du protocole Gemini sur Android pour explorer l'espace Gemini avec style.
Cette application n'a aucune dépendance externe et ne nécessite aucun service non gratuit, en utilisant uniquement les bibliothèques système.
Bourane est basé sur le navigateur Ariane d'ÖLAB autorisé conformément aux termes de la Licence Publique de l'Union Européenne.

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1 @@
Navigateur minimaliste pour le protocole Gemini

View File

@ -0,0 +1 @@
Bourane