Compare commits

..

147 Commits

Author SHA1 Message Date
akwizgran
a438050e68 Merge branch '1081-share-app-via-wifi-hotspot' into offline-testing 2021-07-09 10:41:37 +01:00
Torsten Grote
4d0fe24722 Merge branch '1802-sync-via-removable-storage' into offline-testing
# Conflicts:
#	bramble-api/src/main/java/org/briarproject/bramble/api/FeatureFlags.java
#	bramble-core/build.gradle
#	bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java
#	bramble-core/witness.gradle
#	bramble-java/src/main/java/org/briarproject/bramble/plugin/tor/UnixTorPluginFactory.java
#	briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
#	briar-android/src/main/java/org/briarproject/briar/android/AppModule.java
#	briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
#	briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
#	briar-android/src/main/res/values/strings.xml
#	briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt
#	briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt
2021-07-07 15:36:08 -03:00
akwizgran
559b29e8b5 Merge branch '2080-improve-offline-sharing-texts' into '1081-share-app-via-wifi-hotspot'
Improve texts on offline hotspot for a better UX

See merge request briar/briar!1493
2021-07-07 10:59:40 +00:00
Torsten Grote
1b7b285862 Merge remote-tracking branch 'origin/1081-share-app-via-wifi-hotspot' into offline-testing 2021-07-06 17:15:23 -03:00
Torsten Grote
178810241f Merge branch '2031-increase-max-latency' into '1802-sync-via-removable-storage'
Increase max latency of removable drive plugin to 28 days

See merge request briar/briar!1503
2021-07-06 20:06:48 +00:00
Torsten Grote
61c601cb6d Merge branch '2103-check-transport-keys' into '1802-sync-via-removable-storage'
Check whether we have transport keys before trying to send data

See merge request briar/briar!1502
2021-07-06 19:56:24 +00:00
Torsten Grote
2002ad08ca Merge branch '2105-let-contacts-know-drive-not-supported' into '1802-sync-via-removable-storage'
Don't configure the removable drive plugin on API < 19

See merge request briar/briar!1501
2021-07-06 19:53:50 +00:00
akwizgran
d134a67ee9 Increase max latency of removable drive plugin to 28 days. 2021-07-06 17:33:00 +01:00
akwizgran
04cf8e16a9 Check whether we have transport keys before trying to send data. 2021-07-06 16:24:43 +01:00
akwizgran
227d345858 Don't configure the removable drive plugin on API < 19. 2021-07-06 16:13:48 +01:00
Torsten Grote
07ef73ab56 Merge branch '2102-clear-introduction-state' into '1802-sync-via-removable-storage'
Clear keys from session when moving to AWAIT_ACTIVATE state

See merge request briar/briar!1500
2021-07-06 12:26:05 +00:00
Torsten Grote
ea2b1ff4d8 Merge branch '2079-reject-old-timestamps' into '1802-sync-via-removable-storage'
Reject old timestamps when deriving rotation mode keys

See merge request briar/briar!1481
2021-07-06 12:22:42 +00:00
akwizgran
69fac86a0c Clear keys from session when moving to AWAIT_ACTIVATE state. 2021-07-05 18:02:22 +01:00
akwizgran
bd6b6c1cd6 Reject old timestamps when deriving rotation mode keys. 2021-07-05 16:35:32 +01:00
Torsten Grote
be6c868135 Merge branch '2101-dont-increment-attempt-twice' into '1081-share-app-via-wifi-hotspot'
Do not increment the attempt variable twice when requesting group info

See merge request briar/briar!1499
2021-07-05 11:44:51 +00:00
Torsten Grote
041a296666 Merge branch '2090-avoid-double-tap-on-start-sharing-button' into '1081-share-app-via-wifi-hotspot'
Do not allow the user to tap the start sharing button twice quickly

See merge request briar/briar!1495
2021-07-05 11:24:47 +00:00
Sebastian Kürten
276eeb1c20 Do not increment the attempt variable twice when requesting group info 2021-07-05 12:48:58 +02:00
Sebastian Kürten
d46cfb757e Do not allow the user to tap the start sharing button twice quickly 2021-07-05 10:32:49 +02:00
Torsten Grote
d81c4e7982 Merge branch '2065-transfer-data-ui' into '1802-sync-via-removable-storage'
Implement UI of transfer data feature

See merge request briar/briar!1486
2021-07-02 18:07:57 +00:00
Torsten Grote
8c4d6ed5e4 Remove guidelines for percent based laout width 2021-07-02 14:53:10 -03:00
akwizgran
b21b319cb7 Use guidelines to set image sizes. 2021-07-02 14:53:10 -03:00
Torsten Grote
780f6e97b9 Check if the chosen contact supports removable drive transport
and show message if not
2021-07-02 14:53:10 -03:00
Torsten Grote
1756215183 Combine transfer data graphics to reduce layout complexity
and make scaling work better on smaller screens
2021-07-02 14:53:09 -03:00
Torsten Grote
7e3db6c6df Address review feedback for Transfer Data UI 2021-07-02 14:53:09 -03:00
Torsten Grote
1adf408ade Migrate all image file pickers to ActivityResultLauncher
startActivityForResult is deprecated and the new API is nicer. Also, we can use the same launcher types in various places.
2021-07-02 14:53:09 -03:00
Torsten Grote
d662ae49ee Try to force file chooser to show internal/external storage by default 2021-07-02 14:53:08 -03:00
Torsten Grote
7939c8b213 Calculate percentages for send progress bar 2021-07-02 14:53:08 -03:00
Torsten Grote
1899873da3 Remove manual initial state and oldTask state argument
The latter is now handled via a LiveEvent
2021-07-02 14:53:08 -03:00
Torsten Grote
5beffb21f1 Hide Transfer Data feature behind feature flag 2021-07-02 14:53:05 -03:00
Torsten Grote
0f9afda329 Check if there is data to send and show a message if not 2021-07-02 14:46:08 -03:00
Torsten Grote
c16663b530 Always inform new observers about current state 2021-07-02 14:46:07 -03:00
Torsten Grote
77767b45c9 Re-organize conversations overflow menu 2021-07-02 14:46:07 -03:00
Torsten Grote
79ae8fea8d Transfer Data UI 2021-07-02 14:46:06 -03:00
Daniel Lublin
7e3eb1201a Start of UI for transfer data feature 2021-07-02 14:46:04 -03:00
akwizgran
eb283d81c5 Merge branch '2069-transport-key-agreement-integration-tests' into '1802-sync-via-removable-storage'
Integration tests for transport key agreement client

See merge request briar/briar!1492
2021-07-02 11:11:06 +00:00
Torsten Grote
be3700d364 Remove FIXME in test since we won't fix it this way 2021-06-30 16:57:32 -03:00
Torsten Grote
ccec17f28a Also test that messages arrive and activate keys 2021-06-30 16:49:03 -03:00
Torsten Grote
e8428925ae Add two more tests to TransportKeyAgreementIntegrationTest 2021-06-30 16:49:03 -03:00
Torsten Grote
195123e669 Ensure that private key is not stored anymore 2021-06-30 16:49:02 -03:00
Torsten Grote
abe570e905 Add first integration test for TransportKeyAgreementManager 2021-06-30 16:49:02 -03:00
Torsten Grote
a93b1f18ac Refactor base of BriarIntegrationTest into BrambleIntegrationTest 2021-06-30 16:49:02 -03:00
Torsten Grote
e4bd6fdf95 Put FeatureFlags for tests into a TestFeatureFlagModule 2021-06-30 16:49:01 -03:00
Torsten Grote
793d81bd93 Merge branch '2093-inconsistency-when-navigating-back-to-intro' into '1081-share-app-via-wifi-hotspot'
Don't move to HotspotFragment on rotate when user navigated back to introduction

See merge request briar/briar!1494
2021-06-30 15:16:33 +00:00
Torsten Grote
be637cef65 Merge branch '2091-supported-property' into '1802-sync-via-removable-storage'
Add a transport property to signal support for removable drives

See merge request briar/briar!1496
2021-06-30 14:53:15 +00:00
Sebastian Kürten
9370062e41 Don't move to HotspotFragment on rotate when user navigated back to introduction 2021-06-30 12:07:30 +02:00
akwizgran
8c1f721015 Add method for checking whether contact supports transport. 2021-06-28 16:51:30 +01:00
akwizgran
22ea4ced0d Add transport property to indicate support for removable drives. 2021-06-28 16:51:30 +01:00
Sebastian Kürten
312d31b40e Improve texts on offline hotspot for a better UX 2021-06-28 15:09:40 +02:00
Torsten Grote
b15d42b0cd Merge branch '2087-fix-issue-when-hotspot-activity-and-viewmodel-get-destroyed' into '1081-share-app-via-wifi-hotspot'
Fix bug that occurs when HotspotActivity gets destroyed

See merge request briar/briar!1489
2021-06-25 19:52:56 +00:00
Torsten Grote
9274b8ef4a Merge branch '2084-aborted-introduction-sessions' into '1802-sync-via-removable-storage'
Allow aborted introduction sessions to be retried

See merge request briar/briar!1490
2021-06-23 16:05:31 +00:00
Torsten Grote
7ef4ea51b3 Merge branch '2061-check-clock-sanity-at-startup' into '1802-sync-via-removable-storage'
Check whether system clock is reasonable at startup

See merge request briar/briar!1491
2021-06-23 15:59:52 +00:00
akwizgran
e285f21d1c Check whether system clock is reasonable at startup. 2021-06-23 16:40:42 +01:00
Sebastian Kürten
160cef25af Fix bug that occurs when HotspotActivity gets destroyed
If HotspotActivity gets destroyed, so will be its viewmodel, resulting
in an undefined state when the activity gets created again. While the
fragments will be restored, the view model and hotspot/webserver state
will not. Fix this by resetting the UI to reflect the reset of hotspot
and webserver.
2021-06-23 17:23:59 +02:00
akwizgran
c0293a1327 Merge branch '2070-transport-key-agreement-validator-test' into '1802-sync-via-removable-storage'
Add unit test for transport key agreement validator

See merge request briar/briar!1488
2021-06-23 10:16:52 +00:00
Torsten Grote
035c639aa0 Add TransportKeyAgreementValidatorTest 2021-06-22 17:20:47 -03:00
Torsten Grote
29d31e79c3 Merge branch '2086-fix-margin-on-qr-cardview' into '1081-share-app-via-wifi-hotspot'
Fix background color of cardview for qr code

See merge request briar/briar!1487
2021-06-22 11:49:58 +00:00
Sebastian Kürten
7f7210becd Fix background color of cardview for qr code 2021-06-22 09:46:34 +02:00
akwizgran
ce74fcaab5 Store ID of message that triggered abort. 2021-06-21 16:22:51 +01:00
Torsten Grote
d4c1e132f7 Merge branch '2077-anything-to-send' into '1802-sync-via-removable-storage'
Add DB method for checking whether there's anything to send

See merge request briar/briar!1485
2021-06-17 13:30:31 +00:00
akwizgran
6b976df6a8 Add RemovableDriveManager method. 2021-06-17 13:01:33 +01:00
Torsten Grote
3e4db3b9da Merge branch '2045-flexible-sync' into '1802-sync-via-removable-storage'
Make retransmissions in the sync protocol more flexible

See merge request briar/briar!1482
2021-06-16 17:40:25 +00:00
akwizgran
0bf59eec20 Add comment explaining second client versioning message. 2021-06-16 16:26:29 +01:00
akwizgran
9f828a2222 Add DB method for checking whether there's anything to send 2021-06-16 16:25:11 +01:00
Torsten Grote
7be77b8c60 Merge branch '2038-transport-key-agreement-client' into '1802-sync-via-removable-storage'
Add transport key agreement client

See merge request briar/briar!1474
2021-06-16 12:28:00 +00:00
akwizgran
d5853e8403 Add integration test for eager retransmission. 2021-06-16 12:29:49 +01:00
akwizgran
32e9bf01ec Update DB method that gets total size of messages to send. 2021-06-16 12:29:49 +01:00
akwizgran
a5ce400341 Use eager retransmission if the transport is lossy and cheap. 2021-06-16 12:29:49 +01:00
akwizgran
a960bfb2c1 Add tests for eager retransmission. 2021-06-16 12:29:49 +01:00
akwizgran
847650f280 Replace inner classes with lambdas. 2021-06-16 12:29:49 +01:00
akwizgran
77a3199aac Update SimplexOutgoingSession to support sending unacked messages. 2021-06-16 12:29:49 +01:00
akwizgran
9a58b37ce2 Add database methods for sending unacked messages. 2021-06-16 12:29:49 +01:00
Torsten Grote
608e1eac6b Merge branch '2071-removable-drive-task-refactoring' into '1802-sync-via-removable-storage'
Refactor removable drive task management

See merge request briar/briar!1480
2021-06-15 12:23:27 +00:00
akwizgran
09de768e7e Merge branch '2038-key-manager-methods' into '1802-sync-via-removable-storage'
Key manager changes to support transport key agreement client

See merge request briar/briar!1473
2021-06-15 10:55:31 +00:00
akwizgran
faab80f0ea Hold lock while calling notifyObservers(). 2021-06-15 11:47:10 +01:00
akwizgran
07162cad8b Refactor removable drive tasks. 2021-06-15 11:44:10 +01:00
Torsten Grote
a5a1cdfabb Merge branch '2039-implement-hotspot-error-fragment' into '1081-share-app-via-wifi-hotspot'
Resolve "Implement HotspotErrorFragment"

See merge request briar/briar!1469
2021-06-14 20:23:18 +00:00
Sebastian Kürten
bfcb469d49 Use FragmentContainerView for displaying FallbackFragment 2021-06-14 17:14:50 +02:00
Sebastian Kürten
f8b645d2b1 Improve hotspot error fragment UI
* Use different highlighting for error message
* Improve margins in fragment_hotspot_save_apk.xml
* Address some review feedback
2021-06-14 17:14:35 +02:00
Sebastian Kürten
052eb03c9e Pass error message to feedback activity 2021-06-14 17:13:14 +02:00
Sebastian Kürten
83bf3f4ca7 Create FallbackFragment for alternative apk sharing method 2021-06-14 17:12:02 +02:00
Sebastian Kürten
f9181fa021 Log hotspot errors 2021-06-14 17:10:52 +02:00
Sebastian Kürten
79dae27c24 Wire feedback button to show feedback fragment 2021-06-14 17:10:46 +02:00
akwizgran
ac0fc21e6e Merge branch '2038-db-methods' into '1802-sync-via-removable-storage'
New DB methods to support transport key agreement client

See merge request briar/briar!1472
2021-06-10 15:28:20 +00:00
akwizgran
dab736ce0e Merge branch '1802-defer-delivery' into '1802-sync-via-removable-storage'
Allow sync clients to defer delivery of messages

See merge request briar/briar!1471
2021-06-10 15:27:08 +00:00
Sebastian Kürten
8ef21637a9 Outline specific error fragment for hotspot 2021-06-09 18:53:59 +02:00
Sebastian Kürten
e3a1fca22e Let HotspotActivity implement BaseFragmentListener 2021-06-09 18:53:54 +02:00
Torsten Grote
ea9a2789ab Move hotspot help ActivityResultLauncher into method 2021-06-09 17:48:17 +02:00
Torsten Grote
cbdbd10cb3 Adapt hotspot buttons to latest design and add a nullability annotation 2021-06-09 17:48:17 +02:00
Torsten Grote
d6f985174a Make HotspotHelpFragment headlines bold 2021-06-09 17:48:17 +02:00
Torsten Grote
d184fbd3fe Handle returned Uri being null 2021-06-09 17:48:17 +02:00
Torsten Grote
ef623370b6 Save the APK as a hotspot fallback 2021-06-09 17:48:17 +02:00
Sebastian Kürten
5ac636d52d Add feature flag for sharing the app via offline hotspot 2021-06-09 17:48:17 +02:00
Sebastian Kürten
f1c71ec5a7 Recommend to undo settings to install apps from unknown sources 2021-06-09 17:48:16 +02:00
Torsten Grote
5cc280be61 Add missing hotspot nullability annotations 2021-06-09 17:48:16 +02:00
Torsten Grote
a5d8faef3c Move savedNetworkConfig into HotspotManager and use constructor injection 2021-06-09 17:48:16 +02:00
Torsten Grote
e22e9dcade Make hotspot SSID and passphrase persistent 2021-06-09 17:48:16 +02:00
Sebastian Kürten
7474ad8606 Use better filename for apk files shared via hotspot 2021-06-09 17:48:16 +02:00
Torsten Grote
1c3d90f7fc Show a snackbar when a peer connected to the hotspot 2021-06-09 17:48:16 +02:00
Torsten Grote
6f8d7167db Don't start hotspot while running and use proper ErrorFragment 2021-06-09 17:48:16 +02:00
Torsten Grote
99da50d37c Port code from Offline hotspot test app 2021-06-09 17:48:15 +02:00
Torsten Grote
15f5c8deee Fix hotspot notification on old APIs 2021-06-09 17:48:15 +02:00
Torsten Grote
7913cd322e Rename tab fragments
and remove redundant NonNull annotations
2021-06-09 17:48:15 +02:00
Torsten Grote
de8ad8f6f9 Show notification while hotspot is active 2021-06-09 17:48:15 +02:00
Torsten Grote
d0bc17e634 Add hotspot troubleshooting info 2021-06-09 17:48:15 +02:00
Torsten Grote
85433611a5 Add offline sharing entry point to Settings/Actions 2021-06-09 17:48:15 +02:00
Torsten Grote
ebd5879761 Let info screens scroll in case of insufficient space 2021-06-09 17:48:15 +02:00
Torsten Grote
b255ab07ae Implement info screens for offline app sharing 2021-06-09 17:48:14 +02:00
Torsten Grote
a86ba50dec Implement intro screen for offline app sharing 2021-06-09 17:48:09 +02:00
Torsten Grote
bcbc96dc2d Merge branch '2037-create-removabledriveviewmodel' into '1802-sync-via-removable-storage'
Add RemovableDriveViewModel

See merge request briar/briar!1475
2021-06-09 11:32:10 +00:00
akwizgran
a72e92de24 Timestamp isn't needed for deriving root key. 2021-06-09 10:08:07 +01:00
Daniel Lublin
1ddcd6cfff Make pkg private 2021-06-08 20:31:23 +02:00
akwizgran
5dfd9e3546 Make tests more readable. 2021-06-08 17:13:18 +01:00
akwizgran
e05575b956 Add unit tests for addRotationKeys() methods. 2021-06-08 15:51:29 +01:00
Daniel Lublin
fd810f5c16 Move to new removabledrive package 2021-06-08 12:25:09 +02:00
Daniel Lublin
3f5e131250 Use US locale for now 2021-06-08 12:18:33 +02:00
Daniel Lublin
3ee516599d Add initial RemovableDriveViewModel 2021-06-07 13:17:50 +02:00
akwizgran
c703d90636 Remove unused remote timestamp from session. 2021-06-01 14:50:14 +01:00
akwizgran
e228b9fcbf Add transport key agreement client. 2021-06-01 14:18:02 +01:00
akwizgran
6e6cadd3ad Refactor KeyManager startup so managers are created earlier. 2021-06-01 14:17:12 +01:00
akwizgran
9cc8d44778 Add a key manager method for adding a single set of transport keys. 2021-06-01 11:34:27 +01:00
akwizgran
ee6f571c31 Add a DB method for checking whether transport keys exist. 2021-06-01 11:34:26 +01:00
akwizgran
2ac3bdd3ae Add database method for getting transports with keys. 2021-06-01 11:34:26 +01:00
akwizgran
e35ffe0cf0 Add javadocs for message states. 2021-06-01 11:33:06 +01:00
akwizgran
8a04d8edc4 Allow sync clients to defer delivery of messages. 2021-06-01 11:24:55 +01:00
akwizgran
a5fb3bb4a4 Merge branch '2016-2017-2018-removable-drive-reader-writer' into '1802-sync-via-removable-storage'
Create removable drive manager and reader/writer tasks

See merge request briar/briar!1458
2021-05-11 14:01:53 +00:00
akwizgran
eae329cdfa Refactor manager and tasks to remove reliance on files. 2021-05-11 12:19:16 +01:00
akwizgran
0ce0551f0d Update progress of writer task. 2021-05-11 12:19:16 +01:00
akwizgran
a198e7d08e Ensure that observers see the final state even if they're added late. 2021-05-11 12:19:16 +01:00
akwizgran
bca6f1506e Add integration test for syncing via removable drives. 2021-05-11 12:19:16 +01:00
akwizgran
e420201b00 Implement RemovableDriveWriterTask, except for progress updates. 2021-05-11 12:19:16 +01:00
akwizgran
03248d04e5 Fix typo in class names. 2021-05-11 12:19:16 +01:00
akwizgran
2c39b02644 Implement RemovableDriverReaderTask. 2021-05-11 12:19:16 +01:00
akwizgran
c9c6f3682c Add task factory. 2021-05-11 12:19:16 +01:00
akwizgran
8f4a0ef030 Add removable drive manager with placeholder task implementations. 2021-05-11 12:19:14 +01:00
akwizgran
5fe22bcd57 Merge branch '2035-android-removable-drive-plugin' into '1802-sync-via-removable-storage'
Add Android implementation of RemovableDrivePlugin

See merge request briar/briar!1457
2021-05-11 11:13:14 +00:00
akwizgran
b4880af7e2 Add Android implementation of RemovableDrivePlugin. 2021-05-10 14:19:24 +01:00
akwizgran
51d21bd669 Decouple RemovableDrivePlugin from FileConstants. 2021-05-10 13:48:12 +01:00
Torsten Grote
b8f3728a0d Merge branch '2015-removable-drive-plugin' into '1802-sync-via-removable-storage'
Create RemovableDrivePlugin

See merge request briar/briar!1454
2021-05-10 12:47:21 +00:00
akwizgran
bbfd4f137d Merge branch '2013-db-method-for-amount-of-data-to-sync' into '1802-sync-via-removable-storage'
Add DB method for getting amount of data to sync

See merge request briar/briar!1452
2021-05-10 12:00:11 +00:00
Daniel Lublin
7e3ca76dd1 Merge branch '2014-messages-sent-event' into '1802-sync-via-removable-storage'
Update MessagesSentEvent to include amount of data sent

See merge request briar/briar!1453
2021-05-10 11:44:27 +00:00
akwizgran
524c8d26f8 Don't inject default RemovableDrivePluginFactory on Android. 2021-05-07 17:48:39 +01:00
akwizgran
7eccf7dac1 Decouple removable drive plugin from java.io.File for portability. 2021-05-07 17:36:10 +01:00
akwizgran
0bc06248ed Clean up plugin injection code, remove unused module. 2021-05-06 16:59:45 +01:00
akwizgran
c999f05cc7 Configure removable drive plugin for Android. 2021-05-06 16:59:45 +01:00
akwizgran
428269b312 Add removable drive plugin. 2021-05-06 16:59:45 +01:00
akwizgran
588e05ce83 Update MessagesSentEvent to include amount of data sent. 2021-05-06 16:20:15 +01:00
akwizgran
f7875c99b6 Add DB method for getting amount of data to sync. 2021-05-05 17:52:37 +01:00
75 changed files with 3377 additions and 688 deletions

View File

