mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 02:39:05 +01:00
Compare commits
147 Commits
release-1.
...
offline-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a438050e68 | ||
|
|
4d0fe24722 | ||
|
|
559b29e8b5 | ||
|
|
1b7b285862 | ||
|
|
178810241f | ||
|
|
61c601cb6d | ||
|
|
2002ad08ca | ||
|
|
d134a67ee9 | ||
|
|
04cf8e16a9 | ||
|
|
227d345858 | ||
|
|
07ef73ab56 | ||
|
|
ea2b1ff4d8 | ||
|
|
69fac86a0c | ||
|
|
bd6b6c1cd6 | ||
|
|
be6c868135 | ||
|
|
041a296666 | ||
|
|
276eeb1c20 | ||
|
|
d46cfb757e | ||
|
|
d81c4e7982 | ||
|
|
8c4d6ed5e4 | ||
|
|
b21b319cb7 | ||
|
|
780f6e97b9 | ||
|
|
1756215183 | ||
|
|
7e3db6c6df | ||
|
|
1adf408ade | ||
|
|
d662ae49ee | ||
|
|
7939c8b213 | ||
|
|
1899873da3 | ||
|
|
5beffb21f1 | ||
|
|
0f9afda329 | ||
|
|
c16663b530 | ||
|
|
77767b45c9 | ||
|
|
79ae8fea8d | ||
|
|
7e3eb1201a | ||
|
|
eb283d81c5 | ||
|
|
be3700d364 | ||
|
|
ccec17f28a | ||
|
|
e8428925ae | ||
|
|
195123e669 | ||
|
|
abe570e905 | ||
|
|
a93b1f18ac | ||
|
|
e4bd6fdf95 | ||
|
|
793d81bd93 | ||
|
|
be637cef65 | ||
|
|
9370062e41 | ||
|
|
8c1f721015 | ||
|
|
22ea4ced0d | ||
|
|
312d31b40e | ||
|
|
b15d42b0cd | ||
|
|
9274b8ef4a | ||
|
|
7ef4ea51b3 | ||
|
|
e285f21d1c | ||
|
|
160cef25af | ||
|
|
c0293a1327 | ||
|
|
035c639aa0 | ||
|
|
29d31e79c3 | ||
|
|
7f7210becd | ||
|
|
ce74fcaab5 | ||
|
|
d4c1e132f7 | ||
|
|
6b976df6a8 | ||
|
|
3e4db3b9da | ||
|
|
0bf59eec20 | ||
|
|
9f828a2222 | ||
|
|
7be77b8c60 | ||
|
|
d5853e8403 | ||
|
|
32e9bf01ec | ||
|
|
a5ce400341 | ||
|
|
a960bfb2c1 | ||
|
|
847650f280 | ||
|
|
77a3199aac | ||
|
|
9a58b37ce2 | ||
|
|
608e1eac6b | ||
|
|
09de768e7e | ||
|
|
faab80f0ea | ||
|
|
07162cad8b | ||
|
|
a5a1cdfabb | ||
|
|
bfcb469d49 | ||
|
|
f8b645d2b1 | ||
|
|
052eb03c9e | ||
|
|
83bf3f4ca7 | ||
|
|
f9181fa021 | ||
|
|
79dae27c24 | ||
|
|
ac0fc21e6e | ||
|
|
dab736ce0e | ||
|
|
8ef21637a9 | ||
|
|
e3a1fca22e | ||
|
|
ea9a2789ab | ||
|
|
cbdbd10cb3 | ||
|
|
d6f985174a | ||
|
|
d184fbd3fe | ||
|
|
ef623370b6 | ||
|
|
5ac636d52d | ||
|
|
f1c71ec5a7 | ||
|
|
5cc280be61 | ||
|
|
a5d8faef3c | ||
|
|
e22e9dcade | ||
|
|
7474ad8606 | ||
|
|
1c3d90f7fc | ||
|
|
6f8d7167db | ||
|
|
99da50d37c | ||
|
|
15f5c8deee | ||
|
|
7913cd322e | ||
|
|
de8ad8f6f9 | ||
|
|
d0bc17e634 | ||
|
|
85433611a5 | ||
|
|
ebd5879761 | ||
|
|
b255ab07ae | ||
|
|
a86ba50dec | ||
|
|
bcbc96dc2d | ||
|
|
a72e92de24 | ||
|
|
1ddcd6cfff | ||
|
|
5dfd9e3546 | ||
|
|
e05575b956 | ||
|
|
fd810f5c16 | ||
|
|
3f5e131250 | ||
|
|
3ee516599d | ||
|
|
c703d90636 | ||
|
|
e228b9fcbf | ||
|
|
6e6cadd3ad | ||
|
|
9cc8d44778 | ||
|
|
ee6f571c31 | ||
|
|
2ac3bdd3ae | ||
|
|
e35ffe0cf0 | ||
|
|
8a04d8edc4 | ||
|
|
a5fb3bb4a4 | ||
|
|
eae329cdfa | ||
|
|
0ce0551f0d | ||
|
|
a198e7d08e | ||
|
|
bca6f1506e | ||
|
|
e420201b00 | ||
|
|
03248d04e5 | ||
|
|
2c39b02644 | ||
|
|
c9c6f3682c | ||
|
|
8f4a0ef030 | ||
|
|
5fe22bcd57 | ||
|
|
b4880af7e2 | ||
|
|
51d21bd669 | ||
|
|
b8f3728a0d | ||
|
|
bbfd4f137d | ||
|
|
7e3ca76dd1 | ||
|
|
524c8d26f8 | ||
|
|
7eccf7dac1 | ||
|
|
0bc06248ed | ||
|
|
c999f05cc7 | ||
|
|
428269b312 | ||
|
|
588e05ce83 | ||
|
|
f7875c99b6 |
@@ -15,8 +15,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 10306
|
||||
versionName "1.3.6"
|
||||
versionCode 10305
|
||||
versionName "1.3.5"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -20,7 +20,7 @@ import static org.briarproject.bramble.api.plugin.file.RemovableDriveConstants.I
|
||||
public class AndroidRemovableDrivePluginFactory implements
|
||||
SimplexPluginFactory {
|
||||
|
||||
private static final long MAX_LATENCY = DAYS.toMillis(28);
|
||||
private static final int MAX_LATENCY = (int) DAYS.toMillis(14);
|
||||
|
||||
private final Application app;
|
||||
|
||||
|
||||
@@ -13,5 +13,7 @@ public interface FeatureFlags {
|
||||
|
||||
boolean shouldEnableConnectViaBluetooth();
|
||||
|
||||
boolean shouldEnableShareAppViaOfflineHotspot();
|
||||
|
||||
boolean shouldEnableTransferData();
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ public interface RemovableDriveTask extends Runnable {
|
||||
TransportProperties getTransportProperties();
|
||||
|
||||
/**
|
||||
* Adds an observer to the task. The observer will be notified on the
|
||||
* event thread of the current state of the task and any subsequent state
|
||||
* changes.
|
||||
* Adds an observer to the task. The observer will be notified of state
|
||||
* changes on the event thread. If the task has already finished, the
|
||||
* observer will be notified of its final state.
|
||||
*/
|
||||
void addObserver(Consumer<State> observer);
|
||||
|
||||
|
||||
@@ -2331,7 +2331,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
ps.setInt(2, DELIVERED.getValue());
|
||||
rs = ps.executeQuery();
|
||||
rs.next();
|
||||
long total = rs.getLong(1);
|
||||
long total = rs.getInt(1);
|
||||
rs.close();
|
||||
ps.close();
|
||||
return total;
|
||||
|
||||
@@ -30,6 +30,11 @@ public class TestFeatureFlagModule {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnableShareAppViaOfflineHotspot() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnableTransferData() {
|
||||
return true;
|
||||
|
||||
@@ -26,8 +26,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 10306
|
||||
versionName "1.3.6"
|
||||
versionCode 10305
|
||||
versionName "1.3.5"
|
||||
applicationId "org.briarproject.briar.android"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -121,6 +121,7 @@ dependencies {
|
||||
exclude group: 'com.android.support'
|
||||
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.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.briarproject.briar.android;
|
||||
import org.briarproject.bramble.BrambleAndroidModule;
|
||||
import org.briarproject.bramble.BrambleCoreModule;
|
||||
import org.briarproject.bramble.account.BriarAccountModule;
|
||||
import org.briarproject.bramble.plugin.file.RemovableDriveModule;
|
||||
import org.briarproject.bramble.system.ClockModule;
|
||||
import org.briarproject.briar.BriarCoreModule;
|
||||
import org.briarproject.briar.android.account.SignInTestCreateAccount;
|
||||
@@ -22,7 +21,6 @@ import dagger.Component;
|
||||
AttachmentModule.class,
|
||||
ClockModule.class,
|
||||
MediaModule.class,
|
||||
RemovableDriveModule.class,
|
||||
BriarCoreModule.class,
|
||||
BrambleAndroidModule.class,
|
||||
BriarAccountModule.class,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_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_ADMIN" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
@@ -343,13 +344,7 @@
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.StartupFailureActivity"
|
||||
android:excludeFromRecents="true"
|
||||
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" />
|
||||
android:label="@string/startup_failed_activity_title" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.settings.SettingsActivity"
|
||||
@@ -456,6 +451,11 @@
|
||||
android:label="@string/pending_contact_requests"
|
||||
android:theme="@style/BriarTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".android.hotspot.HotspotActivity"
|
||||
android:label="@string/hotspot_title"
|
||||
android:theme="@style/BriarTheme" />
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
|
||||
103
briar-android/src/main/assets/hotspot.html
Normal file
103
briar-android/src/main/assets/hotspot.html
Normal 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>
|
||||
@@ -36,6 +36,11 @@ import org.briarproject.briar.BriarCoreModule;
|
||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||
import org.briarproject.briar.android.attachment.media.MediaModule;
|
||||
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.login.SignInReminderReceiver;
|
||||
import org.briarproject.briar.android.removabledrive.ChooserFragment;
|
||||
@@ -216,6 +221,16 @@ public interface AndroidComponent
|
||||
|
||||
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(SendFragment sendFragment);
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.briarproject.bramble.util.StringUtils;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||
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.navdrawer.NavDrawerActivity;
|
||||
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.NotificationManager.IMPORTANCE_DEFAULT;
|
||||
import static android.app.NotificationManager.IMPORTANCE_LOW;
|
||||
import static android.app.PendingIntent.getActivity;
|
||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
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_SINGLE_TOP;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
|
||||
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.setOngoing(true);
|
||||
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) {
|
||||
b.setCategory(CATEGORY_SERVICE);
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
@@ -619,13 +622,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
public void showSignInNotification() {
|
||||
if (blockSignInReminder) return;
|
||||
if (SDK_INT >= 26) {
|
||||
NotificationChannel channel =
|
||||
new NotificationChannel(REMINDER_CHANNEL_ID, appContext
|
||||
.getString(
|
||||
R.string.reminder_notification_channel_title),
|
||||
IMPORTANCE_LOW);
|
||||
channel.setLockscreenVisibility(
|
||||
NotificationCompat.VISIBILITY_SECRET);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
REMINDER_CHANNEL_ID, appContext
|
||||
.getString(R.string.reminder_notification_channel_title),
|
||||
IMPORTANCE_LOW);
|
||||
channel.setLockscreenVisibility(VISIBILITY_SECRET);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
@@ -652,7 +653,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
|
||||
Intent i = new Intent(appContext, SplashScreenActivity.class);
|
||||
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());
|
||||
}
|
||||
@@ -720,4 +721,40 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
public void unblockAllBlogPostNotifications() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.briarproject.briar.android.blog.BlogModule;
|
||||
import org.briarproject.briar.android.contact.ContactListModule;
|
||||
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactModule;
|
||||
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.logging.LoggingModule;
|
||||
import org.briarproject.briar.android.login.LoginModule;
|
||||
@@ -94,6 +95,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
GroupListModule.class,
|
||||
GroupConversationModule.class,
|
||||
SharingModule.class,
|
||||
HotspotModule.class,
|
||||
TransferDataModule.class,
|
||||
})
|
||||
public class AppModule {
|
||||
@@ -155,8 +157,7 @@ public class AppModule {
|
||||
@Singleton
|
||||
PluginConfig providePluginConfig(AndroidBluetoothPluginFactory bluetooth,
|
||||
AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan,
|
||||
AndroidRemovableDrivePluginFactory drive,
|
||||
FeatureFlags featureFlags) {
|
||||
AndroidRemovableDrivePluginFactory drive) {
|
||||
@NotNullByDefault
|
||||
PluginConfig pluginConfig = new PluginConfig() {
|
||||
|
||||
@@ -167,11 +168,7 @@ public class AppModule {
|
||||
|
||||
@Override
|
||||
public Collection<SimplexPluginFactory> getSimplexFactories() {
|
||||
if (SDK_INT >= 19 && featureFlags.shouldEnableTransferData()) {
|
||||
return singletonList(drive);
|
||||
} else {
|
||||
return emptyList();
|
||||
}
|
||||
return SDK_INT >= 19 ? singletonList(drive) : emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -318,6 +315,11 @@ public class AppModule {
|
||||
public boolean shouldEnableTransferData() {
|
||||
return IS_DEBUG_BUILD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnableShareAppViaOfflineHotspot() {
|
||||
return IS_DEBUG_BUILD;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
@@ -33,7 +34,11 @@ import java.util.logging.Logger;
|
||||
import javax.annotation.Nullable;
|
||||
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.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static android.content.Intent.ACTION_SHUTDOWN;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
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.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_NOTIFICATION_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_NOTIFICATION_ID;
|
||||
@@ -60,6 +66,8 @@ public class BriarService extends Service {
|
||||
|
||||
public static String EXTRA_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 =
|
||||
"org.briarproject.briar.STARTUP_FAILED";
|
||||
|
||||
@@ -127,11 +135,12 @@ public class BriarService extends Service {
|
||||
ongoingChannel.setLockscreenVisibility(VISIBILITY_SECRET);
|
||||
ongoingChannel.setShowBadge(false);
|
||||
nm.createNotificationChannel(ongoingChannel);
|
||||
// Delete the unused channel previously used for startup
|
||||
// failure notifications
|
||||
// TODO: Remove this ID after a reasonable upgrade period
|
||||
// (added 2021-07-12)
|
||||
nm.deleteNotificationChannel(FAILURE_CHANNEL_ID);
|
||||
NotificationChannel failureChannel = new NotificationChannel(
|
||||
FAILURE_CHANNEL_ID,
|
||||
getString(R.string.startup_failed_notification_title),
|
||||
IMPORTANCE_DEFAULT);
|
||||
failureChannel.setLockscreenVisibility(VISIBILITY_SECRET);
|
||||
nm.createNotificationChannel(failureChannel);
|
||||
}
|
||||
Notification foregroundNotification =
|
||||
notificationManager.getForegroundNotification();
|
||||
@@ -147,7 +156,7 @@ public class BriarService extends Service {
|
||||
} else {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.warning("Startup failed: " + result);
|
||||
showStartupFailure(result);
|
||||
showStartupFailureNotification(result);
|
||||
stopSelf();
|
||||
}
|
||||
}, "LifecycleStartup");
|
||||
@@ -173,13 +182,29 @@ public class BriarService extends Service {
|
||||
Localizer.getInstance().setLocale(this);
|
||||
}
|
||||
|
||||
private void showStartupFailure(StartResult result) {
|
||||
private void showStartupFailureNotification(StartResult result) {
|
||||
androidExecutor.runOnUiThread(() -> {
|
||||
// Bring the entry activity to the front to clear the back stack
|
||||
Intent i = new Intent(BriarService.this, ENTRY_ACTIVITY);
|
||||
NotificationCompat.Builder b = new NotificationCompat.Builder(
|
||||
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.putExtra(EXTRA_STARTUP_FAILED, true);
|
||||
i.putExtra(EXTRA_START_RESULT, result);
|
||||
startActivity(i);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.briar.android;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
@@ -14,7 +15,9 @@ import org.briarproject.briar.android.fragment.ErrorFragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
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;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@@ -38,6 +41,14 @@ public class StartupFailureActivity extends BaseActivity implements
|
||||
private void handleIntent(Intent i) {
|
||||
StartResult 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
|
||||
int errorRes;
|
||||
@@ -67,4 +78,5 @@ public class StartupFailureActivity extends BaseActivity implements
|
||||
public void runOnDbThread(@NonNull Runnable runnable) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.briarproject.briar.android.forum.CreateForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumListFragment;
|
||||
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.IntroductionActivity;
|
||||
import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
|
||||
@@ -177,6 +178,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(CrashReportActivity crashReportActivity);
|
||||
|
||||
void inject(HotspotActivity hotspotActivity);
|
||||
|
||||
void inject(RemovableDriveActivity activity);
|
||||
|
||||
// Fragments
|
||||
|
||||
@@ -46,6 +46,7 @@ import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
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.showFragment;
|
||||
|
||||
/**
|
||||
* Warning: Some activities don't extend {@link BaseActivity}.
|
||||
@@ -177,13 +178,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
|
||||
public void showNextFragment(BaseFragment f) {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(STARTED)) return;
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.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();
|
||||
showFragment(getSupportFragmentManager(), f, f.getUniqueTag());
|
||||
}
|
||||
|
||||
protected boolean isFragmentAdded(String fragmentTag) {
|
||||
|
||||
@@ -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.KeyAgreementStarted;
|
||||
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.MutableLiveEvent;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import androidx.annotation.Nullable;
|
||||
@ParametersNotNullByDefault
|
||||
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";
|
||||
|
||||
@@ -40,8 +40,7 @@ public class ErrorFragment extends BaseFragment {
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle args = getArguments();
|
||||
if (args == null) throw new AssertionError();
|
||||
Bundle args = requireArguments();
|
||||
errorMessage = args.getString(ERROR_MSG);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.briar.R;
|
||||
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.BriarActivity;
|
||||
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.STARTING_STOPPING;
|
||||
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.activity.RequestCodes.REQUEST_PASSWORD;
|
||||
import static org.briarproject.briar.android.navdrawer.IntentRouter.handleExternalIntent;
|
||||
@@ -252,11 +250,6 @@ public class NavDrawerActivity extends BriarActivity implements
|
||||
|
||||
private void exitIfStartupFailed(Intent intent) {
|
||||
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();
|
||||
LOG.info("Exiting");
|
||||
System.exit(0);
|
||||
|
||||
@@ -47,8 +47,6 @@ public class ReceiveFragment extends Fragment {
|
||||
private Button button;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
private boolean checkForStateLoss = false;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
@@ -75,10 +73,6 @@ public class ReceiveFragment extends Fragment {
|
||||
.observeEvent(getViewLifecycleOwner(), this::onOldTaskResumed);
|
||||
viewModel.getState()
|
||||
.observe(getViewLifecycleOwner(), this::onStateChanged);
|
||||
|
||||
// need to check for lost ViewModel state when creating with prior state
|
||||
if (savedInstanceState != null) checkForStateLoss = true;
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -90,23 +84,6 @@ public class ReceiveFragment extends Fragment {
|
||||
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) {
|
||||
if (resumed) {
|
||||
Toast.makeText(requireContext(),
|
||||
@@ -127,8 +104,6 @@ public class ReceiveFragment extends Fragment {
|
||||
|
||||
private void onDocumentChosen(@Nullable Uri uri) {
|
||||
if (uri == null) return;
|
||||
// we just got our document, so don't treat this as a state loss
|
||||
checkForStateLoss = false;
|
||||
viewModel.importData(uri);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -51,8 +51,6 @@ public class SendFragment extends Fragment {
|
||||
private Button button;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
private boolean checkForStateLoss = false;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
@@ -82,9 +80,6 @@ public class SendFragment extends Fragment {
|
||||
viewModel.getState()
|
||||
.observe(getViewLifecycleOwner(), this::onStateChanged);
|
||||
|
||||
// need to check for lost ViewModel state when creating with prior state
|
||||
if (savedInstanceState != null) checkForStateLoss = true;
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -96,23 +91,6 @@ public class SendFragment extends Fragment {
|
||||
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) {
|
||||
if (resumed) {
|
||||
Toast.makeText(requireContext(),
|
||||
@@ -149,8 +127,6 @@ public class SendFragment extends Fragment {
|
||||
|
||||
private void onDocumentCreated(@Nullable Uri uri) {
|
||||
if (uri == null) return;
|
||||
// we just got our document, so don't treat this as a state loss
|
||||
checkForStateLoss = false;
|
||||
viewModel.exportData(uri);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class BriarExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
// activity runs in its own process, so we can kill the old one
|
||||
startDevReportActivity(app.getApplicationContext(),
|
||||
CrashReportActivity.class, e, appStartTime, logKey);
|
||||
CrashReportActivity.class, e, appStartTime, logKey, null);
|
||||
Process.killProcess(Process.myPid());
|
||||
System.exit(10);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import static java.util.Objects.requireNonNull;
|
||||
public class CrashReportActivity extends BaseActivity
|
||||
implements BaseFragmentListener {
|
||||
|
||||
public static final String EXTRA_INITIAL_COMMENT = "initialComment";
|
||||
public static final String EXTRA_THROWABLE = "throwable";
|
||||
public static final String EXTRA_APP_START_TIME = "appStartTime";
|
||||
public static final String EXTRA_APP_LOGCAT = "logcat";
|
||||
@@ -55,10 +56,11 @@ public class CrashReportActivity extends BaseActivity
|
||||
setContentView(R.layout.activity_dev_report);
|
||||
|
||||
Intent intent = getIntent();
|
||||
String initialComment = intent.getStringExtra(EXTRA_INITIAL_COMMENT);
|
||||
Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE);
|
||||
long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, -1);
|
||||
byte[] logKey = intent.getByteArrayExtra(EXTRA_APP_LOGCAT);
|
||||
viewModel.init(t, appStartTime, logKey);
|
||||
viewModel.init(t, appStartTime, logKey, initialComment);
|
||||
viewModel.getShowReport().observeEvent(this, show -> {
|
||||
if (show) displayFragment(true);
|
||||
});
|
||||
|
||||
@@ -78,6 +78,9 @@ public class ReportFormFragment extends BaseFragment {
|
||||
list = v.findViewById(R.id.list);
|
||||
progress = v.findViewById(R.id.progress_wheel);
|
||||
|
||||
if (viewModel.getInitialComment() != null)
|
||||
userCommentView.setText(viewModel.getInitialComment());
|
||||
|
||||
if (viewModel.isFeedback()) {
|
||||
includeDebugReport
|
||||
.setText(getString(R.string.include_debug_report_feedback));
|
||||
|
||||
@@ -64,6 +64,8 @@ class ReportViewModel extends AndroidViewModel {
|
||||
private final MutableLiveEvent<Integer> closeReport =
|
||||
new MutableLiveEvent<>();
|
||||
private boolean isFeedback;
|
||||
@Nullable
|
||||
private String initialComment;
|
||||
|
||||
@Inject
|
||||
ReportViewModel(@NonNull Application application,
|
||||
@@ -80,7 +82,8 @@ class ReportViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
void init(@Nullable Throwable t, long appStartTime,
|
||||
@Nullable byte[] logKey) {
|
||||
@Nullable byte[] logKey, @Nullable String initialComment) {
|
||||
this.initialComment = initialComment;
|
||||
isFeedback = t == null;
|
||||
if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> {
|
||||
String decryptedLogs;
|
||||
@@ -103,6 +106,11 @@ class ReportViewModel extends AndroidViewModel {
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getInitialComment() {
|
||||
return initialComment;
|
||||
}
|
||||
|
||||
boolean isFeedback() {
|
||||
return isFeedback;
|
||||
}
|
||||
@@ -140,7 +148,7 @@ class ReportViewModel extends AndroidViewModel {
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return reportData;
|
||||
|
||||
@@ -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_DEV = "pref_key_dev";
|
||||
private static final String PREF_KEY_EXPLODE = "pref_key_explode";
|
||||
private static final String PREF_KEY_SHARE_APP = "pref_key_share_app";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
@@ -85,6 +86,12 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV));
|
||||
dev.setVisible(false);
|
||||
}
|
||||
|
||||
if (!viewModel.shouldEnableShareAppViaOfflineHotspot()) {
|
||||
Preference shareApp =
|
||||
requireNonNull(findPreference(PREF_KEY_SHARE_APP));
|
||||
shareApp.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -262,4 +262,8 @@ class SettingsViewModel extends DbViewModel implements EventListener {
|
||||
return screenLockTimeout;
|
||||
}
|
||||
|
||||
boolean shouldEnableShareAppViaOfflineHotspot() {
|
||||
return featureFlags.shouldEnableShareAppViaOfflineHotspot();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.briarproject.briar.android.contact.add.nearby;
|
||||
package org.briarproject.briar.android.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -22,12 +22,12 @@ import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
class QrCodeUtils {
|
||||
public class QrCodeUtils {
|
||||
|
||||
private static final Logger LOG = getLogger(QrCodeUtils.class.getName());
|
||||
|
||||
@Nullable
|
||||
static Bitmap createQrCode(DisplayMetrics dm, String input) {
|
||||
public static Bitmap createQrCode(DisplayMetrics dm, String input) {
|
||||
int smallestDimen = Math.min(dm.widthPixels, dm.heightPixels);
|
||||
try {
|
||||
// Generate QR code
|
||||
@@ -61,6 +61,8 @@ import androidx.core.util.Consumer;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
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.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_INITIAL_COMMENT;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@@ -137,13 +140,18 @@ public class UiUtils {
|
||||
|
||||
public static void showFragment(FragmentManager fm, Fragment f,
|
||||
@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,
|
||||
R.anim.step_previous_out, R.anim.step_previous_in,
|
||||
R.anim.step_next_out)
|
||||
.replace(R.id.fragmentContainer, f, tag)
|
||||
.addToBackStack(tag)
|
||||
.commit();
|
||||
.replace(R.id.fragmentContainer, f, tag);
|
||||
if (addToBackStack) ta.addToBackStack(tag);
|
||||
ta.commit();
|
||||
}
|
||||
|
||||
public static String getContactDisplayName(Author author,
|
||||
@@ -333,6 +341,11 @@ public class UiUtils {
|
||||
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,
|
||||
* 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) {
|
||||
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,
|
||||
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);
|
||||
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
dialogIntent.putExtra(EXTRA_THROWABLE, t);
|
||||
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
|
||||
dialogIntent.putExtra(EXTRA_APP_LOGCAT, logKey);
|
||||
dialogIntent.putExtra(EXTRA_INITIAL_COMMENT, initialComment);
|
||||
ctx.startActivity(dialogIntent);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public interface AndroidNotificationManager {
|
||||
int FORUM_POST_NOTIFICATION_ID = 6;
|
||||
int BLOG_POST_NOTIFICATION_ID = 7;
|
||||
int CONTACT_ADDED_NOTIFICATION_ID = 8;
|
||||
int HOTSPOT_NOTIFICATION_ID = 9;
|
||||
|
||||
// Channel IDs
|
||||
String CONTACT_CHANNEL_ID = "contacts";
|
||||
@@ -41,13 +42,13 @@ public interface AndroidNotificationManager {
|
||||
// that will sort below the main channels such as contacts
|
||||
String ONGOING_CHANNEL_OLD_ID = "zForegroundService";
|
||||
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 REMINDER_CHANNEL_ID = "zSignInReminder";
|
||||
String HOTSPOT_CHANNEL_ID = "zHotspot";
|
||||
|
||||
// Actions for pending intents
|
||||
String ACTION_DISMISS_REMINDER = "dismissReminder";
|
||||
String ACTION_STOP_HOTSPOT = "stopHotspot";
|
||||
|
||||
Notification getForegroundNotification();
|
||||
|
||||
@@ -96,4 +97,8 @@ public interface AndroidNotificationManager {
|
||||
void blockAllBlogPostNotifications();
|
||||
|
||||
void unblockAllBlogPostNotifications();
|
||||
|
||||
void showHotspotNotification();
|
||||
|
||||
void clearHotspotNotification();
|
||||
}
|
||||
|
||||
@@ -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 |
10
briar-android/src/main/res/drawable/ic_circle_small.xml
Normal file
10
briar-android/src/main/res/drawable/ic_circle_small.xml
Normal 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>
|
||||
10
briar-android/src/main/res/drawable/ic_portable_wifi_off.xml
Normal file
10
briar-android/src/main/res/drawable/ic_portable_wifi_off.xml
Normal 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>
|
||||
40
briar-android/src/main/res/drawable/ic_qr_code.xml
Normal file
40
briar-android/src/main/res/drawable/ic_qr_code.xml
Normal 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>
|
||||
12
briar-android/src/main/res/drawable/ic_settings_share.xml
Normal file
12
briar-android/src/main/res/drawable/ic_settings_share.xml
Normal 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>
|
||||
10
briar-android/src/main/res/drawable/ic_wifi_tethering.xml
Normal file
10
briar-android/src/main/res/drawable/ic_wifi_tethering.xml
Normal 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>
|
||||
@@ -9,11 +9,7 @@
|
||||
android:id="@+id/errorIcon"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -25,11 +21,7 @@
|
||||
android:id="@+id/errorTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/sorry"
|
||||
android:textSize="@dimen/text_size_xlarge"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -49,6 +41,6 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/errorTitle"
|
||||
tools:text="@string/qr_code_unsupported" />
|
||||
tools:text="@string/startup_failed_service_error" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
80
briar-android/src/main/res/layout/fragment_hotspot_error.xml
Normal file
80
briar-android/src/main/res/layout/fragment_hotspot_error.xml
Normal 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>
|
||||
126
briar-android/src/main/res/layout/fragment_hotspot_help.xml
Normal file
126
briar-android/src/main/res/layout/fragment_hotspot_help.xml
Normal 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>
|
||||
86
briar-android/src/main/res/layout/fragment_hotspot_intro.xml
Normal file
86
briar-android/src/main/res/layout/fragment_hotspot_intro.xml
Normal 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>
|
||||
@@ -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>
|
||||
50
briar-android/src/main/res/layout/fragment_hotspot_qr.xml
Normal file
50
briar-android/src/main/res/layout/fragment_hotspot_qr.xml
Normal 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>
|
||||
@@ -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>
|
||||
93
briar-android/src/main/res/layout/fragment_hotspot_tabs.xml
Normal file
93
briar-android/src/main/res/layout/fragment_hotspot_tabs.xml
Normal 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>
|
||||
11
briar-android/src/main/res/menu/hotspot_help_action.xml
Normal file
11
briar-android/src/main/res/menu/hotspot_help_action.xml
Normal 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>
|
||||
@@ -1,111 +1,43 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<resources>
|
||||
<!--Setup-->
|
||||
<string name="setup_title">Добре дошли в Briar</string>
|
||||
<string name="setup_name_explanation">Прякорът ви ще бъде видим до всяка ваша публикация. Няма да можете да го промените след като създадете профил.</string>
|
||||
<string name="setup_next">Напред</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="setup_next">Следващ</string>
|
||||
<string name="choose_nickname">Изберете име</string>
|
||||
<string name="choose_password">Изберете парола</string>
|
||||
<string name="confirm_password">Потвърдете парола</string>
|
||||
<string name="name_too_long">Името е твърде дълго</string>
|
||||
<string name="password_too_weak">Паролата е твърде слаба</string>
|
||||
<string name="passwords_do_not_match">Паролите не съвпадат</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-->
|
||||
<string name="enter_password">Парола</string>
|
||||
<string name="try_again">Грешна парола, опитайте отново</string>
|
||||
<string name="dialog_title_cannot_check_password">Паролата не може да бъде проверена</string>
|
||||
<string name="dialog_message_cannot_check_password">Briar не може да провери вашата парола. Рестартирате устройството, за да разрешите проблема.</string>
|
||||
<string name="sign_in_button">Влизане</string>
|
||||
<string name="try_again">Грешна парола, опитайте пак</string>
|
||||
<string name="sign_in_button">Вход</string>
|
||||
<string name="forgotten_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_text">Докоснете за повече информация.</string>
|
||||
<string name="startup_failed_activity_title">Неуспешно стартиране</string>
|
||||
<string name="startup_failed_db_error">По някаква причина банката от данни на Briar е непоправимо повредена. Вашият профил, данни и всичките ви контакти са загубени. За жалост, се налага да преинсталирате Briar или да създадете нов профил, избирайки „Забравена парола“ от екрана за вход.</string>
|
||||
<string name="startup_failed_data_too_old_error">Вашия профил е създаден със по-ранно издание на приложението и не може да бъде отворен. Трябва или да инсталирате по-ранното издание или да създадете нов профил, избирайки „Забравена парола“ от екрана за вход.</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>
|
||||
<string name="startup_failed_service_error">Briar не успя да стартира задължителен плъгин. Обикновено преинсталирането на Briar решава този проблем. Моля, имайте предвид, че ще изгубите профила си и всички данни, асоциирани с него, тъй като Briar не съхранява данните ви в централни сървъри.</string>
|
||||
<string name="expiry_date_reached">Софтуерът е невалиден.\nБлагодарим ви за тестването!</string>
|
||||
<!--Navigation Drawer-->
|
||||
<string name="nav_drawer_open_description">Отваря навигационната лента</string>
|
||||
<string name="nav_drawer_close_description">Затваря навигационната лента</string>
|
||||
<string name="nav_drawer_open_description">Отвори навигационно чекмедже</string>
|
||||
<string name="nav_drawer_close_description">Затвори навигационно чекмедже</string>
|
||||
<string name="contact_list_button">Контакти</string>
|
||||
<string name="groups_button">Частни групи</string>
|
||||
<string name="forums_button">Форуми</string>
|
||||
<string name="blogs_button">Блогове</string>
|
||||
<!--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="sign_out_button">Отписване</string>
|
||||
<string name="transports_onboarding_text">Докоснете, за да изберете как Briar да се свързва с контактите ви.</string>
|
||||
<!--Transports: Tor-->
|
||||
<!--Transports-->
|
||||
<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="bt_device_status_on">Устройството е с включен Bluetooth</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>
|
||||
<string name="transport_lan">Wi-Fi</string>
|
||||
<!--Notifications-->
|
||||
<string name="reminder_notification_title">Отписани сте от Briar</string>
|
||||
<string name="reminder_notification_text">Докоснете за повторно влизане.</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>
|
||||
<string name="ongoing_notification_title">Вписан сте в Briar</string>
|
||||
<string name="ongoing_notification_text">Докоснете, за да отворите Briar.</string>
|
||||
<plurals name="private_message_notification_text">
|
||||
<item quantity="one">Ново лично съобщение.</item>
|
||||
<item quantity="other">%d нови лични съобщения.</item>
|
||||
@@ -115,543 +47,281 @@
|
||||
<item quantity="other">%d нови групови съобщения.</item>
|
||||
</plurals>
|
||||
<plurals name="forum_post_notification_text">
|
||||
<item quantity="one">Нова публикация във форум.</item>
|
||||
<item quantity="other">%d нови публикации във форуми.</item>
|
||||
<item quantity="one">Нова форумна публикация.</item>
|
||||
<item quantity="other">%d нови форумни публикации.</item>
|
||||
</plurals>
|
||||
<plurals name="blog_post_notification_text">
|
||||
<item quantity="one">Нова публикация в блог.</item>
|
||||
<item quantity="other">%d нови публикации в блогове.</item>
|
||||
<item quantity="one">Нова блог публикация.</item>
|
||||
<item quantity="other">%d нови блог публикации.</item>
|
||||
</plurals>
|
||||
<!--Misc-->
|
||||
<string name="now">току-що</string>
|
||||
<string name="show">Показване</string>
|
||||
<string name="hide">Скриване</string>
|
||||
<string name="ok">Добре</string>
|
||||
<string name="now">сега</string>
|
||||
<string name="show">Покажи</string>
|
||||
<string name="hide">Скрий</string>
|
||||
<string name="ok">ОК</string>
|
||||
<string name="cancel">Отказ</string>
|
||||
<string name="got_it">Разбрах</string>
|
||||
<string name="delete">Изтриване</string>
|
||||
<string name="accept">Приемане</string>
|
||||
<string name="decline">Отказване</string>
|
||||
<string name="online">На линия</string>
|
||||
<string name="offline">Извън линия</string>
|
||||
<string name="send">Изпращане</string>
|
||||
<string name="allow">Разрешаване</string>
|
||||
<string name="open">Отваряне</string>
|
||||
<string name="change">Променяне</string>
|
||||
<string name="start">Старт</string>
|
||||
<string name="delete">Изтрий</string>
|
||||
<string name="accept">Приеми</string>
|
||||
<string name="decline">Откажи</string>
|
||||
<string name="options">Опции</string>
|
||||
<string name="online">Онлайн</string>
|
||||
<string name="offline">Офлайн</string>
|
||||
<string name="send">Изпрати</string>
|
||||
<string name="allow">Позволи</string>
|
||||
<string name="open">Отвори</string>
|
||||
<string name="no_data">Няма данни</string>
|
||||
<string name="ellipsis">...</string>
|
||||
<string name="text_too_long">Въведеният текст е твърде дълъг</string>
|
||||
<string name="show_onboarding">Показване на помощен диалог</string>
|
||||
<string name="fix">Поправяне</string>
|
||||
<string name="help">Помощ</string>
|
||||
<string name="sorry">Съжаляваме</string>
|
||||
<string name="error_start_activity">Недостъпно на вашата система</string>
|
||||
<string name="status_heading">Състояние</string>
|
||||
<!--Contacts and Private Conversations-->
|
||||
<string name="no_contacts">Няма контакти</string>
|
||||
<string name="no_contacts_action">Докоснете иконата с +, за да добавите контакти</string>
|
||||
<string name="date_no_private_messages">Няма съобщения.</string>
|
||||
<string name="no_private_messages">Няма съобщения</string>
|
||||
<string name="message_hint">Съобщение</string>
|
||||
<string name="message_hint_auto_delete">Изчезващо съобщение</string>
|
||||
<string name="message_error">Грешка при изпращане на съобщение</string>
|
||||
<string name="image_caption_hint">Добавете описание (по желание)</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>
|
||||
<string name="message_hint">Напиши съобщение</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>
|
||||
<!--Adding Contacts-->
|
||||
<string name="add_contact_title">Добавяне на контакт на живо</string>
|
||||
<string name="face_to_face">Трябва да се срещнете лично с човека, чиито контакт искате да добавите.\n\nТака никой не може да се представи за вас или да чете съобщенията ви в бъдеще.</string>
|
||||
<string name="add_contact_title">Добавяне на контакт</string>
|
||||
<string name="face_to_face">Трябва да се срещнете лично с човека, когото искате да добавите в Контакти.\n\nПо този начин никой не може да се представи за вас или да чете съобщенията ви в бъдеще.</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="exchanging_contact_details">Обмяна на данни за контакт\u2026</string>
|
||||
<string name="contact_added_toast">Добавен контакт: %s</string>
|
||||
<string name="exchanging_contact_details">Обмен на данни за контакт\u2026</string>
|
||||
<string name="contact_added_toast">Добавен конктакт: %s</string>
|
||||
<string name="contact_already_exists">Контактът %s вече съществува</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="qr_code_invalid">QR кодът е невалиден</string>
|
||||
<string name="connecting_to_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-->
|
||||
<string name="introduction_onboarding_title">Запознаване на контакти</string>
|
||||
<string name="introduction_onboarding_text">Можете да запознавате контакти помежду им. Така няма да им се наложи да се срещат лично, за да се свържат в Briar.</string>
|
||||
<string name="introduction_menu_item">Запознаване</string>
|
||||
<string name="introduction_activity_title">Избор на контакт</string>
|
||||
<string name="introduction_not_possible">С тези контакти вече имате запознанство в процес. Нека първо завърши. Ако вие или контактите ви сте рядко на линия може да отнеме известно време.</string>
|
||||
<string name="introduction_message_title">Запознаване на контакти</string>
|
||||
<string name="introduction_onboarding_title">Представете контактите си</string>
|
||||
<string name="introduction_onboarding_text">Можете да представите контактите си един на друг, за да не им се налага да се срещат лично, когато се свързват чрез Briar.</string>
|
||||
<string name="introduction_menu_item">Представи</string>
|
||||
<string name="introduction_activity_title">Избери контакт</string>
|
||||
<string name="introduction_message_title">Представи контакти</string>
|
||||
<string name="introduction_message_hint">Добавете съобщение (незадължително)</string>
|
||||
<string name="introduction_button">Запознаване</string>
|
||||
<string name="introduction_sent">Покана за запознанство е изпратена.</string>
|
||||
<string name="introduction_error">Грешка при изпращане на покана за запознанство.</string>
|
||||
<string name="introduction_request_sent">Пожелахте да запознаете %1$s и %2$s.</string>
|
||||
<string name="introduction_request_received">%1$s пожела да ви запознае с/ъс %2$s. Желаете ли да добавите %2$s към контактите си?</string>
|
||||
<string name="introduction_request_exists_received">%1$s пожела да ви запознае с/ъс %2$s, но %2$s вече е в списъка ви с контакти. Тъй като %1$s може би не знае, все пак можете да отговорите:</string>
|
||||
<string name="introduction_request_answered_received">%1$s пожела да ви запознае с/ъс %2$s.</string>
|
||||
<string name="introduction_response_accepted_sent">Приехте запознанство с/ъс %1$s.</string>
|
||||
<string name="introduction_response_accepted_sent_info">Преди %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_declined_received">%1$s отказа запознанство с/ъс %2$s.</string>
|
||||
<string name="introduction_response_declined_received_by_introducee">%1$s казва, че %2$s отказва запознанство.</string>
|
||||
<string name="introduction_button">Представи</string>
|
||||
<string name="introduction_sent">Представянето ви е изпратено.</string>
|
||||
<string name="introduction_error">Възникна грешка при представянето.</string>
|
||||
<string name="introduction_response_error">Грешка при отговор на представянето</string>
|
||||
<string name="introduction_request_sent">Помолихте да представите %1$s на %2$s.</string>
|
||||
<string name="introduction_request_received">%1$s помоли да ви представи %2$s. Искате ли да добавите %2$s към контактите си?</string>
|
||||
<string name="introduction_request_exists_received">%1$s помоли да ви представи на %2$s, но %2$s вече е в списъка ви с контакти. Тъй като %1$s може би не знае, все пак можете да отговорите:</string>
|
||||
<string name="introduction_request_answered_received">%1$s помоли да ви представи на %2$s.</string>
|
||||
<string name="introduction_response_accepted_sent">Приехте представянето на %1$s.</string>
|
||||
<string name="introduction_response_declined_sent">Отказахте представянето на %1$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_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-->
|
||||
<string name="groups_list_empty">Няма групи</string>
|
||||
<string name="groups_list_empty_action">Докоснете иконата с +, за да създадете своя или поискайте от контактите си да споделят група с вас</string>
|
||||
<string name="groups_created_by">Основател %s</string>
|
||||
<string name="groups_created_by">Създаден от %s</string>
|
||||
<plurals name="messages">
|
||||
<item quantity="one">%d съобщение</item>
|
||||
<item quantity="other">%d съобщения</item>
|
||||
</plurals>
|
||||
<string name="groups_group_is_empty">Групата е празна</string>
|
||||
<string name="groups_group_is_dissolved">Групата е разпусната</string>
|
||||
<string name="groups_remove">Премахване</string>
|
||||
<string name="groups_create_group_title">Създаване на частна група</string>
|
||||
<string name="groups_create_group_button">Създаване на група</string>
|
||||
<string name="groups_create_group_invitation_button">Изпращане на покана</string>
|
||||
<string name="groups_create_group_hint">Име на частната група</string>
|
||||
<string name="groups_invitation_sent">Поканата за членство в група е изпратена</string>
|
||||
<string name="groups_group_is_dissolved">Групата се е разпаднала</string>
|
||||
<string name="groups_remove">Премахни</string>
|
||||
<string name="groups_create_group_title">Създаване на група</string>
|
||||
<string name="groups_create_group_button">Създай група</string>
|
||||
<string name="groups_create_group_invitation_button">Изпрати покана</string>
|
||||
<string name="groups_create_group_hint">Изберете име за групата</string>
|
||||
<string name="groups_invitation_sent">Поканата в група е изпратена</string>
|
||||
<string name="groups_message_sent">Съобщението е изпратено</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">%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_leave">Напускане на групата</string>
|
||||
<string name="groups_leave_dialog_title">Потвърждение на напускане</string>
|
||||
<string name="groups_leave">Напусни групата</string>
|
||||
<string name="groups_leave_dialog_title">Потвърждение на напускането</string>
|
||||
<string name="groups_leave_dialog_message">Сигурни ли сте, че искате да напуснете тази група?</string>
|
||||
<string name="groups_dissolve">Разпускане на група</string>
|
||||
<string name="groups_dissolve_dialog_title">Потвърждение на разпускане на група</string>
|
||||
<string name="groups_dissolve_dialog_message">Сигурни ли сте, че искате да разпуснете групата?\n\nОстаналите членове няма да могат да продължат разговорите си и може да не получат последните съобщения.</string>
|
||||
<string name="groups_dissolve_button">Разпускане</string>
|
||||
<string name="groups_dissolved_dialog_title">Разпусната група</string>
|
||||
<string name="groups_dissolved_dialog_message">Групата е разпусната от нейния основател.\n\nНе можете да изпращате съобщения и може да не сте получили всички изпратени до групата съобщения.</string>
|
||||
<string name="groups_dissolve">Затвори групата</string>
|
||||
<string name="groups_dissolve_dialog_title">Потвърди затварянето на групата</string>
|
||||
<string name="groups_dissolve_dialog_message">Сигурни ли сте, че искате да затворите групата?\n\nВсички други участници няма да могат да продължат разговора и може да не получат най-новите съобщения. </string>
|
||||
<string name="groups_dissolve_button">Затвори</string>
|
||||
<string name="groups_dissolved_dialog_title">Групата е затворена</string>
|
||||
<string name="groups_dissolved_dialog_message">Групата е затворена от създателя.\n\nНе можете да пишете нови съобщения в груповия чат и може да не получите най-новите съобщения. </string>
|
||||
<!--Private Group Invitations-->
|
||||
<string name="groups_invitations_title">Покани за членство в група</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_title">Покани в група</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_joined">Включихте се в групата</string>
|
||||
<string name="groups_invitations_declined">Отказана покана за присъединяване в група</string>
|
||||
<string name="groups_invitations_declined">Отказана покана в група</string>
|
||||
<plurals name="groups_invitations_open">
|
||||
<item quantity="one">%d получена покана за членство в група</item>
|
||||
<item quantity="other">%d получени покани за членство в група</item>
|
||||
<item quantity="one">%d отворена покана в група</item>
|
||||
<item quantity="other">%d отворени покани в група</item>
|
||||
</plurals>
|
||||
<string name="groups_invitations_response_accepted_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_declined_received">%s отказа поканата за членство в група. </string>
|
||||
<string name="sharing_status_groups">Само основателят може да кани нови участници в групата. По-долу е списъкът с текущите участници.</string>
|
||||
<string name="groups_invitations_response_accepted_sent">Приехте поканата в група на %s.</string>
|
||||
<string name="groups_invitations_response_declined_sent">Отказахте поканата в група на %s.</string>
|
||||
<string name="groups_invitations_response_accepted_received">%s прие поканата в група.</string>
|
||||
<string name="groups_invitations_response_declined_received">%s отказа поканата в група. </string>
|
||||
<string name="sharing_status_groups">Само създателят може да покани нови участници в групата. По-долу са изброени сегашните участници в групата.</string>
|
||||
<!--Private Groups Revealing Contacts-->
|
||||
<string name="groups_reveal_contacts">Разкриване на контакти</string>
|
||||
<string name="groups_reveal_dialog_message">Можете да изберете дали да разкриете контактите на всички сегашни и бъдещи членове на групата.\n\nС разкриването им връзката с групата става по-бърза и надеждна, защото общувате с разкритите контакти, даже и основателят на групата да е извън мрежа.</string>
|
||||
<string name="groups_reveal_visible">Отношенията ви с контакта са видими за групата</string>
|
||||
<string name="groups_reveal_visible_revealed_by_us">Отношенията ви с контакта са видими за групата (разкрити от вас)</string>
|
||||
<string name="groups_reveal_visible_revealed_by_contact">Отношенията ви с контакта са видими за групата (разкрити от %s)</string>
|
||||
<string name="groups_reveal_invisible">Отношенията ви с контакта не са видими за групата</string>
|
||||
<string name="groups_reveal_contacts">Разкрий контакти</string>
|
||||
<string name="groups_reveal_dialog_message">Може да изберете да разкриете контактите на всички сегашни и бъдещи участници в тази група.\n\nРазкриването на контактите прави връзката с групата по-бърза и сигурна, тъй като можете да общувате с разкрити контакти, дори когато създателят на групата е офлайн.</string>
|
||||
<string name="groups_reveal_visible">Връзка с контакта се вижда от групата</string>
|
||||
<string name="groups_reveal_visible_revealed_by_us">Връзка с контакта се вижда от групата (разкрита от вас)</string>
|
||||
<string name="groups_reveal_visible_revealed_by_contact">Връзка с контакта се вижда от групата (разкрита от %s)</string>
|
||||
<string name="groups_reveal_invisible">Връзка с контакта не се вижда от групата</string>
|
||||
<!--Forums-->
|
||||
<string name="no_forums">Няма форуми</string>
|
||||
<string name="no_forums_action">Докоснете иконата с +, за да създадете свой или поискайте от контактите си да споделят форум с вас</string>
|
||||
<string name="create_forum_title">Създаване на форум</string>
|
||||
<string name="choose_forum_hint">Име на форума</string>
|
||||
<string name="create_forum_button">Създаване на форум</string>
|
||||
<string name="choose_forum_hint">Изберете име за форума</string>
|
||||
<string name="create_forum_button">Създай форум</string>
|
||||
<string name="forum_created_toast">Форумът е създаден</string>
|
||||
<string name="no_forum_posts">Няма публикации</string>
|
||||
<string name="no_posts">Няма публикации</string>
|
||||
<plurals name="posts">
|
||||
<item quantity="one">%d публикация</item>
|
||||
<item quantity="other">%d публикации</item>
|
||||
</plurals>
|
||||
<string name="forum_new_message_hint">Публикация</string>
|
||||
<string name="forum_message_reply_hint">Отговор</string>
|
||||
<string name="btn_reply">Отговаряне</string>
|
||||
<string name="forum_leave">Напускане на форума</string>
|
||||
<string name="dialog_title_leave_forum">Потвърждение на напускане на форум</string>
|
||||
<string name="dialog_message_leave_forum">Сигурни ли сте, че искате да напуснете този форум?\n\nКонтактите, с които сте го споделили може да спрат да получават публикации.</string>
|
||||
<string name="dialog_button_leave">Напускане</string>
|
||||
<string name="forum_left_toast">Напуснахте форума</string>
|
||||
<string name="forum_message_reply_hint">Нов отговор</string>
|
||||
<string name="btn_reply">Отговори</string>
|
||||
<string name="forum_leave">Напусни форума</string>
|
||||
<string name="dialog_title_leave_forum">Потвърдете напускането на форума</string>
|
||||
<string name="dialog_button_leave">Напусни</string>
|
||||
<!--Forum Sharing-->
|
||||
<string name="forum_share_button">Споделяне форума</string>
|
||||
<string name="forum_share_button">Сподели форум</string>
|
||||
<string name="contacts_selected">Избрани контакти</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_share_message">Добавете съобщение (незадължително)</string>
|
||||
<string name="forum_share_error">Грешка при споделянето на форума.</string>
|
||||
<string name="forum_invitation_received">%1$s сподели с вас форума „%2$s“.</string>
|
||||
<string name="forum_invitation_sent">Споделихте форума „%1$s“ с/ъс %2$s.</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="forum_share_error">Възникна грешка при споделянето на този форум.</string>
|
||||
<string name="forum_invitation_received">%1$s сподели форума \"%2$s\" с вас.</string>
|
||||
<string name="forum_invitation_sent">Споделихте форума \"%1$s\" с %2$s.</string>
|
||||
<string name="forum_invitations_title">Покани във форум</string>
|
||||
<string name="shared_by_format">Споделен от %s</string>
|
||||
<string name="forum_invitation_already_sharing">Вече е споделен</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_auto">Поканата от %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_accepted_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="shared_with">Споделен %1$d (на линия %2$d)</string>
|
||||
<string name="shared_with">Споделен с %1$d (%2$d онлайн)</string>
|
||||
<plurals name="forums_shared">
|
||||
<item quantity="one">%d форум, споделен от контакти</item>
|
||||
<item quantity="other">%d форума, споделени от контакти</item>
|
||||
</plurals>
|
||||
<string name="nobody">Никого</string>
|
||||
<!--Blogs-->
|
||||
<string name="blogs_other_blog_empty_state">Няма публикации</string>
|
||||
<string name="read_more">повече</string>
|
||||
<string name="blogs_write_blog_post">Нова публикация в блога</string>
|
||||
<string name="blogs_write_blog_post_body_hint">Въведете своята публикация</string>
|
||||
<string name="read_more">прочети още</string>
|
||||
<string name="blogs_write_blog_post">Нова блог публикация</string>
|
||||
<string name="blogs_publish_blog_post">Публикуване</string>
|
||||
<string name="blogs_blog_post_created">Публикацията в блога е създадена</string>
|
||||
<string name="blogs_blog_post_received">Получена е нова публикация в блога</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_blog_post_created">Блог публикацията е създадена</string>
|
||||
<string name="blogs_blog_post_received">Нова блог публикация</string>
|
||||
<string name="blogs_blog_post_scroll_to">Отвори</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_blog_removed">Блогът е премахнат</string>
|
||||
<string name="blogs_reblog_comment_hint">Добавете съобщение (незадължително)</string>
|
||||
<string name="blogs_reblog_button">Препубликуване</string>
|
||||
<string name="blogs_reblog_button">Реблог</string>
|
||||
<!--Blog Sharing-->
|
||||
<string name="blogs_sharing_share">Споделяне на блог</string>
|
||||
<string name="blogs_sharing_error">Грешка при споделяне блога.</string>
|
||||
<string name="blogs_sharing_button">Споделяне на блог</string>
|
||||
<string name="blogs_sharing_error">Възникна грешка при споделянето на този блог.</string>
|
||||
<string name="blogs_sharing_button">Сподели блог</string>
|
||||
<string name="blogs_sharing_snackbar">Блогът е споделен с избраните контакти</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_auto">Поканата на %s за абонамент за блог е отказана автоматично.</string>
|
||||
<string name="blogs_sharing_response_accepted_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_sent">Споделихте блога „%1$s“ с/ъс %2$s.</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="blogs_sharing_response_accepted_sent">Приехте поканата в блог от %s.</string>
|
||||
<string name="blogs_sharing_response_declined_sent">Отказахте поканата в блог от %s.</string>
|
||||
<string name="blogs_sharing_response_accepted_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_sent">Споделихте блога \"%1$s\" с %2$s.</string>
|
||||
<string name="blogs_sharing_invitations_title">Блог покани</string>
|
||||
<string name="sharing_status_blog">Всеки абонат на блога може да го сподели с контактите си. Споделяте този блог със следните контакти. Възможно е да има и други, които не можете да видите.</string>
|
||||
<!--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_hint">Aдрес на емисия</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_import_hint">Въведете URL адреса на RSS емисията</string>
|
||||
<string name="blogs_rss_feeds_import_error">Възникна грешка при внасянето на емисия.</string>
|
||||
<string name="blogs_rss_feeds_manage_imported">Внесена:</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_dialog_message">Сигурни ли сте, че желаете да изтриете емисията?\n\nПубликациите ще бъдат премахнати от устройството ви, но не и от устройствата на другите членове.\n\nКонтактите, с които сте споделили тази емисия може да спрат да получават обновявания.</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>
|
||||
<!--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-->
|
||||
<string name="pref_language_title">Език и регион</string>
|
||||
<string name="pref_language_changed">Тази настройка ще име ефект след рестарт на Briar. Отпишете се и рестартирайте Briar.</string>
|
||||
<string name="pref_language_default">Спрямо системата</string>
|
||||
<string name="display_settings_title">Външен вид</string>
|
||||
<string name="pref_theme_title">Тема</string>
|
||||
<string name="pref_theme_light">Светла</string>
|
||||
<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 Network-->
|
||||
<string name="network_settings_title">Мрежа</string>
|
||||
<string name="bluetooth_setting">Свързване чрез Bluetooth</string>
|
||||
<string name="bluetooth_setting_enabled">Когато контактите са наблизо</string>
|
||||
<string name="bluetooth_setting_disabled">Само при добавяне на контакти</string>
|
||||
<!--How and when Tor will connect after Automatic: E.g. Don't connect (in China) or Use Tor with bridges (in Belarus)-->
|
||||
<!--Settings Security and Panic-->
|
||||
<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-->
|
||||
<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"-->
|
||||
<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"-->
|
||||
<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"-->
|
||||
<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"-->
|
||||
<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"-->
|
||||
<string name="pref_lock_timeout_60">1 час</string>
|
||||
<string name="pref_lock_timeout_never">Никога</string>
|
||||
<string name="pref_lock_timeout_never_summary">Briar никога да не се заключва автоматично</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="panic_setting">Настройка на бутон за паника</string>
|
||||
<string name="panic_setting_title">Бутон за паника</string>
|
||||
<string name="panic_setting_hint">Настройва се реакцията на Briar при използване на приложение за бутон за паника</string>
|
||||
<string name="panic_app_setting_title">Приложение бутон за паника</string>
|
||||
<string name="panic_setting">Настройка на паник бутон</string>
|
||||
<string name="panic_setting_title">Паник бутон</string>
|
||||
<string name="panic_setting_hint">Конфигурация на приложение за паник бутон</string>
|
||||
<string name="panic_app_setting_title">Приложение за паник бутон</string>
|
||||
<string name="unknown_app">непознато приложение</string>
|
||||
<string name="panic_app_setting_summary">Няма зададено приложение</string>
|
||||
<string name="panic_app_setting_none">Няма</string>
|
||||
<string name="dialog_title_connect_panic_app">Потвърждение на приложение при паника</string>
|
||||
<string name="dialog_message_connect_panic_app">Сигурни ли сте, че желаете да позволите на %1$s да задейства разрушителните действия на бутона за паника?</string>
|
||||
<string name="panic_setting_destructive_action">Разрушителни действия</string>
|
||||
<string name="dialog_title_connect_panic_app">Потвърждение на паник приложение</string>
|
||||
<string name="dialog_message_connect_panic_app">Сигурни ли сте, че искате да позволите на %1$s да задейства унищожителни действия на паник бутон?</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_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-->
|
||||
<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_summary">Известия за лични съобщения</string>
|
||||
<string name="notify_private_messages_setting_summary_26">Настройки на известия за лични съобщения</string>
|
||||
<string name="notify_private_messages_setting_summary">Показвай известия за лични съобщения</string>
|
||||
<string name="notify_group_messages_setting_title">Групови съобщения</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_summary">Известия за публикации във форуми</string>
|
||||
<string name="notify_forum_posts_setting_summary_26">Настройки на известия за публикации във форуми</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_group_messages_setting_summary">Показвай известия за групови съобщения</string>
|
||||
<string name="notify_forum_posts_setting_title">Форумни публикации</string>
|
||||
<string name="notify_forum_posts_setting_summary">Показвай известия за форумни публикации</string>
|
||||
<string name="notify_blog_posts_setting_title">Блог публикации</string>
|
||||
<string name="notify_blog_posts_setting_summary">Показвай известия за блог публикации</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_default">Подразбиран тон на звънене</string>
|
||||
<string name="notify_sound_setting_disabled">Няма</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>
|
||||
<string name="notify_sound_setting_default">Мелодия по подразбиране</string>
|
||||
<string name="notify_sound_setting_disabled">Никакви</string>
|
||||
<string name="choose_ringtone_title">Изберете рингтон</string>
|
||||
<!--Settings Feedback-->
|
||||
<string name="send_feedback">Изпращане на отзив</string>
|
||||
<string name="feedback_settings_title">Отзиви</string>
|
||||
<string name="send_feedback">Изпращане на отзиви</string>
|
||||
<!--Link Warning-->
|
||||
<string name="link_warning_title">Предупреждение за препратка</string>
|
||||
<string name="link_warning_intro">Препратката ще бъде отворена от външно приложение.</string>
|
||||
<string name="link_warning_text">Така отворена може да бъде използвана, за да бъдете идентифицирани. Преценете дали имате доверие на подателя и обмислете дали да не я отворите с Tor Browser.</string>
|
||||
<string name="link_warning_open_link">Отваряне</string>
|
||||
<string name="link_warning_title">Предупреждение за линк</string>
|
||||
<string name="link_warning_intro">Линкът ще се отвори във външно приложение.</string>
|
||||
<string name="link_warning_text">Линкът може да се използва, за да ви идентифицира. Помислете дали имате доверие на човека, който ви изпраща линка, и обмислете дали да не го отворите с Orfox.</string>
|
||||
<string name="link_warning_open_link">Отвори линк</string>
|
||||
<!--Crash Reporter-->
|
||||
<string name="crash_report_title">Доклад на срив</string>
|
||||
<string name="briar_crashed">Извинете, Briar се срина.</string>
|
||||
<string name="not_your_fault">Не е по ваша вина.</string>
|
||||
<string name="please_send_report">Помогнете да направим Briar по-добър като ни изпратите доклад.</string>
|
||||
<string name="report_is_encrypted">Даваме обещание, че докладът е шифрован и е изпратен добре защитен.</string>
|
||||
<string name="feedback_title">Обратна връзка</string>
|
||||
<string name="describe_crash">Опишете случилото се (незадължително)</string>
|
||||
<string name="enter_feedback">Въведете обратна връзка</string>
|
||||
<string name="optional_contact_email">Адрес на електронна поща (по желание)</string>
|
||||
<string name="include_debug_report_crash">Изпращане на анонимни данни за срива</string>
|
||||
<string name="include_debug_report_feedback">Изпращане на анонимни данни за устройството</string>
|
||||
<string name="dev_report_user_info">Данни за потербителя</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="please_send_report">Моля, помогнете да изградим по-добър Briar, като ни изпратите доклад.</string>
|
||||
<string name="report_is_encrypted">Гарантираме, че докладът е криптиран и изпратен безопасно.</string>
|
||||
<string name="feedback_title">Отзиви</string>
|
||||
<string name="describe_crash">Опишете станалото (незадължително)</string>
|
||||
<string name="enter_feedback">Въведете отзив</string>
|
||||
<string name="optional_contact_email">Имейл адресът ви (незадължително)</string>
|
||||
<string name="include_debug_report_crash">Добави анонимни данни за срива</string>
|
||||
<string name="include_debug_report_feedback">Добави анонимни данни за това устройствo</string>
|
||||
<string name="could_not_load_report_data">Данните за доклада не можаха да заредят.</string>
|
||||
<string name="send_report">Изпращане на доклад</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_error">Грешка при изпращане на доклад</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Отписване от Briar…</string>
|
||||
<string name="progress_title_logout">Отписване от Briar...</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
<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>
|
||||
<string name="screen_filter_title">Открит е овърлей на екрана</string>
|
||||
<!--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-->
|
||||
<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>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<string name="confirm_password">Potwierdź hasło</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="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="more_info">Więcej Informacji</string>
|
||||
<string name="don_t_ask_again">Nie pytaj ponownie</string>
|
||||
@@ -27,11 +27,10 @@
|
||||
<!--Login-->
|
||||
<string name="enter_password">Hasło</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="forgotten_password">Zapomniałem hasło</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_text">Naciśnij, aby uzyskać więcej informacji</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>
|
||||
</plurals>
|
||||
<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="download_briar_button">Pobierz najnowszą wersję</string>
|
||||
<string name="startup_open_database">Deszyfruję Bazę Danych...</string>
|
||||
<string name="startup_migrate_database">Aktualizuję Bazę Danych...</string>
|
||||
<string name="startup_compact_database">Kompaktowanie Bazy Danych…</string>
|
||||
@@ -63,16 +60,12 @@
|
||||
<string name="lock_button">Zablokuj Aplikację</string>
|
||||
<string name="settings_button">Ustawienia</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-->
|
||||
<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-->
|
||||
<string name="transport_lan">Wi-Fi</string>
|
||||
<!--Transports: Bluetooth-->
|
||||
<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-->
|
||||
<string name="reminder_notification_title">Wylogowano z Briar</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="no_private_messages">Brak wiadomości do pokazania</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_attach">Załącz obraz</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="set_contact_alias">Zmień nazwę 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="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 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."-->
|
||||
<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="delete_contact">Usuń kontakt</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="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_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-->
|
||||
<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>
|
||||
@@ -415,8 +401,6 @@
|
||||
<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_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_author">Autor:</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_error">Wystąpił problem podczas ładowania twoich kanałów RSS. Proszę spróbować ponownie później.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="dialog_confirm_profile_picture_title">Zmień zdjęcie profilowe</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">Język & region</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>
|
||||
<!--Settings Connections-->
|
||||
<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>
|
||||
<!--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>
|
||||
@@ -507,7 +489,6 @@
|
||||
<string name="choose_ringtone_title">Wybierz dzwonek</string>
|
||||
<string name="cannot_load_ringtone">Nie mogę załadować dzwonka</string>
|
||||
<!--Conversation Settings-->
|
||||
<string name="disappearing_messages_title">Znikające wiadomości</string>
|
||||
<string name="learn_more">Dowiedz się więcej</string>
|
||||
<!--Settings Feedback-->
|
||||
<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="feedback_title">Twoja opinia</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="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>
|
||||
@@ -534,8 +515,6 @@
|
||||
<string name="dev_report_connectivity">Łączenie</string>
|
||||
<string name="send_report">Wyślij raport</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>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Wylogowywanie z Briar...</string>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<color name="briar_blue_600">#1b69b6</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_red_500">#db3b21</color>
|
||||
|
||||
@@ -46,12 +46,14 @@
|
||||
<string name="forgotten_password">I have forgotten my 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="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_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_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_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_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_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_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">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. 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">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 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">
|
||||
<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>
|
||||
@@ -160,6 +162,7 @@
|
||||
<string name="sorry">Sorry</string>
|
||||
<string name="error_start_activity">Unavailable on your system</string>
|
||||
<string name="status_heading">Status:</string>
|
||||
<string name="error">Error</string>
|
||||
|
||||
<!-- Contacts and Private Conversations-->
|
||||
<string name="no_contacts">No contacts to show</string>
|
||||
@@ -615,7 +618,8 @@
|
||||
<string name="learn_more">Learn more</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>
|
||||
|
||||
<!-- Link Warning -->
|
||||
@@ -699,7 +703,7 @@
|
||||
<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_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_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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
<!-- This is a name to be used in screenshots. Feel free to change it to a local name. -->
|
||||
|
||||
@@ -24,10 +24,24 @@
|
||||
app:fragment="org.briarproject.briar.android.settings.NotificationsFragment"
|
||||
app:icon="@drawable/ic_notifications" />
|
||||
|
||||
<Preference
|
||||
android:key="pref_key_send_feedback"
|
||||
android:title="@string/send_feedback"
|
||||
app:icon="@drawable/ic_feedback" />
|
||||
<PreferenceCategory
|
||||
android:key="pref_key_actions"
|
||||
android:layout="@layout/preferences_category"
|
||||
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
|
||||
android:key="pref_key_dev"
|
||||
|
||||
@@ -225,6 +225,7 @@ dependencyVerification {
|
||||
'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.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.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',
|
||||
|
||||
@@ -97,5 +97,6 @@ internal class HeadlessModule(private val appDir: File) {
|
||||
override fun shouldEnableDisappearingMessages() = false
|
||||
override fun shouldEnableConnectViaBluetooth() = false
|
||||
override fun shouldEnableTransferData() = false
|
||||
override fun shouldEnableShareAppViaOfflineHotspot() = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user