@@ -15,8 +15,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
versionCode 10306 versionCode 10305
versionName "1.3.6" versionName "1.3.5"
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -20,7 +20,7 @@ import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.I
public class AndroidRemovableDrivePluginFactory implements public class AndroidRemovableDrivePluginFactory implements
SimplexPluginFactory { SimplexPluginFactory {
private static final long MAX_LATENCY = DAYS.toMillis(28); private static final int MAX_LATENCY = (int) DAYS.toMillis(14);
private final Application app; private final Application app;

View File

@@ -13,5 +13,7 @@ public interface FeatureFlags {
boolean shouldEnableConnectViaBluetooth(); boolean shouldEnableConnectViaBluetooth();
boolean shouldEnableShareAppViaOfflineHotspot();
boolean shouldEnableTransferData(); boolean shouldEnableTransferData();
} }

View File

@@ -14,9 +14,9 @@ public interface RemovableDriveTask extends Runnable {
TransportProperties getTransportProperties(); TransportProperties getTransportProperties();
/** /**
* Adds an observer to the task. The observer will be notified on the * Adds an observer to the task. The observer will be notified of state
* event thread of the current state of the task and any subsequent state * changes on the event thread. If the task has already finished, the
* changes. * observer will be notified of its final state.
*/ */
void addObserver(Consumer<State> observer); void addObserver(Consumer<State> observer);

View File

@@ -2331,7 +2331,7 @@ abstract class JdbcDatabase implements Database<Connection> {
ps.setInt(2, DELIVERED.getValue()); ps.setInt(2, DELIVERED.getValue());
rs = ps.executeQuery(); rs = ps.executeQuery();
rs.next(); rs.next();
long total = rs.getLong(1); long total = rs.getInt(1);
rs.close(); rs.close();
ps.close(); ps.close();
return total; return total;

View File

@@ -30,6 +30,11 @@ public class TestFeatureFlagModule {
return true; return true;
} }
@Override
public boolean shouldEnableShareAppViaOfflineHotspot() {
return true;
}
@Override @Override
public boolean shouldEnableTransferData() { public boolean shouldEnableTransferData() {
return true; return true;

View File

@@ -26,8 +26,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 30
versionCode 10306 versionCode 10305
versionName "1.3.6" versionName "1.3.5"
applicationId "org.briarproject.briar.android" applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@@ -121,6 +121,7 @@ dependencies {
exclude group: 'com.android.support' exclude group: 'com.android.support'
exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it
} }
implementation 'org.nanohttpd:nanohttpd:2.3.1'
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version" annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"

View File

@@ -3,7 +3,6 @@ package org.briarproject.briar.android;
import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.plugin.file.RemovableDriveModule;
import org.briarproject.bramble.system.ClockModule; import org.briarproject.bramble.system.ClockModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.account.SignInTestCreateAccount; import org.briarproject.briar.android.account.SignInTestCreateAccount;
@@ -22,7 +21,6 @@ import dagger.Component;
AttachmentModule.class, AttachmentModule.class,
ClockModule.class, ClockModule.class,
MediaModule.class, MediaModule.class,
RemovableDriveModule.class,
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,
BriarAccountModule.class, BriarAccountModule.class,

View File

@@ -18,6 +18,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
@@ -343,13 +344,7 @@
<activity <activity
android:name="org.briarproject.briar.android.StartupFailureActivity" android:name="org.briarproject.briar.android.StartupFailureActivity"
android:excludeFromRecents="true" android:label="@string/startup_failed_activity_title" />
android:exported="false"
android:finishOnTaskLaunch="true"
android:label="@string/startup_failed_activity_title"
android:launchMode="singleInstance"
android:process=":briar_startup_failure"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity <activity
android:name="org.briarproject.briar.android.settings.SettingsActivity" android:name="org.briarproject.briar.android.settings.SettingsActivity"
@@ -456,6 +451,11 @@
android:label="@string/pending_contact_requests" android:label="@string/pending_contact_requests"
android:theme="@style/BriarTheme" /> android:theme="@style/BriarTheme" />
<activity
android:name=".android.hotspot.HotspotActivity"
android:label="@string/hotspot_title"
android:theme="@style/BriarTheme" />
</application> </application>
<queries> <queries>

View File

@@ -0,0 +1,103 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
background-color: #F2F2F2;
font-family: Roboto,Arial,Helvetica,sans-serif;
font-size: 14px;
margin: 0;
height: 100%;
}
div#top {
background-color: #FFFFFF;
padding: 16px;
}
div#bottom {
padding: 16px 32px;
margin-top: 12px;
}
a.button {
background-color: #82C91E;
width: 100%;
display: block;
box-sizing: border-box;
padding: 12px 32px !important;
border: 1px solid transparent;
border-radius: 2px;
color: #000000 !important;
cursor: pointer;
font-weight: 500;
text-decoration: none;
text-transform: uppercase;
text-align: center;
margin: 20px auto 20px auto;
}
ol {
list-style: none;
counter-reset: briar-counter;
padding-left: 40px;
}
ol li {
counter-increment: briar-counter;
margin-bottom: 2em;
}
ol li::before {
content: counter(briar-counter);
background-color: #82C91E;
color: #000000 !important;
font-weight: bold;
border-radius: 70px;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
position: absolute;
left: 32px;
}
</style>
</head>
<body>
<div id="top">
<svg style="width:156px;height:47px;" viewBox="0 0 778 235">
<path style="fill:#87c214"
d="m 64.900391,0 c -9.7,0 -17.701172,7.9992183 -17.701172,17.699219 v 22.5 h 43.601562 v -22.5 C 90.800781,7.9992183 82.899219,0 73.199219,0 Z m 96.999999,0 c -9.7,0 -17.70117,7.9992183 -17.70117,17.699219 V 137.19922 h 43.60156 V 17.699219 C 187.80078,7.9992183 179.89922,0 170.19922,0 Z M 47.199219,97.800781 V 217.30078 c 0,9.7 7.901172,17.69922 17.701172,17.69922 h 8.298828 c 9.7,0 17.701172,-7.99922 17.701172,-17.69922 V 97.800781 Z m 97.000001,96.999999 v 22.5 c 0,9.7 8.00117,17.69922 17.70117,17.69922 h 8.29883 c 9.7,0 17.70117,-7.99922 17.70117,-17.69922 v -22.5 z"/>
<path style="fill:#95d220"
d="M 17.699219,47.199219 C 7.9992186,47.199219 0,55.100391 0,64.900391 v 8.298828 c 0,9.7 7.8992186,17.701172 17.699219,17.701172 H 137.19922 V 47.199219 Z m 177.101561,0 v 43.701172 h 22.5 c 9.7,0 17.69922,-7.901172 17.69922,-17.701172 v -8.298828 c 0,-9.8 -7.99922,-17.701172 -17.69922,-17.701172 z M 17.699219,144.19922 C 7.9992186,144.19922 0,152.10039 0,161.90039 v 8.29883 c 0,9.7 7.8992186,17.70117 17.699219,17.70117 h 22.5 v -43.70117 z m 80.101562,0 v 43.70117 H 217.30078 c 9.7,0 17.69922,-8.00117 17.69922,-17.70117 v -8.29883 c 0,-9.8 -7.99922,-17.70117 -17.69922,-17.70117 z"/>
<path d="M 301,60.564864 V 174.43514 h 53.31362 c 25.13729,0 38.31622,-12.58548 38.31622,-32.27441 0,-12.78766 -5.88,-22.32687 -17.63776,-27.60431 v -0.20217 c 8.91968,-5.48043 12.77339,-12.38249 12.77339,-23.140374 0,-16.238294 -11.14945,-30.648991 -34.66495,-30.648991 z m 110.68683,0 V 174.43514 h 13.37598 v -45.67022 l -1.41529,-1.41926 h 26.95811 c 15.00127,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 h 15.00139 l -16.82503,-35.52128 c -3.64896,-7.91617 -9.52848,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36593,-3.24765 22.70429,-14.41228 22.70429,-29.229734 0,-22.530633 -17.43208,-33.693671 -38.31224,-33.693671 z m 111.08726,0 V 174.43514 h 13.37992 V 60.564864 Z m 78.65821,0 -50.07469,113.870276 h 14.59701 l 12.16287,-27.40213 -0.60656,-1.41926 h 62.2336 l -0.60655,1.41926 12.16286,27.40213 h 14.59701 L 615.62098,60.564864 Z m 79.463,0 V 174.43514 h 13.37994 v -45.67022 l -1.41927,-1.41926 h 26.96209 c 15.00128,0 23.51842,5.27428 28.99185,17.04704 l 14.1887,30.04244 H 778 l -16.82503,-35.52128 c -3.64895,-7.91617 -9.52851,-12.99064 -14.79921,-15.22341 v -0.20216 c 12.36591,-3.24765 22.70427,-14.41228 22.70427,-29.229734 0,-22.530633 -17.43209,-33.693671 -38.31223,-33.693671 z M 312.96068,73.147961 h 38.72057 c 14.59584,0 22.29593,5.887175 22.29593,18.065895 0,10.148944 -6.07834,18.268094 -22.29593,18.268094 h -38.72057 l 1.41927,-1.41927 V 74.571187 Z m 110.68684,0 h 37.90786 c 13.78495,0 24.32519,5.684988 24.52791,20.908395 0,12.178724 -9.52687,20.702244 -25.94718,20.702244 h -36.48859 l 1.41529,-1.41927 V 74.571187 Z m 269.00626,0 h 37.90788 c 13.98769,0 24.53187,5.684988 24.53187,20.908395 0,12.178724 -9.52688,20.702244 -25.94718,20.702244 h -36.49257 l 1.41927,-1.41927 V 74.571187 Z m -83.92693,1.423226 h 0.20615 l 3.44509,11.366019 20.06794,45.670224 1.41924,1.41926 h -50.07071 l 1.41926,-1.41926 20.06793,-45.670224 z M 312.96068,122.06505 h 41.35294 c 16.82575,0 24.53189,7.71398 24.53189,20.09568 0,12.58468 -7.09797,19.69131 -24.53189,19.69131 h -41.35294 l 1.41927,-1.42322 v -36.94055 z"/>
</svg>
<h2 id="download_title">Download Briar 1.2.20</h2>
<span id="download_intro">Someone nearby shared Briar with you.</span>
<a href="/app.apk" class="button">
<svg aria-hidden="true"
style="width:24px;height:24px;margin-right:6px;vertical-align:middle;"
viewBox="0 0 24 24">
<path fill="currentColor" d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/>
</svg>
<span id="download_button">Download Briar</span>
</a>
<span id="download_outro">After the download is complete, open the downloaded file and install it.</span>
</div>
<div id="bottom">
<h3 id="troubleshooting_title">Troubleshooting</h3>
<ol>
<li id="troubleshooting_1">If you can't download the app, try it with a different web
browser app.
</li>
<li id="troubleshooting_2">Ensure that your browser is allowed to download apps directly by
giving it the permission or enabling the installation of apps from "Unknown Sources" in
system settings.
</li>
</ol>
</div>
</body>
</html>

View File

@@ -36,6 +36,11 @@ import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader; import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.hotspot.AbstractTabsFragment;
import org.briarproject.briar.android.hotspot.FallbackFragment;
import org.briarproject.briar.android.hotspot.HotspotIntroFragment;
import org.briarproject.briar.android.hotspot.ManualHotspotFragment;
import org.briarproject.briar.android.hotspot.QrHotspotFragment;
import org.briarproject.briar.android.logging.CachingLogHandler; import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.removabledrive.ChooserFragment; import org.briarproject.briar.android.removabledrive.ChooserFragment;
@@ -216,6 +221,16 @@ public interface AndroidComponent
void inject(NotificationsFragment notificationsFragment); void inject(NotificationsFragment notificationsFragment);
void inject(HotspotIntroFragment hotspotIntroFragment);
void inject(AbstractTabsFragment abstractTabsFragment);
void inject(QrHotspotFragment qrHotspotFragment);
void inject(ManualHotspotFragment manualHotspotFragment);
void inject(FallbackFragment fallbackFragment);
void inject(ChooserFragment chooserFragment); void inject(ChooserFragment chooserFragment);
void inject(SendFragment sendFragment); void inject(SendFragment sendFragment);

View File

@@ -30,6 +30,7 @@ import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.ConversationActivity; import org.briarproject.briar.android.conversation.ConversationActivity;
import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.hotspot.HotspotActivity;
import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity; import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity; import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
@@ -63,9 +64,11 @@ import static android.app.Notification.DEFAULT_SOUND;
import static android.app.Notification.DEFAULT_VIBRATE; import static android.app.Notification.DEFAULT_VIBRATE;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.PendingIntent.getActivity;
import static android.content.Context.NOTIFICATION_SERVICE; import static android.content.Context.NOTIFICATION_SERVICE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE; import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
import static androidx.core.app.NotificationCompat.CATEGORY_SERVICE; import static androidx.core.app.NotificationCompat.CATEGORY_SERVICE;
@@ -274,7 +277,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
b.setWhen(0); // Don't show the time b.setWhen(0); // Don't show the time
b.setOngoing(true); b.setOngoing(true);
Intent i = new Intent(appContext, SplashScreenActivity.class); Intent i = new Intent(appContext, SplashScreenActivity.class);
b.setContentIntent(PendingIntent.getActivity(appContext, 0, i, 0)); b.setContentIntent(getActivity(appContext, 0, i, 0));
if (SDK_INT >= 21) { if (SDK_INT >= 21) {
b.setCategory(CATEGORY_SERVICE); b.setCategory(CATEGORY_SERVICE);
b.setVisibility(VISIBILITY_SECRET); b.setVisibility(VISIBILITY_SECRET);
@@ -619,13 +622,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
public void showSignInNotification() { public void showSignInNotification() {
if (blockSignInReminder) return; if (blockSignInReminder) return;
if (SDK_INT >= 26) { if (SDK_INT >= 26) {
NotificationChannel channel = NotificationChannel channel = new NotificationChannel(
new NotificationChannel(REMINDER_CHANNEL_ID, appContext REMINDER_CHANNEL_ID, appContext
.getString( .getString(R.string.reminder_notification_channel_title),
R.string.reminder_notification_channel_title), IMPORTANCE_LOW);
IMPORTANCE_LOW); channel.setLockscreenVisibility(VISIBILITY_SECRET);
channel.setLockscreenVisibility(
NotificationCompat.VISIBILITY_SECRET);
notificationManager.createNotificationChannel(channel); notificationManager.createNotificationChannel(channel);
} }
@@ -652,7 +653,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
Intent i = new Intent(appContext, SplashScreenActivity.class); Intent i = new Intent(appContext, SplashScreenActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
b.setContentIntent(PendingIntent.getActivity(appContext, 0, i, 0)); b.setContentIntent(getActivity(appContext, 0, i, 0));
notificationManager.notify(REMINDER_NOTIFICATION_ID, b.build()); notificationManager.notify(REMINDER_NOTIFICATION_ID, b.build());
} }
@@ -720,4 +721,40 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
public void unblockAllBlogPostNotifications() { public void unblockAllBlogPostNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = false); androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = false);
} }
@Override
public void showHotspotNotification() {
if (SDK_INT >= 26) {
String channelTitle = appContext
.getString(R.string.hotspot_notification_channel_title);
NotificationChannel channel = new NotificationChannel(
HOTSPOT_CHANNEL_ID, channelTitle, IMPORTANCE_LOW);
channel.setLockscreenVisibility(VISIBILITY_SECRET);
notificationManager.createNotificationChannel(channel);
}
BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext, HOTSPOT_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_hotspot);
b.setColorRes(R.color.briar_brand_green);
b.setContentTitle(
appContext.getText(R.string.hotspot_notification_title));
b.setNotificationCategory(CATEGORY_SERVICE);
b.setOngoing(true);
b.setShowWhen(true);
String actionTitle =
appContext.getString(R.string.hotspot_button_stop_sharing);
Intent i = new Intent(appContext, HotspotActivity.class);
i.addFlags(FLAG_ACTIVITY_SINGLE_TOP);
i.setAction(ACTION_STOP_HOTSPOT);
PendingIntent actionIntent = getActivity(appContext, 0, i, 0);
int icon = SDK_INT >= 21 ? R.drawable.ic_portable_wifi_off : 0;
b.addAction(icon, actionTitle, actionIntent);
notificationManager.notify(HOTSPOT_NOTIFICATION_ID, b.build());
}
@Override
public void clearHotspotNotification() {
notificationManager.cancel(HOTSPOT_NOTIFICATION_ID);
}
} }

View File

@@ -36,6 +36,7 @@ import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.contact.ContactListModule; import org.briarproject.briar.android.contact.ContactListModule;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactModule; import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactModule;
import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.hotspot.HotspotModule;
import org.briarproject.briar.android.introduction.IntroductionModule; import org.briarproject.briar.android.introduction.IntroductionModule;
import org.briarproject.briar.android.logging.LoggingModule; import org.briarproject.briar.android.logging.LoggingModule;
import org.briarproject.briar.android.login.LoginModule; import org.briarproject.briar.android.login.LoginModule;
@@ -94,6 +95,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
GroupListModule.class, GroupListModule.class,
GroupConversationModule.class, GroupConversationModule.class,
SharingModule.class, SharingModule.class,
HotspotModule.class,
TransferDataModule.class, TransferDataModule.class,
}) })
public class AppModule { public class AppModule {
@@ -155,8 +157,7 @@ public class AppModule {
@Singleton @Singleton
PluginConfig providePluginConfig(AndroidBluetoothPluginFactory bluetooth, PluginConfig providePluginConfig(AndroidBluetoothPluginFactory bluetooth,
AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan, AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan,
AndroidRemovableDrivePluginFactory drive, AndroidRemovableDrivePluginFactory drive) {
FeatureFlags featureFlags) {
@NotNullByDefault @NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() { PluginConfig pluginConfig = new PluginConfig() {
@@ -167,11 +168,7 @@ public class AppModule {
@Override @Override
public Collection<SimplexPluginFactory> getSimplexFactories() { public Collection<SimplexPluginFactory> getSimplexFactories() {
if (SDK_INT >= 19 && featureFlags.shouldEnableTransferData()) { return SDK_INT >= 19 ? singletonList(drive) : emptyList();
return singletonList(drive);
} else {
return emptyList();
}
} }
@Override @Override
@@ -318,6 +315,11 @@ public class AppModule {
public boolean shouldEnableTransferData() { public boolean shouldEnableTransferData() {
return IS_DEBUG_BUILD; return IS_DEBUG_BUILD;
} }
@Override
public boolean shouldEnableShareAppViaOfflineHotspot() {
return IS_DEBUG_BUILD;
}
}; };
} }
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
@@ -33,7 +34,11 @@ import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.core.app.NotificationCompat;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.content.Intent.ACTION_SHUTDOWN; import static android.content.Intent.ACTION_SHUTDOWN;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
@@ -50,6 +55,7 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResul
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY; import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_CHANNEL_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_NOTIFICATION_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_OLD_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_OLD_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_NOTIFICATION_ID; import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_NOTIFICATION_ID;
@@ -60,6 +66,8 @@ public class BriarService extends Service {
public static String EXTRA_START_RESULT = public static String EXTRA_START_RESULT =
"org.briarproject.briar.START_RESULT"; "org.briarproject.briar.START_RESULT";
public static String EXTRA_NOTIFICATION_ID =
"org.briarproject.briar.FAILURE_NOTIFICATION_ID";
public static String EXTRA_STARTUP_FAILED = public static String EXTRA_STARTUP_FAILED =
"org.briarproject.briar.STARTUP_FAILED"; "org.briarproject.briar.STARTUP_FAILED";
@@ -127,11 +135,12 @@ public class BriarService extends Service {
ongoingChannel.setLockscreenVisibility(VISIBILITY_SECRET); ongoingChannel.setLockscreenVisibility(VISIBILITY_SECRET);
ongoingChannel.setShowBadge(false); ongoingChannel.setShowBadge(false);
nm.createNotificationChannel(ongoingChannel); nm.createNotificationChannel(ongoingChannel);
// Delete the unused channel previously used for startup NotificationChannel failureChannel = new NotificationChannel(
// failure notifications FAILURE_CHANNEL_ID,
// TODO: Remove this ID after a reasonable upgrade period getString(R.string.startup_failed_notification_title),
// (added 2021-07-12) IMPORTANCE_DEFAULT);
nm.deleteNotificationChannel(FAILURE_CHANNEL_ID); failureChannel.setLockscreenVisibility(VISIBILITY_SECRET);
nm.createNotificationChannel(failureChannel);
} }
Notification foregroundNotification = Notification foregroundNotification =
notificationManager.getForegroundNotification(); notificationManager.getForegroundNotification();
@@ -147,7 +156,7 @@ public class BriarService extends Service {
} else { } else {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.warning("Startup failed: " + result); LOG.warning("Startup failed: " + result);
showStartupFailure(result); showStartupFailureNotification(result);
stopSelf(); stopSelf();
} }
}, "LifecycleStartup"); }, "LifecycleStartup");
@@ -173,13 +182,29 @@ public class BriarService extends Service {
Localizer.getInstance().setLocale(this); Localizer.getInstance().setLocale(this);
} }
private void showStartupFailure(StartResult result) { private void showStartupFailureNotification(StartResult result) {
androidExecutor.runOnUiThread(() -> { androidExecutor.runOnUiThread(() -> {
// Bring the entry activity to the front to clear the back stack NotificationCompat.Builder b = new NotificationCompat.Builder(
Intent i = new Intent(BriarService.this, ENTRY_ACTIVITY); BriarService.this, FAILURE_CHANNEL_ID);
b.setSmallIcon(android.R.drawable.stat_notify_error);
b.setContentTitle(getText(
R.string.startup_failed_notification_title));
b.setContentText(getText(
R.string.startup_failed_notification_text));
Intent i = new Intent(BriarService.this,
StartupFailureActivity.class);
i.setFlags(FLAG_ACTIVITY_NEW_TASK);
i.putExtra(EXTRA_START_RESULT, result);
i.putExtra(EXTRA_NOTIFICATION_ID, FAILURE_NOTIFICATION_ID);
b.setContentIntent(PendingIntent.getActivity(BriarService.this,
0, i, FLAG_UPDATE_CURRENT));
NotificationManager nm = (NotificationManager)
requireNonNull(getSystemService(NOTIFICATION_SERVICE));
nm.notify(FAILURE_NOTIFICATION_ID, b.build());
// Bring the dashboard to the front to clear the back stack
i = new Intent(BriarService.this, ENTRY_ACTIVITY);
i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
i.putExtra(EXTRA_STARTUP_FAILED, true); i.putExtra(EXTRA_STARTUP_FAILED, true);
i.putExtra(EXTRA_START_RESULT, result);
startActivity(i); startActivity(i);
}); });
} }

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android; package org.briarproject.briar.android;
import android.app.NotificationManager;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
@@ -14,7 +15,9 @@ import org.briarproject.briar.android.fragment.ErrorFragment;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult;
import static org.briarproject.briar.android.BriarService.EXTRA_NOTIFICATION_ID;
import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT; import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -38,6 +41,14 @@ public class StartupFailureActivity extends BaseActivity implements
private void handleIntent(Intent i) { private void handleIntent(Intent i) {
StartResult result = StartResult result =
(StartResult) i.getSerializableExtra(EXTRA_START_RESULT); (StartResult) i.getSerializableExtra(EXTRA_START_RESULT);
int notificationId = i.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
// cancel notification
if (notificationId > -1) {
Object o = getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) requireNonNull(o);
nm.cancel(notificationId);
}
// show proper error message // show proper error message
int errorRes; int errorRes;
@@ -67,4 +78,5 @@ public class StartupFailureActivity extends BaseActivity implements
public void runOnDbThread(@NonNull Runnable runnable) { public void runOnDbThread(@NonNull Runnable runnable) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
} }

View File

@@ -38,6 +38,7 @@ import org.briarproject.briar.android.forum.CreateForumActivity;
import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.forum.ForumListFragment; import org.briarproject.briar.android.forum.ForumListFragment;
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.hotspot.HotspotActivity;
import org.briarproject.briar.android.introduction.ContactChooserFragment; import org.briarproject.briar.android.introduction.ContactChooserFragment;
import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.introduction.IntroductionMessageFragment; import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
@@ -177,6 +178,8 @@ public interface ActivityComponent {
void inject(CrashReportActivity crashReportActivity); void inject(CrashReportActivity crashReportActivity);
void inject(HotspotActivity hotspotActivity);
void inject(RemovableDriveActivity activity); void inject(RemovableDriveActivity activity);
// Fragments // Fragments

View File

@@ -46,6 +46,7 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS; import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard; import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.showFragment;
/** /**
* Warning: Some activities don't extend {@link BaseActivity}. * Warning: Some activities don't extend {@link BaseActivity}.
@@ -177,13 +178,7 @@ public abstract class BaseActivity extends AppCompatActivity
public void showNextFragment(BaseFragment f) { public void showNextFragment(BaseFragment f) {
if (!getLifecycle().getCurrentState().isAtLeast(STARTED)) return; if (!getLifecycle().getCurrentState().isAtLeast(STARTED)) return;
getSupportFragmentManager().beginTransaction() showFragment(getSupportFragmentManager(), f, f.getUniqueTag());
.setCustomAnimations(R.anim.step_next_in,
R.anim.step_previous_out, R.anim.step_previous_in,
R.anim.step_next_out)
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
.addToBackStack(f.getUniqueTag())
.commit();
} }
protected boolean isFragmentAdded(String fragmentTag) { protected boolean isFragmentAdded(String fragmentTag) {

View File

@@ -53,6 +53,7 @@ import org.briarproject.briar.android.contact.add.nearby.AddContactState.Contact
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementListening; import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementListening;
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementStarted; import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementStarted;
import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementWaiting; import org.briarproject.briar.android.contact.add.nearby.AddContactState.KeyAgreementWaiting;
import org.briarproject.briar.android.util.QrCodeUtils;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;

View File

@@ -17,7 +17,7 @@ import androidx.annotation.Nullable;
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ErrorFragment extends BaseFragment { public class ErrorFragment extends BaseFragment {
private static final String TAG = ErrorFragment.class.getName(); public static final String TAG = ErrorFragment.class.getName();
private static final String ERROR_MSG = "errorMessage"; private static final String ERROR_MSG = "errorMessage";
@@ -40,8 +40,7 @@ public class ErrorFragment extends BaseFragment {
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Bundle args = getArguments(); Bundle args = requireArguments();
if (args == null) throw new AssertionError();
errorMessage = args.getString(ERROR_MSG); errorMessage = args.getString(ERROR_MSG);
} }

View File

@@ -0,0 +1,129 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import static androidx.core.app.ActivityCompat.finishAfterTransition;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.util.UiUtils.showFragment;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class AbstractTabsFragment extends Fragment {
static String ARG_FOR_WIFI_CONNECT = "forWifiConnect";
@Inject
ViewModelProvider.Factory viewModelFactory;
protected HotspotViewModel viewModel;
protected Button stopButton;
protected Button connectedButton;
@Override
public void onAttach(Context context) {
super.onAttach(context);
getAndroidComponent(requireContext()).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(HotspotViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
setHasOptionsMenu(true);
return inflater
.inflate(R.layout.fragment_hotspot_tabs, container, false);
}
@Override
@CallSuper
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
TabAdapter tabAdapter = new TabAdapter(this);
ViewPager2 viewPager = view.findViewById(R.id.pager);
viewPager.setAdapter(tabAdapter);
TabLayout tabLayout = view.findViewById(R.id.tabLayout);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
// tabs are set in XML, but are just dummies that don't get added
if (position == 0) {
tab.setText(R.string.hotspot_tab_manual);
tab.setIcon(R.drawable.forum_item_create_white);
} else if (position == 1) {
tab.setText(R.string.qr_code);
tab.setIcon(R.drawable.ic_qr_code);
} else throw new AssertionError();
}).attach();
stopButton = view.findViewById(R.id.stopButton);
stopButton.setOnClickListener(v -> {
// also clears hotspot
finishAfterTransition(requireActivity());
});
connectedButton = view.findViewById(R.id.connectedButton);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.hotspot_help_action, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_help) {
Fragment f = new HotspotHelpFragment();
String tag = HotspotHelpFragment.TAG;
showFragment(getParentFragmentManager(), f, tag);
return true;
}
return super.onOptionsItemSelected(item);
}
protected abstract Fragment getFirstFragment();
protected abstract Fragment getSecondFragment();
private class TabAdapter extends FragmentStateAdapter {
private TabAdapter(Fragment fragment) {
super(fragment);
}
@Override
public Fragment createFragment(int position) {
if (position == 0) return getFirstFragment();
if (position == 1) return getSecondFragment();
throw new AssertionError();
}
@Override
public int getItemCount() {
return 2;
}
}
}

View File

@@ -0,0 +1,171 @@
package org.briarproject.briar.android.hotspot;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.net.wifi.WifiManager;
import android.provider.Settings;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.content.Context.WIFI_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
/**
* This class ensures that the conditions to open a hotspot are fulfilled.
* <p>
* Be sure to call {@link #onRequestPermissionResult(Boolean)} and
* {@link #onRequestWifiEnabledResult()} when you get the
* {@link ActivityResult}.
* <p>
* As soon as {@link #checkAndRequestConditions()} returns true,
* all conditions are fulfilled.
*/
@NotNullByDefault
class ConditionManager {
private enum Permission {
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
}
private Permission locationPermission = Permission.UNKNOWN;
private Permission wifiSetting = Permission.SHOW_RATIONALE;
private final FragmentActivity ctx;
private final WifiManager wifiManager;
private final ActivityResultLauncher<String> locationRequest;
private final ActivityResultLauncher<Intent> wifiRequest;
ConditionManager(FragmentActivity ctx,
ActivityResultLauncher<String> locationRequest,
ActivityResultLauncher<Intent> wifiRequest) {
this.ctx = ctx;
this.wifiManager = (WifiManager) ctx.getApplicationContext()
.getSystemService(WIFI_SERVICE);
this.locationRequest = locationRequest;
this.wifiRequest = wifiRequest;
}
/**
* Call this to reset state when UI starts,
* because state might have changed.
*/
void resetPermissions() {
locationPermission = Permission.UNKNOWN;
wifiSetting = Permission.SHOW_RATIONALE;
}
/**
* This makes a request for location permission.
* If {@link #checkAndRequestConditions()} returns true, you can continue.
*/
void startConditionChecks() {
locationRequest.launch(ACCESS_FINE_LOCATION);
}
/**
* @return true if conditions are fulfilled and flow can continue.
*/
boolean checkAndRequestConditions() {
if (areEssentialPermissionsGranted()) return true;
// If an essential permission has been permanently denied, ask the
// user to change the setting
if (locationPermission == Permission.PERMANENTLY_DENIED) {
showDenialDialog(R.string.permission_location_title,
R.string.permission_hotspot_location_denied_body,
getGoToSettingsListener(ctx));
return false;
}
if (wifiSetting == Permission.PERMANENTLY_DENIED) {
showDenialDialog(R.string.wifi_settings_title,
R.string.wifi_settings_request_denied_body,
(d, w) -> requestEnableWiFi());
return false;
}
// Should we show the rationale for location permission or Wi-Fi?
if (locationPermission == Permission.SHOW_RATIONALE) {
showRationale(R.string.permission_location_title,
R.string.permission_hotspot_location_request_body,
this::requestPermissions);
} else if (wifiSetting == Permission.SHOW_RATIONALE) {
showRationale(R.string.wifi_settings_title,
R.string.wifi_settings_request_enable_body,
this::requestEnableWiFi);
}
return false;
}
void onRequestPermissionResult(@Nullable Boolean granted) {
if (granted != null && granted) {
locationPermission = Permission.GRANTED;
} else if (shouldShowRequestPermissionRationale(ctx,
ACCESS_FINE_LOCATION)) {
locationPermission = Permission.SHOW_RATIONALE;
} else {
locationPermission = Permission.PERMANENTLY_DENIED;
}
}
void onRequestWifiEnabledResult() {
wifiSetting = wifiManager.isWifiEnabled() ? Permission.GRANTED :
Permission.PERMANENTLY_DENIED;
}
private boolean areEssentialPermissionsGranted() {
if (SDK_INT < 29) {
if (!wifiManager.isWifiEnabled()) {
//noinspection deprecation
return wifiManager.setWifiEnabled(true);
}
return true;
} else {
return locationPermission == Permission.GRANTED
&& wifiManager.isWifiEnabled();
}
}
private void showDenialDialog(@StringRes int title, @StringRes int body,
OnClickListener onOkClicked) {
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(title);
builder.setMessage(body);
builder.setPositiveButton(R.string.ok, onOkClicked);
builder.setNegativeButton(R.string.cancel,
(dialog, which) -> ctx.supportFinishAfterTransition());
builder.show();
}
private void showRationale(@StringRes int title, @StringRes int body,
Runnable onContinueClicked) {
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(title);
builder.setMessage(body);
builder.setNeutralButton(R.string.continue_button,
(dialog, which) -> onContinueClicked.run());
builder.show();
}
private void requestPermissions() {
locationRequest.launch(ACCESS_FINE_LOCATION);
}
private void requestEnableWiFi() {
Intent i = SDK_INT < 29 ?
new Intent(Settings.ACTION_WIFI_SETTINGS) :
new Intent(Settings.Panel.ACTION_WIFI);
wifiRequest.launch(i);
}
}

View File

@@ -0,0 +1,130 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment;
import java.util.List;
import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static android.content.Intent.ACTION_SEND;
import static android.content.Intent.EXTRA_STREAM;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static androidx.activity.result.contract.ActivityResultContracts.CreateDocument;
import static androidx.transition.TransitionManager.beginDelayedTransition;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.hotspot.HotspotViewModel.getApkFileName;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class FallbackFragment extends BaseFragment {
public static final String TAG = FallbackFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private HotspotViewModel viewModel;
private final ActivityResultLauncher<String> launcher =
registerForActivityResult(new CreateDocument(),
this::onDocumentCreated);
private Button fallbackButton;
private ProgressBar progressBar;
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
FragmentActivity activity = requireActivity();
getAndroidComponent(activity).inject(this);
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(HotspotViewModel.class);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater
.inflate(R.layout.fragment_hotspot_save_apk, container, false);
}
@Override
public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
fallbackButton = v.findViewById(R.id.fallbackButton);
progressBar = v.findViewById(R.id.progressBar);
fallbackButton.setOnClickListener(view -> {
beginDelayedTransition((ViewGroup) v);
fallbackButton.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE);
if (SDK_INT >= 19) launcher.launch(getApkFileName());
else viewModel.exportApk();
});
viewModel.getSavedApkToUri()
.observeEvent(this, uri -> shareUri(this, uri));
}
private void onDocumentCreated(@Nullable Uri uri) {
showButton();
if (uri != null) viewModel.exportApk(uri);
}
private void showButton() {
beginDelayedTransition((ViewGroup) requireView());
fallbackButton.setVisibility(VISIBLE);
progressBar.setVisibility(INVISIBLE);
}
static void shareUri(Fragment fragment, Uri uri) {
Intent i = new Intent(ACTION_SEND);
i.putExtra(EXTRA_STREAM, uri);
i.setType("*/*"); // gives us all sharing options
i.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
Context ctx = fragment.requireContext();
if (SDK_INT <= 19) {
// Workaround for Android bug:
// ctx.grantUriPermission also needed for Android 4
List<ResolveInfo> resInfoList = ctx.getPackageManager()
.queryIntentActivities(i, MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
ctx.grantUriPermission(packageName, uri,
FLAG_GRANT_READ_URI_PERMISSION);
}
}
fragment.startActivity(Intent.createChooser(i, null));
}
}

View File

@@ -0,0 +1,142 @@
package org.briarproject.briar.android.hotspot;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.util.UiUtils.showFragment;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ACTION_STOP_HOTSPOT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotActivity extends BriarActivity
implements BaseFragmentListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
private HotspotViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(HotspotViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_container);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
FragmentManager fm = getSupportFragmentManager();
viewModel.getState().observe(this, hotspotState -> {
if (hotspotState instanceof HotspotStarted) {
HotspotStarted started = (HotspotStarted) hotspotState;
String tag = HotspotFragment.TAG;
// check if fragment is already added
// to not lose state on configuration changes
if (fm.findFragmentByTag(tag) == null) {
if (!started.consume()) {
showFragment(fm, new HotspotFragment(), tag);
}
}
} else if (hotspotState instanceof HotspotError) {
HotspotError error = ((HotspotError) hotspotState);
showErrorFragment(error.getError());
}
});
if (savedInstanceState == null) {
// If there is no saved instance state, just start with the intro fragment.
fm.beginTransaction()
.replace(R.id.fragmentContainer, new HotspotIntroFragment(),
HotspotIntroFragment.TAG)
.commit();
} else if (viewModel.getState().getValue() == null) {
// If there is saved instance state, then there's either been an
// configuration change like rotated device or the activity has been
// destroyed and is now being re-created.
// In the latter case, the view model will have been destroyed, too.
// The activity can only have been destroyed if the user navigated
// away from the HotspotActivity which is nothing we
// intend to support, so we want to detect that and start from scratch
// in this case. We need to clean up existing fragments in order not
// to stack new fragments on top of old ones.
// If it is a configuration change and we moved past the intro
// fragment already, then the view model state will be != null,
// hence we can use this check for null to determine the destroyed
// activity. It can also be null if the user has not pressed
// "start sharing" yet, but in that case it won't harm to start from
// scratch.
Fragment current = fm.findFragmentById(R.id.fragmentContainer);
if (current instanceof HotspotIntroFragment) {
// If the currently displayed fragment is the intro fragment,
// there's nothing we need to do.
return;
}
// Remove everything from the back stack.
fm.popBackStackImmediate(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
// Start fresh with the intro fragment.
fm.beginTransaction()
.replace(R.id.fragmentContainer, new HotspotIntroFragment(),
HotspotIntroFragment.TAG)
.commit();
}
}
private void showErrorFragment(String error) {
FragmentManager fm = getSupportFragmentManager();
String tag = HotspotErrorFragment.TAG;
if (fm.findFragmentByTag(tag) == null) {
Fragment f = HotspotErrorFragment.newInstance(error);
showFragment(fm, f, tag, false);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (ACTION_STOP_HOTSPOT.equals(intent.getAction())) {
// also closes hotspot
supportFinishAfterTransition();
}
}
}

View File

@@ -0,0 +1,78 @@
package org.briarproject.briar.android.hotspot;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.util.UiUtils.triggerFeedback;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotErrorFragment extends BaseFragment {
public static final String TAG = HotspotErrorFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private static final String ERROR_MSG = "errorMessage";
public static HotspotErrorFragment newInstance(String message) {
HotspotErrorFragment f = new HotspotErrorFragment();
Bundle args = new Bundle();
args.putString(ERROR_MSG, message);
f.setArguments(args);
return f;
}
private String errorMessage;
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = requireArguments();
errorMessage = args.getString(ERROR_MSG);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireActivity().setTitle(R.string.error);
return inflater
.inflate(R.layout.fragment_hotspot_error, container, false);
}
@Override
public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
TextView msg = v.findViewById(R.id.errorMessageDetail);
msg.setText(errorMessage);
Button feedbackButton = v.findViewById(R.id.feedbackButton);
feedbackButton.setOnClickListener(
button -> triggerFeedback(requireContext(), errorMessage));
}
}

View File

@@ -0,0 +1,59 @@
package org.briarproject.briar.android.hotspot;
import android.os.Bundle;
import android.view.View;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import static org.briarproject.briar.android.util.UiUtils.showFragment;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotFragment extends AbstractTabsFragment {
public final static String TAG = HotspotFragment.class.getName();
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
connectedButton.setOnClickListener(v -> showNextFragment());
viewModel.getPeerConnectedEvent().observeEvent(getViewLifecycleOwner(),
this::onPeerConnected);
}
@Override
protected Fragment getFirstFragment() {
return ManualHotspotFragment.newInstance(true);
}
@Override
protected Fragment getSecondFragment() {
return QrHotspotFragment.newInstance(true);
}
private void onPeerConnected(boolean connected) {
if (!connected) return;
new BriarSnackbarBuilder()
.setAction(R.string.hotspot_peer_connected_action, v ->
showNextFragment())
.make(connectedButton, R.string.hotspot_peer_connected,
Snackbar.LENGTH_LONG)
.setAnchorView(connectedButton)
.show();
}
private void showNextFragment() {
Fragment f = new WebsiteFragment();
String tag = WebsiteFragment.TAG;
showFragment(getParentFragmentManager(), f, tag);
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.briar.android.hotspot;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotHelpFragment extends Fragment {
public final static String TAG = HotspotHelpFragment.class.getName();
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater
.inflate(R.layout.fragment_hotspot_help, container, false);
}
}

View File

@@ -0,0 +1,130 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static androidx.transition.TransitionManager.beginDelayedTransition;
import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotIntroFragment extends Fragment {
public final static String TAG = HotspotIntroFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private HotspotViewModel viewModel;
private ConditionManager conditionManager;
private Button startButton;
private ProgressBar progressBar;
private TextView progressTextView;
private final ActivityResultLauncher<String> locationRequest =
registerForActivityResult(new RequestPermission(), granted -> {
conditionManager.onRequestPermissionResult(granted);
startHotspot();
});
private final ActivityResultLauncher<Intent> wifiRequest =
registerForActivityResult(new StartActivityForResult(), result -> {
conditionManager.onRequestWifiEnabledResult();
startHotspot();
});
@Override
public void onAttach(Context context) {
super.onAttach(context);
FragmentActivity activity = requireActivity();
getAndroidComponent(activity).inject(this);
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(HotspotViewModel.class);
conditionManager =
new ConditionManager(activity, locationRequest, wifiRequest);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater
.inflate(R.layout.fragment_hotspot_intro, container, false);
startButton = v.findViewById(R.id.startButton);
progressBar = v.findViewById(R.id.progressBar);
progressTextView = v.findViewById(R.id.progressTextView);
startButton.setOnClickListener(button -> {
startButton.setEnabled(false);
conditionManager.startConditionChecks();
});
return v;
}
@Override
public void onStart() {
super.onStart();
conditionManager.resetPermissions();
}
private void startHotspot() {
startButton.setEnabled(true);
if (conditionManager.checkAndRequestConditions()) {
showInstallWarningIfNeeded();
beginDelayedTransition((ViewGroup) requireView());
startButton.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE);
progressTextView.setVisibility(VISIBLE);
viewModel.startHotspot();
}
}
private void showInstallWarningIfNeeded() {
Context ctx = requireContext();
ApplicationInfo applicationInfo;
try {
applicationInfo = ctx.getPackageManager()
.getApplicationInfo(ctx.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError(e);
}
// test only apps can not be installed
if ((applicationInfo.flags & FLAG_TEST_ONLY) == FLAG_TEST_ONLY) {
int color = getResources().getColor(R.color.briar_red_500);
Snackbar.make(requireView(), R.string.hotspot_flag_test,
LENGTH_LONG).setBackgroundTint(color).show();
}
}
}

View File

@@ -0,0 +1,396 @@
package org.briarproject.briar.android.hotspot;
import android.app.Application;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.wifi.WifiManager;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pGroup;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
import android.os.Handler;
import android.util.DisplayMetrics;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
import org.briarproject.briar.android.util.QrCodeUtils;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import static android.content.Context.WIFI_P2P_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
import static android.net.wifi.p2p.WifiP2pConfig.GROUP_OWNER_BAND_2GHZ;
import static android.net.wifi.p2p.WifiP2pManager.BUSY;
import static android.net.wifi.p2p.WifiP2pManager.ERROR;
import static android.net.wifi.p2p.WifiP2pManager.NO_SERVICE_REQUESTS;
import static android.net.wifi.p2p.WifiP2pManager.P2P_UNSUPPORTED;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.util.UiUtils.handleException;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class HotspotManager implements ActionListener {
interface HotspotListener {
void onStartingHotspot();
@IoExecutor
void onHotspotStarted(NetworkConfig networkConfig);
@UiThread
void onDeviceConnected();
void onHotspotStopped();
void onHotspotError(String error);
}
private static final Logger LOG = getLogger(HotspotManager.class.getName());
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
private static final int RETRY_DELAY_MILLIS = 1000;
private static final String HOTSPOT_NAMESPACE = "hotspot";
private static final String HOTSPOT_KEY_SSID = "ssid";
private static final String HOTSPOT_KEY_PASS = "pass";
private final Context ctx;
@DatabaseExecutor
private final Executor dbExecutor;
@IoExecutor
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
private final SettingsManager settingsManager;
private final SecureRandom random;
private final WifiManager wifiManager;
private final WifiP2pManager wifiP2pManager;
private final Handler handler;
private final String lockTag;
private HotspotListener listener;
private WifiManager.WifiLock wifiLock;
private WifiP2pManager.Channel channel;
@RequiresApi(29)
private volatile NetworkConfig savedNetworkConfig;
@Inject
HotspotManager(Application ctx,
@DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor,
AndroidExecutor androidExecutor,
SettingsManager settingsManager,
SecureRandom random) {
this.ctx = ctx.getApplicationContext();
this.dbExecutor = dbExecutor;
this.ioExecutor = ioExecutor;
this.androidExecutor = androidExecutor;
this.settingsManager = settingsManager;
this.random = random;
wifiManager = (WifiManager) ctx.getApplicationContext()
.getSystemService(WIFI_SERVICE);
wifiP2pManager =
(WifiP2pManager) ctx.getSystemService(WIFI_P2P_SERVICE);
handler = new Handler(ctx.getMainLooper());
lockTag = ctx.getPackageName() + ":app-sharing-hotspot";
}
void setHotspotListener(HotspotListener listener) {
this.listener = listener;
}
@UiThread
void startWifiP2pHotspot() {
if (wifiP2pManager == null) {
listener.onHotspotError(
ctx.getString(R.string.hotspot_error_no_wifi_direct));
return;
}
listener.onStartingHotspot();
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
if (channel == null) {
listener.onHotspotError(
ctx.getString(R.string.hotspot_error_no_wifi_direct));
return;
}
try {
if (SDK_INT >= 29) {
dbExecutor.execute(() -> {
// load savedNetworkConfig before starting hotspot
loadSavedNetworkConfig();
androidExecutor.runOnUiThread(() -> {
WifiP2pConfig config = new WifiP2pConfig.Builder()
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
.setNetworkName(savedNetworkConfig.ssid)
.setPassphrase(savedNetworkConfig.password)
.build();
acquireLock();
wifiP2pManager.createGroup(channel, config, this);
});
});
} else {
acquireLock();
wifiP2pManager.createGroup(channel, this);
}
} catch (SecurityException e) {
// this should never happen, because we request permissions before
throw new AssertionError(e);
}
}
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onSuccess() {
requestGroupInfo(1);
}
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onFailure(int reason) {
if (reason == BUSY) {
// Hotspot already running
requestGroupInfo(1);
} else if (reason == P2P_UNSUPPORTED) {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed,
"p2p unsupported"));
} else if (reason == ERROR) {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed, "p2p error"));
} else if (reason == NO_SERVICE_REQUESTS) {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed,
"no service requests"));
} else {
// all cases covered, in doubt set to error
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed_unknown,
reason));
}
}
void stopWifiP2pHotspot() {
if (channel == null) return;
wifiP2pManager.removeGroup(channel, new ActionListener() {
@Override
public void onSuccess() {
releaseHotspot();
}
@Override
public void onFailure(int reason) {
// not propagating back error
releaseHotspot();
}
});
}
private void acquireLock() {
// WIFI_MODE_FULL has no effect on API >= 29
int lockType =
SDK_INT >= 29 ? WIFI_MODE_FULL_HIGH_PERF : WIFI_MODE_FULL;
wifiLock = wifiManager.createWifiLock(lockType, lockTag);
wifiLock.acquire();
}
private void releaseHotspot() {
listener.onHotspotStopped();
closeChannelAndReleaseLock();
}
private void releaseHotspotWithError(String error) {
listener.onHotspotError(error);
closeChannelAndReleaseLock();
}
private void closeChannelAndReleaseLock() {
if (SDK_INT >= 27) channel.close();
channel = null;
wifiLock.release();
}
private void requestGroupInfo(int attempt) {
if (LOG.isLoggable(INFO)) {
LOG.info("requestGroupInfo attempt: " + attempt);
}
GroupInfoListener groupListener = group -> {
boolean valid = isGroupValid(group);
// If the group is valid, set the hotspot to started. If we don't
// have any attempts left, we try what we got
if (valid || attempt >= MAX_GROUP_INFO_ATTEMPTS) {
onHotspotStarted(group);
} else {
retryRequestingGroupInfo(attempt);
}
};
try {
if (channel == null) return;
wifiP2pManager.requestGroupInfo(channel, groupListener);
} catch (SecurityException e) {
// this should never happen, because we request permissions before
throw new AssertionError(e);
}
}
@UiThread
private void onHotspotStarted(WifiP2pGroup group) {
DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
ioExecutor.execute(() -> {
String content = createWifiLoginString(group.getNetworkName(),
group.getPassphrase());
Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
NetworkConfig config = new NetworkConfig(group.getNetworkName(),
group.getPassphrase(), qrCode);
listener.onHotspotStarted(config);
});
requestGroupInfoForConnection();
}
private boolean isGroupValid(@Nullable WifiP2pGroup group) {
if (group == null) {
LOG.info("group is null");
return false;
} else if (!group.getNetworkName().startsWith("DIRECT-")) {
if (LOG.isLoggable(INFO)) {
LOG.info("received networkName without prefix 'DIRECT-': " +
group.getNetworkName());
}
return false;
} else if (SDK_INT >= 29) {
// if we get here, the savedNetworkConfig must have a value
String networkName = savedNetworkConfig.ssid;
if (!networkName.equals(group.getNetworkName())) {
if (LOG.isLoggable(INFO)) {
LOG.info("expected networkName: " + networkName);
LOG.info("received networkName: " + group.getNetworkName());
}
return false;
}
}
return true;
}
private void retryRequestingGroupInfo(int attempt) {
LOG.info("retrying");
// On some devices we need to wait for the group info to become available
if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
handler.postDelayed(() -> requestGroupInfo(attempt + 1),
RETRY_DELAY_MILLIS);
} else {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_no_group_info));
}
}
@UiThread
private void requestGroupInfoForConnection() {
if (LOG.isLoggable(INFO)) {
LOG.info("requestGroupInfo for connection");
}
GroupInfoListener groupListener = group -> {
if (group == null || group.getClientList().isEmpty()) {
handler.postDelayed(this::requestGroupInfoForConnection,
RETRY_DELAY_MILLIS);
} else {
if (LOG.isLoggable(INFO)) {
LOG.info("client list " + group.getClientList());
}
listener.onDeviceConnected();
}
};
try {
if (channel == null) return;
wifiP2pManager.requestGroupInfo(channel, groupListener);
} catch (SecurityException e) {
throw new AssertionError(e);
}
}
/**
* Store persistent Wi-Fi SSID and passphrase in Settings to improve UX
* so that users don't have to change them when attempting to connect.
* Works only on API 29 and above.
*/
@RequiresApi(29)
@DatabaseExecutor
private void loadSavedNetworkConfig() {
try {
Settings settings = settingsManager.getSettings(HOTSPOT_NAMESPACE);
String ssid = settings.get(HOTSPOT_KEY_SSID);
String pass = settings.get(HOTSPOT_KEY_PASS);
if (ssid == null || pass == null) {
ssid = getSsid();
pass = getPassword();
settings.put(HOTSPOT_KEY_SSID, ssid);
settings.put(HOTSPOT_KEY_PASS, pass);
settingsManager.mergeSettings(settings, HOTSPOT_NAMESPACE);
}
savedNetworkConfig = new NetworkConfig(ssid, pass, null);
} catch (DbException e) {
handleException(ctx, androidExecutor, LOG, e);
// probably never happens, but if lets use non-persistent data
String ssid = getSsid();
String pass = getPassword();
savedNetworkConfig = new NetworkConfig(ssid, pass, null);
}
}
@RequiresApi(29)
private String getSsid() {
return "DIRECT-" + getRandomString(2) + "-" +
getRandomString(10);
}
@RequiresApi(29)
private String getPassword() {
return getRandomString(8);
}
private static String createWifiLoginString(String ssid, String password) {
// https://en.wikipedia.org/wiki/QR_code#WiFi_network_login
// do not remove the dangling ';', it can cause problems to omit it
return "WIFI:S:" + ssid + ";T:WPA;P:" + password + ";;";
}
private static final String digits = "123456789"; // avoid 0
private static final String letters = "abcdefghijkmnopqrstuvwxyz"; // no l
private static final String LETTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // no I, O
private String getRandomString(int length) {
char[] c = new char[length];
for (int i = 0; i < length; i++) {
if (random.nextBoolean()) {
c[i] = random(digits);
} else if (random.nextBoolean()) {
c[i] = random(letters);
} else {
c[i] = random(LETTERS);
}
}
return new String(c);
}
private char random(String universe) {
return universe.charAt(random.nextInt(universe.length()));
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.briar.android.hotspot;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
@Module
public interface HotspotModule {
@Binds
@IntoMap
@ViewModelKey(HotspotViewModel.class)
ViewModel bindHotspotViewModel(HotspotViewModel hotspotViewModel);
}

View File

@@ -0,0 +1,85 @@
package org.briarproject.briar.android.hotspot;
import android.graphics.Bitmap;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
abstract class HotspotState {
static class StartingHotspot extends HotspotState {
}
static class NetworkConfig {
final String ssid, password;
@Nullable
final Bitmap qrCode;
NetworkConfig(String ssid, String password, @Nullable Bitmap qrCode) {
this.ssid = ssid;
this.password = password;
this.qrCode = qrCode;
}
}
static class WebsiteConfig {
final String url;
@Nullable
final Bitmap qrCode;
WebsiteConfig(String url, @Nullable Bitmap qrCode) {
this.url = url;
this.qrCode = qrCode;
}
}
static class HotspotStarted extends HotspotState {
private final NetworkConfig networkConfig;
private final WebsiteConfig websiteConfig;
// 'consumed' is set to true once this state triggered a UI change, i.e.
// moving to the next fragment.
private boolean consumed = false;
HotspotStarted(NetworkConfig networkConfig,
WebsiteConfig websiteConfig) {
this.networkConfig = networkConfig;
this.websiteConfig = websiteConfig;
}
NetworkConfig getNetworkConfig() {
return networkConfig;
}
WebsiteConfig getWebsiteConfig() {
return websiteConfig;
}
/**
* Mark this state as consumed, i.e. the UI has already done something
* as a result of the state changing to this. This can be used in order
* to not repeat actions such as showing fragments on rotation changes.
*/
@UiThread
boolean consume() {
boolean old = consumed;
consumed = true;
return old;
}
}
static class HotspotError extends HotspotState {
private final String error;
HotspotError(String error) {
this.error = error;
}
String getError() {
return error;
}
}
}

View File

@@ -0,0 +1,230 @@
package org.briarproject.briar.android.hotspot;
import android.app.Application;
import android.net.Uri;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.hotspot.HotspotManager.HotspotListener;
import org.briarproject.briar.android.hotspot.HotspotState.HotspotError;
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
import org.briarproject.briar.android.hotspot.HotspotState.StartingHotspot;
import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
import org.briarproject.briar.android.hotspot.WebServerManager.WebServerListener;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Environment.DIRECTORY_DOWNLOADS;
import static android.os.Environment.getExternalStoragePublicDirectory;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.briar.BuildConfig.DEBUG;
import static org.briarproject.briar.BuildConfig.VERSION_NAME;
@NotNullByDefault
class HotspotViewModel extends DbViewModel
implements HotspotListener, WebServerListener {
private static final Logger LOG =
getLogger(HotspotViewModel.class.getName());
@IoExecutor
private final Executor ioExecutor;
private final AndroidNotificationManager notificationManager;
private final HotspotManager hotspotManager;
private final WebServerManager webServerManager;
private final MutableLiveData<HotspotState> state =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> peerConnected =
new MutableLiveEvent<>();
private final MutableLiveEvent<Uri> savedApkToUri =
new MutableLiveEvent<>();
@Nullable
// Field to temporarily store the network config received via onHotspotStarted()
// in order to post it along with a HotspotStarted status
private volatile NetworkConfig networkConfig;
@Inject
HotspotViewModel(Application app,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
@IoExecutor Executor ioExecutor,
HotspotManager hotspotManager,
WebServerManager webServerManager,
AndroidNotificationManager notificationManager) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
this.ioExecutor = ioExecutor;
this.notificationManager = notificationManager;
this.hotspotManager = hotspotManager;
this.hotspotManager.setHotspotListener(this);
this.webServerManager = webServerManager;
this.webServerManager.setListener(this);
}
@UiThread
void startHotspot() {
HotspotState s = state.getValue();
if (s instanceof HotspotStarted) {
// This can happen if the user navigates back to intro fragment and
// taps 'start sharing' again. In this case, don't try to start the
// hotspot again. Instead, just create a new, unconsumed HotspotStarted
// event with the same config.
HotspotStarted old = (HotspotStarted) s;
state.setValue(new HotspotStarted(old.getNetworkConfig(),
old.getWebsiteConfig()));
} else {
hotspotManager.startWifiP2pHotspot();
notificationManager.showHotspotNotification();
}
}
@UiThread
private void stopHotspot() {
ioExecutor.execute(webServerManager::stopWebServer);
hotspotManager.stopWifiP2pHotspot();
notificationManager.clearHotspotNotification();
}
@Override
protected void onCleared() {
super.onCleared();
stopHotspot();
}
@Override
public void onStartingHotspot() {
state.setValue(new StartingHotspot());
}
@Override
@IoExecutor
public void onHotspotStarted(NetworkConfig networkConfig) {
this.networkConfig = networkConfig;
LOG.info("starting webserver");
webServerManager.startWebServer();
}
@UiThread
@Override
public void onDeviceConnected() {
peerConnected.setEvent(true);
}
@Override
public void onHotspotStopped() {
LOG.info("stopping webserver");
ioExecutor.execute(webServerManager::stopWebServer);
}
@Override
public void onHotspotError(String error) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Hotspot error: " + error);
}
state.postValue(new HotspotError(error));
ioExecutor.execute(webServerManager::stopWebServer);
notificationManager.clearHotspotNotification();
}
@Override
@IoExecutor
public void onWebServerStarted(WebsiteConfig websiteConfig) {
NetworkConfig nc = requireNonNull(networkConfig);
state.postValue(new HotspotStarted(nc, websiteConfig));
networkConfig = null;
}
@Override
@IoExecutor
public void onWebServerError() {
state.postValue(new HotspotError(getApplication()
.getString(R.string.hotspot_error_web_server_start)));
hotspotManager.stopWifiP2pHotspot();
}
void exportApk(Uri uri) {
if (SDK_INT < 19) throw new IllegalStateException();
try {
OutputStream out = getApplication().getContentResolver()
.openOutputStream(uri, "wt");
writeApk(out, uri);
} catch (FileNotFoundException e) {
handleException(e);
}
}
void exportApk() {
if (SDK_INT >= 19) throw new IllegalStateException();
File path = getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS);
//noinspection ResultOfMethodCallIgnored
path.mkdirs();
File file = new File(path, getApkFileName());
try {
OutputStream out = new FileOutputStream(file);
writeApk(out, Uri.fromFile(file));
} catch (FileNotFoundException e) {
handleException(e);
}
}
static String getApkFileName() {
return "briar" + (DEBUG ? "-debug-" : "-") + VERSION_NAME + ".apk";
}
private void writeApk(OutputStream out, Uri uriToShare) {
File apk = new File(getApplication().getPackageCodePath());
ioExecutor.execute(() -> {
try {
FileInputStream in = new FileInputStream(apk);
copyAndClose(in, out);
savedApkToUri.postEvent(uriToShare);
} catch (IOException e) {
handleException(e);
}
});
}
LiveData<HotspotState> getState() {
return state;
}
LiveEvent<Boolean> getPeerConnectedEvent() {
return peerConnected;
}
LiveEvent<Uri> getSavedApkToUri() {
return savedApkToUri;
}
}

View File

@@ -0,0 +1,119 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.viewpager2.widget.ViewPager2;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import static android.view.View.GONE;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.hotspot.AbstractTabsFragment.ARG_FOR_WIFI_CONNECT;
import static org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ManualHotspotFragment extends Fragment {
public final static String TAG = ManualHotspotFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private HotspotViewModel viewModel;
static ManualHotspotFragment newInstance(boolean forWifiConnect) {
ManualHotspotFragment f = new ManualHotspotFragment();
Bundle bundle = new Bundle();
bundle.putBoolean(ARG_FOR_WIFI_CONNECT, forWifiConnect);
f.setArguments(bundle);
return f;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
getAndroidComponent(requireContext()).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(HotspotViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater
.inflate(R.layout.fragment_hotspot_manual, container, false);
}
@Override
public void onViewCreated(View v, @Nullable Bundle savedInstanceState) {
super.onViewCreated(v, savedInstanceState);
TextView manualIntroView = v.findViewById(R.id.manualIntroView);
TextView ssidLabelView = v.findViewById(R.id.ssidLabelView);
TextView ssidView = v.findViewById(R.id.ssidView);
TextView passwordView = v.findViewById(R.id.passwordView);
Consumer<HotspotStarted> consumer;
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
linkify(manualIntroView, R.string.hotspot_manual_wifi);
ssidLabelView.setText(R.string.hotspot_manual_wifi_ssid);
consumer = state -> {
ssidView.setText(state.getNetworkConfig().ssid);
passwordView.setText(state.getNetworkConfig().password);
};
} else {
linkify(manualIntroView, R.string.hotspot_manual_site);
ssidLabelView.setText(R.string.hotspot_manual_site_address);
consumer = state -> ssidView.setText(state.getWebsiteConfig().url);
v.findViewById(R.id.passwordLabelView).setVisibility(GONE);
passwordView.setVisibility(GONE);
}
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
// we only expect to be in this state here
if (state instanceof HotspotStarted) {
consumer.accept((HotspotStarted) state);
}
});
}
private void linkify(TextView textView, int resPattern) {
String pattern = getString(resPattern);
String replacement = getString(R.string.hotspot_scanning_a_qr_code);
String text = String.format(pattern, replacement);
int start = pattern.indexOf("%s");
int end = start + replacement.length();
SpannableString spannable = new SpannableString(text);
ClickableSpan clickable = new ClickableSpan() {
@Override
public void onClick(View textView) {
ViewPager2 pager = requireActivity().findViewById(R.id.pager);
pager.setCurrentItem(1);
}
};
spannable.setSpan(clickable, start, end, SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannable);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
}

View File

@@ -0,0 +1,81 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.hotspot.HotspotState.HotspotStarted;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.hotspot.AbstractTabsFragment.ARG_FOR_WIFI_CONNECT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class QrHotspotFragment extends Fragment {
public final static String TAG = QrHotspotFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private HotspotViewModel viewModel;
static QrHotspotFragment newInstance(boolean forWifiConnect) {
QrHotspotFragment f = new QrHotspotFragment();
Bundle bundle = new Bundle();
bundle.putBoolean(ARG_FOR_WIFI_CONNECT, forWifiConnect);
f.setArguments(bundle);
return f;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
getAndroidComponent(requireContext()).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(HotspotViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater
.inflate(R.layout.fragment_hotspot_qr, container, false);
TextView qrIntroView = v.findViewById(R.id.qrIntroView);
ImageView qrCodeView = v.findViewById(R.id.qrCodeView);
Consumer<HotspotStarted> consumer;
if (requireArguments().getBoolean(ARG_FOR_WIFI_CONNECT)) {
qrIntroView.setText(R.string.hotspot_qr_wifi);
consumer = state ->
qrCodeView.setImageBitmap(state.getNetworkConfig().qrCode);
} else {
qrIntroView.setText(R.string.hotspot_qr_site);
consumer = state ->
qrCodeView.setImageBitmap(state.getWebsiteConfig().qrCode);
}
viewModel.getState().observe(getViewLifecycleOwner(), state -> {
if (state instanceof HotspotStarted) {
consumer.accept((HotspotStarted) state);
}
});
return v;
}
}

View File

@@ -0,0 +1,135 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.Nullable;
import fi.iki.elonen.NanoHTTPD;
import static android.util.Xml.Encoding.UTF_8;
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND;
import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.BuildConfig.VERSION_NAME;
import static org.briarproject.briar.android.hotspot.HotspotViewModel.getApkFileName;
@NotNullByDefault
class WebServer extends NanoHTTPD {
final static int PORT = 9999;
private static final Logger LOG = getLogger(WebServer.class.getName());
private static final String FILE_HTML = "hotspot.html";
private static final Pattern REGEX_AGENT =
Pattern.compile("Android ([0-9]+)");
private final Context ctx;
WebServer(Context ctx) {
super(PORT);
this.ctx = ctx;
}
@Override
public void start() throws IOException {
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}
@Override
public Response serve(IHTTPSession session) {
if (session.getUri().endsWith("favicon.ico")) {
return newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
NOT_FOUND.getDescription());
}
if (session.getUri().endsWith(".apk")) {
return serveApk();
}
Response res;
try {
String html = getHtml(session.getHeaders().get("user-agent"));
res = newFixedLengthResponse(OK, MIME_HTML, html);
} catch (Exception e) {
logException(LOG, WARNING, e);
res = newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT,
ctx.getString(R.string.hotspot_error_web_server_serve));
}
return res;
}
private String getHtml(@Nullable String userAgent) throws Exception {
Document doc;
try (InputStream is = ctx.getAssets().open(FILE_HTML)) {
doc = Jsoup.parse(is, UTF_8.name(), "");
}
String app = ctx.getString(R.string.app_name);
String appV = app + " " + VERSION_NAME;
String filename = getApkFileName();
doc.select("#download_title").first()
.text(ctx.getString(R.string.website_download_title, appV));
doc.select("#download_intro").first()
.text(ctx.getString(R.string.website_download_intro, app));
doc.select(".button").first().attr("href", filename);
doc.select("#download_button").first()
.text(ctx.getString(R.string.website_download_title, app));
doc.select("#download_outro").first()
.text(ctx.getString(R.string.website_download_outro));
doc.select("#troubleshooting_title").first()
.text(ctx.getString(R.string.website_troubleshooting_title));
doc.select("#troubleshooting_1").first()
.text(ctx.getString(R.string.website_troubleshooting_1));
doc.select("#troubleshooting_2").first()
.text(getUnknownSourcesString(userAgent));
return doc.outerHtml();
}
private String getUnknownSourcesString(@Nullable String userAgent) {
boolean is8OrHigher = false;
if (userAgent != null) {
Matcher matcher = REGEX_AGENT.matcher(userAgent);
if (matcher.find()) {
int androidMajorVersion =
Integer.parseInt(requireNonNull(matcher.group(1)));
is8OrHigher = androidMajorVersion >= 8;
}
}
return is8OrHigher ?
ctx.getString(R.string.website_troubleshooting_2_new) :
ctx.getString(R.string.website_troubleshooting_2_old);
}
private Response serveApk() {
String mime = "application/vnd.android.package-archive";
File file = new File(ctx.getPackageCodePath());
long fileLen = file.length();
Response res;
try {
FileInputStream fis = new FileInputStream(file);
res = newFixedLengthResponse(OK, mime, fis, fileLen);
res.addHeader("Content-Length", "" + fileLen);
} catch (FileNotFoundException e) {
logException(LOG, WARNING, e);
res = newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT,
ctx.getString(R.string.hotspot_error_web_server_serve));
}
return res;
}
}

View File

@@ -0,0 +1,125 @@
package org.briarproject.briar.android.hotspot;
import android.app.Application;
import android.graphics.Bitmap;
import android.util.DisplayMetrics;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.hotspot.HotspotState.WebsiteConfig;
import org.briarproject.briar.android.util.QrCodeUtils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.Collections.emptyList;
import static java.util.Collections.list;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.hotspot.WebServer.PORT;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class WebServerManager {
interface WebServerListener {
@IoExecutor
void onWebServerStarted(WebsiteConfig websiteConfig);
@IoExecutor
void onWebServerError();
}
private static final Logger LOG =
getLogger(WebServerManager.class.getName());
private final WebServer webServer;
private final DisplayMetrics dm;
private WebServerListener listener;
@Inject
WebServerManager(Application ctx) {
webServer = new WebServer(ctx);
dm = ctx.getResources().getDisplayMetrics();
}
void setListener(WebServerListener listener) {
this.listener = listener;
}
@IoExecutor
void startWebServer() {
try {
webServer.start();
onWebServerStarted();
} catch (IOException e) {
logException(LOG, WARNING, e);
listener.onWebServerError();
}
}
@IoExecutor
private void onWebServerStarted() {
String url = "http://192.168.49.1:" + PORT;
InetAddress address = getAccessPointAddress();
if (address == null) {
LOG.info(
"Could not find access point address, assuming 192.168.49.1");
} else {
if (LOG.isLoggable(INFO)) {
LOG.info("Access point address " + address.getHostAddress());
}
url = "http://" + address.getHostAddress() + ":" + PORT;
}
Bitmap qrCode = QrCodeUtils.createQrCode(dm, url);
listener.onWebServerStarted(new WebsiteConfig(url, qrCode));
}
/**
* It is safe to call this more than once and it won't throw.
*/
@IoExecutor
void stopWebServer() {
webServer.stop();
}
@Nullable
private static InetAddress getAccessPointAddress() {
for (NetworkInterface i : getNetworkInterfaces()) {
if (i.getName().startsWith("p2p")) {
for (InterfaceAddress a : i.getInterfaceAddresses()) {
// we consider only IPv4 addresses
if (a.getAddress().getAddress().length == 4)
return a.getAddress();
}
}
}
return null;
}
private static List<NetworkInterface> getNetworkInterfaces() {
try {
Enumeration<NetworkInterface> ifaces =
NetworkInterface.getNetworkInterfaces();
return ifaces == null ? emptyList() : list(ifaces);
} catch (SocketException e) {
logException(LOG, WARNING, e);
return emptyList();
}
}
}

View File

@@ -0,0 +1,36 @@
package org.briarproject.briar.android.hotspot;
import android.os.Bundle;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import static android.view.View.GONE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class WebsiteFragment extends AbstractTabsFragment {
public final static String TAG = WebsiteFragment.class.getName();
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
connectedButton.setVisibility(GONE);
}
@Override
protected Fragment getFirstFragment() {
return ManualHotspotFragment.newInstance(false);
}
@Override
protected Fragment getSecondFragment() {
return QrHotspotFragment.newInstance(false);
}
}

View File

@@ -29,7 +29,6 @@ import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.StartupFailureActivity;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.FeedFragment; import org.briarproject.briar.android.blog.FeedFragment;
@@ -74,7 +73,6 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING; import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED; import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED;
import static org.briarproject.briar.android.BriarService.EXTRA_START_RESULT;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
import static org.briarproject.briar.android.navdrawer.IntentRouter.handleExternalIntent; import static org.briarproject.briar.android.navdrawer.IntentRouter.handleExternalIntent;
@@ -252,11 +250,6 @@ public class NavDrawerActivity extends BriarActivity implements
private void exitIfStartupFailed(Intent intent) { private void exitIfStartupFailed(Intent intent) {
if (intent.getBooleanExtra(EXTRA_STARTUP_FAILED, false)) { if (intent.getBooleanExtra(EXTRA_STARTUP_FAILED, false)) {
// Launch StartupFailureActivity in its own process, then exit
Intent i = new Intent(this, StartupFailureActivity.class);
i.putExtra(EXTRA_START_RESULT,
intent.getSerializableExtra(EXTRA_START_RESULT));
startActivity(i);
finish(); finish();
LOG.info("Exiting"); LOG.info("Exiting");
System.exit(0); System.exit(0);

View File

@@ -47,8 +47,6 @@ public class ReceiveFragment extends Fragment {
private Button button; private Button button;
private ProgressBar progressBar; private ProgressBar progressBar;
private boolean checkForStateLoss = false;
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
@@ -75,10 +73,6 @@ public class ReceiveFragment extends Fragment {
.observeEvent(getViewLifecycleOwner(), this::onOldTaskResumed); .observeEvent(getViewLifecycleOwner(), this::onOldTaskResumed);
viewModel.getState() viewModel.getState()
.observe(getViewLifecycleOwner(), this::onStateChanged); .observe(getViewLifecycleOwner(), this::onStateChanged);
// need to check for lost ViewModel state when creating with prior state
if (savedInstanceState != null) checkForStateLoss = true;
return v; return v;
} }
@@ -90,23 +84,6 @@ public class ReceiveFragment extends Fragment {
scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
} }
@Override
public void onResume() {
super.onResume();
// This code gets called *after* launcher had a chance
// to return the activity result.
if (checkForStateLoss && viewModel.hasNoState()) {
// We were recreated, but have lost the ViewModel state,
// because our activity was destroyed.
//
// Remove the current fragment from the stack
// to prevent duplicates on the back stack.
getParentFragmentManager().popBackStack();
// Start again (picks up existing task or allows to start a new one)
viewModel.startReceiveData();
}
}
private void onOldTaskResumed(boolean resumed) { private void onOldTaskResumed(boolean resumed) {
if (resumed) { if (resumed) {
Toast.makeText(requireContext(), Toast.makeText(requireContext(),
@@ -127,8 +104,6 @@ public class ReceiveFragment extends Fragment {
private void onDocumentChosen(@Nullable Uri uri) { private void onDocumentChosen(@Nullable Uri uri) {
if (uri == null) return; if (uri == null) return;
// we just got our document, so don't treat this as a state loss
checkForStateLoss = false;
viewModel.importData(uri); viewModel.importData(uri);
} }

View File

@@ -75,12 +75,6 @@ class RemovableDriveViewModel extends DbViewModel {
} }
} }
@UiThread
boolean hasNoState() {
return action.getLastValue() == null && state.getValue() == null &&
task == null;
}
/** /**
* Set this as soon as it becomes available. * Set this as soon as it becomes available.
*/ */

View File

@@ -51,8 +51,6 @@ public class SendFragment extends Fragment {
private Button button; private Button button;
private ProgressBar progressBar; private ProgressBar progressBar;
private boolean checkForStateLoss = false;
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
@@ -82,9 +80,6 @@ public class SendFragment extends Fragment {
viewModel.getState() viewModel.getState()
.observe(getViewLifecycleOwner(), this::onStateChanged); .observe(getViewLifecycleOwner(), this::onStateChanged);
// need to check for lost ViewModel state when creating with prior state
if (savedInstanceState != null) checkForStateLoss = true;
return v; return v;
} }
@@ -96,23 +91,6 @@ public class SendFragment extends Fragment {
scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN)); scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
} }
@Override
public void onResume() {
super.onResume();
// This code gets called *after* launcher had a chance
// to return the activity result.
if (checkForStateLoss && viewModel.hasNoState()) {
// We were recreated, but have lost the ViewModel state,
// because our activity was destroyed.
//
// Remove the current fragment from the stack
// to prevent duplicates on the back stack.
getParentFragmentManager().popBackStack();
// Start again (picks up existing task or allows to start a new one)
viewModel.startSendData();
}
}
private void onOldTaskResumed(boolean resumed) { private void onOldTaskResumed(boolean resumed) {
if (resumed) { if (resumed) {
Toast.makeText(requireContext(), Toast.makeText(requireContext(),
@@ -149,8 +127,6 @@ public class SendFragment extends Fragment {
private void onDocumentCreated(@Nullable Uri uri) { private void onDocumentCreated(@Nullable Uri uri) {
if (uri == null) return; if (uri == null) return;
// we just got our document, so don't treat this as a state loss
checkForStateLoss = false;
viewModel.exportData(uri); viewModel.exportData(uri);
} }

View File

@@ -34,7 +34,7 @@ class BriarExceptionHandler implements UncaughtExceptionHandler {
// activity runs in its own process, so we can kill the old one // activity runs in its own process, so we can kill the old one
startDevReportActivity(app.getApplicationContext(), startDevReportActivity(app.getApplicationContext(),
CrashReportActivity.class, e, appStartTime, logKey); CrashReportActivity.class, e, appStartTime, logKey, null);
Process.killProcess(Process.myPid()); Process.killProcess(Process.myPid());
System.exit(10); System.exit(10);
} }

View File

@@ -33,6 +33,7 @@ import static java.util.Objects.requireNonNull;
public class CrashReportActivity extends BaseActivity public class CrashReportActivity extends BaseActivity
implements BaseFragmentListener { implements BaseFragmentListener {
public static final String EXTRA_INITIAL_COMMENT = "initialComment";
public static final String EXTRA_THROWABLE = "throwable"; public static final String EXTRA_THROWABLE = "throwable";
public static final String EXTRA_APP_START_TIME = "appStartTime"; public static final String EXTRA_APP_START_TIME = "appStartTime";
public static final String EXTRA_APP_LOGCAT = "logcat"; public static final String EXTRA_APP_LOGCAT = "logcat";
@@ -55,10 +56,11 @@ public class CrashReportActivity extends BaseActivity
setContentView(R.layout.activity_dev_report); setContentView(R.layout.activity_dev_report);
Intent intent = getIntent(); Intent intent = getIntent();
String initialComment = intent.getStringExtra(EXTRA_INITIAL_COMMENT);
Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE); Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE);
long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, -1); long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, -1);
byte[] logKey = intent.getByteArrayExtra(EXTRA_APP_LOGCAT); byte[] logKey = intent.getByteArrayExtra(EXTRA_APP_LOGCAT);
viewModel.init(t, appStartTime, logKey); viewModel.init(t, appStartTime, logKey, initialComment);
viewModel.getShowReport().observeEvent(this, show -> { viewModel.getShowReport().observeEvent(this, show -> {
if (show) displayFragment(true); if (show) displayFragment(true);
}); });

View File

@@ -78,6 +78,9 @@ public class ReportFormFragment extends BaseFragment {
list = v.findViewById(R.id.list); list = v.findViewById(R.id.list);
progress = v.findViewById(R.id.progress_wheel); progress = v.findViewById(R.id.progress_wheel);
if (viewModel.getInitialComment() != null)
userCommentView.setText(viewModel.getInitialComment());
if (viewModel.isFeedback()) { if (viewModel.isFeedback()) {
includeDebugReport includeDebugReport
.setText(getString(R.string.include_debug_report_feedback)); .setText(getString(R.string.include_debug_report_feedback));

View File

@@ -64,6 +64,8 @@ class ReportViewModel extends AndroidViewModel {
private final MutableLiveEvent<Integer> closeReport = private final MutableLiveEvent<Integer> closeReport =
new MutableLiveEvent<>(); new MutableLiveEvent<>();
private boolean isFeedback; private boolean isFeedback;
@Nullable
private String initialComment;
@Inject @Inject
ReportViewModel(@NonNull Application application, ReportViewModel(@NonNull Application application,
@@ -80,7 +82,8 @@ class ReportViewModel extends AndroidViewModel {
} }
void init(@Nullable Throwable t, long appStartTime, void init(@Nullable Throwable t, long appStartTime,
@Nullable byte[] logKey) { @Nullable byte[] logKey, @Nullable String initialComment) {
this.initialComment = initialComment;
isFeedback = t == null; isFeedback = t == null;
if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> { if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> {
String decryptedLogs; String decryptedLogs;
@@ -103,6 +106,11 @@ class ReportViewModel extends AndroidViewModel {
}).start(); }).start();
} }
@Nullable
String getInitialComment() {
return initialComment;
}
boolean isFeedback() { boolean isFeedback() {
return isFeedback; return isFeedback;
} }
@@ -140,7 +148,7 @@ class ReportViewModel extends AndroidViewModel {
/** /**
* The content of the report that will be loaded after * The content of the report that will be loaded after
* {@link #init(Throwable, long, byte[])} was called. * {@link #init(Throwable, long, byte[], String)} was called.
*/ */
LiveData<ReportData> getReportData() { LiveData<ReportData> getReportData() {
return reportData; return reportData;

View File

@@ -36,6 +36,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
private static final String PREF_KEY_FEEDBACK = "pref_key_send_feedback"; private static final String PREF_KEY_FEEDBACK = "pref_key_send_feedback";
private static final String PREF_KEY_DEV = "pref_key_dev"; private static final String PREF_KEY_DEV = "pref_key_dev";
private static final String PREF_KEY_EXPLODE = "pref_key_explode"; private static final String PREF_KEY_EXPLODE = "pref_key_explode";
private static final String PREF_KEY_SHARE_APP = "pref_key_share_app";
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@@ -85,6 +86,12 @@ public class SettingsFragment extends PreferenceFragmentCompat {
PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV)); PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV));
dev.setVisible(false); dev.setVisible(false);
} }
if (!viewModel.shouldEnableShareAppViaOfflineHotspot()) {
Preference shareApp =
requireNonNull(findPreference(PREF_KEY_SHARE_APP));
shareApp.setVisible(false);
}
} }
@Override @Override

View File

@@ -262,4 +262,8 @@ class SettingsViewModel extends DbViewModel implements EventListener {
return screenLockTimeout; return screenLockTimeout;
} }
boolean shouldEnableShareAppViaOfflineHotspot() {
return featureFlags.shouldEnableShareAppViaOfflineHotspot();
}
} }

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.contact.add.nearby; package org.briarproject.briar.android.util;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@@ -22,12 +22,12 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault @NotNullByDefault
class QrCodeUtils { public class QrCodeUtils {
private static final Logger LOG = getLogger(QrCodeUtils.class.getName()); private static final Logger LOG = getLogger(QrCodeUtils.class.getName());
@Nullable @Nullable
static Bitmap createQrCode(DisplayMetrics dm, String input) { public static Bitmap createQrCode(DisplayMetrics dm, String input) {
int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels); int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels);
try { try {
// Generate QR code // Generate QR code

View File

@@ -61,6 +61,8 @@ import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
@@ -111,6 +113,7 @@ import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE; import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_LOGCAT; import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_LOGCAT;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_START_TIME; import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_START_TIME;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_INITIAL_COMMENT;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE; import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -137,13 +140,18 @@ public class UiUtils {
public static void showFragment(FragmentManager fm, Fragment f, public static void showFragment(FragmentManager fm, Fragment f,
@Nullable String tag) { @Nullable String tag) {
fm.beginTransaction() showFragment(fm, f, tag, true);
}
public static void showFragment(FragmentManager fm, Fragment f,
@Nullable String tag, boolean addToBackStack) {
FragmentTransaction ta = fm.beginTransaction()
.setCustomAnimations(R.anim.step_next_in, .setCustomAnimations(R.anim.step_next_in,
R.anim.step_previous_out, R.anim.step_previous_in, R.anim.step_previous_out, R.anim.step_previous_in,
R.anim.step_next_out) R.anim.step_next_out)
.replace(R.id.fragmentContainer, f, tag) .replace(R.id.fragmentContainer, f, tag);
.addToBackStack(tag) if (addToBackStack) ta.addToBackStack(tag);
.commit(); ta.commit();
} }
public static String getContactDisplayName(Author author, public static String getContactDisplayName(Author author,
@@ -333,6 +341,11 @@ public class UiUtils {
return i; return i;
} }
public static void putShowAdvancedExtra(Intent i) {
i.putExtra(SDK_INT <= 28 ? "android.content.extra.SHOW_ADVANCED" :
"android.provider.extra.SHOW_ADVANCED", true);
}
/** /**
* @return true if location is enabled, * @return true if location is enabled,
* or it isn't required due to this being a SDK < 28 device. * or it isn't required due to this being a SDK < 28 device.
@@ -410,17 +423,25 @@ public class UiUtils {
} }
public static void triggerFeedback(Context ctx) { public static void triggerFeedback(Context ctx) {
startDevReportActivity(ctx, FeedbackActivity.class, null, null, null); triggerFeedback(ctx, null);
}
public static void triggerFeedback(Context ctx,
@Nullable String initialComment) {
startDevReportActivity(ctx, FeedbackActivity.class, null, null, null,
initialComment);
} }
public static void startDevReportActivity(Context ctx, public static void startDevReportActivity(Context ctx,
Class<? extends FragmentActivity> activity, @Nullable Throwable t, Class<? extends FragmentActivity> activity, @Nullable Throwable t,
@Nullable Long appStartTime, @Nullable byte[] logKey) { @Nullable Long appStartTime, @Nullable byte[] logKey, @Nullable
String initialComment) {
final Intent dialogIntent = new Intent(ctx, activity); final Intent dialogIntent = new Intent(ctx, activity);
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
dialogIntent.putExtra(EXTRA_THROWABLE, t); dialogIntent.putExtra(EXTRA_THROWABLE, t);
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime); dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
dialogIntent.putExtra(EXTRA_APP_LOGCAT, logKey); dialogIntent.putExtra(EXTRA_APP_LOGCAT, logKey);
dialogIntent.putExtra(EXTRA_INITIAL_COMMENT, initialComment);
ctx.startActivity(dialogIntent); ctx.startActivity(dialogIntent);
} }

View File

@@ -31,6 +31,7 @@ public interface AndroidNotificationManager {
int FORUM_POST_NOTIFICATION_ID = 6; int FORUM_POST_NOTIFICATION_ID = 6;
int BLOG_POST_NOTIFICATION_ID = 7; int BLOG_POST_NOTIFICATION_ID = 7;
int CONTACT_ADDED_NOTIFICATION_ID = 8; int CONTACT_ADDED_NOTIFICATION_ID = 8;
int HOTSPOT_NOTIFICATION_ID = 9;
// Channel IDs // Channel IDs
String CONTACT_CHANNEL_ID = "contacts"; String CONTACT_CHANNEL_ID = "contacts";
@@ -41,13 +42,13 @@ public interface AndroidNotificationManager {
// that will sort below the main channels such as contacts // that will sort below the main channels such as contacts
String ONGOING_CHANNEL_OLD_ID = "zForegroundService"; String ONGOING_CHANNEL_OLD_ID = "zForegroundService";
String ONGOING_CHANNEL_ID = "zForegroundService2"; String ONGOING_CHANNEL_ID = "zForegroundService2";
String REMINDER_CHANNEL_ID = "zSignInReminder";
// This channel is no longer used - keep the ID so we can remove the
// channel from existing installations
String FAILURE_CHANNEL_ID = "zStartupFailure"; String FAILURE_CHANNEL_ID = "zStartupFailure";
String REMINDER_CHANNEL_ID = "zSignInReminder";
String HOTSPOT_CHANNEL_ID = "zHotspot";
// Actions for pending intents // Actions for pending intents
String ACTION_DISMISS_REMINDER = "dismissReminder"; String ACTION_DISMISS_REMINDER = "dismissReminder";
String ACTION_STOP_HOTSPOT = "stopHotspot";
Notification getForegroundNotification(); Notification getForegroundNotification();
@@ -96,4 +97,8 @@ public interface AndroidNotificationManager {
void blockAllBlogPostNotifications(); void blockAllBlogPostNotifications();
void unblockAllBlogPostNotifications(); void unblockAllBlogPostNotifications();
void showHotspotNotification();
void clearHotspotNotification();
} }

View File

@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M12,11c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,13c0,-3.31 -2.69,-6 -6,-6s-6,2.69 -6,6c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,1.48 -0.81,2.75 -2,3.45l1,1.74c1.79,-1.04 3,-2.97 3,-5.19zM12,3C6.48,3 2,7.48 2,13c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,18.53 4,15.96 4,13c0,-4.42 3.58,-8 8,-8s8,3.58 8,8c0,2.96 -1.61,5.53 -4,6.92l1,1.73c2.99,-1.73 5,-4.95 5,-8.65 0,-5.52 -4.48,-10 -10,-10z" />
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="8dp"
android:height="8dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17.56,14.24c0.28,-0.69 0.44,-1.45 0.44,-2.24 0,-3.31 -2.69,-6 -6,-6 -0.79,0 -1.55,0.16 -2.24,0.44l1.62,1.62c0.2,-0.03 0.41,-0.06 0.62,-0.06 2.21,0 4,1.79 4,4 0,0.21 -0.02,0.42 -0.05,0.63l1.61,1.61zM12,4c4.42,0 8,3.58 8,8 0,1.35 -0.35,2.62 -0.95,3.74l1.47,1.47C21.46,15.69 22,13.91 22,12c0,-5.52 -4.48,-10 -10,-10 -1.91,0 -3.69,0.55 -5.21,1.47l1.46,1.46C9.37,4.34 10.65,4 12,4zM3.27,2.5L2,3.77l2.1,2.1C2.79,7.57 2,9.69 2,12c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,17.53 4,14.96 4,12c0,-1.76 0.57,-3.38 1.53,-4.69l1.43,1.44C6.36,9.68 6,10.8 6,12c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-0.65 0.17,-1.25 0.44,-1.79l1.58,1.58L10,12c0,1.1 0.9,2 2,2l0.21,-0.02 0.01,0.01 7.51,7.51L21,20.23 4.27,3.5l-1,-1z" />
</vector>

View File

@@ -0,0 +1,40 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z" />
<path
android:fillColor="@android:color/white"
android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z" />
<path
android:fillColor="@android:color/white"
android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z" />
<path
android:fillColor="@android:color/white"
android:pathData="M19,19h2v2h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M13,13h2v2h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M15,15h2v2h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M13,17h2v2h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M15,19h2v2h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M17,17h2v2h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M17,13h2v2h-2z" />
<path
android:fillColor="@android:color/white"
android:pathData="M19,15h2v2h-2z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
tools:ignore="NewApi">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,11c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,13c0,-3.31 -2.69,-6 -6,-6s-6,2.69 -6,6c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,1.48 -0.81,2.75 -2,3.45l1,1.74c1.79,-1.04 3,-2.97 3,-5.19zM12,3C6.48,3 2,7.48 2,13c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,18.53 4,15.96 4,13c0,-4.42 3.58,-8 8,-8s8,3.58 8,8c0,2.96 -1.61,5.53 -4,6.92l1,1.73c2.99,-1.73 5,-4.95 5,-8.65 0,-5.52 -4.48,-10 -10,-10z" />
</vector>

View File

@@ -9,11 +9,7 @@
android:id="@+id/errorIcon" android:id="@+id/errorIcon"
android:layout_width="128dp" android:layout_width="128dp"
android:layout_height="128dp" android:layout_height="128dp"
android:layout_marginStart="8dp" android:layout_margin="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -25,11 +21,7 @@
android:id="@+id/errorTitle" android:id="@+id/errorTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_margin="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/sorry" android:text="@string/sorry"
android:textSize="@dimen/text_size_xlarge" android:textSize="@dimen/text_size_xlarge"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -49,6 +41,6 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorTitle" app:layout_constraintTop_toBottomOf="@+id/errorTitle"
tools:text="@string/qr_code_unsupported" /> tools:text="@string/startup_failed_service_error" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginVertical="8dp"
app:layout_constraintBottom_toBottomOf="@id/errorMessageIntro"
app:layout_constraintEnd_toStartOf="@id/errorMessageIntro"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/alerts_and_states_error"
app:tint="@color/briar_red_500"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/errorMessageIntro"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/hotspot_error_intro"
android:textSize="@dimen/text_size_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/errorIcon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/errorMessageDetail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"
android:background="@color/briar_orange_200"
android:gravity="center"
android:padding="8dp"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_medium"
android:typeface="monospace"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorMessageIntro"
tools:text="@string/hotspot_error_no_wifi_direct" />
<Button
android:id="@+id/feedbackButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/send_feedback"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorMessageDetail" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fallbackFragment"
android:name="org.briarproject.briar.android.hotspot.FallbackFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/feedbackButton"
tools:layout="@layout/fragment_hotspot_save_apk" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/wifiTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/hotspot_help_wifi_title"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/wifi1View"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:drawablePadding="6dp"
android:text="@string/hotspot_help_wifi_1"
app:drawableLeftCompat="@drawable/ic_circle_small"
app:drawableStartCompat="@drawable/ic_circle_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wifiTitleView" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/wifi2View"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:drawablePadding="6dp"
android:text="@string/hotspot_help_wifi_2"
app:drawableLeftCompat="@drawable/ic_circle_small"
app:drawableStartCompat="@drawable/ic_circle_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wifi1View" />
<TextView
android:id="@+id/siteTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/hotspot_help_site_title"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wifi2View" />
<TextView
android:id="@+id/site1View"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:drawablePadding="6dp"
android:text="@string/hotspot_help_site_1"
app:drawableLeftCompat="@drawable/ic_circle_small"
app:drawableStartCompat="@drawable/ic_circle_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/siteTitleView" />
<TextView
android:id="@+id/site2View"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:drawablePadding="6dp"
android:text="@string/hotspot_help_site_2"
app:drawableLeftCompat="@drawable/ic_circle_small"
app:drawableStartCompat="@drawable/ic_circle_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/site1View" />
<TextView
android:id="@+id/site3View"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:drawablePadding="6dp"
android:text="@string/hotspot_help_site_3"
app:drawableLeftCompat="@drawable/ic_circle_small"
app:drawableStartCompat="@drawable/ic_circle_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/site2View" />
<TextView
android:id="@+id/site4View"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:drawablePadding="6dp"
android:text="@string/hotspot_help_site_4"
app:drawableLeftCompat="@drawable/ic_circle_small"
app:drawableStartCompat="@drawable/ic_circle_small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/site3View" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fallbackFragment"
android:name="org.briarproject.briar.android.hotspot.FallbackFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/site4View"
tools:layout="@layout/fragment_hotspot_save_apk" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.hotspot.HotspotIntroFragment">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="64dp"
android:layout_marginLeft="64dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="64dp"
android:layout_marginRight="64dp"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toTopOf="@+id/introView"
app:layout_constraintDimensionRatio="1,1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside"
app:srcCompat="@drawable/ic_nickname"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/introView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/hotspot_intro"
app:layout_constraintBottom_toTopOf="@+id/startButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<Button
android:id="@+id/startButton"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:drawablePadding="6dp"
android:text="@string/hotspot_button_start_sharing"
app:drawableLeftCompat="@drawable/ic_wifi_tethering"
app:drawableStartCompat="@drawable/ic_wifi_tethering"
app:drawableTint="@color/button_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.812"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/introView"
app:layout_constraintVertical_bias="1.0"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/startButton"
app:layout_constraintEnd_toStartOf="@+id/progressTextView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@+id/startButton"
app:layout_constraintTop_toTopOf="@+id/startButton"
tools:visibility="visible" />
<TextView
android:id="@+id/progressTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="@string/hotspot_progress_text_start"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/startButton"
app:layout_constraintEnd_toEndOf="@+id/startButton"
app:layout_constraintStart_toEndOf="@+id/progressBar"
app:layout_constraintTop_toTopOf="@+id/startButton"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/manualIntroView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/hotspot_manual_site"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/ssidLabelView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/hotspot_manual_wifi_ssid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manualIntroView" />
<TextView
android:id="@+id/ssidView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:background="@color/briar_primary"
android:padding="8dp"
android:textColor="@color/briar_text_primary_inverse"
android:typeface="monospace"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssidLabelView"
tools:text="DIRECT-42-dfoln3lncsoij23" />
<TextView
android:id="@+id/passwordLabelView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/enter_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssidView" />
<TextView
android:id="@+id/passwordView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:background="@color/briar_primary"
android:padding="8dp"
android:textColor="@color/briar_text_primary_inverse"
android:typeface="monospace"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passwordLabelView"
tools:text="sdfsdgt2334rfw" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/qrIntroView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/hotspot_qr_wifi"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cardView"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardBackgroundColor="#ffffffff"
app:cardCornerRadius="16dp"
app:layout_constrainedHeight="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/qrIntroView"
app:layout_constraintVertical_bias="0.0">
<ImageView
android:id="@+id/qrCodeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="ContentDescription"
tools:src="@tools:sample/avatars" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@id/fallbackTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/hotspot_help_fallback_title"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/fallbackIntro"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/hotspot_help_fallback_intro"
android:textSize="@dimen/text_size_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fallbackTitleView" />
<Button
android:id="@+id/fallbackButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/hotspot_help_fallback_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fallbackIntro" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/fallbackButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/fallbackButton" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/connectedButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways"
app:tabBackground="@color/briar_primary"
app:tabGravity="fill"
app:tabIconTint="@color/action_bar_text"
app:tabIndicatorColor="@color/briar_lime_400"
app:tabIndicatorHeight="4dp"
app:tabInlineLabel="true"
app:tabMaxWidth="0dp"
app:tabMode="fixed"
app:tabTextColor="@color/action_bar_text">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/forum_item_create_white"
android:text="@string/hotspot_tab_manual" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_qr_code"
android:text="@string/qr_code" />
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toTopOf="@+id/connectedButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<Button
android:id="@+id/connectedButton"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/hotspot_button_connected"
app:drawableLeftCompat="@drawable/ic_check_white"
app:drawableStartCompat="@drawable/ic_check_white"
app:drawableTint="@color/button_text"
app:layout_constraintBottom_toTopOf="@+id/stopButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<Button
android:id="@+id/stopButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:drawablePadding="8dp"
android:text="@string/hotspot_button_stop_sharing"
app:drawableLeftCompat="@drawable/ic_portable_wifi_off"
app:drawableStartCompat="@drawable/ic_portable_wifi_off"
app:drawableTint="@color/briar_red_500"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_help"
android:icon="@drawable/ic_help_outline_white"
android:title="@string/help"
app:showAsAction="always" />
</menu>

View File

@@ -1,111 +1,43 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!--Setup--> <!--Setup-->
<string name="setup_title">Добре дошли в Briar</string> <string name="setup_title">Добре дошли в Briar</string>
<string name="setup_name_explanation">Прякорът ви ще бъде видим до всяка ваша публикация. Няма да можете да го промените след като създадете профил.</string> <string name="setup_next">Следващ</string>
<string name="setup_next">Напред</string> <string name="choose_nickname">Изберете име</string>
<string name="setup_password_intro">Изберете парола</string>
<string name="setup_password_explanation">Профилът в Briar се съхранява шифриран на устройството ви, а не в облака. Ако забравите паролата си или премахнете Briar, няма начин да го възстановите.\n\nИзберете дълга, трудна за отгатване парола, например: четири случайни думи или десет случайни букви, числа и знаци.</string>
<string name="setup_doze_title">Свързаност на заден план</string>
<string name="setup_doze_intro">За да получавате съобщения, Briar трябва да е свързан на заден план.</string>
<string name="setup_doze_explanation">За да получавате съобщения, Briar трябва да е свързан на заден план. Изключете оптимизацията на батерията, за да може Briar да остане свързан.</string>
<string name="setup_doze_button">Разрешаване на свързаност</string>
<string name="choose_nickname">Изберете прякор</string>
<string name="choose_password">Изберете парола</string> <string name="choose_password">Изберете парола</string>
<string name="confirm_password">Потвърдете парола</string> <string name="confirm_password">Потвърдете парола</string>
<string name="name_too_long">Името е твърде дълго</string> <string name="name_too_long">Името е твърде дълго</string>
<string name="password_too_weak">Паролата е твърде слаба</string> <string name="password_too_weak">Паролата е твърде слаба</string>
<string name="passwords_do_not_match">Паролите не съвпадат</string> <string name="passwords_do_not_match">Паролите не съвпадат</string>
<string name="create_account_button">Създаване на профил</string> <string name="create_account_button">Създаване на профил</string>
<string name="more_info">Повече информация</string>
<string name="don_t_ask_again">Спиране на този въпрос</string>
<string name="setup_huawei_text">Докоснете бутона по-долу и се уверете, че Briar е защитен в екрана за „Защитени приложения“.</string>
<string name="setup_huawei_button">Защитаване на Briar</string>
<string name="setup_huawei_help">Ако не добавите Briar в списъка на защитени приложения, няма да може да работи на заден план.</string>
<string name="setup_huawei_app_launch_text">Докоснете бутона по-долу, отворете „Стартиране на приложения“ и се уверете, че за Briar е избрано „Ръчно управление“.</string>
<string name="setup_huawei_app_launch_button">Настройки на батерия</string>
<string name="setup_huawei_app_launch_help">Ако за Briar не е избрано „Ръчно управление“ в екрана „Стартиране на приложения“, тогава няма да може да работи на заден план.</string>
<string name="setup_xiaomi_text">За да работи на заден план Briar трябва да бъде заключен в списъка с последно използваните приложения.</string>
<string name="setup_xiaomi_button">Защитаване на Briar</string>
<string name="setup_xiaomi_help">Ако Briar не е заключен в списъка с последно използваните приложения, няма да работи на заден план.</string>
<string name="setup_xiaomi_dialog_body_old">1. Отворете списъка с отворени приложения (списък за превключване на приложения)\n\n2. Плъзнете надолу върху изображението на Briar докато се покаже икона на катинар\n\n3. Ако катинарът е отключен го докоснете, за да го заключите</string>
<string name="setup_xiaomi_dialog_body_new">1. Отворете списъка с отворени приложения (списък за превключване на приложения)\n\n2. Докоснете и задръжте върху изображението на Briar докато се покаже икона на катинар\n\n3. Ако катинарът е отключен го докоснете, за да го заключите</string>
<string name="warning_dozed">%s не може да работи на заден план</string>
<!--Login--> <!--Login-->
<string name="enter_password">Парола</string> <string name="enter_password">Парола</string>
<string name="try_again">Грешна парола, опитайте отново</string> <string name="try_again">Грешна парола, опитайте пак</string>
<string name="dialog_title_cannot_check_password">Паролата не може да бъде проверена</string> <string name="sign_in_button">Вход</string>
<string name="dialog_message_cannot_check_password">Briar не може да провери вашата парола. Рестартирате устройството, за да разрешите проблема.</string>
<string name="sign_in_button">Влизане</string>
<string name="forgotten_password">Забравена парола</string> <string name="forgotten_password">Забравена парола</string>
<string name="dialog_title_lost_password">Забравена парола</string> <string name="dialog_title_lost_password">Забравена парола</string>
<string name="dialog_message_lost_password">Профилът в Briar се съхранява шифриран на вашето устройство, а не в облака и за това паролата не може да бъде сменена. Желаете ли да профилът да бъде премахнат и да бъде направен нов?\n\nВнимание: Профилът, контактите и съобщенията ще бъдат безвъзвратно загубени.</string> <string name="dialog_message_lost_password">Briar профилът се съхранява криптиран във вашето устройство, не в облака, така че не можем да зададем нова парола. Искате ли да изтриете профила си и да започнете отначало?\n\nВнимание: Вашият профил, контакти и съобщения ще бъдат изтрити завинаги.</string>
<string name="startup_failed_notification_title">Briar не можа да стартира</string> <string name="startup_failed_notification_title">Briar не можа да стартира</string>
<string name="startup_failed_notification_text">Докоснете за повече информация.</string>
<string name="startup_failed_activity_title">Неуспешно стартиране</string> <string name="startup_failed_activity_title">Неуспешно стартиране</string>
<string name="startup_failed_db_error">По някаква причина банката от данни на Briar е непоправимо повредена. Вашият профил, данни и всичките ви контакти са загубени. За жалост, се налага да преинсталирате Briar или да създадете нов профил, избирайки „Забравена парола“ от екрана за вход.</string> <string name="startup_failed_service_error">Briar не успя да стартира задължителен плъгин. Обикновено преинсталирането на Briar решава този проблем. Моля, имайте предвид, че ще изгубите профила си и всички данни, асоциирани с него, тъй като Briar не съхранява данните ви в централни сървъри.</string>
<string name="startup_failed_data_too_old_error">Вашия профил е създаден със по-ранно издание на приложението и не може да бъде отворен. Трябва или да инсталирате по-ранното издание или да създадете нов профил, избирайки „Забравена парола“ от екрана за вход.</string> <string name="expiry_date_reached">Софтуерът е невалиден.\nБлагодарим ви за тестването!</string>
<string name="startup_failed_data_too_new_error">Тава издание на приложението е твърде старо. Обновете до последно издание и опитайте отново.</string>
<string name="startup_failed_service_error">Briar не може да стартира задължителна приставка. Обикновено преинсталирането на Briar решава този проблем. Имайте предвид, че ще изгубите профила си и всички свързани с него данни, тъй като Briar не ги съхранява в централни сървъри.</string>
<plurals name="expiry_warning">
<item quantity="one">Това е тестова версия на Briar. Вашият акаунт ще бъде изтече след %d ден и не може да бъде подновена.</item>
<item quantity="other">Това е изпитателно издание на Briar. Валидността на профила ще изтече след %d дена и не може да бъде подновена.</item>
</plurals>
<string name="expiry_date_reached">Софтуерът е с изтекъл срок.\nБлагодарим за изпитването!</string>
<string name="download_briar">За да продължите да използвате Briar изтеглете последното издание.</string>
<string name="create_new_account">Ще трябва да създадете нов профил, но ще можете да използвате същия прякор.</string>
<string name="download_briar_button">Изтегляне</string>
<string name="startup_open_database">Хранилището се дешифрира…</string>
<string name="startup_migrate_database">Хранилището се обновява…</string>
<string name="startup_compact_database">Хранилището се уплътнява…</string>
<!--Navigation Drawer--> <!--Navigation Drawer-->
<string name="nav_drawer_open_description">Отваря навигационната лента</string> <string name="nav_drawer_open_description">Отвори навигационно чекмедже</string>
<string name="nav_drawer_close_description">Затваря навигационната лента</string> <string name="nav_drawer_close_description">Затвори навигационно чекмедже</string>
<string name="contact_list_button">Контакти</string> <string name="contact_list_button">Контакти</string>
<string name="groups_button">Частни групи</string> <string name="groups_button">Частни групи</string>
<string name="forums_button">Форуми</string> <string name="forums_button">Форуми</string>
<string name="blogs_button">Блогове</string> <string name="blogs_button">Блогове</string>
<!--This is part of the main menu. The app will be locked when this is tapped.--> <!--This is part of the main menu. The app will be locked when this is tapped.-->
<string name="lock_button">Заключване</string>
<string name="settings_button">Настройки</string> <string name="settings_button">Настройки</string>
<string name="sign_out_button">Отписване</string> <string name="sign_out_button">Отписване</string>
<string name="transports_onboarding_text">Докоснете, за да изберете как Briar да се свързва с контактите ви.</string> <!--Transports-->
<!--Transports: Tor-->
<string name="transport_tor">Интернет</string> <string name="transport_tor">Интернет</string>
<string name="tor_device_status_online_wifi">Устройството има достъп до интернет през Wi-Fi</string>
<string name="tor_device_status_online_mobile">Устройството има достъп до интернет през мобилни данни</string>
<string name="tor_device_status_offline">Устройството няма достъп до интернет</string>
<string name="tor_plugin_status_enabling">Briar се свързва с интернет</string>
<string name="tor_plugin_status_active">Briar се свързва с интернет</string>
<string name="tor_plugin_status_inactive">Briar не може да се свърже с интернет</string>
<string name="tor_plugin_status_disabled">Briar е настроен да не използва интернет</string>
<string name="tor_plugin_status_disabled_mobile_data">Briar е настроен да не използва мобилни данни</string>
<string name="tor_plugin_status_disabled_battery">Briar е настроен да не използва интернет докато използва батерия</string>
<string name="tor_plugin_status_disabled_country_blocked">Briar е настроен да не използва интернет в тази държава</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string>
<string name="transport_lan_long">Същата безжична мрежа</string>
<string name="lan_device_status_on">Устройството е свързан с безжична мрежа</string>
<string name="lan_device_status_off">Устройството не е свързан с безжична мрежа</string>
<string name="lan_plugin_status_enabling">Briar се свързва с безжична мрежа</string>
<string name="lan_plugin_status_active">Briar е свързан с безжична мрежа</string>
<string name="lan_plugin_status_inactive">Briar не е свързан с безжична мрежа</string>
<string name="lan_plugin_status_disabled">Briar е настроен да не използва безжична мрежа</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">Bluetooth</string> <string name="transport_bt">Bluetooth</string>
<string name="bt_device_status_on">Устройството е с включен Bluetooth</string> <string name="transport_lan">Wi-Fi</string>
<string name="bt_device_status_off">Устройството е с изключен Bluetooth</string>
<string name="bt_plugin_status_enabling">Briar се свързва чрез Bluetooth</string>
<string name="bt_plugin_status_active">Briar е свързан чрез Bluetooth</string>
<string name="bt_plugin_status_inactive">Briar не може да се свърже чрез Bluetooth</string>
<string name="bt_plugin_status_disabled">Briar е настроен да не използва Bluetooth</string>
<!--Notifications--> <!--Notifications-->
<string name="reminder_notification_title">Отписани сте от Briar</string> <string name="ongoing_notification_title">Вписан сте в Briar</string>
<string name="reminder_notification_text">Докоснете за повторно влизане.</string> <string name="ongoing_notification_text">Докоснете, за да отворите Briar.</string>
<string name="reminder_notification_channel_title">Напомняне за вход в Briar</string>
<string name="reminder_notification_dismiss">Отказ</string>
<string name="ongoing_notification_title">Вписани в Briar</string>
<string name="ongoing_notification_text">Докоснете за отваряне на Briar.</string>
<plurals name="private_message_notification_text"> <plurals name="private_message_notification_text">
<item quantity="one">Ново лично съобщение.</item> <item quantity="one">Ново лично съобщение.</item>
<item quantity="other">%d нови лични съобщения.</item> <item quantity="other">%d нови лични съобщения.</item>
@@ -115,543 +47,281 @@
<item quantity="other">%d нови групови съобщения.</item> <item quantity="other">%d нови групови съобщения.</item>
</plurals> </plurals>
<plurals name="forum_post_notification_text"> <plurals name="forum_post_notification_text">
<item quantity="one">Нова публикация във форум.</item> <item quantity="one">Нова форумна публикация.</item>
<item quantity="other">%d нови публикации във форуми.</item> <item quantity="other">%d нови форумни публикации.</item>
</plurals> </plurals>
<plurals name="blog_post_notification_text"> <plurals name="blog_post_notification_text">
<item quantity="one">Нова публикация в блог.</item> <item quantity="one">Нова блог публикация.</item>
<item quantity="other">%d нови публикации в блогове.</item> <item quantity="other">%d нови блог публикации.</item>
</plurals> </plurals>
<!--Misc--> <!--Misc-->
<string name="now">току-що</string> <string name="now">сега</string>
<string name="show">Показване</string> <string name="show">Покажи</string>
<string name="hide">Скриване</string> <string name="hide">Скрий</string>
<string name="ok">Добре</string> <string name="ok">ОК</string>
<string name="cancel">Отказ</string> <string name="cancel">Отказ</string>
<string name="got_it">Разбрах</string> <string name="got_it">Разбрах</string>
<string name="delete">Изтриване</string> <string name="delete">Изтрий</string>
<string name="accept">Приемане</string> <string name="accept">Приеми</string>
<string name="decline">Отказване</string> <string name="decline">Откажи</string>
<string name="online">На линия</string> <string name="options">Опции</string>
<string name="offline">Извън линия</string> <string name="online">Онлайн</string>
<string name="send">Изпращане</string> <string name="offline">Офлайн</string>
<string name="allow">Разрешаване</string> <string name="send">Изпрати</string>
<string name="open">Отваряне</string> <string name="allow">Позволи</string>
<string name="change">Променяне</string> <string name="open">Отвори</string>
<string name="start">Старт</string>
<string name="no_data">Няма данни</string> <string name="no_data">Няма данни</string>
<string name="ellipsis">...</string> <string name="ellipsis">...</string>
<string name="text_too_long">Въведеният текст е твърде дълъг</string> <string name="text_too_long">Въведеният текст е твърде дълъг</string>
<string name="show_onboarding">Показване на помощен диалог</string> <string name="show_onboarding">Показване на помощен диалог</string>
<string name="fix">Поправяне</string>
<string name="help">Помощ</string> <string name="help">Помощ</string>
<string name="sorry">Съжаляваме</string>
<string name="error_start_activity">Недостъпно на вашата система</string>
<string name="status_heading">Състояние</string>
<!--Contacts and Private Conversations--> <!--Contacts and Private Conversations-->
<string name="no_contacts">Няма контакти</string>
<string name="no_contacts_action">Докоснете иконата с +, за да добавите контакти</string>
<string name="date_no_private_messages">Няма съобщения.</string> <string name="date_no_private_messages">Няма съобщения.</string>
<string name="no_private_messages">Няма съобщения</string> <string name="message_hint">Напиши съобщение</string>
<string name="message_hint">Съобщение</string> <string name="delete_contact">Изтрий контакт</string>
<string name="message_hint_auto_delete">Изчезващо съобщение</string> <string name="dialog_title_delete_contact">Потвърди изтриването на контакт</string>
<string name="message_error">Грешка при изпращане на съобщение</string> <string name="dialog_message_delete_contact">Сигурни ли сте, че искате да изтриете този контакт и всички съобщения, обменени с този контакт?</string>
<string name="image_caption_hint">Добавете описание (по желание)</string> <string name="contact_deleted_toast">Контактът е изтрит</string>
<string name="image_attach">Прикачване на изображение</string>
<string name="image_attach_error">Грешка при прикачване на изображения</string>
<string name="image_attach_error_too_big">Изображението е твърде голямо. Има ограничение от %dМБ.</string>
<string name="image_attach_error_invalid_mime_type">Неподдържан формат на изображение: %s</string>
<string name="set_contact_alias">Преименуване на контакт</string>
<string name="set_contact_alias_hint">Име на контакта</string>
<string name="menu_item_disappearing_messages">Изчезващи съобщения</string>
<string name="menu_item_connect_via_bluetooth">Свързване чрез Bluetooth</string>
<string name="dialog_title_connect_via_bluetooth">Свързване чрез Bluetooth</string>
<string name="dialog_message_connect_via_bluetooth">За да сработи този метод, контактът трябва да бъде близо до вас.\n\nДвамата трябва да натиснете „Start“ едновременно.</string>
<string name="toast_connect_via_bluetooth_already_discovering">Има започнат опит за връзка чрез Bluetooth</string>
<string name="toast_connect_via_bluetooth_not_discoverable">Не може да продължи без Bluetooth</string>
<string name="toast_connect_via_bluetooth_no_location_permission">Не може да продължи без разрешение за местоположение</string>
<string name="toast_connect_via_bluetooth_start">Свързване чрез Bluetooth…</string>
<string name="toast_connect_via_bluetooth_success">Успешно свързване чрез Bluetooth</string>
<string name="toast_connect_via_bluetooth_error">Не може да се установи връзка чрез Bluetooth</string>
<!--The first placeholder will show a duration like "7 days". The second placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_you_enabled">Съобщението ще изчезне след %1$s. %2$s</string>
<!--The placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_you_disabled">Съобщенията ви няма да изчезнат. %1$s</string>
<!--The first placeholder will show a contact's name. The second placeholder will show a duration like "7 days". The third placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_contact_enabled">Съобщението от %1$s ще изчезне след %2$s. %3$s</string>
<plurals name="duration_minutes">
<item quantity="one">%d минута</item>
<item quantity="other">%d минути</item>
</plurals>
<plurals name="duration_hours">
<item quantity="one">%d час</item>
<item quantity="other">%d часа</item>
</plurals>
<plurals name="duration_days">
<item quantity="one">%d ден</item>
<item quantity="other">%d дни</item>
</plurals>
<!--The first placeholder will show a contact's name. The second placeholder at the end will add "Tap to learn more."-->
<string name="auto_delete_msg_contact_disabled">Съобщението от %1$s няма да изчезне. %2$s</string>
<string name="tap_to_learn_more">Научете повече</string>
<string name="auto_delete_changed_warning_title">Промяна при изчезващи съобщения</string>
<string name="auto_delete_changed_warning_message_enabled">Откакто съставяте съобщението е бил включен механизмът за изчезващи съобщения.</string>
<string name="auto_delete_changed_warning_message_disabled">Откакто съставяте съобщението е бил изключен механизмът за изчезващи съобщения.</string>
<string name="auto_delete_changed_warning_send">Изпращане въпреки това</string>
<string name="delete_all_messages">Изтриване на всички</string>
<string name="dialog_title_delete_all_messages">Потвърждение на премахване на съобщение</string>
<string name="dialog_message_delete_all_messages">Сигурни ли сте, че желаете всички съобщения да бъдат премахнати?</string>
<string name="dialog_title_not_all_messages_deleted">Не премахнати съобщения</string>
<string name="dialog_message_not_deleted_ongoing_both">Съобщения, свързани с изпратени покани и запознанства не се премахват докато процесът не приключи.</string>
<string name="dialog_message_not_deleted_ongoing_introductions">Съобщения, свързани с изпратени запознанства не се премахват докато процесът не приключи.</string>
<string name="dialog_message_not_deleted_ongoing_invitations">Съобщения, свързани с изпратени покани не се премахват докато процесът не приключи.</string>
<string name="dialog_message_not_deleted_not_all_selected_both">За да премахнете покана или запознанство трябва да изберете заявката и отговора.</string>
<string name="dialog_message_not_deleted_not_all_selected_introductions">За да премахнете запознанство трябва да изберете заявката и отговора.</string>
<string name="dialog_message_not_deleted_not_all_selected_invitations">За да премахнете покана трябва да изберете заявката и отговора.</string>
<string name="delete_contact">Премахване на контакт</string>
<string name="dialog_title_delete_contact">Потвърждение на премахване на контакт</string>
<string name="dialog_message_delete_contact">Сигурни ли сте, че желаете да изтриете контакта и всички обменени с него съобщения?</string>
<string name="contact_deleted_toast">Контактът е премахнат</string>
<!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">Вие</string>
<string name="save_image">Запазване на изображение</string>
<string name="dialog_title_save_image">Запазване на изображението?</string>
<string name="dialog_message_save_image">Запазвайки изображението ще дадете възможност на други приложения да го достъпват.\n\nСигурни ли сте, че желаете да бъде запазено?</string>
<string name="save_image_success">Изображението е запазено</string>
<string name="save_image_error">Грешка при запазване на изображение</string>
<string name="dialog_title_no_image_support">Недостъпни изображения</string>
<string name="dialog_message_no_image_support">Приложението на вашия контакт все още не поддържа изпращане на изображения. След като го обновят тази икона ще се промени.</string>
<string name="dialog_title_image_support">Вече можете да изпращате изображения на този контакт</string>
<string name="dialog_message_image_support">Докоснете иконата за да изпратите изображение</string>
<string name="messaging_too_many_attachments_toast">Само първите %dизображения ще бъдат изпратени</string>
<!--Adding Contacts--> <!--Adding Contacts-->
<string name="add_contact_title">Добавяне на контакт на живо</string> <string name="add_contact_title">Добавяне на контакт</string>
<string name="face_to_face">Трябва да се срещнете лично с човека, чиито контакт искате да добавите.\n\nТака никой не може да се представи за вас или да чете съобщенията ви в бъдеще.</string> <string name="face_to_face">Трябва да се срещнете лично с човека, когото искате да добавите в Контакти.\n\nПо този начин никой не може да се представи за вас или да чете съобщенията ви в бъдеще.</string>
<string name="continue_button">Напред</string> <string name="continue_button">Напред</string>
<string name="try_again_button">Нов опит</string> <string name="try_again_button">Опитай пак</string>
<string name="waiting_for_contact_to_scan">Изчакване контактът да сканира и да се свърже\u2026</string> <string name="waiting_for_contact_to_scan">Изчакване контактът да сканира и да се свърже\u2026</string>
<string name="exchanging_contact_details">Обмяна на данни за контакт\u2026</string> <string name="exchanging_contact_details">Обмен на данни за контакт\u2026</string>
<string name="contact_added_toast">Добавен контакт: %s</string> <string name="contact_added_toast">Добавен конктакт: %s</string>
<string name="contact_already_exists">Контактът %s вече съществува</string> <string name="contact_already_exists">Контактът %s вече съществува</string>
<string name="qr_code_invalid">Кодът за QR е недействителен</string> <string name="qr_code_invalid">QR кодът е невалиден</string>
<string name="qr_code_too_old">Сканираният код за QR е от по-ранно издание на %s.\n\nНека вашия контакт инсталира последното издание и да пробва отново.</string>
<string name="qr_code_too_new">Сканираният код за QR е от по-ново издание на %s.\n\nИнсталирайте последното издание и пробвайте отново.</string>
<string name="camera_error">Грешка в камерата</string>
<string name="connecting_to_device">Свързване с устройство\u2026</string> <string name="connecting_to_device">Свързване с устройство\u2026</string>
<string name="authenticating_with_device">Удостоверяване с устройство\u2026</string> <string name="authenticating_with_device">Удостоверяване с устройство\u2026</string>
<string name="connection_error_title">Не може да бъде установена връзка с контакта</string>
<string name="connection_error_feedback">Ако проблемът продължава, <a href="feedback">изпратете обратна връзка</a>, за да ни помогнете да подобрим приложението.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Добавяне на контакт отдалечено</string>
<string name="add_contact_nearby_title">Добавяне на контакт на живо</string>
<string name="add_contact_remotely_title">Добавяне на контакт отдалечено</string>
<string name="contact_link_intro">Въведете препратката от вашия контакт</string>
<string name="contact_link_hint">Препратка от контакт</string>
<string name="paste_button">Поставяне</string>
<string name="add_contact_button">Добавяне на контакт</string>
<string name="copy_button">Копиране</string>
<string name="share_button">Споделяне</string>
<string name="send_link_title">Размяна на препратки</string>
<string name="add_contact_choose_nickname">Избиране на прякор</string>
<string name="add_contact_choose_a_nickname">Прякор</string>
<string name="nickname_intro">Изберете прякор на контакта. Той е видим само за вас.</string>
<string name="your_link">Споделете тази препратка с контакта, когото добавяте</string>
<string name="link_clip_label">Препратка на Briar</string>
<string name="link_copied_toast">Препратката е копирана</string>
<string name="adding_contact_error">Грешка при добавяне на контакт.</string>
<string name="pending_contact_requests_snackbar">Има чакащи заявки за контакт</string>
<string name="pending_contact_requests">Чакащи заявки за контакт</string>
<string name="no_pending_contacts">Няма заявки за контакт</string>
<string name="waiting_for_contact_to_come_online">Изчакване на контакта да излезе на линия…</string>
<string name="connecting">Свързване…</string>
<string name="adding_contact">Добавяне на контакт…</string>
<string name="adding_contact_failed">Грешка при добавяне на контакт</string>
<string name="dialog_title_remove_pending_contact">Потвърждение на премахване</string>
<string name="dialog_message_remove_pending_contact">Контактът е в процес на добавяне. Ако сега го премахнете няма да бъде добавен.</string>
<string name="own_link_error">Въведете препратка от ваш контакт, не своята</string>
<string name="nickname_missing">Въведете прякор</string>
<string name="invalid_link">Препратката е недействителна</string>
<string name="unsupported_link">Препратката е от по-ново издание на Briar. Инсталирайте последното издание и пробвайте отново.</string>
<string name="intent_own_link">Отваряте своята препратка. Използвайте тази от контакта, когото добавяте.</string>
<string name="missing_link">Въведете препратка</string>
<!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<plurals name="contact_added_notification_text">
<item quantity="one">Добавен е нов контакт.</item>
<item quantity="other">Добавени са %d нови контакта.</item>
</plurals>
<string name="offline_state">Няма връзка с интернет.</string>
<string name="duplicate_link_dialog_title">Дублираща се препратка</string>
<string name="duplicate_link_dialog_text_1">Вече имате чакаща заявка за контакт с тази препратка: %s</string>
<string name="duplicate_link_dialog_text_1_contact">Вече имате контакт с тази препратка: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">%s и %s един и същи човек ли са?</string>
<!--This is a button for answering that two nicknames do indeed refer to the same person. This
string will be used in a dialog button, so if the translation of this string is longer than 20
characters, please use "Yes" instead, and use "No" for the "Different Person" button-->
<string name="same_person_button">Да</string>
<!--This is a button for answering that two nicknames refer to different people. This string
will be used in a dialog button, so if the translation of this string longer than 20 characters,
please use "No" instead, and use "Yes" for the "Same Person" button-->
<string name="different_person_button">Не</string>
<string name="duplicate_link_dialog_text_3">%s и %s са изпратили еднакви препратки.\n\nЕдиния от двамата вероятно се опитва да разбере кои са контактите ви.\n\nНе им споделяйте, че сте получили същата препратка от друг човек.</string>
<string name="pending_contact_updated_toast">Обновена чакаща заявка за контакт</string>
<!--Introductions--> <!--Introductions-->
<string name="introduction_onboarding_title">Запознаване на контакти</string> <string name="introduction_onboarding_title">Представете контактите си</string>
<string name="introduction_onboarding_text">Можете да запознавате контакти помежду им. Така няма да им се наложи да се срещат лично, за да се свържат в Briar.</string> <string name="introduction_onboarding_text">Можете да представите контактите си един на друг, за да не им се налага да се срещат лично, когато се свързват чрез Briar.</string>
<string name="introduction_menu_item">Запознаване</string> <string name="introduction_menu_item">Представи</string>
<string name="introduction_activity_title">Избор на контакт</string> <string name="introduction_activity_title">Избери контакт</string>
<string name="introduction_not_possible">С тези контакти вече имате запознанство в процес. Нека първо завърши. Ако вие или контактите ви сте рядко на линия може да отнеме известно време.</string> <string name="introduction_message_title">Представи контакти</string>
<string name="introduction_message_title">Запознаване на контакти</string>
<string name="introduction_message_hint">Добавете съобщение (незадължително)</string> <string name="introduction_message_hint">Добавете съобщение (незадължително)</string>
<string name="introduction_button">Запознаване</string> <string name="introduction_button">Представи</string>
<string name="introduction_sent">Покана за запознанство е изпратена.</string> <string name="introduction_sent">Представянето ви е изпратено.</string>
<string name="introduction_error">Грешка при изпращане на покана за запознанство.</string> <string name="introduction_error">Възникна грешка при представянето.</string>
<string name="introduction_request_sent">Пожелахте да запознаете %1$s и %2$s.</string> <string name="introduction_response_error">Грешка при отговор на представянето</string>
<string name="introduction_request_received">%1$s пожела да ви запознае с/ъс %2$s. Желаете ли да добавите %2$s към контактите си?</string> <string name="introduction_request_sent">Помолихте да представите %1$s на %2$s.</string>
<string name="introduction_request_exists_received">%1$s пожела да ви запознае с/ъс %2$s, но %2$s вече е в списъка ви с контакти. Тъй като %1$s може би не знае, все пак можете да отговорите:</string> <string name="introduction_request_received">%1$s помоли да ви представи %2$s. Искате ли да добавите %2$s към контактите си?</string>
<string name="introduction_request_answered_received">%1$s пожела да ви запознае с/ъс %2$s.</string> <string name="introduction_request_exists_received">%1$s помоли да ви представи на %2$s, но %2$s вече е в списъка ви с контакти. Тъй като %1$s може би не знае, все пак можете да отговорите:</string>
<string name="introduction_response_accepted_sent">Приехте запознанство с/ъс %1$s.</string> <string name="introduction_request_answered_received">%1$s помоли да ви представи на %2$s.</string>
<string name="introduction_response_accepted_sent_info">Преди %1$s да бъде добавен/а към контактите ви той/тя също трябва да приеме поканата. Може да отнеме известно време.</string> <string name="introduction_response_accepted_sent">Приехте представянето на %1$s.</string>
<string name="introduction_response_declined_sent">Отказахте запознанство с/ъс %1$s.</string> <string name="introduction_response_declined_sent">Отказахте представянето на %1$s.</string>
<string name="introduction_response_declined_auto">Запознанството с/ъс %1$s е отказано автоматично.</string> <string name="introduction_response_accepted_received">%1$s прие представянето на %2$s.</string>
<string name="introduction_response_accepted_received">%1$s приема запознанство с/ъс %2$s.</string> <string name="introduction_response_declined_received">%1$s отказа представянето на %2$s.</string>
<string name="introduction_response_declined_received">%1$s отказа запознанство с/ъс %2$s.</string> <string name="introduction_response_declined_received_by_introducee">%1$s казва, че %2$s отказва представянето.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s казва, че %2$s отказва запознанство.</string> <plurals name="contact_added_notification_text">
<item quantity="one">Добавен нов контакт.</item>
<item quantity="other">%d добавени нови контакти.</item>
</plurals>
<!--Private Groups--> <!--Private Groups-->
<string name="groups_list_empty">Няма групи</string> <string name="groups_created_by">Създаден от %s</string>
<string name="groups_list_empty_action">Докоснете иконата с +, за да създадете своя или поискайте от контактите си да споделят група с вас</string>
<string name="groups_created_by">Основател %s</string>
<plurals name="messages"> <plurals name="messages">
<item quantity="one">%d съобщение</item> <item quantity="one">%d съобщение</item>
<item quantity="other">%d съобщения</item> <item quantity="other">%d съобщения</item>
</plurals> </plurals>
<string name="groups_group_is_empty">Групата е празна</string> <string name="groups_group_is_empty">Групата е празна</string>
<string name="groups_group_is_dissolved">Групата е разпусната</string> <string name="groups_group_is_dissolved">Групата се е разпаднала</string>
<string name="groups_remove">Премахване</string> <string name="groups_remove">Премахни</string>
<string name="groups_create_group_title">Създаване на частна група</string> <string name="groups_create_group_title">Създаване на група</string>
<string name="groups_create_group_button">Създаване на група</string> <string name="groups_create_group_button">Създай група</string>
<string name="groups_create_group_invitation_button">Изпращане на покана</string> <string name="groups_create_group_invitation_button">Изпрати покана</string>
<string name="groups_create_group_hint">Име на частната група</string> <string name="groups_create_group_hint">Изберете име за групата</string>
<string name="groups_invitation_sent">Поканата за членство в група е изпратена</string> <string name="groups_invitation_sent">Поканата в група е изпратена</string>
<string name="groups_message_sent">Съобщението е изпратено</string>
<string name="groups_member_list">Списък с участници</string> <string name="groups_member_list">Списък с участници</string>
<string name="groups_invite_members">Покани за членство</string> <string name="groups_invite_members">Поканете участници</string>
<string name="groups_member_created_you">Вие създадохте групата</string> <string name="groups_member_created_you">Вие създадохте групата</string>
<string name="groups_member_created">%s създаде групата</string> <string name="groups_member_created">%s създаде групата</string>
<string name="groups_member_joined_you">Вие се включихте в групата</string> <string name="groups_member_joined_you">Включихте се в групата</string>
<string name="groups_member_joined">%s се включи в групата</string> <string name="groups_member_joined">%s се включи в групата</string>
<string name="groups_leave">Напускане на групата</string> <string name="groups_leave">Напусни групата</string>
<string name="groups_leave_dialog_title">Потвърждение на напускане</string> <string name="groups_leave_dialog_title">Потвърждение на напускането</string>
<string name="groups_leave_dialog_message">Сигурни ли сте, че искате да напуснете тази група?</string> <string name="groups_leave_dialog_message">Сигурни ли сте, че искате да напуснете тази група?</string>
<string name="groups_dissolve">Разпускане на група</string> <string name="groups_dissolve">Затвори групата</string>
<string name="groups_dissolve_dialog_title">Потвърждение на разпускане на група</string> <string name="groups_dissolve_dialog_title">Потвърди затварянето на групата</string>
<string name="groups_dissolve_dialog_message">Сигурни ли сте, че искате да разпуснете групата?\n\nОстаналите членове няма да могат да продължат разговорите си и може да не получат последните съобщения.</string> <string name="groups_dissolve_dialog_message">Сигурни ли сте, че искате да затворите групата?\n\nВсички други участници няма да могат да продължат разговора и може да не получат най-новите съобщения. </string>
<string name="groups_dissolve_button">Разпускане</string> <string name="groups_dissolve_button">Затвори</string>
<string name="groups_dissolved_dialog_title">Разпусната група</string> <string name="groups_dissolved_dialog_title">Групата е затворена</string>
<string name="groups_dissolved_dialog_message">Групата е разпусната от нейния основател.\n\nНе можете да изпращате съобщения и може да не сте получили всички изпратени до групата съобщения.</string> <string name="groups_dissolved_dialog_message">Групата е затворена от създателя.\n\nНе можете да пишете нови съобщения в груповия чат и може да не получите най-новите съобщения. </string>
<!--Private Group Invitations--> <!--Private Group Invitations-->
<string name="groups_invitations_title">Покани за членство в група</string> <string name="groups_invitations_title">Покани в група</string>
<string name="groups_invitations_invitation_sent">Поканихте %1$s в групата %2$s</string> <string name="groups_invitations_invitation_sent">Поканихте %1$s в групата \"%2$s\"</string>
<string name="groups_invitations_invitation_received">%1$s ви покани в групата %2$s.</string> <string name="groups_invitations_invitation_received">%1$s ви покани в групата \"%2$s\".</string>
<string name="groups_invitations_joined">Включихте се в групата</string> <string name="groups_invitations_joined">Включихте се в групата</string>
<string name="groups_invitations_declined">Отказана покана за присъединяване в група</string> <string name="groups_invitations_declined">Отказана покана в група</string>
<plurals name="groups_invitations_open"> <plurals name="groups_invitations_open">
<item quantity="one">%d получена покана за членство в група</item> <item quantity="one">%d отворена покана в група</item>
<item quantity="other">%d получени покани за членство в група</item> <item quantity="other">%d отворени покани в група</item>
</plurals> </plurals>
<string name="groups_invitations_response_accepted_sent">Приехте поканата от %s за членство в група.</string> <string name="groups_invitations_response_accepted_sent">Приехте поканата в група на %s.</string>
<string name="groups_invitations_response_declined_sent">Отказахте поканата от %s за членство в група.</string> <string name="groups_invitations_response_declined_sent">Отказахте поканата в група на %s.</string>
<string name="groups_invitations_response_declined_auto">Поканата от %s за членство в група е отказана автоматично.</string> <string name="groups_invitations_response_accepted_received">%s прие поканата в група.</string>
<string name="groups_invitations_response_accepted_received">%s прие поканата за членство в група.</string> <string name="groups_invitations_response_declined_received">%s отказа поканата в група. </string>
<string name="groups_invitations_response_declined_received">%s отказа поканата за членство в група. </string> <string name="sharing_status_groups">Само създателят може да покани нови участници в групата. По-долу са изброени сегашните участници в групата.</string>
<string name="sharing_status_groups">Само основателят може да кани нови участници в групата. По-долу е списъкът с текущите участници.</string>
<!--Private Groups Revealing Contacts--> <!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">Разкриване на контакти</string> <string name="groups_reveal_contacts">Разкрий контакти</string>
<string name="groups_reveal_dialog_message">Можете да изберете дали да разкриете контактите на всички сегашни и бъдещи членове на групата.\n\nС разкриването им връзката с групата става по-бърза и надеждна, защото общувате с разкритите контакти, даже и основателят на групата да е извън мрежа.</string> <string name="groups_reveal_dialog_message">Може да изберете да разкриете контактите на всички сегашни и бъдещи участници в тази група.\n\nРазкриването на контактите прави връзката с групата по-бърза и сигурна, тъй като можете да общувате с разкрити контакти, дори когато създателят на групата е офлайн.</string>
<string name="groups_reveal_visible">Отношенията ви с контакта са видими за групата</string> <string name="groups_reveal_visible">Връзка с контакта се вижда от групата</string>
<string name="groups_reveal_visible_revealed_by_us">Отношенията ви с контакта са видими за групата (разкрити от вас)</string> <string name="groups_reveal_visible_revealed_by_us">Връзка с контакта се вижда от групата (разкрита от вас)</string>
<string name="groups_reveal_visible_revealed_by_contact">Отношенията ви с контакта са видими за групата (разкрити от %s)</string> <string name="groups_reveal_visible_revealed_by_contact">Връзка с контакта се вижда от групата (разкрита от %s)</string>
<string name="groups_reveal_invisible">Отношенията ви с контакта не са видими за групата</string> <string name="groups_reveal_invisible">Връзка с контакта не се вижда от групата</string>
<!--Forums--> <!--Forums-->
<string name="no_forums">Няма форуми</string>
<string name="no_forums_action">Докоснете иконата с +, за да създадете свой или поискайте от контактите си да споделят форум с вас</string>
<string name="create_forum_title">Създаване на форум</string> <string name="create_forum_title">Създаване на форум</string>
<string name="choose_forum_hint">Име на форума</string> <string name="choose_forum_hint">Изберете име за форума</string>
<string name="create_forum_button">Създаване на форум</string> <string name="create_forum_button">Създай форум</string>
<string name="forum_created_toast">Форумът е създаден</string> <string name="forum_created_toast">Форумът е създаден</string>
<string name="no_forum_posts">Няма публикации</string>
<string name="no_posts">Няма публикации</string> <string name="no_posts">Няма публикации</string>
<plurals name="posts"> <plurals name="posts">
<item quantity="one">%d публикация</item> <item quantity="one">%d публикация</item>
<item quantity="other">%d публикации</item> <item quantity="other">%d публикации</item>
</plurals> </plurals>
<string name="forum_new_message_hint">Публикация</string> <string name="forum_message_reply_hint">Нов отговор</string>
<string name="forum_message_reply_hint">Отговор</string> <string name="btn_reply">Отговори</string>
<string name="btn_reply">Отговаряне</string> <string name="forum_leave">Напусни форума</string>
<string name="forum_leave">Напускане на форума</string> <string name="dialog_title_leave_forum">Потвърдете напускането на форума</string>
<string name="dialog_title_leave_forum">Потвърждение на напускане на форум</string> <string name="dialog_button_leave">Напусни</string>
<string name="dialog_message_leave_forum">Сигурни ли сте, че искате да напуснете този форум?\n\nКонтактите, с които сте го споделили може да спрат да получават публикации.</string>
<string name="dialog_button_leave">Напускане</string>
<string name="forum_left_toast">Напуснахте форума</string>
<!--Forum Sharing--> <!--Forum Sharing-->
<string name="forum_share_button">Споделяне форума</string> <string name="forum_share_button">Сподели форум</string>
<string name="contacts_selected">Избрани контакти</string> <string name="contacts_selected">Избрани контакти</string>
<string name="activity_share_toolbar_header">Избиране на контакти</string> <string name="activity_share_toolbar_header">Избиране на контакти</string>
<string name="no_contacts_selector">Няма контакти</string>
<string name="no_contacts_selector_action">Върнете се след като добавите контакти</string>
<string name="forum_shared_snackbar">Форумът е споделен с избраните контакти</string> <string name="forum_shared_snackbar">Форумът е споделен с избраните контакти</string>
<string name="forum_share_message">Добавете съобщение (незадължително)</string> <string name="forum_share_message">Добавете съобщение (незадължително)</string>
<string name="forum_share_error">Грешка при споделянето на форума.</string> <string name="forum_share_error">Възникна грешка при споделянето на този форум.</string>
<string name="forum_invitation_received">%1$s сподели с вас форума %2$s.</string> <string name="forum_invitation_received">%1$s сподели форума \"%2$s\" с вас.</string>
<string name="forum_invitation_sent">Споделихте форума %1$sсс %2$s.</string> <string name="forum_invitation_sent">Споделихте форума \"%1$s\" с %2$s.</string>
<string name="forum_invitations_title">Покани за членство във форум</string> <string name="forum_invitations_title">Покани във форум</string>
<string name="forum_invitation_exists">Вече приехте покана за този форум.\n\nС приемане на повече покани връзката с групата става по-бърза и надеждна.</string>
<string name="forum_joined_toast">Присъединихте се към форума</string>
<string name="forum_declined_toast">Поканата е отказана</string>
<string name="shared_by_format">Споделен от %s</string> <string name="shared_by_format">Споделен от %s</string>
<string name="forum_invitation_already_sharing">Вече е споделен</string> <string name="forum_invitation_already_sharing">Вече е споделен</string>
<string name="forum_invitation_response_accepted_sent">Приехте поканата от %s за членство във форум.</string> <string name="forum_invitation_response_accepted_sent">Приехте поканата за форум на %s.</string>
<string name="forum_invitation_response_declined_sent">Отказахте поканата на %s за членство във форум.</string> <string name="forum_invitation_response_declined_sent">Отказахте поканата във форум от %s.</string>
<string name="forum_invitation_response_declined_auto">Поканата от %s за членство във форум е отказана автоматично.</string>
<string name="forum_invitation_response_accepted_received">%s прие поканата във форум.</string> <string name="forum_invitation_response_accepted_received">%s прие поканата във форум.</string>
<string name="forum_invitation_response_declined_received">%s отказа поканата във форум.</string> <string name="forum_invitation_response_declined_received">%s отказа поканата във форум.</string>
<string name="sharing_status">Състояние на споделяне</string> <string name="sharing_status">Статус на споделянето</string>
<string name="sharing_status_forum">Всеки участник във форума може да го сподели с контактите си. Споделяте този форум със следните контакти. Възможно е да има и други, които не можете да видите.</string> <string name="sharing_status_forum">Всеки участник във форума може да го сподели с контактите си. Споделяте този форум със следните контакти. Възможно е да има и други, които не можете да видите.</string>
<string name="shared_with">Споделен %1$d (на линия %2$d)</string> <string name="shared_with">Споделен с %1$d (%2$d онлайн)</string>
<plurals name="forums_shared"> <plurals name="forums_shared">
<item quantity="one">%d форум, споделен от контакти</item> <item quantity="one">%d форум, споделен от контакти</item>
<item quantity="other">%d форума, споделени от контакти</item> <item quantity="other">%d форума, споделени от контакти</item>
</plurals> </plurals>
<string name="nobody">Никого</string> <string name="nobody">Никого</string>
<!--Blogs--> <!--Blogs-->
<string name="blogs_other_blog_empty_state">Няма публикации</string> <string name="read_more">прочети още</string>
<string name="read_more">повече</string> <string name="blogs_write_blog_post">Нова блог публикация</string>
<string name="blogs_write_blog_post">Нова публикация в блога</string>
<string name="blogs_write_blog_post_body_hint">Въведете своята публикация</string>
<string name="blogs_publish_blog_post">Публикуване</string> <string name="blogs_publish_blog_post">Публикуване</string>
<string name="blogs_blog_post_created">Публикацията в блога е създадена</string> <string name="blogs_blog_post_created">Блог публикацията е създадена</string>
<string name="blogs_blog_post_received">Получена е нова публикация в блога</string> <string name="blogs_blog_post_received">Нова блог публикация</string>
<string name="blogs_blog_post_scroll_to">Плъзване до нея</string> <string name="blogs_blog_post_scroll_to">Отвори</string>
<string name="blogs_feed_empty_state">Няма публикации</string>
<string name="blogs_feed_empty_state_action">Публикации от вашите контакти и абонираните блогове се показват тук.\n\nДокоснете иконата на писалка, за да направите публикация.</string>
<string name="blogs_remove_blog">Премахване на блог</string> <string name="blogs_remove_blog">Премахване на блог</string>
<string name="blogs_remove_blog_dialog_message">Сигурни ли сте, че желаете да изтриете блога?\n\nПубликациите ще бъдат премахнати от устройството ви, но не и от устройствата на другите членове.\n\nКонтактите, с които сте споделили този блог може да спрат да получават обновявания.</string>
<string name="blogs_remove_blog_ok">Премахване</string> <string name="blogs_remove_blog_ok">Премахване</string>
<string name="blogs_blog_removed">Блогът е премахнат</string>
<string name="blogs_reblog_comment_hint">Добавете съобщение (незадължително)</string> <string name="blogs_reblog_comment_hint">Добавете съобщение (незадължително)</string>
<string name="blogs_reblog_button">Препубликуване</string> <string name="blogs_reblog_button">Реблог</string>
<!--Blog Sharing--> <!--Blog Sharing-->
<string name="blogs_sharing_share">Споделяне на блог</string> <string name="blogs_sharing_share">Споделяне на блог</string>
<string name="blogs_sharing_error">Грешка при споделяне блога.</string> <string name="blogs_sharing_error">Възникна грешка при споделянето на този блог.</string>
<string name="blogs_sharing_button">Споделяне на блог</string> <string name="blogs_sharing_button">Сподели блог</string>
<string name="blogs_sharing_snackbar">Блогът е споделен с избраните контакти</string> <string name="blogs_sharing_snackbar">Блогът е споделен с избраните контакти</string>
<string name="blogs_sharing_response_accepted_sent">Приехте поканата на %s за абонамент за блог.</string> <string name="blogs_sharing_response_accepted_sent">Приехте поканата в блог от %s.</string>
<string name="blogs_sharing_response_declined_sent">Отказахте поканата на %s за абонамент за блог.</string> <string name="blogs_sharing_response_declined_sent">Отказахте поканата в блог от %s.</string>
<string name="blogs_sharing_response_declined_auto">Поканата на %s за абонамент за блог е отказана автоматично.</string> <string name="blogs_sharing_response_accepted_received">%s прие поканата в блог.</string>
<string name="blogs_sharing_response_accepted_received">%s прие поканата за абонамент за блог.</string> <string name="blogs_sharing_response_declined_received">%s отказа поканата в блог.</string>
<string name="blogs_sharing_response_declined_received">%s отказа поканата за абонамент за блог.</string> <string name="blogs_sharing_invitation_received">%1$s сподели блога \"%2$s\" с вас.</string>
<string name="blogs_sharing_invitation_received">%1$s сподели с вас блога „%2$s.</string> <string name="blogs_sharing_invitation_sent">Споделихте блога \"%1$s\" с %2$s.</string>
<string name="blogs_sharing_invitation_sent">Споделихте блога „%1$s“ с/ъс %2$s.</string> <string name="blogs_sharing_invitations_title">Блог покани</string>
<string name="blogs_sharing_invitations_title">Покани за абонамент за блог</string>
<string name="blogs_sharing_joined_toast">Абонирахте се за блогa</string>
<string name="blogs_sharing_declined_toast">Поканата е отказана</string>
<string name="sharing_status_blog">Всеки абонат на блога може да го сподели с контактите си. Споделяте този блог със следните контакти. Възможно е да има и други, които не можете да видите.</string> <string name="sharing_status_blog">Всеки абонат на блога може да го сподели с контактите си. Споделяте този блог със следните контакти. Възможно е да има и други, които не можете да видите.</string>
<!--RSS Feeds--> <!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Внасяне на емисия на RSS</string> <string name="blogs_rss_feeds_import">Внасяне на RSS емисия</string>
<string name="blogs_rss_feeds_import_button">Внасяне</string> <string name="blogs_rss_feeds_import_button">Внасяне</string>
<string name="blogs_rss_feeds_import_hint">Aдрес на емисия</string> <string name="blogs_rss_feeds_import_hint">Въведете URL адреса на RSS емисията</string>
<string name="blogs_rss_feeds_import_error">Грешка при внасяне на емисията.</string> <string name="blogs_rss_feeds_import_error">Възникна грешка при внасянето на емисия.</string>
<string name="blogs_rss_feeds_import_exists">Емисията вече е внесена.</string>
<string name="blogs_rss_feeds">Емисии на RSS</string>
<string name="blogs_rss_feeds_manage_imported">Внесена:</string> <string name="blogs_rss_feeds_manage_imported">Внесена:</string>
<string name="blogs_rss_feeds_manage_author">Автор:</string> <string name="blogs_rss_feeds_manage_author">Автор:</string>
<string name="blogs_rss_feeds_manage_updated">Последно обновяване:</string> <string name="blogs_rss_feeds_manage_updated">Последно актуализиране:</string>
<string name="blogs_rss_remove_feed">Премахване на емисия</string> <string name="blogs_rss_remove_feed">Премахване на емисия</string>
<string name="blogs_rss_remove_feed_dialog_message">Сигурни ли сте, че желаете да изтриете емисията?\n\nПубликациите ще бъдат премахнати от устройството ви, но не и от устройствата на другите членове.\n\nКонтактите, с които сте споделили тази емисия може да спрат да получават обновявания.</string>
<string name="blogs_rss_remove_feed_ok">Премахване</string> <string name="blogs_rss_remove_feed_ok">Премахване</string>
<string name="blogs_rss_feeds_manage_empty_state">Няма емисии на RSS\n\nДокоснете иконата с +, за да внесете емисия</string>
<string name="blogs_rss_feeds_manage_error">Възникна проблем при зареждането на емисиите ви. Моля, опитайте пак по-късно.</string> <string name="blogs_rss_feeds_manage_error">Възникна проблем при зареждането на емисиите ви. Моля, опитайте пак по-късно.</string>
<!--Settings Profile Picture-->
<string name="change_profile_picture">Докоснете за смяна на профилната снимка</string>
<string name="dialog_confirm_profile_picture_title">Смяна на профилна снимка</string>
<string name="dialog_confirm_profile_picture_remark">Само вашите контакти виждат това изображение</string>
<string name="change_profile_picture_failed_message">Грешка при смяна на профилната снимка</string>
<!--Settings Display--> <!--Settings Display-->
<string name="pref_language_title">Език и регион</string> <!--Settings Network-->
<string name="pref_language_changed">Тази настройка ще име ефект след рестарт на Briar. Отпишете се и рестартирайте Briar.</string> <string name="network_settings_title">Мрежа</string>
<string name="pref_language_default">Спрямо системата</string> <string name="bluetooth_setting">Свързване чрез Bluetooth</string>
<string name="display_settings_title">Външен вид</string> <string name="bluetooth_setting_enabled">Когато контактите са наблизо</string>
<string name="pref_theme_title">Тема</string> <string name="bluetooth_setting_disabled">Само при добавяне на контакти</string>
<string name="pref_theme_light">Светла</string> <!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
<string name="pref_theme_dark">Тъмна</string>
<string name="pref_theme_auto">Автоматична (ден или нощ)</string>
<string name="pref_theme_system">Спрямо системата</string>
<!--Settings Connections-->
<string name="network_settings_title">Свързаност</string>
<string name="bluetooth_setting">Свързване с контактите чрез Bluetooth</string>
<string name="wifi_setting">Свързване с контактите в същата безжична мрежа</string>
<string name="tor_enable_title">Свързване с контактите през интернет</string>
<string name="tor_enable_summary">За повече поверителност цялата връзка към интернет се пренасочва през мрежата на Tor</string>
<string name="tor_network_setting">Начин на свързване към мрежата на Tor</string>
<string name="tor_network_setting_automatic">Автоматично, на база местоположение</string>
<string name="tor_network_setting_without_bridges">Използване на мрежата на Tor без мостове</string>
<string name="tor_network_setting_with_bridges">Използване на мрежата на Tor с мостове</string>
<string name="tor_network_setting_never">Без свързване с интернет</string>
<!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"-->
<string name="tor_network_setting_summary">Автоматично: %1$s (в/ъв %2$s)</string>
<string name="tor_mobile_data_title">Използване на мобилни данни</string>
<string name="tor_only_when_charging_title">Свързване към интернет само на зарядно</string>
<string name="tor_only_when_charging_summary">Изключва се връзката с интернет, когато устройството се използва на батерия</string>
<!--Settings Security and Panic--> <!--Settings Security and Panic-->
<string name="security_settings_title">Сигурност</string> <string name="security_settings_title">Сигурност</string>
<string name="pref_lock_title">Заключване на приложението</string>
<string name="pref_lock_summary">Заключва се екрана, за да предпази Briar докато сте вписани</string>
<string name="pref_lock_disabled_summary">За да се възползвате от тази възможност, настройте заключване на екрана</string>
<string name="pref_lock_timeout_title">Заключване при бездействие</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour--> <!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<string name="pref_lock_timeout_summary">Briar се изключва автоматично при неактивност от %s</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"--> <!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_1">1 минута</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"--> <!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_5">5 минути</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"--> <!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_15">15 минути</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"--> <!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_30">30 минути</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"--> <!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
<string name="pref_lock_timeout_60">1 час</string>
<string name="pref_lock_timeout_never">Никога</string> <string name="pref_lock_timeout_never">Никога</string>
<string name="pref_lock_timeout_never_summary">Briar никога да не се заключва автоматично</string>
<string name="change_password">Промяна на парола</string> <string name="change_password">Промяна на парола</string>
<string name="current_password">Текуща парола</string>
<string name="choose_new_password">Нова парола</string>
<string name="confirm_new_password">Потвърдете новата парола</string>
<string name="password_changed">Паролата е променена.</string> <string name="password_changed">Паролата е променена.</string>
<string name="panic_setting">Настройка на бутон за паника</string> <string name="panic_setting">Настройка на паник бутон</string>
<string name="panic_setting_title">Бутон за паника</string> <string name="panic_setting_title">Паник бутон</string>
<string name="panic_setting_hint">Настройва се реакцията на Briar при използване на приложение за бутон за паника</string> <string name="panic_setting_hint">Конфигурация на приложение за паник бутон</string>
<string name="panic_app_setting_title">Приложение бутон за паника</string> <string name="panic_app_setting_title">Приложение за паник бутон</string>
<string name="unknown_app">непознато приложение</string> <string name="unknown_app">непознато приложение</string>
<string name="panic_app_setting_summary">Няма зададено приложение</string> <string name="panic_app_setting_summary">Няма зададено приложение</string>
<string name="panic_app_setting_none">Няма</string> <string name="panic_app_setting_none">Няма</string>
<string name="dialog_title_connect_panic_app">Потвърждение на приложение при паника</string> <string name="dialog_title_connect_panic_app">Потвърждение на паник приложение</string>
<string name="dialog_message_connect_panic_app">Сигурни ли сте, че желаете да позволите на %1$s да задейства разрушителните действия на бутона за паника?</string> <string name="dialog_message_connect_panic_app">Сигурни ли сте, че искате да позволите на %1$s да задейства унищожителни действия на паник бутон?</string>
<string name="panic_setting_destructive_action">Разрушителни действия</string>
<string name="panic_setting_signout_title">Отписване</string> <string name="panic_setting_signout_title">Отписване</string>
<string name="panic_setting_signout_summary">Отписване от Briar при натиснат бутон за паника</string> <string name="panic_setting_signout_summary">Отписване от Briar, ако паник бутонът е натиснат</string>
<string name="purge_setting_title">Изтриване на профил</string> <string name="purge_setting_title">Изтриване на профил</string>
<string name="purge_setting_summary">Профила на Briar се изтрива при натиснат бутон за паника. Внимание: Изтрива безвъзвратно профила, контактите и съобщенията</string> <string name="purge_setting_summary">Изтриване на Briar профила, ако паник бутонът е натиснат. Внимание: Изтрива завинаги вашия профил, контакти и съобщения</string>
<string name="uninstall_setting_title">Деинсталиране на Briar</string>
<string name="uninstall_setting_summary">Изисква ръчно потвърждение в паник случай</string>
<!--Settings Notifications--> <!--Settings Notifications-->
<string name="notification_settings_title">Известия</string> <string name="notification_settings_title">Известия</string>
<string name="notify_sign_in_title">Напомняне за вписване</string>
<string name="notify_sign_in_summary">Известие след рестарт на устройството или обновяване на приложението</string>
<string name="notify_private_messages_setting_title">Лични съобщения</string> <string name="notify_private_messages_setting_title">Лични съобщения</string>
<string name="notify_private_messages_setting_summary">Известия за лични съобщения</string> <string name="notify_private_messages_setting_summary">Показвай известия за лични съобщения</string>
<string name="notify_private_messages_setting_summary_26">Настройки на известия за лични съобщения</string>
<string name="notify_group_messages_setting_title">Групови съобщения</string> <string name="notify_group_messages_setting_title">Групови съобщения</string>
<string name="notify_group_messages_setting_summary">Известия за групови съобщения</string> <string name="notify_group_messages_setting_summary">Показвай известия за групови съобщения</string>
<string name="notify_group_messages_setting_summary_26">Настройки на известия за групови съобщения</string> <string name="notify_forum_posts_setting_title">Форумни публикации</string>
<string name="notify_forum_posts_setting_title">Публикации във форуми</string> <string name="notify_forum_posts_setting_summary">Показвай известия за форумни публикации</string>
<string name="notify_forum_posts_setting_summary">Известия за публикации във форуми</string> <string name="notify_blog_posts_setting_title">Блог публикации</string>
<string name="notify_forum_posts_setting_summary_26">Настройки на известия за публикации във форуми</string> <string name="notify_blog_posts_setting_summary">Показвай известия за блог публикации</string>
<string name="notify_blog_posts_setting_title">Публикации в блог</string>
<string name="notify_blog_posts_setting_summary">Известия за публикации в блог</string>
<string name="notify_blog_posts_setting_summary_26">Настройки на известия за публикации в блог</string>
<string name="notify_vibration_setting">Вибрация</string> <string name="notify_vibration_setting">Вибрация</string>
<string name="notify_lock_screen_setting_title">Заключен екран</string>
<string name="notify_lock_screen_setting_summary">Показвай известия за на заключен екран</string>
<string name="notify_sound_setting">Звук</string> <string name="notify_sound_setting">Звук</string>
<string name="notify_sound_setting_default">Подразбиран тон на звънене</string> <string name="notify_sound_setting_default">Мелодия по подразбиране</string>
<string name="notify_sound_setting_disabled">Няма</string> <string name="notify_sound_setting_disabled">Никакви</string>
<string name="choose_ringtone_title">Избор на тон за звънене</string> <string name="choose_ringtone_title">Изберете рингтон</string>
<string name="cannot_load_ringtone">Тонът за звънене не може да бъде зареден</string>
<!--Conversation Settings-->
<string name="disappearing_messages_title">Изчезващи съобщения</string>
<string name="disappearing_messages_explanation_long">При включване, тази настройка прави бъдещите съобщения в този разговор да изчезват след 7\u00A0дни.
\n\nОтброяването при изпращача започва след като съобщението е било доставено, а при получателя след като го е прочел.
\n\nСъобщенията, които ще изчезнат са отбелязани с бомба.
\n\nИмайте предвид, че получателите могат да правят копия на получените от вас съобщения.
\n\nАко вие промените настройката промяната ще влезе в действие веднага, още върху следващото ви съобщение, а при вашите контакти след получаването му. Контактите ви също могат да правят промяна на тази настройка, което ще се отрази и на двама ви.</string>
<string name="learn_more">Научете повече</string>
<string name="disappearing_messages_summary">Бъдещите съобщения в разговора изчезват след 7\u00A0дни</string>
<!--Settings Feedback--> <!--Settings Feedback-->
<string name="send_feedback">Изпращане на отзив</string> <string name="feedback_settings_title">Отзиви</string>
<string name="send_feedback">Изпращане на отзиви</string>
<!--Link Warning--> <!--Link Warning-->
<string name="link_warning_title">Предупреждение за препратка</string> <string name="link_warning_title">Предупреждение за линк</string>
<string name="link_warning_intro">Препратката ще бъде отворена от външно приложение.</string> <string name="link_warning_intro">Линкът ще се отвори във външно приложение.</string>
<string name="link_warning_text">Така отворена може да бъде използвана, за да бъдете идентифицирани. Преценете дали имате доверие на подателя и обмислете дали да не я отворите с Tor Browser.</string> <string name="link_warning_text">Линкът може да се използва, за да ви идентифицира. Помислете дали имате доверие на човека, който ви изпраща линка, и обмислете дали да не го отворите с Orfox.</string>
<string name="link_warning_open_link">Отваряне</string> <string name="link_warning_open_link">Отвори линк</string>
<!--Crash Reporter--> <!--Crash Reporter-->
<string name="crash_report_title">Доклад на срив</string> <string name="crash_report_title">Доклад на срив</string>
<string name="briar_crashed">Извинете, Briar се срина.</string> <string name="briar_crashed">Извинете, Briar се срина.</string>
<string name="not_your_fault">Не е по ваша вина.</string> <string name="not_your_fault">Не е по ваша вина.</string>
<string name="please_send_report">Помогнете да направим Briar по-добър като ни изпратите доклад.</string> <string name="please_send_report">Моля, помогнете да изградим по-добър Briar, като ни изпратите доклад.</string>
<string name="report_is_encrypted">Даваме обещание, че докладът е шифрован и е изпратен добре защитен.</string> <string name="report_is_encrypted">Гарантираме, че докладът е криптиран и изпратен безопасно.</string>
<string name="feedback_title">Обратна връзка</string> <string name="feedback_title">Отзиви</string>
<string name="describe_crash">Опишете случилото се (незадължително)</string> <string name="describe_crash">Опишете станалото (незадължително)</string>
<string name="enter_feedback">Въведете обратна връзка</string> <string name="enter_feedback">Въведете отзив</string>
<string name="optional_contact_email">Адрес на електронна поща (по желание)</string> <string name="optional_contact_email">Имейл адресът ви (незадължително)</string>
<string name="include_debug_report_crash">Изпращане на анонимни данни за срива</string> <string name="include_debug_report_crash">Добави анонимни данни за срива</string>
<string name="include_debug_report_feedback">Изпращане на анонимни данни за устройството</string> <string name="include_debug_report_feedback">Добави анонимни данни за това устройствo</string>
<string name="dev_report_user_info">Данни за потербителя</string> <string name="could_not_load_report_data">Данните за доклада не можаха да заредят.</string>
<string name="dev_report_basic_info">Основна информация</string>
<string name="dev_report_device_info">Данни за устройството</string>
<string name="dev_report_stacktrace">Следа в стека</string>
<string name="dev_report_time_info">Данни за времената</string>
<string name="dev_report_memory">Памет</string>
<string name="dev_report_storage">Хранилище</string>
<string name="dev_report_connectivity">Свързаност</string>
<string name="dev_report_build_config">Настройка на изданието</string>
<string name="dev_report_logcat">Журнал на приложението</string>
<string name="dev_report_device_features">Характеристики</string>
<string name="send_report">Изпращане на доклад</string> <string name="send_report">Изпращане на доклад</string>
<string name="close">Затваряне</string> <string name="close">Затваряне</string>
<string name="dev_report_sending">Изпращане на доклад…</string>
<string name="dev_report_sent">Обратната връзка е изпратена</string>
<string name="dev_report_saved">Докладът е запазен. Ще бъде изпратен при следващото влизане в Briar.</string> <string name="dev_report_saved">Докладът е запазен. Ще бъде изпратен при следващото влизане в Briar.</string>
<string name="dev_report_error">Грешка при изпращане на доклад</string>
<!--Sign Out--> <!--Sign Out-->
<string name="progress_title_logout">Отписване от Briar</string> <string name="progress_title_logout">Отписване от Briar...</string>
<!--Screen Filters & Tapjacking--> <!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Открито е приложение отгоре</string> <string name="screen_filter_title">Открит е овърлей на екрана</string>
<string name="screen_filter_body">Друго приложение се изчертава върху Briar. За ваша сигурност, Briar няма да реагира на докосване, докато друго приложение се изчертава отгоре.\n\nСледните приложения биха могли да се изчертават отгоре:\n\n%1$s</string>
<string name="screen_filter_body_api_30">Друго приложение се изчертава върху Briar. За ваша сигурност, Briar няма да реагира на докосване, докато друго приложение се изчертава отгоре.\n\nПрегледайте приложенията, за да го намерите.</string>
<string name="screen_filter_allow">Разрешаване на тези приложения да се изчертават отгоре</string>
<string name="screen_filter_review_apps">Преглеждане</string>
<!--Permission Requests--> <!--Permission Requests-->
<string name="permission_camera_title">Разрешение за камера</string>
<string name="permission_camera_request_body">За да сканира кода за QR, Briar трябва да използва камерата.</string>
<string name="permission_location_title">Разрешение за местоположение</string>
<string name="permission_location_request_body">За да открива устройства чрез Bluetooth, на Briar му е необходимо разрешение за достъп до местоположението.\n\nBriar не го пази и не го споделя с никого.</string>
<string name="permission_camera_location_title">Камера и местоположение</string>
<string name="permission_camera_location_request_body">За да сканира кодове за QR, на Briar му е необходимо разрешение за достъп до камерата.\n\nЗа да открива устройства чрез Bluetooth, на Briar му е необходимо разрешение за достъп до местоположението.\n\nBriar не го пази и не го споделя с никого.</string>
<string name="permission_camera_denied_body">Отказахте достъп до камерата, но тя е необходима за добавянето на контакти.\n\nОбмислете дали да не дадете разрешение.</string>
<string name="permission_location_denied_body">Отказахте достъп до местоположението, но то е необходимо за откриване на устройства чрез Bluetooth.\n\nОбмислете дали да не дадете разрешение.</string>
<string name="permission_location_setting_title">Настройки на местоположението</string>
<string name="permission_location_setting_body">Местоположението на устройството ви трябва да е включено, за да бъдат откривани устройства чрез Bluetooth. За да продължите включете местоположението. След това можете да го изключите.</string>
<string name="permission_location_setting_button">Включване на местеположение</string>
<string name="qr_code">Код за QR</string>
<string name="show_qr_code_fullscreen">Код за QR на цял екран</string>
<!--App Locking--> <!--App Locking-->
<string name="lock_unlock">Отключете Briar</string>
<string name="lock_unlock_verbose">Въведете своя PIN, фигура или парола</string>
<string name="lock_unlock_fingerprint_description">Докоснете сензора за отпечатъци с регистрирания пръст</string>
<string name="lock_unlock_password">Използване на парола</string>
<string name="lock_is_locked">Briar е заключен</string>
<string name="lock_tap_to_unlock">Докоснете за отключване</string>
<!--Connections Screen-->
<string name="transports_help_text">Briar може да се свърже с контактите ви през интернет, Wi-Fi или Bluetooth.\n\nЗа повече поверителност цялата връзка към интернет се пренасочва през мрежата на Tor.\n\nАко даден контакт може да бъде достъпен чрез няколко метода Briar ги използва успоредно.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Ани</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_bob">Боби</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_carol">Васко</string>
<!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
<string name="screenshot_message_1">Здравей, Боби!</string>
<!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
<string name="screenshot_message_2">Здравей, Ани! Благодаря ти, че ми каза за Briar!</string>
<!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">Радвам се, че ти харесва! И ти би направил същото 😀</string>
</resources> </resources>

View File

@@ -15,7 +15,7 @@
<string name="confirm_password">Potwierdź hasło</string> <string name="confirm_password">Potwierdź hasło</string>
<string name="name_too_long">Nazwa użytkownika jest zbyt długa</string> <string name="name_too_long">Nazwa użytkownika jest zbyt długa</string>
<string name="password_too_weak">Hasło jest zbyt długie</string> <string name="password_too_weak">Hasło jest zbyt długie</string>
<string name="passwords_do_not_match">Hasła różnią się</string> <string name="passwords_do_not_match">Hasła się nie zgadzają</string>
<string name="create_account_button">Utwórz Konto</string> <string name="create_account_button">Utwórz Konto</string>
<string name="more_info">Więcej Informacji</string> <string name="more_info">Więcej Informacji</string>
<string name="don_t_ask_again">Nie pytaj ponownie</string> <string name="don_t_ask_again">Nie pytaj ponownie</string>
@@ -27,11 +27,10 @@
<!--Login--> <!--Login-->
<string name="enter_password">Hasło</string> <string name="enter_password">Hasło</string>
<string name="try_again">Złe hasło, spróbuj ponownie</string> <string name="try_again">Złe hasło, spróbuj ponownie</string>
<string name="dialog_title_cannot_check_password">Nie można zweryfikować hasła</string>
<string name="sign_in_button">Zaloguj Się</string> <string name="sign_in_button">Zaloguj Się</string>
<string name="forgotten_password">Zapomniałem hasło</string> <string name="forgotten_password">Zapomniałem hasło</string>
<string name="dialog_title_lost_password">Nie pamiętam hasła</string> <string name="dialog_title_lost_password">Nie pamiętam hasła</string>
<string name="dialog_message_lost_password">Twoje konto Briar jest zaszyfrowane na Twoim urządzeniu, nie w chmurze, więc nie będzie można zresetować Twojego hasła. Czy chcesz usunąć swoje konto i stworzyć nowe?\n\nUwaga: Twoje hasła, kontakty i wiadomości będą utracone.</string> <string name="dialog_message_lost_password">Twoje konto Briar jest zaszyfrowane na Twoim urządzeniu nie w chmurze, więc nie będzie można zresetować Twojego hasła. Czy chcesz usunąć swoje konto i stworzyć nowe?\n\nUwaga: Twoje hasła, kontakty i wiadomości będą utracone.</string>
<string name="startup_failed_notification_title">Briar nie mógł się uruchomić</string> <string name="startup_failed_notification_title">Briar nie mógł się uruchomić</string>
<string name="startup_failed_notification_text">Naciśnij, aby uzyskać więcej informacji</string> <string name="startup_failed_notification_text">Naciśnij, aby uzyskać więcej informacji</string>
<string name="startup_failed_activity_title">Briar nie mógł się uruchomić</string> <string name="startup_failed_activity_title">Briar nie mógł się uruchomić</string>
@@ -46,9 +45,7 @@
<item quantity="other">To jest testowa wersja Briar. Twoje konto wygaśnie za %d dni i nie będzie odnowione.</item> <item quantity="other">To jest testowa wersja Briar. Twoje konto wygaśnie za %d dni i nie będzie odnowione.</item>
</plurals> </plurals>
<string name="expiry_date_reached">Ten program wygasł.\nDziękujemy za testy!</string> <string name="expiry_date_reached">Ten program wygasł.\nDziękujemy za testy!</string>
<string name="download_briar">Aby móc nadal korzystać z Briar, pobierz najnowszą wersję.</string>
<string name="create_new_account">Musisz utworzyć nowe konto, ale możesz użyć takiej samej nazwy użytkownika.</string> <string name="create_new_account">Musisz utworzyć nowe konto, ale możesz użyć takiej samej nazwy użytkownika.</string>
<string name="download_briar_button">Pobierz najnowszą wersję</string>
<string name="startup_open_database">Deszyfruję Bazę Danych...</string> <string name="startup_open_database">Deszyfruję Bazę Danych...</string>
<string name="startup_migrate_database">Aktualizuję Bazę Danych...</string> <string name="startup_migrate_database">Aktualizuję Bazę Danych...</string>
<string name="startup_compact_database">Kompaktowanie Bazy Danych…</string> <string name="startup_compact_database">Kompaktowanie Bazy Danych…</string>
@@ -63,16 +60,12 @@
<string name="lock_button">Zablokuj Aplikację</string> <string name="lock_button">Zablokuj Aplikację</string>
<string name="settings_button">Ustawienia</string> <string name="settings_button">Ustawienia</string>
<string name="sign_out_button">Wyloguj się</string> <string name="sign_out_button">Wyloguj się</string>
<string name="transports_onboarding_text">Dotknij tutaj aby zmienić w jaki sposób Briar łączy się z twoimi kontaktami.</string>
<!--Transports: Tor--> <!--Transports: Tor-->
<string name="transport_tor">Internet</string> <string name="transport_tor">Internet</string>
<string name="tor_plugin_status_inactive">Briar nie może połączyć się z Internetem</string>
<!--Transports: Wi-Fi--> <!--Transports: Wi-Fi-->
<string name="transport_lan">Wi-Fi</string> <string name="transport_lan">Wi-Fi</string>
<!--Transports: Bluetooth--> <!--Transports: Bluetooth-->
<string name="transport_bt">Bluetooth</string> <string name="transport_bt">Bluetooth</string>
<string name="bt_device_status_on">Bluetooth jest włączony</string>
<string name="bt_device_status_off">Bluetooth jest wyłączony</string>
<!--Notifications--> <!--Notifications-->
<string name="reminder_notification_title">Wylogowano z Briar</string> <string name="reminder_notification_title">Wylogowano z Briar</string>
<string name="reminder_notification_text">Dotknij, aby zalogować się ponownie</string> <string name="reminder_notification_text">Dotknij, aby zalogować się ponownie</string>
@@ -135,7 +128,6 @@
<string name="date_no_private_messages">Brak wiadomości.</string> <string name="date_no_private_messages">Brak wiadomości.</string>
<string name="no_private_messages">Brak wiadomości do pokazania</string> <string name="no_private_messages">Brak wiadomości do pokazania</string>
<string name="message_hint">Nowa wiadomość</string> <string name="message_hint">Nowa wiadomość</string>
<string name="message_hint_auto_delete">Nowa znikająca wiadomość</string>
<string name="image_caption_hint">Dodaj podpis (opcjonalne)</string> <string name="image_caption_hint">Dodaj podpis (opcjonalne)</string>
<string name="image_attach">Załącz obraz</string> <string name="image_attach">Załącz obraz</string>
<string name="image_attach_error">Nie udało się dołączyć obrazu(ów)</string> <string name="image_attach_error">Nie udało się dołączyć obrazu(ów)</string>
@@ -143,18 +135,12 @@
<string name="image_attach_error_invalid_mime_type">Format obrazu jest nieobsługiwany: %s</string> <string name="image_attach_error_invalid_mime_type">Format obrazu jest nieobsługiwany: %s</string>
<string name="set_contact_alias">Zmień nazwę kontaktu</string> <string name="set_contact_alias">Zmień nazwę kontaktu</string>
<string name="set_contact_alias_hint">Nazwa kontaktu</string> <string name="set_contact_alias_hint">Nazwa kontaktu</string>
<string name="menu_item_disappearing_messages">Znikające wiadomości</string>
<string name="menu_item_connect_via_bluetooth">Połącz przez Bluetooth</string> <string name="menu_item_connect_via_bluetooth">Połącz przez Bluetooth</string>
<string name="dialog_title_connect_via_bluetooth">Połącz przez Bluetooth</string> <string name="dialog_title_connect_via_bluetooth">Połącz przez Bluetooth</string>
<!--The first placeholder will show a duration like "7 days". The second placeholder at the end will add "Tap to learn more."--> <!--The first placeholder will show a duration like "7 days". The second placeholder at the end will add "Tap to learn more."-->
<!--The placeholder at the end will add "Tap to learn more."--> <!--The placeholder at the end will add "Tap to learn more."-->
<!--The first placeholder will show a contact's name. The second placeholder will show a duration like "7 days". The third placeholder at the end will add "Tap to learn more."--> <!--The first placeholder will show a contact's name. The second placeholder will show a duration like "7 days". The third placeholder at the end will add "Tap to learn more."-->
<!--The first placeholder will show a contact's name. The second placeholder at the end will add "Tap to learn more."--> <!--The first placeholder will show a contact's name. The second placeholder at the end will add "Tap to learn more."-->
<string name="tap_to_learn_more">Dotknij tutaj aby dowiedzieć się więcej.</string>
<string name="auto_delete_changed_warning_send">Wyślij mimo to</string>
<string name="delete_all_messages">Usuń wszystkie wiadomości</string>
<string name="dialog_title_delete_all_messages">Potwierdź usunięcie wiadomości</string>
<string name="dialog_message_delete_all_messages">Na pewno chcesz usunąć wszystkie wiadomości?</string>
<string name="dialog_title_not_all_messages_deleted">Nie mogłem usunąć wszystkich wiadomości</string> <string name="dialog_title_not_all_messages_deleted">Nie mogłem usunąć wszystkich wiadomości</string>
<string name="delete_contact">Usuń kontakt</string> <string name="delete_contact">Usuń kontakt</string>
<string name="dialog_title_delete_contact">Potwierdź usunięcie kontaktu</string> <string name="dialog_title_delete_contact">Potwierdź usunięcie kontaktu</string>
@@ -187,7 +173,7 @@
<string name="connecting_to_device">Łączenie z urządzeniem\u2026</string> <string name="connecting_to_device">Łączenie z urządzeniem\u2026</string>
<string name="authenticating_with_device">Autoryzowanie z urządzeniem\u2026</string> <string name="authenticating_with_device">Autoryzowanie z urządzeniem\u2026</string>
<string name="connection_error_title">Nie udało się połączyć z kontaktem</string> <string name="connection_error_title">Nie udało się połączyć z kontaktem</string>
<string name="connection_error_feedback">Jeśli problem będzie występować dalej, proszę <a href="feedback">wysłać opinię</a> aby pomóc nam ulepszyć aplikację.</string> <string name="connection_error_feedback">Jeśli problem będzie występować dalej, proszę <a href="feedback">wysłać zgłoszenie</a> aby pomóc nam ulepszyć aplikację.</string>
<!--Adding Contacts Remotely--> <!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Dodaj Kontakt na odległość</string> <string name="add_contact_remotely_title_case">Dodaj Kontakt na odległość</string>
<string name="add_contact_nearby_title">Dodaj kontakt w pobliżu</string> <string name="add_contact_nearby_title">Dodaj kontakt w pobliżu</string>
@@ -415,8 +401,6 @@
<string name="blogs_rss_feeds_import_button">Zaimportuj</string> <string name="blogs_rss_feeds_import_button">Zaimportuj</string>
<string name="blogs_rss_feeds_import_hint">Wprowadź adres URL do kanału RSS</string> <string name="blogs_rss_feeds_import_hint">Wprowadź adres URL do kanału RSS</string>
<string name="blogs_rss_feeds_import_error">Przepraszamy! Wystąpił błąd podczas importowania twojego kanału RSS</string> <string name="blogs_rss_feeds_import_error">Przepraszamy! Wystąpił błąd podczas importowania twojego kanału RSS</string>
<string name="blogs_rss_feeds_import_exists">Ten feed został już zaimportowany.</string>
<string name="blogs_rss_feeds">Feedy RSS</string>
<string name="blogs_rss_feeds_manage_imported">Zaimportowane:</string> <string name="blogs_rss_feeds_manage_imported">Zaimportowane:</string>
<string name="blogs_rss_feeds_manage_author">Autor:</string> <string name="blogs_rss_feeds_manage_author">Autor:</string>
<string name="blogs_rss_feeds_manage_updated">Ostatnio Zaktualizowane:</string> <string name="blogs_rss_feeds_manage_updated">Ostatnio Zaktualizowane:</string>
@@ -426,7 +410,6 @@
<string name="blogs_rss_feeds_manage_empty_state">Brak RSS do wyświetlenia\n\nDotknij ikonki + aby zaimportować kanał.</string> <string name="blogs_rss_feeds_manage_empty_state">Brak RSS do wyświetlenia\n\nDotknij ikonki + aby zaimportować kanał.</string>
<string name="blogs_rss_feeds_manage_error">Wystąpił problem podczas ładowania twoich kanałów RSS. Proszę spróbować ponownie później.</string> <string name="blogs_rss_feeds_manage_error">Wystąpił problem podczas ładowania twoich kanałów RSS. Proszę spróbować ponownie później.</string>
<!--Settings Profile Picture--> <!--Settings Profile Picture-->
<string name="dialog_confirm_profile_picture_title">Zmień zdjęcie profilowe</string>
<!--Settings Display--> <!--Settings Display-->
<string name="pref_language_title">Język &amp; region</string> <string name="pref_language_title">Język &amp; region</string>
<string name="pref_language_changed">Te ustawienia zostaną zastosowane gdy zrestartujesz Briar. Proszę się wylogować i zrestartować Briar.</string> <string name="pref_language_changed">Te ustawienia zostaną zastosowane gdy zrestartujesz Briar. Proszę się wylogować i zrestartować Briar.</string>
@@ -439,7 +422,6 @@
<string name="pref_theme_system">Domyślny systemu</string> <string name="pref_theme_system">Domyślny systemu</string>
<!--Settings Connections--> <!--Settings Connections-->
<string name="network_settings_title">Połączenia</string> <string name="network_settings_title">Połączenia</string>
<string name="tor_enable_summary">Dla zapewnienia prywatności, wszystkie połączenia przechodzą przez sieć Tor</string>
<string name="tor_network_setting_automatic">Automatycznie bazując na lokalizacji</string> <string name="tor_network_setting_automatic">Automatycznie bazując na lokalizacji</string>
<!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"--> <!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"-->
<string name="tor_network_setting_summary">Automatycznie: %1$s (za %2$s)</string> <string name="tor_network_setting_summary">Automatycznie: %1$s (za %2$s)</string>
@@ -507,7 +489,6 @@
<string name="choose_ringtone_title">Wybierz dzwonek</string> <string name="choose_ringtone_title">Wybierz dzwonek</string>
<string name="cannot_load_ringtone">Nie mogę załadować dzwonka</string> <string name="cannot_load_ringtone">Nie mogę załadować dzwonka</string>
<!--Conversation Settings--> <!--Conversation Settings-->
<string name="disappearing_messages_title">Znikające wiadomości</string>
<string name="learn_more">Dowiedz się więcej</string> <string name="learn_more">Dowiedz się więcej</string>
<!--Settings Feedback--> <!--Settings Feedback-->
<string name="send_feedback">Wyślij opinię</string> <string name="send_feedback">Wyślij opinię</string>
@@ -524,7 +505,7 @@
<string name="report_is_encrypted">Obiecujemy, że raport z awarii jest zaszyfrowany i wysyłany bezpiecznie.</string> <string name="report_is_encrypted">Obiecujemy, że raport z awarii jest zaszyfrowany i wysyłany bezpiecznie.</string>
<string name="feedback_title">Twoja opinia</string> <string name="feedback_title">Twoja opinia</string>
<string name="describe_crash">Opisz co się stało (opcjonalne)</string> <string name="describe_crash">Opisz co się stało (opcjonalne)</string>
<string name="enter_feedback">Napisz swoją opinię</string> <string name="enter_feedback">Wprowadź swoje uwagi</string>
<string name="optional_contact_email">Twój adres email (opcjonalne)</string> <string name="optional_contact_email">Twój adres email (opcjonalne)</string>
<string name="include_debug_report_crash">Załącz anonimowe dane na temat awarii</string> <string name="include_debug_report_crash">Załącz anonimowe dane na temat awarii</string>
<string name="include_debug_report_feedback">Załącz anonimowe dane o tym urządzeniu</string> <string name="include_debug_report_feedback">Załącz anonimowe dane o tym urządzeniu</string>
@@ -534,8 +515,6 @@
<string name="dev_report_connectivity">Łączenie</string> <string name="dev_report_connectivity">Łączenie</string>
<string name="send_report">Wyślij raport</string> <string name="send_report">Wyślij raport</string>
<string name="close">Zamknij</string> <string name="close">Zamknij</string>
<string name="dev_report_sending">Wysyłanie opinii...</string>
<string name="dev_report_sent">Wysłano opinię</string>
<string name="dev_report_saved">Raport zapisany. Zostanie wysłany następnym razem kiedy zalogujesz się do Briar.</string> <string name="dev_report_saved">Raport zapisany. Zostanie wysłany następnym razem kiedy zalogujesz się do Briar.</string>
<!--Sign Out--> <!--Sign Out-->
<string name="progress_title_logout">Wylogowywanie z Briar...</string> <string name="progress_title_logout">Wylogowywanie z Briar...</string>

View File

@@ -7,6 +7,7 @@
<color name="briar_blue_600">#1b69b6</color> <color name="briar_blue_600">#1b69b6</color>
<color name="briar_blue_400">#418cd8</color> <color name="briar_blue_400">#418cd8</color>
<color name="briar_orange_200">#fed69f</color>
<color name="briar_orange_500">#fc9403</color> <color name="briar_orange_500">#fc9403</color>
<color name="briar_red_500">#db3b21</color> <color name="briar_red_500">#db3b21</color>

View File

@@ -46,12 +46,14 @@
<string name="forgotten_password">I have forgotten my password</string> <string name="forgotten_password">I have forgotten my password</string>
<string name="dialog_title_lost_password">Lost Password</string> <string name="dialog_title_lost_password">Lost Password</string>
<string name="dialog_message_lost_password">Your Briar account is stored encrypted on your device, not in the cloud, so we can\'t reset your password. Would you like to delete your account and start again?\n\nCaution: Your identities, contacts and messages will be permanently lost.</string> <string name="dialog_message_lost_password">Your Briar account is stored encrypted on your device, not in the cloud, so we can\'t reset your password. Would you like to delete your account and start again?\n\nCaution: Your identities, contacts and messages will be permanently lost.</string>
<string name="startup_failed_notification_title">Briar could not start</string>
<string name="startup_failed_notification_text">Tap for more information.</string>
<string name="startup_failed_activity_title">Briar Startup Failure</string> <string name="startup_failed_activity_title">Briar Startup Failure</string>
<string name="startup_failed_clock_error">Briar was unable to start because your device\'s clock is wrong.\n\nPlease set your device\'s clock to the right time and try again.</string> <string name="startup_failed_clock_error">Briar was unable to start because your device\'s clock is wrong. Please set your device\'s clock to the right time and try again.</string>
<string name="startup_failed_db_error">Briar was unable to open the database containing your account, your contacts and your messages.\n\nPlease upgrade to the latest version of the app and try again, or set up a new account by choosing \'I have forgotten my password\' at the password prompt.</string> <string name="startup_failed_db_error">For some reason, your Briar database is corrupted beyond repair. Your account, your data and all your contacts are lost. Unfortunately, you need to reinstall Briar or set up a new account by choosing \'I have forgotten my password\' at the password prompt.</string>
<string name="startup_failed_data_too_old_error">Your account was created with an old version of this app and cannot be opened with this version.\n\nYou must either reinstall the old version or set up a new account by choosing \'I have forgotten my password\' at the password prompt.</string> <string name="startup_failed_data_too_old_error">Your account was created with an old version of this app and cannot be opened with this version. You must either reinstall the old version or set up a new account by choosing \'I have forgotten my password\' at the password prompt.</string>
<string name="startup_failed_data_too_new_error">Your account was created with a newer version of this app and cannot be opened with this version.\n\nPlease upgrade to the latest version and try again.</string> <string name="startup_failed_data_too_new_error">This version of the app is too old. Please upgrade to the latest version and try again.</string>
<string name="startup_failed_service_error">Briar was unable to start a required component.\n\nPlease upgrade to the latest version of the app and try again.</string> <string name="startup_failed_service_error">Briar was unable to start a required plugin. Reinstalling Briar usually solves this problem. However, please note that you will then lose your account and all data associated with it since Briar is not using central servers to store your data on.</string>
<plurals name="expiry_warning"> <plurals name="expiry_warning">
<item quantity="one">This is a test version of Briar. Your account will expire in %d day and cannot be renewed.</item> <item quantity="one">This is a test version of Briar. Your account will expire in %d day and cannot be renewed.</item>
<item quantity="other">This is a test version of Briar. Your account will expire in %d days and cannot be renewed.</item> <item quantity="other">This is a test version of Briar. Your account will expire in %d days and cannot be renewed.</item>
@@ -160,6 +162,7 @@
<string name="sorry">Sorry</string> <string name="sorry">Sorry</string>
<string name="error_start_activity">Unavailable on your system</string> <string name="error_start_activity">Unavailable on your system</string>
<string name="status_heading">Status:</string> <string name="status_heading">Status:</string>
<string name="error">Error</string>
<!-- Contacts and Private Conversations--> <!-- Contacts and Private Conversations-->
<string name="no_contacts">No contacts to show</string> <string name="no_contacts">No contacts to show</string>
@@ -615,7 +618,8 @@
<string name="learn_more">Learn more</string> <string name="learn_more">Learn more</string>
<string name="disappearing_messages_summary">Make future messages in this conversation automatically disappear after 7\u00A0days.</string> <string name="disappearing_messages_summary">Make future messages in this conversation automatically disappear after 7\u00A0days.</string>
<!-- Settings Feedback --> <!-- Settings Actions -->
<string name="pref_category_actions">Actions</string>
<string name="send_feedback">Send feedback</string> <string name="send_feedback">Send feedback</string>
<!-- Link Warning --> <!-- Link Warning -->
@@ -699,7 +703,7 @@
<string name="removable_drive_title_receive">Receive data</string> <string name="removable_drive_title_receive">Receive data</string>
<string name="removable_drive_send_intro">Tap the button below to create a new file containing the encrypted messages. You can choose where the file will be saved.\n\nIf you want to save the file on a removable drive, insert the drive now.</string> <string name="removable_drive_send_intro">Tap the button below to create a new file containing the encrypted messages. You can choose where the file will be saved.\n\nIf you want to save the file on a removable drive, insert the drive now.</string>
<string name="removable_drive_send_no_data">There are currently no messages waiting to be sent to this contact.</string> <string name="removable_drive_send_no_data">There are currently no messages waiting to be sent to this contact.</string>
<string name="removable_drive_send_not_supported">This contact is using an old version of Briar or an old device which does not support this feature.</string> <string name="removable_drive_send_not_supported">This contact is using an old version of Briar which does not yet support this feature.</string>
<string name="removable_drive_send_button">Choose file for export</string> <string name="removable_drive_send_button">Choose file for export</string>
<string name="removable_drive_ongoing">Please wait for ongoing task to complete</string> <string name="removable_drive_ongoing">Please wait for ongoing task to complete</string>
<string name="removable_drive_receive_intro">Tap the button below to choose the file that your contact sent you.\n\nIf the file is on a removable drive, insert the drive now.</string> <string name="removable_drive_receive_intro">Tap the button below to choose the file that your contact sent you.\n\nIf the file is on a removable drive, insert the drive now.</string>
@@ -714,6 +718,72 @@
<string name="removable_drive_error_receive_text">The selected file did not contain anything that Briar could recognize.\n\nPlease check that you chose the right file.\n\nIf your contact created the file more than 28 days ago, Briar will not be able to recognize it.</string> <string name="removable_drive_error_receive_text">The selected file did not contain anything that Briar could recognize.\n\nPlease check that you chose the right file.\n\nIf your contact created the file more than 28 days ago, Briar will not be able to recognize it.</string>
<!-- Share app offline -->
<string name="hotspot_title">Share Briar offline</string>
<string name="hotspot_intro">Share this app with someone nearby without internet connection by using your phone\'s Wi-Fi.
\n\nYour phone will start a Wi-Fi hotspot. People nearby can connect to the hotspot and download the Briar app from your phone.</string>
<string name="hotspot_button_start_sharing">Start hotspot</string>
<string name="hotspot_button_stop_sharing">Stop hotspot</string>
<string name="hotspot_progress_text_start">Setting up hotspot…</string>
<string name="hotspot_notification_channel_title">Wi-Fi hotspot</string>
<string name="hotspot_notification_title">Sharing Briar offline</string>
<string name="hotspot_button_connected">Next</string>
<string name="permission_hotspot_location_request_body">To create a Wi-Fi hotspot, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string>
<string name="permission_hotspot_location_denied_body">You have denied access to your location, but Briar needs this permission to create a Wi-Fi hotspot.\n\nPlease consider granting access.</string>
<string name="wifi_settings_title">Wi-Fi setting</string>
<string name="wifi_settings_request_enable_body">To create a Wi-Fi hotspot, Briar needs to use Wi-Fi. Please enable it.</string>
<string name="wifi_settings_request_denied_body">You have denied to enable Wi-Fi, but Briar needs to use Wi-Fi.\n\nPlease consider enabling it.</string>
<string name="hotspot_tab_manual">Manual</string>
<string name="hotspot_scanning_a_qr_code">scanning a QR code</string>
<!-- Wi-Fi setup -->
<!-- The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code' -->
<string name="hotspot_manual_wifi">Your phone is providing a Wi-Fi hotspot. People who want to download Briar can connect to the hotspot by entering the details below or %s. When they have connected to the hotspot, press \'Next\'.</string>
<string name="hotspot_manual_wifi_ssid">Network name (SSID)</string>
<string name="hotspot_qr_wifi">Your phone is providing a Wi-Fi hotspot. People who want to download Briar can connect to the hotspot by scanning this QR code. When they have connected to the hotspot, press \'Next\'.</string>
<string name="hotspot_peer_connected">Successfully connected</string>
<string name="hotspot_peer_connected_action">Show download info</string>
<!-- Download link -->
<!-- The %s placeholder will be replaced with the translation of 'hotspot_scanning_a_qr_code' -->
<string name="hotspot_manual_site">Your phone is providing a Wi-Fi hotspot. People who are connected to the hotspot can download Briar by typing the following link in a web browser or %s.</string>
<string name="hotspot_manual_site_address">Address (URL)</string>
<string name="hotspot_qr_site">Your phone is providing a Wi-Fi hotspot. People who are connected to the hotspot can download Briar by scanning this QR code.</string>
<!-- e.g. Download Briar 1.2.20 -->
<string name="website_download_title">Download %s</string>
<string name="website_download_intro">Someone nearby shared %s with you.</string>
<string name="website_download_outro">After the download is complete, open the downloaded file and install it.</string>
<string name="website_troubleshooting_title">Troubleshooting</string>
<string name="website_troubleshooting_1">If you cannot download the app, try it with a different web browser app.</string>
<string name="website_troubleshooting_2_old">To install the downloaded app, you might need to allow installation of apps from \"Unknown sources\" in system settings. Afterwards, you may need to download the app again. We recommend to undo that after successful installation.</string>
<string name="website_troubleshooting_2_new">To install the downloaded app, you might need to allow your browser to install unknown apps. We recommend to undo that after successful installation.</string>
<string name="hotspot_help_wifi_title">Problems with connecting to Wi-Fi:</string>
<string name="hotspot_help_wifi_1">Try disabling and re-enabling Wi-Fi on both phones and try again.</string>
<string name="hotspot_help_wifi_2">If your phone complains that the Wi-Fi has no internet, tell it that you want to stay connected anyway.</string>
<string name="hotspot_help_site_title">Problems visiting the local website:</string>
<string name="hotspot_help_site_1">Double check that you entered the address exactly as shown. A small error can make it fail.</string>
<string name="hotspot_help_site_2">Ensure that your phone is still connected to the correct Wi-Fi (see above) when you try to access the site.</string>
<string name="hotspot_help_site_3">Check that you don\'t have any active firewall apps that may block the access.</string>
<string name="hotspot_help_site_4">If you can visit the site, but not download the Briar app, try it with a different web browser app.</string>
<string name="hotspot_help_fallback_title">Nothing works?</string>
<string name="hotspot_help_fallback_intro">You can try to save the app as an .apk file to share in some other way. Once on the other device, it can be used to install Briar.
\n\nTip: For sharing via Bluetooth, you might need to rename the file to end with .zip first.</string>
<string name="hotspot_help_fallback_button">Save app install file</string>
<!-- error handling -->
<string name="hotspot_error_intro">Something went wrong while trying to share the app via Wi-Fi:</string>
<string name="hotspot_error_no_wifi_direct">Device does not support Wi-Fi Direct</string>
<string name="hotspot_error_start_callback_failed">Hotspot failed to start: error %s</string>
<string name="hotspot_error_start_callback_failed_unknown">Hotspot failed to start with an unknown error, reason %d</string>
<string name="hotspot_error_start_callback_no_group_info">Hotspot failed to start: no group info</string>
<string name="hotspot_error_web_server_start">Error starting web server!</string>
<string name="hotspot_error_web_server_serve">Error presenting website.\n\nPlease send feedback (with anonymous data) via the Briar app if the issue persists.</string>
<string name="hotspot_flag_test">Warning: This app was installed with Android Studio and can NOT be installed on another device.</string>
<!-- Screenshots --> <!-- Screenshots -->
<!-- This is a name to be used in screenshots. Feel free to change it to a local name. --> <!-- This is a name to be used in screenshots. Feel free to change it to a local name. -->

View File

@@ -24,10 +24,24 @@
app:fragment="org.briarproject.briar.android.settings.NotificationsFragment" app:fragment="org.briarproject.briar.android.settings.NotificationsFragment"
app:icon="@drawable/ic_notifications" /> app:icon="@drawable/ic_notifications" />
<Preference <PreferenceCategory
android:key="pref_key_send_feedback" android:key="pref_key_actions"
android:title="@string/send_feedback" android:layout="@layout/preferences_category"
app:icon="@drawable/ic_feedback" /> android:title="@string/pref_category_actions"
app:allowDividerAbove="true">
<Preference
android:key="pref_key_share_app"
android:title="@string/hotspot_title"
app:icon="@drawable/ic_settings_share">
<intent
android:targetClass="org.briarproject.briar.android.hotspot.HotspotActivity"
android:targetPackage="@string/app_package" />
</Preference>
<Preference
android:key="pref_key_send_feedback"
android:title="@string/send_feedback"
app:icon="@drawable/ic_feedback" />
</PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="pref_key_dev" android:key="pref_key_dev"

View File

@@ -225,6 +225,7 @@ dependencyVerification {
'org.jmock:jmock:2.12.0:jmock-2.12.0.jar:266d07314c0cd343c46ff8a55601272de8cf406807caf55e6f313295f83d10be', 'org.jmock:jmock:2.12.0:jmock-2.12.0.jar:266d07314c0cd343c46ff8a55601272de8cf406807caf55e6f313295f83d10be',
'org.jvnet.staxex:stax-ex:1.8:stax-ex-1.8.jar:95b05d9590af4154c6513b9c5dc1fb2e55b539972ba0a9ef28e9a0c01d83ad77', 'org.jvnet.staxex:stax-ex:1.8:stax-ex-1.8.jar:95b05d9590af4154c6513b9c5dc1fb2e55b539972ba0a9ef28e9a0c01d83ad77',
'org.mockito:mockito-core:3.9.0:mockito-core-3.9.0.jar:a1f64211407b8dc4cf80b16e07cc11aa9e5228d53dc4a5357326d66825f6a4ac', 'org.mockito:mockito-core:3.9.0:mockito-core-3.9.0.jar:a1f64211407b8dc4cf80b16e07cc11aa9e5228d53dc4a5357326d66825f6a4ac',
'org.nanohttpd:nanohttpd:2.3.1:nanohttpd-2.3.1.jar:de864c47818157141a24c9acb36df0c47d7bf15b7ff48c90610f3eb4e5df0e58',
'org.objenesis:objenesis:3.2:objenesis-3.2.jar:03d960bd5aef03c653eb000413ada15eb77cdd2b8e4448886edf5692805e35f3', 'org.objenesis:objenesis:3.2:objenesis-3.2.jar:03d960bd5aef03c653eb000413ada15eb77cdd2b8e4448886edf5692805e35f3',
'org.ow2.asm:asm-analysis:7.0:asm-analysis-7.0.jar:e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474', 'org.ow2.asm:asm-analysis:7.0:asm-analysis-7.0.jar:e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474',
'org.ow2.asm:asm-commons:7.0:asm-commons-7.0.jar:fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d', 'org.ow2.asm:asm-commons:7.0:asm-commons-7.0.jar:fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d',

View File

@@ -97,5 +97,6 @@ internal class HeadlessModule(private val appDir: File) {
override fun shouldEnableDisappearingMessages() = false override fun shouldEnableDisappearingMessages() = false
override fun shouldEnableConnectViaBluetooth() = false override fun shouldEnableConnectViaBluetooth() = false
override fun shouldEnableTransferData() = false override fun shouldEnableTransferData() = false
override fun shouldEnableShareAppViaOfflineHotspot() = false
} }
} }