Compare commits

...

55 Commits

Author SHA1 Message Date
Sebastian Kürten
c85102fe52 Use BriarActivity#signOut() to exit app after updating language 2021-04-06 07:06:20 +02:00
akwizgran
bebf3bbc39 Merge branch '1826-settings-view-model' into 'master'
Finish migrating SettingsFragment to ViewModel

Closes #1942 and #1826

See merge request briar/briar!1350
2021-04-01 13:20:12 +00:00
akwizgran
62cca1335f Bump version numbers for 1.2.20 release. 2021-03-29 13:33:12 +01:00
akwizgran
11a18859fb Update translations. 2021-03-29 13:33:12 +01:00
akwizgran
1116a7e125 Merge branch 'update-bridges-again' into 'master'
Remove a failing bridge

See merge request briar/briar!1422
2021-03-29 12:32:11 +00:00
akwizgran
415b315292 Add a Tor Browser default bridge. 2021-03-29 13:01:27 +01:00
akwizgran
9818ec2b66 Remove a failing bridge. 2021-03-29 12:55:04 +01:00
Torsten Grote
95ef061a34 Pick up screen lock changes when returning to SecurityFragment 2021-03-26 14:33:58 -03:00
Torsten Grote
aaaf8aa66f Go back to security settings when pressing navigation icon in ChangePasswordActivity 2021-03-26 14:12:02 -03:00
Torsten Grote
29965e38d0 Don't show Toast off the UiThread 2021-03-26 14:10:37 -03:00
Torsten Grote
371d49a213 Use SwitchPreferenceCompat for panic preferences
Addresses #1991
2021-03-26 14:10:36 -03:00
Torsten Grote
6ed95e145e Re-open DisplayFragment after changing theme 2021-03-26 13:48:20 -03:00
Torsten Grote
8c025c1173 review: fix nullability and visibility of settings 2021-03-26 13:48:19 -03:00
Torsten Grote
9ce541cc31 Allow settings titles on more than a single line 2021-03-26 13:48:19 -03:00
Torsten Grote
aa57a4c123 lint ignore icon tinting since it seems to work on Android 4 with VectorDrawableCompat 2021-03-26 13:48:18 -03:00
Torsten Grote
58d9deb3b8 Move avatar layout into own preference
which is only shown on main settings fragment
2021-03-26 13:48:18 -03:00
Torsten Grote
f0685c4a43 Get rid of custom switch preference 2021-03-26 13:48:18 -03:00
Torsten Grote
484817db08 Move notifications settings into own screen 2021-03-26 13:48:17 -03:00
Torsten Grote
670bf15d31 Move security settings into own screen 2021-03-26 13:48:17 -03:00
Torsten Grote
6df1e0fd77 Move connections settings into own screen 2021-03-26 13:48:17 -03:00
Torsten Grote
ec910cb80f Move Display category into its own settings screen 2021-03-26 13:48:16 -03:00
akwizgran
372516646d Merge branch '1970-blog-bugs' into 'master'
Fix issues with blogs after refactoring

See merge request briar/briar!1421
2021-03-26 14:32:11 +00:00
Torsten Grote
72e721b0d3 Don't show snackbar about local blog post again after screen rotation 2021-03-26 10:56:57 -03:00
Torsten Grote
6599093611 Improve blog author clickability
resolves issue where clicking reblogged author opened reblogging author's blog
2021-03-26 10:40:51 -03:00
Torsten Grote
dceeecf1fe Open blog posts from blog feed in BlogActivity 2021-03-26 10:23:31 -03:00
Torsten Grote
ace0b9a3d8 Merge branch 'update-bridges' into 'master'
Replace a failing bridge with a Tor Browser default bridge

See merge request briar/briar!1420
2021-03-26 11:59:03 +00:00
akwizgran
7c45c90de9 Replace a failing bridge with a Tor Browser default bridge. 2021-03-26 09:27:36 +00:00
akwizgran
c2a4b5e26a Bump version numbers for 1.2.19 release. 2021-03-25 17:36:04 +00:00
akwizgran
feac0ad802 Update translations. 2021-03-25 17:34:02 +00:00
akwizgran
60478eba3f Merge branch '1866-blog-controller' into 'master'
Migrate BlogController and FeedController to ViewModel

Closes #1891 and #1866

See merge request briar/briar!1342
2021-03-25 17:25:43 +00:00
akwizgran
3639952612 Merge branch 'espresso-ci' into 'master'
Run instrumentation tests in CI when briar-android changes

Closes admin#20

See merge request briar/briar!1413
2021-03-25 15:47:18 +00:00
akwizgran
c4a654b267 Merge branch '1979-feedback-crash' into 'master'
Don't crash when pressing SHOW with user information when sending feedback

Closes #1979

See merge request briar/briar!1418
2021-03-25 13:29:43 +00:00
Torsten Grote
ecb31a4d32 Don't crash when pressing SHOW with user information when sending feedback 2021-03-25 08:47:18 -03:00
Torsten Grote
76f201bb2f Run Espresso tests manually as they are still too flaky 2021-03-24 16:10:33 -03:00
akwizgran
87799b743c Add Burmese translation to language chooser. 2021-03-24 15:28:33 +00:00
akwizgran
b898a7c370 Update translations, add Burmese translation. 2021-03-24 13:54:00 +00:00
Torsten Grote
f3210e3af2 Allow DbViewModel work on things other than lists. 2021-03-23 12:59:16 -03:00
akwizgran
225fd6fd49 Merge branch 'headless-remove-type-args-in-jar-sorting-algorithm' into 'master'
Remove redundant type args in briar-headless/build.gradle

See merge request briar/briar!1416
2021-03-23 12:38:53 +00:00
Sebastian Kürten
400d259a60 Remove redundant type args in briar-headless/build.gradle
The TreeMap<> doesn't need to repeat <String, JarEntry> from
Map<String, JarEntry>.
2021-03-23 07:44:02 +01:00
Torsten Grote
4074ac8578 Add handleException() to DbViewModel
and use it for blogs
2021-03-22 15:17:30 -03:00
Torsten Grote
b2e6dd4138 publish log files as artifacts when emulator job fails 2021-03-19 14:19:07 -03:00
Torsten Grote
b608b42174 Run instrumentation tests in CI when briar-android changes 2021-03-18 12:15:47 -03:00
Torsten Grote
f603254153 Fix instrumentation tests 2021-03-18 12:15:46 -03:00
Torsten Grote
c851dd228b Add a different (faster) way to exclude large/slow tests 2021-03-18 12:15:46 -03:00
Torsten Grote
e97478a21a Don't reload blog data when configuration changes 2021-03-17 14:16:02 -03:00
Torsten Grote
726ebcea3f Make blog post author clickable when not already in their blog 2021-03-17 14:16:02 -03:00
Torsten Grote
2f969775d8 Remove TransactionManager from blog's BaseViewModel 2021-03-17 14:16:02 -03:00
Torsten Grote
d3b855318c Anticipate review feedback for blog view models after re-basing 2021-03-17 14:16:01 -03:00
Torsten Grote
95104d3383 Clean up after migrating blog controllers to view model 2021-03-17 14:16:01 -03:00
Torsten Grote
6860a04e8b Don't use layoutManager hack to restore scrolling position of blogs
not needed anymore when posts are cached in viewmodels
2021-03-17 14:16:01 -03:00
Torsten Grote
33c24f8655 Migrate blogs to new SharingController
and get rid of the deprecated one
2021-03-17 14:16:00 -03:00
Torsten Grote
1fa4b78474 Migrate BlogController to BlogViewModel 2021-03-17 14:16:00 -03:00
Torsten Grote
b678de7529 Make BlogAdapter final and don't pass in a FragmentManager 2021-03-17 14:16:00 -03:00
Torsten Grote
ab1ed0ff5a Turn FeedController into FeedViewModel 2021-03-17 14:15:59 -03:00
Torsten Grote
ad20e5230a Allow blog posts to be loaded within one transaction 2021-03-17 14:15:59 -03:00
112 changed files with 3921 additions and 3193 deletions

View File

@@ -1,30 +1,52 @@
image: briar/ci-image-android:latest
stages:
- test
- optional_tests
- check_reproducibility
- test
- optional_tests
- check_reproducibility
test:
stage: test
.base-test:
before_script:
- set -e
- export GRADLE_USER_HOME=$PWD/.gradle
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .gradle/wrapper
- .gradle/caches
script:
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources
after_script:
# these file change every time but should not be cached
# these file change every time and should not be cached
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
test:
extends: .base-test
stage: test
script:
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check
android test:
extends: .base-test
stage: optional_tests
image: briar/ci-image-android-emulator:latest
script:
# start emulator first, so it can fail early
- start-emulator.sh
# run normal and screenshot tests together (exclude Large tests)
- ./gradlew -Djava.security.egd=file:/dev/urandom connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest
artifacts:
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
paths:
- kernel.log
- logcat.txt
expire_in: 3 days
when: on_failure
when: manual
except:
- tags
tags:
- kvm
test_reproducible:
stage: check_reproducibility
@@ -40,6 +62,7 @@ test_reproducible:
- export GRADLE_USER_HOME=$PWD/.gradle
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .gradle/wrapper
- .gradle/caches

View File

@@ -0,0 +1,51 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Instrumentation Tests (destroys DB)" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="briar.briar-android" />
<option name="TESTING_TYPE" value="1" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="org.briarproject.briar.android" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -15,8 +15,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 10218
versionName "1.2.18"
versionCode 10220
versionName "1.2.20"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -179,6 +179,13 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
LOG.info("Stopping services");
state = STOPPING;
eventBus.broadcast(new LifecycleEvent(STOPPING));
LOG.info("Sleeping a bit to simulate slowness");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOG.info("Done simulating slowness");
for (Service s : services) {
long start = now();
s.stopService();

View File

@@ -1,8 +1,8 @@
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0
Bridge obfs4 144.217.20.138:80 FB70B257C162BF1038CA669D568D76F5B7F0BABB cert=vYIV5MgrghGQvZPIi1tJwnzorMgqgmlKaB77Y3Z9Q/v94wZBOAXkW+fdx4aSxLVnKO+xNw iat-mode=0
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0

View File

@@ -26,8 +26,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 10218
versionName "1.2.18"
versionCode 10220
versionName "1.2.20"
applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true

View File

@@ -4,16 +4,20 @@ import android.app.Activity;
import android.content.Intent;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.briar.R;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static org.briarproject.briar.android.controller.BriarControllerImpl.DOZE_ASK_AGAIN;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
@SuppressWarnings("WeakerAccess")
@@ -27,6 +31,8 @@ public abstract class UiTest {
protected AccountManager accountManager;
@Inject
protected LifecycleManager lifecycleManager;
@Inject
protected SettingsManager settingsManager;
public UiTest() {
BriarTestComponentApplication app = getApplicationContext();
@@ -39,22 +45,8 @@ public abstract class UiTest {
protected class CleanAccountTestRule<A extends Activity>
extends IntentsTestRule<A> {
@Nullable
private final Runnable runnable;
public CleanAccountTestRule(Class<A> activityClass) {
super(activityClass);
this.runnable = null;
}
/**
* Use this if you need to run code before launching the activity.
* Note: You need to use {@link #launchActivity(Intent)} yourself
* to start the activity.
*/
public CleanAccountTestRule(Class<A> activityClass, Runnable runnable) {
super(activityClass, false, false);
this.runnable = runnable;
}
@Override
@@ -62,16 +54,17 @@ public abstract class UiTest {
super.beforeActivityLaunched();
accountManager.deleteAccount();
accountManager.createAccount(USERNAME, PASSWORD);
if (runnable != null) {
Intent serviceIntent =
new Intent(getApplicationContext(), BriarService.class);
getApplicationContext().startService(serviceIntent);
try {
lifecycleManager.waitForStartup();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
runnable.run();
Intent serviceIntent =
new Intent(getApplicationContext(), BriarService.class);
getApplicationContext().startService(serviceIntent);
try {
lifecycleManager.waitForStartup();
// do not show doze white-listing dialog
Settings settings = new Settings();
settings.putBoolean(DOZE_ASK_AGAIN, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (InterruptedException | DbException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -10,12 +10,15 @@ import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import androidx.test.filters.LargeTest;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@LargeTest
@RunWith(Parameterized.class)
public class PngSuiteImageCompressorTest
extends AbstractImageCompressorTest {

View File

@@ -12,11 +12,14 @@ import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import androidx.test.filters.LargeTest;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
import static org.junit.Assume.assumeTrue;
@LargeTest
@RunWith(Parameterized.class)
public class PngSuiteImageSizeCalculatorTest
extends AbstractImageSizeCalculatorTest {

View File

@@ -1,39 +0,0 @@
package org.briarproject.briar.android.conversation;
import android.content.Context;
import android.content.Intent;
import org.briarproject.briar.R;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
@RunWith(AndroidJUnit4.class)
public class ConversationActivityNotSignedInTest {
@Rule
public ActivityTestRule<ConversationActivity> testRule =
new ActivityTestRule<>(ConversationActivity.class, false, false);
@Test
public void openWithoutSignedIn() {
Context targetContext = getInstrumentation().getTargetContext();
Intent intent = new Intent(targetContext, ConversationActivity.class);
intent.putExtra(CONTACT_ID, 1);
testRule.launchActivity(intent);
onView(withText(R.string.sign_in_button))
.perform(waitUntilMatches(isDisplayed()));
}
}

View File

@@ -16,6 +16,7 @@ import androidx.test.espresso.contrib.DrawerActions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
@@ -29,7 +30,9 @@ import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assume.assumeTrue;
@RunWith(AndroidJUnit4.class)
public class SettingsActivityScreenshotTest extends ScreenshotTest {
@@ -76,6 +79,8 @@ public class SettingsActivityScreenshotTest extends ScreenshotTest {
@Test
public void appLock() {
assumeTrue("device has no screen lock",
hasScreenLock(getApplicationContext()));
// scroll down
onView(withClassName(is(RecyclerView.class.getName())))
.perform(scrollTo(hasDescendant(

View File

@@ -36,6 +36,11 @@ import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.settings.ConnectionsFragment;
import org.briarproject.briar.android.settings.DisplayFragment;
import org.briarproject.briar.android.settings.NotificationsFragment;
import org.briarproject.briar.android.settings.SecurityFragment;
import org.briarproject.briar.android.settings.SettingsFragment;
import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog;
@@ -193,4 +198,14 @@ public interface AndroidComponent
void inject(EmojiTextInputView textInputView);
void inject(BriarModelLoader briarModelLoader);
void inject(SettingsFragment settingsFragment);
void inject(DisplayFragment displayFragment);
void inject(ConnectionsFragment connectionsFragment);
void inject(SecurityFragment securityFragment);
void inject(NotificationsFragment notificationsFragment);
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.StrictMode;
@@ -30,6 +31,7 @@ import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.DozeHelperModule;
import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.account.SetupModule;
import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.contact.ContactListModule;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.introduction.IntroductionModule;
@@ -85,6 +87,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
ContactListModule.class,
IntroductionModule.class,
// below need to be within same scope as ViewModelProvider.Factory
BlogModule.class,
ForumModule.class,
GroupListModule.class,
GroupConversationModule.class,
@@ -113,6 +116,11 @@ public class AppModule {
this.application = application;
}
public static AndroidComponent getAndroidComponent(Context ctx) {
BriarApplication app = (BriarApplication) ctx.getApplicationContext();
return app.getApplicationComponent();
}
@Provides
@Singleton
Application providesApplication() {

View File

@@ -12,7 +12,7 @@ import java.util.Locale;
import javax.annotation.Nullable;
import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE;
import static org.briarproject.briar.android.settings.DisplayFragment.PREF_LANGUAGE;
@NotNullByDefault
public class Localizer {
@@ -25,7 +25,7 @@ public class Localizer {
private Localizer(SharedPreferences sharedPreferences) {
this(Locale.getDefault(), getLocaleFromTag(
sharedPreferences.getString(LANGUAGE, "default")));
sharedPreferences.getString(PREF_LANGUAGE, "default")));
}
private Localizer(Locale systemLocale, @Nullable Locale userLocale) {

View File

@@ -40,8 +40,8 @@ import static android.os.SystemClock.elapsedRealtime;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.settings.SettingsFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SettingsFragment.PREF_SCREEN_LOCK_TIMEOUT;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK_TIMEOUT;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;

View File

@@ -11,10 +11,8 @@ import org.briarproject.briar.android.account.SetupActivity;
import org.briarproject.briar.android.account.UnlockActivity;
import org.briarproject.briar.android.blog.BlogActivity;
import org.briarproject.briar.android.blog.BlogFragment;
import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.blog.BlogPostFragment;
import org.briarproject.briar.android.blog.FeedFragment;
import org.briarproject.briar.android.blog.FeedPostFragment;
import org.briarproject.briar.android.blog.ReblogActivity;
import org.briarproject.briar.android.blog.ReblogFragment;
import org.briarproject.briar.android.blog.RssFeedImportActivity;
@@ -85,7 +83,6 @@ import dagger.Component;
@ActivityScope
@Component(modules = {
ActivityModule.class,
BlogModule.class,
CreateGroupModule.class,
GroupInvitationModule.class,
GroupMemberModule.class,
@@ -152,8 +149,6 @@ public interface ActivityComponent {
void inject(BlogPostFragment fragment);
void inject(FeedPostFragment fragment);
void inject(ReblogFragment fragment);
void inject(ReblogActivity activity);

View File

@@ -6,7 +6,6 @@ public interface RequestCodes {
int REQUEST_INTRODUCTION = 2;
int REQUEST_GROUP_INVITE = 3;
int REQUEST_SHARE_FORUM = 4;
int REQUEST_WRITE_BLOG_POST = 5;
int REQUEST_SHARE_BLOG = 6;
int REQUEST_RINGTONE = 7;
int REQUEST_PERMISSION_CAMERA_LOCATION = 8;

View File

@@ -1,48 +0,0 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
interface BaseController {
@UiThread
void onStart();
@UiThread
void onStop();
void loadBlogPosts(GroupId g,
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void loadBlogPost(GroupId g, MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void repeatPost(BlogPostItem item, @Nullable String comment,
ExceptionHandler<DbException> handler);
@NotNullByDefault
interface BlogListener {
@UiThread
void onBlogPostAdded(BlogPostHeader header, boolean local);
@UiThread
void onBlogRemoved();
}
}

View File

@@ -1,209 +0,0 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BaseControllerImpl extends DbControllerImpl
implements BaseController, EventListener {
private static final Logger LOG =
Logger.getLogger(BaseControllerImpl.class.getName());
protected final EventBus eventBus;
protected final AndroidNotificationManager notificationManager;
protected final IdentityManager identityManager;
protected final BlogManager blogManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, BlogPostHeader> headerCache =
new ConcurrentHashMap<>();
BaseControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
AndroidNotificationManager notificationManager,
IdentityManager identityManager, BlogManager blogManager) {
super(dbExecutor, lifecycleManager);
this.eventBus = eventBus;
this.notificationManager = notificationManager;
this.identityManager = identityManager;
this.blogManager = blogManager;
}
@Override
@CallSuper
public void onStart() {
eventBus.addListener(this);
}
@Override
@CallSuper
public void onStop() {
eventBus.removeListener(this);
}
@Override
public void loadBlogPosts(GroupId groupId,
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
Collection<BlogPostItem> items = loadItems(groupId);
handler.onResult(items);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
Collection<BlogPostItem> loadItems(GroupId groupId) throws DbException {
long start = now();
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(groupId);
logDuration(LOG, "Loading headers", start);
Collection<BlogPostItem> items = new ArrayList<>(headers.size());
start = now();
for (BlogPostHeader h : headers) {
headerCache.put(h.getId(), h);
BlogPostItem item = getItem(h);
items.add(item);
}
logDuration(LOG, "Loading bodies", start);
return items;
}
@Override
public void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> handler) {
String text = textCache.get(header.getId());
if (text != null) {
LOG.info("Loaded text from cache");
handler.onResult(new BlogPostItem(header, text));
return;
}
runOnDbThread(() -> {
try {
long start = now();
BlogPostItem item = getItem(header);
logDuration(LOG, "Loading text", start);
handler.onResult(item);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void loadBlogPost(GroupId g, MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler) {
BlogPostHeader header = headerCache.get(m);
if (header != null) {
LOG.info("Loaded header from cache");
loadBlogPost(header, handler);
return;
}
runOnDbThread(() -> {
try {
long start = now();
BlogPostHeader header1 = getPostHeader(g, m);
BlogPostItem item = getItem(header1);
logDuration(LOG, "Loading post", start);
handler.onResult(item);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void repeatPost(BlogPostItem item, @Nullable String comment,
ExceptionHandler<DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
BlogPostHeader h = item.getHeader();
blogManager.addLocalComment(a, b.getId(), comment, h);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
private BlogPostHeader getPostHeader(GroupId g, MessageId m)
throws DbException {
BlogPostHeader header = headerCache.get(m);
if (header == null) {
header = blogManager.getPostHeader(g, m);
headerCache.put(m, header);
}
return header;
}
@DatabaseExecutor
private BlogPostItem getItem(BlogPostHeader h) throws DbException {
String text;
if (h instanceof BlogCommentHeader) {
BlogCommentHeader c = (BlogCommentHeader) h;
BlogCommentItem item = new BlogCommentItem(c);
text = getPostText(item.getPostHeader().getId());
item.setText(text);
return item;
} else {
text = getPostText(h.getId());
return new BlogPostItem(h, text);
}
}
@DatabaseExecutor
private String getPostText(MessageId m) throws DbException {
String text = textCache.get(m);
if (text == null) {
text = HtmlUtils.clean(blogManager.getPostText(m), ARTICLE);
textCache.put(m, text);
}
return text;
}
}

View File

@@ -1,121 +0,0 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
abstract class BasePostFragment extends BaseFragment {
static final String POST_ID = "briar.POST_ID";
private static final Logger LOG =
getLogger(BasePostFragment.class.getName());
private final Handler handler = new Handler(Looper.getMainLooper());
protected MessageId postId;
private ProgressBar progressBar;
private BlogPostViewHolder ui;
private BlogPostItem post;
private Runnable refresher;
@CallSuper
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// retrieve MessageId of blog post from arguments
byte[] p = requireArguments().getByteArray(POST_ID);
if (p == null) throw new IllegalStateException("No post ID in args");
postId = new MessageId(p);
View view = inflater.inflate(R.layout.fragment_blog_post, container,
false);
progressBar = view.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
ui = new BlogPostViewHolder(view, true, new OnBlogPostClickListener() {
@Override
public void onBlogPostClick(BlogPostItem post) {
// We're already there
}
@Override
public void onAuthorClick(BlogPostItem post) {
if (getContext() == null) return;
Intent i = new Intent(getContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
getContext().startActivity(i);
}
}, getFragmentManager());
return view;
}
@CallSuper
@Override
public void onStart() {
super.onStart();
startPeriodicUpdate();
}
@CallSuper
@Override
public void onStop() {
super.onStop();
stopPeriodicUpdate();
}
@UiThread
protected void onBlogPostLoaded(BlogPostItem post) {
progressBar.setVisibility(INVISIBLE);
this.post = post;
ui.bindItem(post);
}
private void startPeriodicUpdate() {
refresher = () -> {
LOG.info("Updating Content...");
ui.updateDate(post.getTimestamp());
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
};
LOG.info("Adding Handler Callback");
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
private void stopPeriodicUpdate() {
if (refresher != null) {
LOG.info("Removing Handler Callback");
handler.removeCallbacks(refresher);
}
}
}

View File

@@ -0,0 +1,216 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.util.HtmlUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
@NotNullByDefault
abstract class BaseViewModel extends DbViewModel implements EventListener {
private static final Logger LOG = getLogger(BaseViewModel.class.getName());
private final EventBus eventBus;
protected final IdentityManager identityManager;
protected final AndroidNotificationManager notificationManager;
protected final BlogManager blogManager;
protected final MutableLiveData<LiveResult<ListUpdate>> blogPosts =
new MutableLiveData<>();
BaseViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.eventBus = eventBus;
this.identityManager = identityManager;
this.notificationManager = notificationManager;
this.blogManager = blogManager;
eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
@DatabaseExecutor
protected List<BlogPostItem> loadBlogPosts(Transaction txn, GroupId groupId)
throws DbException {
long start = now();
List<BlogPostHeader> headers =
blogManager.getPostHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
List<BlogPostItem> items = new ArrayList<>(headers.size());
start = now();
for (BlogPostHeader h : headers) {
BlogPostItem item = getItem(txn, h);
items.add(item);
}
logDuration(LOG, "Loading bodies", start);
return items;
}
@DatabaseExecutor
protected BlogPostItem getItem(Transaction txn, BlogPostHeader h)
throws DbException {
String text;
if (h instanceof BlogCommentHeader) {
BlogCommentHeader c = (BlogCommentHeader) h;
BlogCommentItem item = new BlogCommentItem(c);
text = getPostText(txn, item.getPostHeader().getId());
item.setText(text);
return item;
} else {
text = getPostText(txn, h.getId());
return new BlogPostItem(h, text);
}
}
@DatabaseExecutor
private String getPostText(Transaction txn, MessageId m)
throws DbException {
return HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE);
}
LiveData<LiveResult<BlogPostItem>> loadBlogPost(GroupId g, MessageId m) {
MutableLiveData<LiveResult<BlogPostItem>> result =
new MutableLiveData<>();
runOnDbThread(true, txn -> {
long start = now();
BlogPostHeader header = blogManager.getPostHeader(txn, g, m);
BlogPostItem item = getItem(txn, header);
logDuration(LOG, "Loading post", start);
result.postValue(new LiveResult<>(item));
}, e -> {
logException(LOG, WARNING, e);
result.postValue(new LiveResult<>(e));
});
return result;
}
protected void onBlogPostAdded(BlogPostHeader header, boolean local) {
runOnDbThread(true, txn -> {
BlogPostItem item = getItem(txn, header);
txn.attach(() -> onBlogPostItemAdded(item, local));
}, this::handleException);
}
@UiThread
private void onBlogPostItemAdded(BlogPostItem item, boolean local) {
List<BlogPostItem> items = addListItem(getBlogPostItems(), item);
if (items != null) {
Collections.sort(items);
blogPosts.setValue(new LiveResult<>(new ListUpdate(local, items)));
}
}
void repeatPost(BlogPostItem item, @Nullable String comment) {
runOnDbThread(() -> {
try {
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
BlogPostHeader h = item.getHeader();
blogManager.addLocalComment(a, b.getId(), comment, h);
} catch (DbException e) {
handleException(e);
}
});
}
LiveData<LiveResult<ListUpdate>> getBlogPosts() {
return blogPosts;
}
@UiThread
@Nullable
protected List<BlogPostItem> getBlogPostItems() {
LiveResult<ListUpdate> value = blogPosts.getValue();
if (value == null) return null;
ListUpdate result = value.getResultOrNull();
return result == null ? null : result.getItems();
}
/**
* Call this after {@link ListUpdate#getPostAddedWasLocal()} was processed.
* This prevents it from getting processed again.
*/
@UiThread
void resetLocalUpdate() {
LiveResult<ListUpdate> value = blogPosts.getValue();
if (value == null) return;
ListUpdate result = value.getResultOrNull();
result.postAddedWasLocal = null;
}
static class ListUpdate {
@Nullable
private Boolean postAddedWasLocal;
private final List<BlogPostItem> items;
ListUpdate(@Nullable Boolean postAddedWasLocal,
List<BlogPostItem> items) {
this.postAddedWasLocal = postAddedWasLocal;
this.items = items;
}
/**
* @return null when not a single post was added with this update.
* true when a single post was added locally and false if remotely.
*/
@Nullable
public Boolean getPostAddedWasLocal() {
return postAddedWasLocal;
}
public List<BlogPostItem> getItems() {
return items;
}
}
}

View File

@@ -6,9 +6,11 @@ import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
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;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
@@ -16,6 +18,10 @@ import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -23,7 +29,16 @@ public class BlogActivity extends BriarActivity
implements BaseFragmentListener {
@Inject
BlogController blogController;
ViewModelProvider.Factory viewModelFactory;
private BlogViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(BlogViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle state) {
@@ -31,32 +46,46 @@ public class BlogActivity extends BriarActivity
// GroupId from Intent
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in intent");
GroupId groupId = new GroupId(b);
blogController.setGroupId(groupId);
GroupId groupId =
new GroupId(requireNonNull(i.getByteArrayExtra(GROUP_ID)));
// Get post info from intent
@Nullable byte[] postId = i.getByteArrayExtra(POST_ID);
viewModel.setGroupId(groupId, postId == null);
setContentView(R.layout.activity_fragment_container_toolbar);
Toolbar toolbar = setUpCustomToolbar(false);
// Open Sharing Status on Toolbar click
if (toolbar != null) {
toolbar.setOnClickListener(v -> {
Intent i1 = new Intent(BlogActivity.this,
BlogSharingStatusActivity.class);
i1.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i1);
});
}
toolbar.setOnClickListener(v -> {
Intent i1 = new Intent(BlogActivity.this,
BlogSharingStatusActivity.class);
i1.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i1);
});
viewModel.getBlog().observe(this, blog ->
setTitle(blog.getBlog().getAuthor().getName())
);
viewModel.getSharingInfo().observe(this, info ->
setToolbarSubTitle(info.total, info.online)
);
if (state == null) {
showInitialFragment(BlogFragment.newInstance(groupId));
if (postId == null) {
showInitialFragment(BlogFragment.newInstance(groupId));
} else {
MessageId messageId = new MessageId(postId);
BaseFragment f =
BlogPostFragment.newInstance(groupId, messageId);
showInitialFragment(f);
}
}
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
private void setToolbarSubTitle(int total, int online) {
requireNonNull(getSupportActionBar())
.setSubtitle(getString(R.string.shared_with, total, online));
}
}

View File

@@ -8,7 +8,9 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
// This class is not thread-safe
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
class BlogCommentItem extends BlogPostItem {
private static final BlogCommentComparator COMPARATOR =

View File

@@ -1,46 +0,0 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import java.util.Collection;
import androidx.annotation.UiThread;
@NotNullByDefault
public interface BlogController extends BaseController {
void setGroupId(GroupId g);
@UiThread
void setBlogSharingListener(BlogSharingListener listener);
@UiThread
void unsetBlogSharingListener(BlogSharingListener listener);
void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
void loadBlogPost(MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void loadBlog(ResultExceptionHandler<BlogItem, DbException> handler);
void deleteBlog(ResultExceptionHandler<Void, DbException> handler);
void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
interface BlogSharingListener extends BlogListener {
@UiThread
void onBlogInvitationAccepted(ContactId c);
@UiThread
void onBlogLeft(ContactId c);
}
}

View File

@@ -1,9 +1,7 @@
package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -12,33 +10,25 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.BlogController.BlogSharingListener;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
import org.briarproject.briar.android.sharing.ShareBlogActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
@@ -48,31 +38,22 @@ import static android.widget.Toast.LENGTH_SHORT;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
import static org.briarproject.briar.android.controller.SharingController.SharingListener;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BlogFragment extends BaseFragment
implements BlogSharingListener, SharingListener,
OnBlogPostClickListener {
implements OnBlogPostClickListener {
private final static String TAG = BlogFragment.class.getName();
@Inject
BlogController blogController;
@Inject
SharingController sharingController;
@Nullable
private Parcelable layoutManagerState;
ViewModelProvider.Factory viewModelFactory;
private GroupId groupId;
private BlogPostAdapter adapter;
private LayoutManager layoutManager;
private BlogViewModel viewModel;
private final BlogPostAdapter adapter = new BlogPostAdapter(false, this);
private BriarRecyclerView list;
private MenuItem writeButton, deleteButton;
private boolean isMyBlog = false, canDeleteBlog = false;
static BlogFragment newInstance(GroupId groupId) {
BlogFragment f = new BlogFragment();
@@ -87,8 +68,8 @@ public class BlogFragment extends BaseFragment
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
blogController.setBlogSharingListener(this);
sharingController.setSharingListener(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
}
@Nullable
@@ -103,106 +84,82 @@ public class BlogFragment extends BaseFragment
View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter = new BlogPostAdapter(requireActivity(), this,
getFragmentManager());
list = v.findViewById(R.id.postList);
layoutManager = new LinearLayoutManager(getActivity());
LayoutManager layoutManager = new LinearLayoutManager(getActivity());
list.setLayoutManager(layoutManager);
list.setAdapter(adapter);
list.showProgressBar();
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
if (savedInstanceState != null) {
layoutManagerState =
savedInstanceState.getParcelable("layoutManager");
}
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
result.onError(this::handleException)
.onSuccess(this::onBlogPostsLoaded)
);
viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> {
if (removed) finish();
});
return v;
}
@Override
public void onStart() {
super.onStart();
sharingController.onStart();
loadBlog();
loadSharedContacts();
loadBlogPosts(false);
viewModel.blockAndClearNotifications();
list.startPeriodicUpdate();
}
@Override
public void onStop() {
super.onStop();
sharingController.onStop();
viewModel.unblockNotifications();
list.stopPeriodicUpdate();
}
@Override
public void onDestroy() {
super.onDestroy();
blogController.unsetBlogSharingListener(this);
sharingController.unsetSharingListener(this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (layoutManager != null) {
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.blogs_blog_actions, menu);
writeButton = menu.findItem(R.id.action_write_blog_post);
if (isMyBlog) writeButton.setVisible(true);
deleteButton = menu.findItem(R.id.action_blog_delete);
if (canDeleteBlog) deleteButton.setEnabled(true);
MenuItem writeButton = menu.findItem(R.id.action_write_blog_post);
MenuItem deleteButton = menu.findItem(R.id.action_blog_delete);
viewModel.getBlog().observe(getViewLifecycleOwner(), blog -> {
if (blog.isOurs()) writeButton.setVisible(true);
if (blog.canBeRemoved()) deleteButton.setEnabled(true);
});
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_write_blog_post:
Intent i = new Intent(getActivity(),
WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_WRITE_BLOG_POST);
return true;
case R.id.action_blog_share:
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_BLOG);
return true;
case R.id.action_blog_sharing_status:
Intent i3 = new Intent(getActivity(),
BlogSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
return true;
case R.id.action_blog_delete:
showDeleteDialog();
return true;
default:
return super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.action_write_blog_post) {
Intent i = new Intent(getActivity(), WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_blog_share) {
Intent i = new Intent(getActivity(), ShareBlogActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_SHARE_BLOG);
return true;
} else if (itemId == R.id.action_blog_sharing_status) {
Intent i =
new Intent(getActivity(), BlogSharingStatusActivity.class);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_blog_delete) {
showDeleteDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) {
displaySnackbar(R.string.blogs_blog_post_created, true);
loadBlogPosts(true);
} else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
displaySnackbar(R.string.blogs_sharing_snackbar, false);
}
}
@@ -212,35 +169,26 @@ public class BlogFragment extends BaseFragment
return TAG;
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
blogController.loadBlogPost(header,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
adapter.add(post);
if (local) {
list.scrollToPosition(0);
displaySnackbar(R.string.blogs_blog_post_created,
false);
} else {
displaySnackbar(R.string.blogs_blog_post_received,
true);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
}
);
private void onBlogPostsLoaded(ListUpdate update) {
adapter.submitList(update.getItems(), () -> {
Boolean wasLocal = update.getPostAddedWasLocal();
if (wasLocal != null && wasLocal) {
list.scrollToPosition(0);
displaySnackbar(R.string.blogs_blog_post_created,
false);
} else if (wasLocal != null) {
displaySnackbar(R.string.blogs_blog_post_received,
true);
}
viewModel.resetLocalUpdate();
list.showData();
});
}
@Override
public void onBlogPostClick(BlogPostItem post) {
BlogPostFragment f = BlogPostFragment.newInstance(post.getId());
BlogPostFragment f =
BlogPostFragment.newInstance(groupId, post.getId());
showNextFragment(f);
}
@@ -256,111 +204,10 @@ public class BlogFragment extends BaseFragment
getContext().startActivity(i);
}
private void loadBlogPosts(boolean reload) {
blogController.loadBlogPosts(
new UiResultExceptionHandler<Collection<BlogPostItem>,
DbException>(this) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
if (posts.isEmpty()) {
list.showData();
} else {
adapter.addAll(posts);
if (reload || layoutManagerState == null) {
list.scrollToPosition(0);
} else {
layoutManager.onRestoreInstanceState(
layoutManagerState);
}
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void loadBlog() {
blogController.loadBlog(
new UiResultExceptionHandler<BlogItem, DbException>(this) {
@Override
public void onResultUi(BlogItem blog) {
setToolbarTitle(blog.getBlog().getAuthor());
if (blog.isOurs())
showWriteButton();
if (blog.canBeRemoved())
enableDeleteButton();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void setToolbarTitle(Author a) {
getActivity().setTitle(a.getName());
}
private void loadSharedContacts() {
blogController.loadSharingContacts(
new UiResultExceptionHandler<Collection<ContactId>,
DbException>(this) {
@Override
public void onResultUi(Collection<ContactId> contacts) {
sharingController.addAll(contacts);
int online = sharingController.getOnlineCount();
setToolbarSubTitle(contacts.size(), online);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onBlogInvitationAccepted(ContactId c) {
sharingController.add(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
@Override
public void onBlogLeft(ContactId c) {
sharingController.remove(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
@Override
public void onSharingInfoUpdated(int total, int online) {
setToolbarSubTitle(total, online);
}
private void setToolbarSubTitle(int total, int online) {
ActionBar actionBar =
((BriarActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setSubtitle(
getString(R.string.shared_with, total, online));
}
}
private void showWriteButton() {
isMyBlog = true;
if (writeButton != null)
writeButton.setVisible(true);
}
private void enableDeleteButton() {
canDeleteBlog = true;
if (deleteButton != null)
deleteButton.setEnabled(true);
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getParentFragmentManager(), f.getUniqueTag());
}
private void displaySnackbar(int stringId, boolean scroll) {
@@ -373,38 +220,21 @@ public class BlogFragment extends BaseFragment
}
private void showDeleteDialog() {
DialogInterface.OnClickListener okListener =
(dialog, which) -> deleteBlog();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_remove_blog));
builder.setMessage(
getString(R.string.blogs_remove_blog_dialog_message));
builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_remove_blog_ok, okListener);
builder.setNegativeButton(R.string.blogs_remove_blog_ok,
(dialog, which) -> deleteBlog());
builder.show();
}
private void deleteBlog() {
blogController.deleteBlog(
new UiResultExceptionHandler<Void, DbException>(this) {
@Override
public void onResultUi(Void result) {
Toast.makeText(getActivity(),
R.string.blogs_blog_removed, LENGTH_SHORT)
.show();
finish();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onBlogRemoved() {
viewModel.deleteBlog();
Toast.makeText(getActivity(), R.string.blogs_blog_removed, LENGTH_SHORT)
.show();
finish();
}

View File

@@ -1,35 +1,23 @@
package org.briarproject.briar.android.blog;
import org.briarproject.briar.android.activity.ActivityScope;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.SharingControllerImpl;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
@Module
public class BlogModule {
public interface BlogModule {
@ActivityScope
@Provides
BlogController provideBlogController(BaseActivity activity,
BlogControllerImpl blogController) {
activity.addLifecycleController(blogController);
return blogController;
}
@Binds
@IntoMap
@ViewModelKey(FeedViewModel.class)
ViewModel bindFeedViewModel(FeedViewModel feedViewModel);
@ActivityScope
@Provides
FeedController provideFeedController(FeedControllerImpl feedController) {
return feedController;
}
@ActivityScope
@Provides
SharingController provideSharingController(
SharingControllerImpl sharingController) {
return sharingController;
}
@Binds
@IntoMap
@ViewModelKey(BlogViewModel.class)
ViewModel bindBlogViewModel(BlogViewModel blogViewModel);
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.blog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -8,52 +7,44 @@ import android.view.ViewGroup;
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.BriarAdapter;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class BlogPostAdapter extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
class BlogPostAdapter extends ListAdapter<BlogPostItem, BlogPostViewHolder> {
private final boolean authorClickable;
private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostAdapter(Context ctx, OnBlogPostClickListener listener,
@Nullable FragmentManager fragmentManager) {
super(ctx, BlogPostItem.class);
BlogPostAdapter(boolean authorClickable, OnBlogPostClickListener listener) {
super(new DiffUtil.ItemCallback<BlogPostItem>() {
@Override
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
return a.getId().equals(b.getId());
}
@Override
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
return a.isRead() == b.isRead();
}
});
this.authorClickable = authorClickable;
this.listener = listener;
this.fragmentManager = fragmentManager;
}
@Override
public BlogPostViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(ctx).inflate(
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_item_blog_post, parent, false);
return new BlogPostViewHolder(v, false, listener, fragmentManager);
return new BlogPostViewHolder(v, false, listener, authorClickable);
}
@Override
public void onBindViewHolder(BlogPostViewHolder ui, int position) {
ui.bindItem(getItemAt(position));
}
@Override
public int compare(BlogPostItem a, BlogPostItem b) {
return a.compareTo(b);
}
@Override
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
return a.isRead() == b.isRead();
}
@Override
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
return a.getId().equals(b.getId());
ui.bindItem(getItem(position));
}
}

View File

@@ -1,76 +1,159 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.BaseController.BlogListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BlogPostFragment extends BasePostFragment implements BlogListener {
public class BlogPostFragment extends BaseFragment
implements OnBlogPostClickListener {
private static final String TAG = BlogPostFragment.class.getName();
private static final Logger LOG = getLogger(TAG);
static final String POST_ID = "briar.POST_ID";
protected BlogViewModel viewModel;
private final Handler handler = new Handler(Looper.getMainLooper());
private ProgressBar progressBar;
private BlogPostViewHolder ui;
private BlogPostItem post;
private Runnable refresher;
@Inject
BlogController blogController;
ViewModelProvider.Factory viewModelFactory;
static BlogPostFragment newInstance(MessageId postId) {
static BlogPostFragment newInstance(GroupId blogId, MessageId postId) {
BlogPostFragment f = new BlogPostFragment();
Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, blogId.getBytes());
bundle.putByteArray(POST_ID, postId.getBytes());
f.setArguments(bundle);
return f;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireArguments();
GroupId groupId =
new GroupId(requireNonNull(args.getByteArray(GROUP_ID)));
MessageId postId =
new MessageId(requireNonNull(args.getByteArray(POST_ID)));
View view = inflater.inflate(R.layout.fragment_blog_post, container,
false);
progressBar = view.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
ui = new BlogPostViewHolder(view, true, this, false);
LifecycleOwner owner = getViewLifecycleOwner();
viewModel.loadBlogPost(groupId, postId).observe(owner, result ->
result.onError(this::handleException)
.onSuccess(this::onBlogPostLoaded)
);
return view;
}
@Override
public void onStart() {
super.onStart();
startPeriodicUpdate();
}
@Override
public void onStop() {
super.onStop();
stopPeriodicUpdate();
}
@UiThread
private void onBlogPostLoaded(BlogPostItem post) {
progressBar.setVisibility(INVISIBLE);
this.post = post;
ui.bindItem(post);
}
@Override
public void onBlogPostClick(BlogPostItem post) {
// We're already there
}
@Override
public void onAuthorClick(BlogPostItem post) {
Intent i = new Intent(requireContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
requireContext().startActivity(i);
}
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getParentFragmentManager(), f.getUniqueTag());
}
private void startPeriodicUpdate() {
refresher = () -> {
LOG.info("Updating Content...");
ui.updateDate(post.getTimestamp());
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
};
LOG.info("Adding Handler Callback");
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
}
private void stopPeriodicUpdate() {
if (refresher != null) {
LOG.info("Removing Handler Callback");
handler.removeCallbacks(refresher);
}
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Override
public void onStart() {
super.onStart();
blogController.loadBlogPost(postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
onBlogPostLoaded(post);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
// doesn't matter here
}
@Override
public void onBlogRemoved() {
finish();
}
}

View File

@@ -17,7 +17,7 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
private final BlogPostHeader header;
@Nullable
protected String text;
private boolean read;
private final boolean read;
BlogPostItem(BlogPostHeader header, @Nullable String text) {
this.header = header;
@@ -74,9 +74,6 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
protected static int compare(BlogPostHeader h1, BlogPostHeader h2) {
// The newest post comes first
long aTime = h1.getTimeReceived(), bTime = h2.getTimeReceived();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
return 0;
return Long.compare(h2.getTimeReceived(), h1.getTimeReceived());
}
}

View File

@@ -9,31 +9,31 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.view.AuthorView;
import org.briarproject.briar.api.blog.BlogCommentHeader;
import org.briarproject.briar.api.blog.BlogPostHeader;
import javax.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH;
import static org.briarproject.briar.android.util.UiUtils.getSpanned;
import static org.briarproject.briar.android.util.UiUtils.getTeaser;
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
import static org.briarproject.briar.api.blog.MessageType.POST;
import static org.briarproject.briar.android.view.AuthorView.COMMENTER;
import static org.briarproject.briar.android.view.AuthorView.REBLOGGER;
import static org.briarproject.briar.android.view.AuthorView.RSS_FEED_REBLOGGED;
@UiThread
@NotNullByDefault
class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final Context ctx;
@@ -43,20 +43,16 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final ImageButton reblogButton;
private final TextView text;
private final ViewGroup commentContainer;
private final boolean fullText;
private final boolean fullText, authorClickable;
@NonNull
private final OnBlogPostClickListener listener;
@Nullable
private final FragmentManager fragmentManager;
BlogPostViewHolder(View v, boolean fullText,
@NonNull OnBlogPostClickListener listener,
@Nullable FragmentManager fragmentManager) {
OnBlogPostClickListener listener, boolean authorClickable) {
super(v);
this.fullText = fullText;
this.listener = listener;
this.fragmentManager = fragmentManager;
this.authorClickable = authorClickable;
ctx = v.getContext();
layout = v.findViewById(R.id.postLayout);
@@ -67,10 +63,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
commentContainer = v.findViewById(R.id.commentContainer);
}
void setVisibility(int visibility) {
layout.setVisibility(visibility);
}
void hideReblogButton() {
reblogButton.setVisibility(GONE);
}
@@ -87,15 +79,15 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
return "blogPost" + id.hashCode();
}
void bindItem(@Nullable BlogPostItem item) {
if (item == null) return;
void bindItem(BlogPostItem item) {
setTransitionName(item.getId());
if (!fullText) {
layout.setClickable(true);
layout.setOnClickListener(v -> listener.onBlogPostClick(item));
}
boolean isReblog = item instanceof BlogCommentItem;
// author and date
BlogPostHeader post = item.getPostHeader();
author.setAuthor(post.getAuthor(), post.getAuthorInfo());
@@ -103,7 +95,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
author.setPersona(
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
// TODO make author clickable more often #624
if (!fullText && item.getHeader().getType() == POST) {
if (authorClickable && !isReblog) {
author.setAuthorClickable(v -> listener.onAuthorClick(item));
} else {
author.setAuthorNotClickable();
@@ -114,7 +106,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
if (fullText) {
text.setText(postText);
text.setTextIsSelectable(true);
makeLinksClickable(text, fragmentManager);
makeLinksClickable(text, listener::onLinkClick);
} else {
text.setTextIsSelectable(false);
if (postText.length() > TEASER_LENGTH)
@@ -132,32 +124,33 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
// comments
commentContainer.removeAllViews();
if (item instanceof BlogCommentItem) {
onBindComment((BlogCommentItem) item);
if (isReblog) {
onBindComment((BlogCommentItem) item, authorClickable);
} else {
reblogger.setVisibility(GONE);
}
}
private void onBindComment(BlogCommentItem item) {
private void onBindComment(BlogCommentItem item, boolean authorClickable) {
// reblogger
reblogger.setAuthor(item.getAuthor(), item.getAuthorInfo());
reblogger.setDate(item.getTimestamp());
if (!fullText) {
if (authorClickable) {
reblogger.setAuthorClickable(v -> listener.onAuthorClick(item));
} else {
reblogger.setAuthorNotClickable();
}
reblogger.setVisibility(VISIBLE);
reblogger.setPersona(AuthorView.REBLOGGER);
reblogger.setPersona(REBLOGGER);
author.setPersona(item.getHeader().getRootPost().isRssFeed() ?
AuthorView.RSS_FEED_REBLOGGED :
AuthorView.COMMENTER);
RSS_FEED_REBLOGGED : COMMENTER);
// comments
// TODO use nested RecyclerView instead like we do for Image Attachments
for (BlogCommentHeader c : item.getComments()) {
View v = LayoutInflater.from(ctx)
.inflate(R.layout.list_item_blog_comment,
commentContainer, false);
View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_blog_comment, commentContainer, false);
AuthorView author = v.findViewById(R.id.authorView);
TextView text = v.findViewById(R.id.textView);

View File

@@ -1,24 +1,24 @@
package org.briarproject.briar.android.blog;
import android.app.Activity;
import android.app.Application;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.sharing.SharingController;
import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogInvitationResponse;
@@ -35,85 +35,54 @@ 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 java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class BlogControllerImpl extends BaseControllerImpl
implements ActivityLifecycleController, BlogController, EventListener {
class BlogViewModel extends BaseViewModel {
private static final Logger LOG =
Logger.getLogger(BlogControllerImpl.class.getName());
private static final Logger LOG = getLogger(BlogViewModel.class.getName());
private final BlogSharingManager blogSharingManager;
private final SharingController sharingController;
// UI thread
@Nullable
private BlogSharingListener listener;
private volatile GroupId groupId;
private volatile GroupId groupId = null;
private final MutableLiveData<BlogItem> blog = new MutableLiveData<>();
private final MutableLiveData<Boolean> blogRemoved =
new MutableLiveData<>();
@Inject
BlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
BlogViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
IdentityManager identityManager, BlogManager blogManager,
BlogSharingManager blogSharingManager) {
super(dbExecutor, lifecycleManager, eventBus, notificationManager,
identityManager, blogManager);
BlogManager blogManager,
BlogSharingManager blogSharingManager,
SharingController sharingController) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
eventBus, identityManager, notificationManager, blogManager);
this.blogSharingManager = blogSharingManager;
}
@Override
public void onActivityCreate(Activity activity) {
}
@Override
public void onActivityStart() {
super.onStart();
notificationManager.blockNotification(groupId);
notificationManager.clearBlogPostNotification(groupId);
}
@Override
public void onActivityStop() {
super.onStop();
notificationManager.unblockNotification(groupId);
}
@Override
public void onActivityDestroy() {
}
@Override
public void setGroupId(GroupId g) {
groupId = g;
}
@Override
public void setBlogSharingListener(BlogSharingListener listener) {
this.listener = listener;
}
@Override
public void unsetBlogSharingListener(BlogSharingListener listener) {
if (this.listener == listener) this.listener = null;
this.sharingController = sharingController;
}
@Override
public void eventOccurred(Event e) {
if (groupId == null || listener == null)
throw new IllegalStateException();
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
if (b.getGroupId().equals(groupId)) {
LOG.info("Blog post added");
listener.onBlogPostAdded(b.getHeader(), b.isLocal());
onBlogPostAdded(b.getHeader(), b.isLocal());
}
} else if (e instanceof BlogInvitationResponseReceivedEvent) {
BlogInvitationResponseReceivedEvent b =
@@ -121,41 +90,36 @@ class BlogControllerImpl extends BaseControllerImpl
BlogInvitationResponse r = b.getMessageHeader();
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
LOG.info("Blog invitation accepted");
listener.onBlogInvitationAccepted(b.getContactId());
sharingController.add(b.getContactId());
}
} else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
if (s.getGroupId().equals(groupId)) {
LOG.info("Blog left by contact");
listener.onBlogLeft(s.getContactId());
sharingController.remove(s.getContactId());
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getId().equals(groupId)) {
LOG.info("Blog removed");
listener.onBlogRemoved();
blogRemoved.setValue(true);
}
}
}
@Override
public void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
loadBlogPosts(groupId, handler);
/**
* Set this before calling any other methods.
*/
@UiThread
public void setGroupId(GroupId groupId, boolean loadAllPosts) {
if (this.groupId == groupId) return; // configuration change
this.groupId = groupId;
loadBlog(groupId);
if (loadAllPosts) loadBlogPosts(groupId);
loadSharingContacts(groupId);
}
@Override
public void loadBlogPost(MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
loadBlogPost(groupId, m, handler);
}
@Override
public void loadBlog(
ResultExceptionHandler<BlogItem, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
private void loadBlog(GroupId groupId) {
runOnDbThread(() -> {
try {
long start = now();
@@ -163,50 +127,65 @@ class BlogControllerImpl extends BaseControllerImpl
Blog b = blogManager.getBlog(groupId);
boolean ours = a.getId().equals(b.getAuthor().getId());
boolean removable = blogManager.canBeRemoved(b);
BlogItem blog = new BlogItem(b, ours, removable);
blog.postValue(new BlogItem(b, ours, removable));
logDuration(LOG, "Loading blog", start);
handler.onResult(blog);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
handleException(e);
}
});
}
@Override
public void deleteBlog(ResultExceptionHandler<Void, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
void blockAndClearNotifications() {
notificationManager.blockNotification(groupId);
notificationManager.clearBlogPostNotification(groupId);
}
void unblockNotifications() {
notificationManager.unblockNotification(groupId);
}
private void loadBlogPosts(GroupId groupId) {
loadFromDb(txn -> new ListUpdate(null, loadBlogPosts(txn, groupId)),
blogPosts::setValue);
}
private void loadSharingContacts(GroupId groupId) {
runOnDbThread(true, txn -> {
Collection<Contact> contacts =
blogSharingManager.getSharedWith(txn, groupId);
txn.attach(() -> onSharingContactsLoaded(contacts));
}, this::handleException);
}
@UiThread
private void onSharingContactsLoaded(Collection<Contact> contacts) {
Collection<ContactId> contactIds = new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
sharingController.addAll(contactIds);
}
void deleteBlog() {
runOnDbThread(() -> {
try {
long start = now();
Blog b = blogManager.getBlog(groupId);
blogManager.removeBlog(b);
logDuration(LOG, "Removing blog", start);
handler.onResult(null);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
handleException(e);
}
});
}
@Override
public void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(() -> {
try {
Collection<Contact> contacts =
blogSharingManager.getSharedWith(groupId);
Collection<ContactId> contactIds =
new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
handler.onResult(contactIds);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
LiveData<BlogItem> getBlog() {
return blog;
}
LiveData<Boolean> getBlogRemoved() {
return blogRemoved;
}
LiveData<SharingInfo> getSharingInfo() {
return sharingController.getSharingInfo();
}
}

View File

@@ -1,32 +0,0 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.blog.Blog;
import java.util.Collection;
import androidx.annotation.UiThread;
@NotNullByDefault
public interface FeedController extends BaseController {
void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
void loadPersonalBlog(ResultExceptionHandler<Blog, DbException> handler);
@UiThread
void setFeedListener(FeedListener listener);
@UiThread
void unsetFeedListener(FeedListener listener);
@NotNullByDefault
interface FeedListener extends BlogListener {
@UiThread
void onBlogAdded();
}
}

View File

@@ -1,143 +0,0 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class FeedControllerImpl extends BaseControllerImpl implements FeedController {
private static final Logger LOG =
Logger.getLogger(FeedControllerImpl.class.getName());
// UI thread
@Nullable
private FeedListener listener;
@Inject
FeedControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, EventBus eventBus,
AndroidNotificationManager notificationManager,
IdentityManager identityManager, BlogManager blogManager) {
super(dbExecutor, lifecycleManager, eventBus, notificationManager,
identityManager, blogManager);
}
@Override
public void onStart() {
super.onStart();
if (listener == null) throw new IllegalStateException();
notificationManager.blockAllBlogPostNotifications();
notificationManager.clearAllBlogPostNotifications();
}
@Override
public void onStop() {
super.onStop();
notificationManager.unblockAllBlogPostNotifications();
}
@Override
public void setFeedListener(FeedListener listener) {
this.listener = listener;
}
@Override
public void unsetFeedListener(FeedListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void eventOccurred(Event e) {
if (listener == null) throw new IllegalStateException();
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
LOG.info("Blog post added");
listener.onBlogPostAdded(b.getHeader(), b.isLocal());
} else if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog added");
listener.onBlogAdded();
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog removed");
listener.onBlogRemoved();
}
}
}
@Override
public void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
Collection<BlogPostItem> posts = new ArrayList<>();
for (Blog b : blogManager.getBlogs()) {
try {
posts.addAll(loadItems(b.getId()));
} catch (NoSuchGroupException | NoSuchMessageException e) {
logException(LOG, WARNING, e);
}
}
logDuration(LOG, "Loading all posts", start);
handler.onResult(posts);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void loadPersonalBlog(
ResultExceptionHandler<Blog, DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
Author a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
logDuration(LOG, "Loading personal blog", start);
handler.onResult(b);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
}

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -10,53 +9,43 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.blog.FeedController.FeedListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogPostHeader;
import java.util.Collection;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class FeedFragment extends BaseFragment implements
OnBlogPostClickListener, FeedListener {
public class FeedFragment extends BaseFragment
implements OnBlogPostClickListener {
public final static String TAG = FeedFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
@Inject
FeedController feedController;
ViewModelProvider.Factory viewModelFactory;
private BlogPostAdapter adapter;
private FeedViewModel viewModel;
private final BlogPostAdapter adapter = new BlogPostAdapter(true, this);
private LinearLayoutManager layoutManager;
private BriarRecyclerView list;
@Nullable
private Blog personalBlog;
@Nullable
private Parcelable layoutManagerState;
public static FeedFragment newInstance() {
FeedFragment f = new FeedFragment();
@@ -70,7 +59,8 @@ public class FeedFragment extends BaseFragment implements
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
feedController.setFeedListener(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(FeedViewModel.class);
}
@Nullable
@@ -82,9 +72,6 @@ public class FeedFragment extends BaseFragment implements
View v = inflater.inflate(R.layout.fragment_blog, container, false);
adapter =
new BlogPostAdapter(getActivity(), this, getFragmentManager());
layoutManager = new LinearLayoutManager(getActivity());
list = v.findViewById(R.id.postList);
list.setLayoutManager(layoutManager);
@@ -93,103 +80,39 @@ public class FeedFragment extends BaseFragment implements
list.setEmptyText(R.string.blogs_feed_empty_state);
list.setEmptyAction(R.string.blogs_feed_empty_state_action);
if (savedInstanceState != null) {
layoutManagerState =
savedInstanceState.getParcelable("layoutManager");
}
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
result.onError(this::handleException)
.onSuccess(this::onBlogPostsLoaded)
);
return v;
}
@Override
public void onActivityResult(int requestCode, int resultCode,
@Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// The BlogPostAddedEvent arrives when the controller is not listening
if (requestCode == REQUEST_WRITE_BLOG_POST && resultCode == RESULT_OK) {
showSnackBar(R.string.blogs_blog_post_created);
}
}
@Override
public void onStart() {
super.onStart();
feedController.onStart();
viewModel.blockAndClearAllBlogPostNotifications();
list.startPeriodicUpdate();
loadPersonalBlog();
loadBlogPosts(false);
}
@Override
public void onStop() {
super.onStop();
feedController.onStop();
adapter.clear();
list.showProgressBar();
viewModel.unblockAllBlogPostNotifications();
list.stopPeriodicUpdate();
// TODO save list position in database/preferences?
}
@Override
public void onDestroy() {
super.onDestroy();
feedController.unsetFeedListener(this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (layoutManager != null) {
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
}
private void loadPersonalBlog() {
feedController.loadPersonalBlog(
new UiResultExceptionHandler<Blog, DbException>(this) {
@Override
public void onResultUi(Blog b) {
personalBlog = b;
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void loadBlogPosts(boolean clear) {
int revision = adapter.getRevision();
feedController.loadBlogPosts(
new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
this) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (clear) adapter.setItems(posts);
else adapter.addAll(posts);
if (posts.isEmpty()) list.showData();
if (layoutManagerState == null) {
list.scrollToPosition(0); // Scroll to the top
} else {
layoutManager.onRestoreInstanceState(
layoutManagerState);
}
} else {
LOG.info("Concurrent update, reloading");
loadBlogPosts(clear);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
private void onBlogPostsLoaded(ListUpdate update) {
adapter.submitList(update.getItems(), () -> {
Boolean wasLocal = update.getPostAddedWasLocal();
if (wasLocal != null && wasLocal) {
showSnackBar(R.string.blogs_blog_post_created);
} else if (wasLocal != null) {
showSnackBar(R.string.blogs_blog_post_received);
}
viewModel.resetLocalUpdate();
list.showData();
});
}
@Override
@@ -200,67 +123,46 @@ public class FeedFragment extends BaseFragment implements
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (personalBlog == null) return false;
switch (item.getItemId()) {
case R.id.action_write_blog_post:
Intent i1 =
new Intent(getActivity(), WriteBlogPostActivity.class);
i1.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivityForResult(i1, REQUEST_WRITE_BLOG_POST);
return true;
case R.id.action_rss_feeds_import:
Intent i2 =
new Intent(getActivity(), RssFeedImportActivity.class);
startActivity(i2);
return true;
case R.id.action_rss_feeds_manage:
Intent i3 =
new Intent(getActivity(), RssFeedManageActivity.class);
i3.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i3);
return true;
default:
return super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.action_write_blog_post) {
Blog personalBlog = viewModel.getPersonalBlog().getValue();
if (personalBlog == null) return false;
Intent i = new Intent(getActivity(), WriteBlogPostActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_import) {
Intent i = new Intent(getActivity(), RssFeedImportActivity.class);
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_manage) {
Blog personalBlog = viewModel.getPersonalBlog().getValue();
if (personalBlog == null) return false;
Intent i = new Intent(getActivity(), RssFeedManageActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i);
return true;
}
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
feedController.loadBlogPost(header,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
adapter.incrementRevision();
adapter.add(post);
if (local) {
showSnackBar(R.string.blogs_blog_post_created);
} else {
showSnackBar(R.string.blogs_blog_post_received);
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
}
);
return super.onOptionsItemSelected(item);
}
@Override
public void onBlogPostClick(BlogPostItem post) {
FeedPostFragment f =
FeedPostFragment.newInstance(post.getGroupId(), post.getId());
showNextFragment(f);
Intent i = getBlogActivityIntent(post.getGroupId());
i.putExtra(POST_ID, post.getId().getBytes());
requireContext().startActivity(i);
}
@Override
public void onAuthorClick(BlogPostItem post) {
Intent i = new Intent(getContext(), BlogActivity.class);
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
getContext().startActivity(i);
Intent i = getBlogActivityIntent(post.getGroupId());
requireContext().startActivity(i);
}
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getParentFragmentManager(), f.getUniqueTag());
}
@Override
@@ -268,6 +170,13 @@ public class FeedFragment extends BaseFragment implements
return TAG;
}
private Intent getBlogActivityIntent(GroupId groupId) {
Intent i = new Intent(requireContext(), BlogActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
return i;
}
private void showSnackBar(int stringRes) {
int firstVisible =
layoutManager.findFirstCompletelyVisibleItemPosition();
@@ -283,14 +192,4 @@ public class FeedFragment extends BaseFragment implements
sb.make(list, stringRes, LENGTH_LONG).show();
}
@Override
public void onBlogAdded() {
loadBlogPosts(false);
}
@Override
public void onBlogRemoved() {
loadBlogPosts(true);
}
}

View File

@@ -1,87 +0,0 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class FeedPostFragment extends BasePostFragment {
private static final String TAG = FeedPostFragment.class.getName();
private GroupId blogId;
@Inject
FeedController feedController;
static FeedPostFragment newInstance(GroupId blogId, MessageId postId) {
FeedPostFragment f = new FeedPostFragment();
Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, blogId.getBytes());
bundle.putByteArray(POST_ID, postId.getBytes());
f.setArguments(bundle);
return f;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Bundle args = requireArguments();
byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in args");
blogId = new GroupId(b);
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onStart() {
super.onStart();
feedController.loadBlogPost(blogId, postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem post) {
onBlogPostLoaded(post);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
}

View File

@@ -0,0 +1,133 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.Blog;
import org.briarproject.briar.api.blog.BlogManager;
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
@NotNullByDefault
class FeedViewModel extends BaseViewModel {
private static final Logger LOG = getLogger(FeedViewModel.class.getName());
private final MutableLiveData<Blog> personalBlog = new MutableLiveData<>();
@Inject
FeedViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
BlogManager blogManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
eventBus, identityManager, notificationManager, blogManager);
loadPersonalBlog();
loadAllBlogPosts();
}
@Override
public void eventOccurred(Event e) {
if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
LOG.info("Blog post added");
onBlogPostAdded(b.getHeader(), b.isLocal());
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
LOG.info("Blog removed");
onBlogRemoved(g.getGroup().getId());
}
}
}
void blockAndClearAllBlogPostNotifications() {
notificationManager.blockAllBlogPostNotifications();
notificationManager.clearAllBlogPostNotifications();
}
void unblockAllBlogPostNotifications() {
notificationManager.unblockAllBlogPostNotifications();
}
private void loadPersonalBlog() {
runOnDbThread(() -> {
try {
long start = now();
Author a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
logDuration(LOG, "Loading personal blog", start);
personalBlog.postValue(b);
} catch (DbException e) {
handleException(e);
}
});
}
LiveData<Blog> getPersonalBlog() {
return personalBlog;
}
private void loadAllBlogPosts() {
loadFromDb(this::loadAllBlogPosts, blogPosts::setValue);
}
@DatabaseExecutor
private ListUpdate loadAllBlogPosts(Transaction txn)
throws DbException {
long start = now();
List<BlogPostItem> posts = new ArrayList<>();
for (GroupId g : blogManager.getBlogIds(txn)) {
posts.addAll(loadBlogPosts(txn, g));
}
Collections.sort(posts);
logDuration(LOG, "Loading all posts", start);
return new ListUpdate(null, posts);
}
@UiThread
private void onBlogRemoved(GroupId g) {
List<BlogPostItem> items = removeListItems(getBlogPostItems(), item ->
item.getGroupId().equals(g)
);
if (items != null) {
blogPosts.setValue(new LiveResult<>(new ListUpdate(null, items)));
}
}
}

View File

@@ -5,4 +5,6 @@ interface OnBlogPostClickListener {
void onBlogPostClick(BlogPostItem post);
void onAuthorClick(BlogPostItem post);
void onLinkClick(String url);
}

View File

@@ -11,7 +11,7 @@ import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
public class ReblogActivity extends BriarActivity implements
BaseFragmentListener {
@@ -39,13 +39,11 @@ public class ReblogActivity extends BriarActivity implements
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override

View File

@@ -7,19 +7,17 @@ import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import java.util.List;
@@ -27,13 +25,15 @@ import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.FOCUS_DOWN;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -42,12 +42,13 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public static final String TAG = ReblogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private BlogViewModel viewModel;
private ViewHolder ui;
private BlogPostItem item;
@Inject
FeedController feedController;
static ReblogFragment newInstance(GroupId groupId, MessageId messageId) {
ReblogFragment f = new ReblogFragment();
@@ -67,6 +68,8 @@ public class ReblogFragment extends BaseFragment implements SendListener {
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(BlogViewModel.class);
}
@Override
@@ -90,30 +93,20 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
showProgressBar();
feedController.loadBlogPost(blogId, postId,
new UiResultExceptionHandler<BlogPostItem, DbException>(
this) {
@Override
public void onResultUi(BlogPostItem result) {
item = result;
bindViewHolder();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
viewModel.loadBlogPost(blogId, postId).observe(getViewLifecycleOwner(),
result -> result.onError(this::handleException)
.onSuccess(this::bindViewHolder)
);
return v;
}
private void bindViewHolder() {
if (item == null) return;
private void bindViewHolder(BlogPostItem item) {
this.item = item;
hideProgressBar();
ui.post.bindItem(item);
ui.post.bindItem(this.item);
ui.post.hideReblogButton();
ui.input.setReady(true);
@@ -124,13 +117,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
viewModel.repeatPost(item, text);
finish();
}
@@ -144,7 +131,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
ui.input.setVisibility(VISIBLE);
}
private class ViewHolder {
private class ViewHolder implements OnBlogPostClickListener {
private final ScrollView scrollView;
private final ProgressBar progressBar;
@@ -155,18 +142,25 @@ public class ReblogFragment extends BaseFragment implements SendListener {
scrollView = v.findViewById(R.id.scrollView);
progressBar = v.findViewById(R.id.progressBar);
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout),
true, new OnBlogPostClickListener() {
@Override
public void onBlogPostClick(BlogPostItem post) {
// do nothing
}
@Override
public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here
}
}, getFragmentManager());
true, this, false);
input = v.findViewById(R.id.inputText);
}
@Override
public void onBlogPostClick(BlogPostItem post) {
// do nothing
}
@Override
public void onAuthorClick(BlogPostItem post) {
// probably don't want to allow author clicks here
}
@Override
public void onLinkClick(String url) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(getParentFragmentManager(), f.getUniqueTag());
}
}
}

View File

@@ -86,7 +86,7 @@ public class ContactsViewModel extends DbViewModel implements EventListener {
}
protected void loadContacts() {
loadList(this::loadContacts, contactListItems::setValue);
loadFromDb(this::loadContacts, contactListItems::setValue);
}
private List<ContactListItem> loadContacts(Transaction txn)
@@ -151,7 +151,7 @@ public class ContactsViewModel extends DbViewModel implements EventListener {
@UiThread
private void updateItem(ContactId c,
Function<ContactListItem, ContactListItem> replacer, boolean sort) {
List<ContactListItem> list = updateListItems(contactListItems,
List<ContactListItem> list = updateListItems(getList(contactListItems),
itemToTest -> itemToTest.getContact().getId().equals(c),
replacer);
if (list == null) return;
@@ -161,10 +161,8 @@ public class ContactsViewModel extends DbViewModel implements EventListener {
@UiThread
private void removeItem(ContactId c) {
List<ContactListItem> list = removeListItems(contactListItems,
removeAndUpdateListItems(contactListItems,
itemToTest -> itemToTest.getContact().getId().equals(c));
if (list == null) return;
contactListItems.setValue(new LiveResult<>(list));
}
}

View File

@@ -1,77 +0,0 @@
package org.briarproject.briar.android.controller;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault
public interface SharingController {
/**
* Sets the listener that is called when contacts go on or offline.
*/
@UiThread
void setSharingListener(SharingListener listener);
/**
* Unsets the listener.
*/
@UiThread
void unsetSharingListener(SharingListener listener);
/**
* Call this when your lifecycle starts,
* so the listener will be called when information changes.
*/
@UiThread
void onStart();
/**
* Call this when your lifecycle stops,
* so that the controller knows it can stops listening to events.
*/
@UiThread
void onStop();
/**
* Adds one contact to be tracked.
*/
@UiThread
void add(ContactId c);
/**
* Adds a collection of contacts to be tracked.
*/
@UiThread
void addAll(Collection<ContactId> contacts);
/**
* Call this when the contact identified by c is no longer sharing
* the given group identified by GroupId g.
*/
@UiThread
void remove(ContactId c);
/**
* Returns the number of online contacts.
*/
@UiThread
int getOnlineCount();
/**
* Returns the total number of contacts that have been added.
*/
@UiThread
int getTotalCount();
interface SharingListener {
@UiThread
void onSharingInfoUpdated(int total, int online);
}
}

View File

@@ -1,109 +0,0 @@
package org.briarproject.briar.android.controller;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault
public class SharingControllerImpl implements SharingController, EventListener {
private final EventBus eventBus;
private final ConnectionRegistry connectionRegistry;
// UI thread
private final Set<ContactId> contacts = new HashSet<>();
// UI thread
@Nullable
private SharingListener listener;
@Inject
SharingControllerImpl(EventBus eventBus,
ConnectionRegistry connectionRegistry) {
this.eventBus = eventBus;
this.connectionRegistry = connectionRegistry;
}
@Override
public void setSharingListener(SharingListener listener) {
this.listener = listener;
}
@Override
public void unsetSharingListener(SharingListener listener) {
if (this.listener == listener) this.listener = null;
}
@Override
public void onStart() {
eventBus.addListener(this);
}
@Override
public void onStop() {
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactConnectedEvent) {
setConnected(((ContactConnectedEvent) e).getContactId());
} else if (e instanceof ContactDisconnectedEvent) {
setConnected(((ContactDisconnectedEvent) e).getContactId());
}
}
@UiThread
private void setConnected(ContactId c) {
if (listener == null) throw new IllegalStateException();
if (contacts.contains(c)) {
int online = getOnlineCount();
listener.onSharingInfoUpdated(contacts.size(), online);
}
}
@Override
public void addAll(Collection<ContactId> c) {
contacts.addAll(c);
}
@Override
public void add(ContactId c) {
contacts.add(c);
}
@Override
public void remove(ContactId c) {
contacts.remove(c);
}
@Override
public int getOnlineCount() {
int online = 0;
for (ContactId c : contacts) {
if (connectionRegistry.isConnected(c)) online++;
}
return online;
}
@Override
public int getTotalCount() {
return contacts.size();
}
}

View File

@@ -127,7 +127,7 @@ class ForumListViewModel extends DbViewModel implements EventListener {
}
public void loadForums() {
loadList(this::loadForums, forumItems::setValue);
loadFromDb(this::loadForums, forumItems::setValue);
}
@DatabaseExecutor
@@ -145,7 +145,7 @@ class ForumListViewModel extends DbViewModel implements EventListener {
@UiThread
private void onForumPostReceived(GroupId g, ForumPostHeader header) {
List<ForumListItem> list = updateListItems(forumItems,
List<ForumListItem> list = updateListItems(getList(forumItems),
itemToTest -> itemToTest.getForum().getId().equals(g),
itemToUpdate -> new ForumListItem(itemToUpdate, header));
if (list == null) return;
@@ -156,11 +156,9 @@ class ForumListViewModel extends DbViewModel implements EventListener {
@UiThread
private void onGroupRemoved(GroupId groupId) {
List<ForumListItem> list = removeListItems(forumItems, i ->
removeAndUpdateListItems(forumItems, i ->
i.getForum().getId().equals(groupId)
);
if (list == null) return;
forumItems.setValue(new LiveResult<>(list));
}
void loadForumInvitations() {

View File

@@ -135,7 +135,7 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
@Override
public void loadItems() {
loadList(txn -> {
loadFromDb(txn -> {
long start = now();
List<ForumPostHeader> headers =
forumManager.getPostHeaders(txn, groupId);

View File

@@ -4,6 +4,7 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -24,7 +25,6 @@ import javax.inject.Inject;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@@ -56,14 +56,18 @@ public class ChangePasswordActivity extends BriarActivity
@VisibleForTesting
ChangePasswordViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(ChangePasswordViewModel.class);
}
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_change_password);
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ChangePasswordViewModel.class);
currentPasswordEntryWrapper =
findViewById(R.id.current_password_entry_wrapper);
newPasswordEntryWrapper = findViewById(R.id.new_password_entry_wrapper);
@@ -77,7 +81,6 @@ public class ChangePasswordActivity extends BriarActivity
progress = findViewById(R.id.progress_wheel);
TextWatcher tw = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
@@ -102,8 +105,12 @@ public class ChangePasswordActivity extends BriarActivity
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private void enableOrDisableContinueButton() {

View File

@@ -1,14 +0,0 @@
package org.briarproject.briar.android.login;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.controller.handler.ResultHandler;
@NotNullByDefault
public interface ChangePasswordController {
float estimatePasswordStrength(String password);
void changePassword(String oldPassword, String newPassword,
ResultHandler<Boolean> resultHandler);
}

View File

@@ -14,7 +14,7 @@ import javax.inject.Inject;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static android.content.Intent.ACTION_MY_PACKAGE_REPLACED;
import static org.briarproject.briar.android.settings.SettingsFragment.NOTIFY_SIGN_IN;
import static org.briarproject.briar.android.settings.NotificationsFragment.PREF_NOTIFY_SIGN_IN;
import static org.briarproject.briar.api.android.AndroidNotificationManager.ACTION_DISMISS_REMINDER;
public class SignInReminderReceiver extends BroadcastReceiver {
@@ -37,7 +37,7 @@ public class SignInReminderReceiver extends BroadcastReceiver {
if (accountManager.accountExists() &&
!accountManager.hasDatabaseKey()) {
SharedPreferences prefs = app.getDefaultSharedPreferences();
if (prefs.getBoolean(NOTIFY_SIGN_IN, true)) {
if (prefs.getBoolean(PREF_NOTIFY_SIGN_IN, true)) {
notificationManager.showSignInNotification();
}
}

View File

@@ -97,8 +97,6 @@ public class NavDrawerActivity extends BriarActivity implements
Uri.parse("briar-content://org.briarproject.briar/blog");
public static Uri CONTACT_ADDED_URI =
Uri.parse("briar-content://org.briarproject.briar/contact/added");
public static Uri SIGN_OUT_URI =
Uri.parse("briar-content://org.briarproject.briar/sign-out");
private final List<Transport> transports = new ArrayList<>(3);
private final MutableLiveData<ImageView> torIcon = new MutableLiveData<>();
@@ -224,8 +222,6 @@ public class NavDrawerActivity extends BriarActivity implements
startFragment(ForumListFragment.newInstance(), R.id.nav_btn_forums);
} else if (BLOG_URI.equals(uri)) {
startFragment(FeedFragment.newInstance(), R.id.nav_btn_blogs);
} else if (SIGN_OUT_URI.equals(uri)) {
signOut(false, false);
}
}

View File

@@ -20,7 +20,7 @@ import javax.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreference;
import androidx.preference.SwitchPreferenceCompat;
import info.guardianproject.panic.PanicResponder;
import static android.app.Activity.RESULT_CANCELED;
@@ -40,7 +40,7 @@ public class PanicPreferencesFragment extends PreferenceFragmentCompat
Logger.getLogger(PanicPreferencesFragment.class.getName());
private PackageManager pm;
private SwitchPreference lockPref, purgePref;
private SwitchPreferenceCompat lockPref, purgePref;
private ListPreference panicAppPref;
@Override
@@ -51,9 +51,9 @@ public class PanicPreferencesFragment extends PreferenceFragmentCompat
private void updatePreferences() {
pm = getActivity().getPackageManager();
lockPref = (SwitchPreference) findPreference(KEY_LOCK);
lockPref = (SwitchPreferenceCompat) findPreference(KEY_LOCK);
panicAppPref = (ListPreference) findPreference(KEY_PANIC_APP);
purgePref = (SwitchPreference) findPreference(KEY_PURGE);
purgePref = (SwitchPreferenceCompat) findPreference(KEY_PURGE);
// check for connect/disconnect intents from panic trigger apps
if (PanicResponder.checkForDisconnectIntent(getActivity())) {

View File

@@ -159,7 +159,7 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
@Override
public void loadItems() {
loadList(txn -> {
loadFromDb(txn -> {
// check first if group is dissolved
isDissolved
.postValue(privateGroupManager.isDissolved(txn, groupId));

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.privategroup.list;
import android.app.Application;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
@@ -142,7 +141,7 @@ class GroupListViewModel extends DbViewModel implements EventListener {
}
void loadGroups() {
loadList(this::loadGroups, groupItems::setValue);
loadFromDb(this::loadGroups, groupItems::setValue);
}
@DatabaseExecutor
@@ -173,7 +172,7 @@ class GroupListViewModel extends DbViewModel implements EventListener {
@UiThread
private void onGroupMessageAdded(GroupMessageHeader header) {
GroupId g = header.getGroupId();
List<GroupItem> list = updateListItems(groupItems,
List<GroupItem> list = updateListItems(getList(groupItems),
itemToTest -> itemToTest.getId().equals(g),
itemToUpdate -> new GroupItem(itemToUpdate, header));
if (list == null) return;
@@ -184,7 +183,7 @@ class GroupListViewModel extends DbViewModel implements EventListener {
@UiThread
private void onGroupDissolved(GroupId groupId) {
List<GroupItem> list = updateListItems(groupItems,
List<GroupItem> list = updateListItems(getList(groupItems),
itemToTest -> itemToTest.getId().equals(groupId),
itemToUpdate -> new GroupItem(itemToUpdate, true));
if (list == null) return;
@@ -193,10 +192,7 @@ class GroupListViewModel extends DbViewModel implements EventListener {
@UiThread
private void onGroupRemoved(GroupId groupId) {
List<GroupItem> list =
removeListItems(groupItems, i -> i.getId().equals(groupId));
if (list == null) return;
groupItems.setValue(new LiveResult<>(list));
removeAndUpdateListItems(groupItems, i -> i.getId().equals(groupId));
}
void removeGroup(GroupId g) {

View File

@@ -51,15 +51,15 @@ class ReportData {
final boolean isOptional;
boolean isIncluded = true;
ReportItem(String name, int nameRes, ReportInfo info) {
ReportItem(String name, @StringRes int nameRes, ReportInfo info) {
this(name, nameRes, info, true);
}
ReportItem(String name, int nameRes, String info) {
ReportItem(String name, @StringRes int nameRes, String info) {
this(name, nameRes, new SingleReportInfo(info), true);
}
ReportItem(String name, int nameRes, ReportInfo info,
ReportItem(String name, @StringRes int nameRes, ReportInfo info,
boolean isOptional) {
this.name = name;
this.nameRes = nameRes;

View File

@@ -15,6 +15,7 @@ import org.briarproject.briar.android.logging.BriefLogFormatter;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.logging.LogDecrypter;
import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo;
import org.briarproject.briar.android.reporting.ReportData.ReportItem;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.json.JSONException;
@@ -156,7 +157,8 @@ class ReportViewModel extends AndroidViewModel {
MultiReportInfo userInfo = new MultiReportInfo();
if (!isNullOrEmpty(comment)) userInfo.add("Comment", comment);
if (!isNullOrEmpty(email)) userInfo.add("Email", email);
data.add(new ReportData.ReportItem("UserInfo", 0, userInfo, false));
data.add(new ReportItem("UserInfo", R.string.dev_report_user_info,
userInfo, false));
}
// check the state of the TorPlugin, if this is feedback

View File

@@ -0,0 +1,47 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import de.hdodenhof.circleimageview.CircleImageView;
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
@NotNullByDefault
public class AvatarPreference extends Preference {
@Nullable
private OwnIdentityInfo info;
public AvatarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_avatar);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
View v = holder.itemView;
if (info != null) {
TextView textViewUserName = v.findViewById(R.id.username);
CircleImageView imageViewAvatar = v.findViewById(R.id.avatarImage);
textViewUserName.setText(info.getLocalAuthor().getName());
setAvatar(imageViewAvatar, info.getLocalAuthor().getId(),
info.getAuthorInfo());
}
}
void setOwnIdentityInfo(OwnIdentityInfo info) {
this.info = info;
notifyChanged();
}
}

View File

@@ -0,0 +1,118 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
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.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ConnectionsFragment extends PreferenceFragmentCompat {
static final String PREF_KEY_BLUETOOTH = "pref_key_bluetooth";
static final String PREF_KEY_WIFI = "pref_key_wifi";
static final String PREF_KEY_TOR_ENABLE = "pref_key_tor_enable";
static final String PREF_KEY_TOR_NETWORK = "pref_key_tor_network";
static final String PREF_KEY_TOR_MOBILE_DATA =
"pref_key_tor_mobile_data";
static final String PREF_KEY_TOR_ONLY_WHEN_CHARGING =
"pref_key_tor_only_when_charging";
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private ConnectionsManager connectionsManager;
private SwitchPreferenceCompat enableBluetooth;
private SwitchPreferenceCompat enableWifi;
private SwitchPreferenceCompat enableTor;
private ListPreference torNetwork;
private SwitchPreferenceCompat torMobile;
private SwitchPreferenceCompat torOnlyWhenCharging;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
connectionsManager = viewModel.connectionsManager;
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_connections);
enableBluetooth = findPreference(PREF_KEY_BLUETOOTH);
enableWifi = findPreference(PREF_KEY_WIFI);
enableTor = findPreference(PREF_KEY_TOR_ENABLE);
torNetwork = findPreference(PREF_KEY_TOR_NETWORK);
torMobile = findPreference(PREF_KEY_TOR_MOBILE_DATA);
torOnlyWhenCharging = findPreference(PREF_KEY_TOR_ONLY_WHEN_CHARGING);
torNetwork.setSummaryProvider(viewModel.torSummaryProvider);
enableBluetooth.setPreferenceDataStore(connectionsManager.btStore);
enableWifi.setPreferenceDataStore(connectionsManager.wifiStore);
enableTor.setPreferenceDataStore(connectionsManager.torStore);
torNetwork.setPreferenceDataStore(connectionsManager.torStore);
torMobile.setPreferenceDataStore(connectionsManager.torStore);
torOnlyWhenCharging.setPreferenceDataStore(connectionsManager.torStore);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// persist changes after setting initial value and enabling
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
connectionsManager.btEnabled().observe(lifecycleOwner, enabled -> {
enableBluetooth.setChecked(enabled);
enableAndPersist(enableBluetooth);
});
connectionsManager.wifiEnabled().observe(lifecycleOwner, enabled -> {
enableWifi.setChecked(enabled);
enableAndPersist(enableWifi);
});
connectionsManager.torEnabled().observe(lifecycleOwner, enabled -> {
enableTor.setChecked(enabled);
enableAndPersist(enableTor);
});
connectionsManager.torNetwork().observe(lifecycleOwner, value -> {
torNetwork.setValue(value);
enableAndPersist(torNetwork);
});
connectionsManager.torMobile().observe(lifecycleOwner, enabled -> {
torMobile.setChecked(enabled);
enableAndPersist(torMobile);
});
connectionsManager.torCharging().observe(lifecycleOwner, enabled -> {
torOnlyWhenCharging.setChecked(enabled);
enableAndPersist(torOnlyWhenCharging);
});
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.network_settings_title);
}
}

View File

@@ -0,0 +1,116 @@
package org.briarproject.briar.android.settings;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.concurrent.Executor;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.briar.android.settings.SettingsViewModel.BT_NAMESPACE;
import static org.briarproject.briar.android.settings.SettingsViewModel.TOR_NAMESPACE;
import static org.briarproject.briar.android.settings.SettingsViewModel.WIFI_NAMESPACE;
@NotNullByDefault
class ConnectionsManager {
final ConnectionsStore btStore;
final ConnectionsStore wifiStore;
final ConnectionsStore torStore;
private final MutableLiveData<Boolean> btEnabled = new MutableLiveData<>();
private final MutableLiveData<Boolean> wifiEnabled =
new MutableLiveData<>();
private final MutableLiveData<Boolean> torEnabled = new MutableLiveData<>();
private final MutableLiveData<String> torNetwork = new MutableLiveData<>();
private final MutableLiveData<Boolean> torMobile = new MutableLiveData<>();
private final MutableLiveData<Boolean> torCharging =
new MutableLiveData<>();
ConnectionsManager(SettingsManager settingsManager,
Executor dbExecutor) {
btStore =
new ConnectionsStore(settingsManager, dbExecutor, BT_NAMESPACE);
wifiStore = new ConnectionsStore(settingsManager, dbExecutor,
WIFI_NAMESPACE);
torStore = new ConnectionsStore(settingsManager, dbExecutor,
TOR_NAMESPACE);
}
void updateBtSetting(Settings btSettings) {
btEnabled.postValue(btSettings.getBoolean(PREF_PLUGIN_ENABLE,
BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE));
}
void updateWifiSettings(Settings wifiSettings) {
wifiEnabled.postValue(wifiSettings.getBoolean(PREF_PLUGIN_ENABLE,
LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE));
}
void updateTorSettings(Settings settings) {
Settings torSettings = migrateTorSettings(settings);
torEnabled.postValue(torSettings.getBoolean(PREF_PLUGIN_ENABLE,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE));
int torNetworkSetting = torSettings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK);
torNetwork.postValue(Integer.toString(torNetworkSetting));
torMobile.postValue(torSettings.getBoolean(PREF_TOR_MOBILE,
DEFAULT_PREF_TOR_MOBILE));
torCharging
.postValue(torSettings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING,
DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING));
}
// TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateTorSettings(Settings s) {
int network = s.getInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) {
s.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
s.putBoolean(PREF_PLUGIN_ENABLE, false);
// We don't need to save the migrated settings - the Tor plugin is
// responsible for that. This code just handles the case where the
// settings are loaded before the plugin migrates them.
}
return s;
}
LiveData<Boolean> btEnabled() {
return btEnabled;
}
LiveData<Boolean> wifiEnabled() {
return wifiEnabled;
}
LiveData<Boolean> torEnabled() {
return torEnabled;
}
LiveData<String> torNetwork() {
return torNetwork;
}
LiveData<Boolean> torMobile() {
return torMobile;
}
LiveData<Boolean> torCharging() {
return torCharging;
}
}

View File

@@ -0,0 +1,63 @@
package org.briarproject.briar.android.settings;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.concurrent.Executor;
import androidx.annotation.Nullable;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_BLUETOOTH;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_ENABLE;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_MOBILE_DATA;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_NETWORK;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.briar.android.settings.ConnectionsFragment.PREF_KEY_WIFI;
@NotNullByDefault
class ConnectionsStore extends SettingsStore {
ConnectionsStore(
SettingsManager settingsManager,
Executor dbExecutor,
String namespace) {
super(settingsManager, dbExecutor, namespace);
}
@Override
public void putBoolean(String key, boolean value) {
String newKey;
// translate between Android UI pref keys and bramble keys
switch (key) {
case PREF_KEY_BLUETOOTH:
case PREF_KEY_WIFI:
case PREF_KEY_TOR_ENABLE:
newKey = PREF_PLUGIN_ENABLE;
break;
case PREF_KEY_TOR_MOBILE_DATA:
newKey = PREF_TOR_MOBILE;
break;
case PREF_KEY_TOR_ONLY_WHEN_CHARGING:
newKey = PREF_TOR_ONLY_WHEN_CHARGING;
break;
default:
throw new AssertionError();
}
super.putBoolean(newKey, value);
}
@Override
public void putString(String key, @Nullable String value) {
// translate between Android UI pref keys and bramble keys
if (key.equals(PREF_KEY_TOR_NETWORK)) {
super.putString(PREF_TOR_NETWORK, value);
} else {
throw new AssertionError(key);
}
}
}

View File

@@ -0,0 +1,177 @@
package org.briarproject.briar.android.settings;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.util.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.core.text.TextUtilsCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
import static org.briarproject.briar.android.settings.SettingsActivity.EXTRA_THEME_CHANGE;
@NotNullByDefault
public class DisplayFragment extends PreferenceFragmentCompat {
public static final String PREF_LANGUAGE = "pref_key_language";
private static final String PREF_THEME = "pref_key_theme";
private static final Logger LOG =
getLogger(DisplayFragment.class.getName());
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_display);
ListPreference language = requireNonNull(findPreference(PREF_LANGUAGE));
setLanguageEntries(language);
language.setOnPreferenceChangeListener(this::onLanguageChanged);
ListPreference theme = requireNonNull(findPreference(PREF_THEME));
setThemeEntries(theme);
theme.setOnPreferenceChangeListener(this::onThemeChanged);
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.display_settings_title);
}
private void setLanguageEntries(ListPreference language) {
CharSequence[] tags = language.getEntryValues();
List<CharSequence> entries = new ArrayList<>(tags.length);
List<CharSequence> entryValues = new ArrayList<>(tags.length);
for (CharSequence cs : tags) {
String tag = cs.toString();
if (tag.equals("default")) {
entries.add(getString(R.string.pref_language_default));
entryValues.add(tag);
continue;
}
Locale locale = Localizer.getLocaleFromTag(tag);
if (locale == null)
throw new IllegalStateException();
// Exclude RTL locales on API < 17, they won't be laid out correctly
if (SDK_INT < 17 && !isLeftToRight(locale)) {
if (LOG.isLoggable(INFO))
LOG.info("Skipping RTL locale " + tag);
continue;
}
String nativeName = locale.getDisplayName(locale);
// Fallback to English if the name is unknown in both native and
// current locale.
if (nativeName.equals(tag)) {
String tmp = locale.getDisplayLanguage(Locale.ENGLISH);
if (!tmp.isEmpty() && !tmp.equals(nativeName))
nativeName = tmp;
}
// Prefix with LRM marker to prevent any RTL direction
entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase()
+ nativeName.substring(1));
entryValues.add(tag);
}
language.setEntries(entries.toArray(new CharSequence[0]));
language.setEntryValues(entryValues.toArray(new CharSequence[0]));
}
private boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
private void setThemeEntries(ListPreference theme) {
if (SDK_INT < 27) {
// remove System Default Theme option from preference entries
// as it is not functional on this API anyway
List<CharSequence> entries =
new ArrayList<>(Arrays.asList(theme.getEntries()));
entries.remove(getString(R.string.pref_theme_system));
theme.setEntries(entries.toArray(new CharSequence[0]));
// also remove corresponding value
List<CharSequence> values =
new ArrayList<>(Arrays.asList(theme.getEntryValues()));
values.remove(getString(R.string.pref_theme_system_value));
theme.setEntryValues(values.toArray(new CharSequence[0]));
}
}
private boolean onThemeChanged(Preference preference, Object newValue) {
// activate new theme
FragmentActivity activity = requireActivity();
UiUtils.setTheme(activity, (String) newValue);
// bring up parent activity, so it can change its theme as well
// upstream bug: https://issuetracker.google.com/issues/38352704
Intent intent = new Intent(getActivity(), ENTRY_ACTIVITY);
intent.setFlags(FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
// bring this activity back to the foreground
intent = new Intent(getActivity(), activity.getClass());
intent.putExtra(EXTRA_THEME_CHANGE, true);
startActivity(intent);
activity.finish();
return true;
}
private boolean onLanguageChanged(Preference preference, Object newValue) {
ListPreference language = (ListPreference) preference;
if (!language.getValue().equals(newValue)) {
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.pref_language_title);
builder.setMessage(R.string.pref_language_changed);
builder.setPositiveButton(R.string.sign_out_button, (d, i) -> {
language.setValue((String) newValue);
viewModel.languageChanged();
});
builder.setNegativeButton(R.string.cancel, null);
builder.setCancelable(false);
builder.show();
}
return false;
}
}

View File

@@ -0,0 +1,235 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
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.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import static android.app.Activity.RESULT_OK;
import static android.media.RingtoneManager.ACTION_RINGTONE_PICKER;
import static android.media.RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_EXISTING_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_PICKED_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_PRIVATE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_SOUND;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_VIBRATION;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class NotificationsFragment extends PreferenceFragmentCompat {
public static final String PREF_NOTIFY_SIGN_IN = "pref_key_notify_sign_in";
private static final int NOTIFICATION_CHANNEL_API = 26;
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private NotificationsManager nm;
private SwitchPreferenceCompat notifyPrivateMessages;
private SwitchPreferenceCompat notifyGroupMessages;
private SwitchPreferenceCompat notifyForumPosts;
private SwitchPreferenceCompat notifyBlogPosts;
private SwitchPreferenceCompat notifyVibration;
private Preference notifySound;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
nm = viewModel.notificationsManager;
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_notifications);
notifyPrivateMessages = findPreference(PREF_NOTIFY_PRIVATE);
notifyGroupMessages = findPreference(PREF_NOTIFY_GROUP);
notifyForumPosts = findPreference(PREF_NOTIFY_FORUM);
notifyBlogPosts = findPreference(PREF_NOTIFY_BLOG);
notifyVibration = findPreference(PREF_NOTIFY_VIBRATION);
notifySound = findPreference(PREF_NOTIFY_SOUND);
if (SDK_INT < NOTIFICATION_CHANNEL_API) {
// NOTIFY_SIGN_IN gets stored in Android's SharedPreferences
notifyPrivateMessages
.setPreferenceDataStore(viewModel.settingsStore);
notifyGroupMessages.setPreferenceDataStore(viewModel.settingsStore);
notifyForumPosts.setPreferenceDataStore(viewModel.settingsStore);
notifyBlogPosts.setPreferenceDataStore(viewModel.settingsStore);
notifyVibration.setPreferenceDataStore(viewModel.settingsStore);
notifySound.setOnPreferenceClickListener(pref ->
onNotificationSoundClicked()
);
} else {
setupNotificationPreference(notifyPrivateMessages,
CONTACT_CHANNEL_ID,
R.string.notify_private_messages_setting_summary_26);
setupNotificationPreference(notifyGroupMessages,
GROUP_CHANNEL_ID,
R.string.notify_group_messages_setting_summary_26);
setupNotificationPreference(notifyForumPosts, FORUM_CHANNEL_ID,
R.string.notify_forum_posts_setting_summary_26);
setupNotificationPreference(notifyBlogPosts, BLOG_CHANNEL_ID,
R.string.notify_blog_posts_setting_summary_26);
notifyVibration.setVisible(false);
notifySound.setVisible(false);
}
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (SDK_INT < NOTIFICATION_CHANNEL_API) {
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
nm.getNotifyPrivateMessages().observe(lifecycleOwner, enabled -> {
notifyPrivateMessages.setChecked(enabled);
enableAndPersist(notifyPrivateMessages);
});
nm.getNotifyGroupMessages().observe(lifecycleOwner, enabled -> {
notifyGroupMessages.setChecked(enabled);
enableAndPersist(notifyGroupMessages);
});
nm.getNotifyForumPosts().observe(lifecycleOwner, enabled -> {
notifyForumPosts.setChecked(enabled);
enableAndPersist(notifyForumPosts);
});
nm.getNotifyBlogPosts().observe(lifecycleOwner, enabled -> {
notifyBlogPosts.setChecked(enabled);
enableAndPersist(notifyBlogPosts);
});
nm.getNotifyVibration().observe(lifecycleOwner, enabled -> {
notifyVibration.setChecked(enabled);
enableAndPersist(notifyVibration);
});
nm.getNotifySound().observe(lifecycleOwner, enabled -> {
String text;
if (enabled) {
String ringtoneName = nm.getRingtoneName();
if (isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
} else {
text = ringtoneName;
}
} else {
text = getString(R.string.notify_sound_setting_disabled);
}
notifySound.setSummary(text);
notifySound.setEnabled(true);
});
}
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.notification_settings_title);
}
@Override
public void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_RINGTONE && result == RESULT_OK &&
data != null) {
Uri uri = data.getParcelableExtra(EXTRA_RINGTONE_PICKED_URI);
nm.onRingtoneSet(uri);
}
}
@TargetApi(NOTIFICATION_CHANNEL_API)
private void setupNotificationPreference(SwitchPreferenceCompat pref,
String channelId, @StringRes int summary) {
pref.setWidgetLayoutResource(0);
pref.setSummary(summary);
pref.setEnabled(true);
pref.setOnPreferenceClickListener(clickedPref -> {
String packageName = requireContext().getPackageName();
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, packageName)
.putExtra(EXTRA_CHANNEL_ID, channelId);
Context ctx = requireContext();
if (intent.resolveActivity(ctx.getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_SHORT)
.show();
}
return true;
});
}
private boolean onNotificationSoundClicked() {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI,
DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (requireNonNull(nm.getNotifySound().getValue())) {
Uri uri;
String ringtoneUri = nm.getRingtoneUri();
if (isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
if (i.resolveActivity(requireActivity().getPackageManager()) != null) {
startActivityForResult(i, REQUEST_RINGTONE);
} else {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
}
return true;
}
}

View File

@@ -0,0 +1,158 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.widget.Toast;
import org.briarproject.bramble.api.db.DbException;
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.briar.R;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_PRIVATE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_NAME;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_URI;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_SOUND;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_VIBRATION;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class NotificationsManager {
private final static Logger LOG =
getLogger(NotificationsManager.class.getName());
private final Context ctx;
private final SettingsManager settingsManager;
private final Executor dbExecutor;
private final MutableLiveData<Boolean> notifyPrivateMessages =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyGroupMessages =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyForumPosts =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyBlogPosts =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifyVibration =
new MutableLiveData<>();
private final MutableLiveData<Boolean> notifySound =
new MutableLiveData<>();
private volatile String ringtoneName, ringtoneUri;
NotificationsManager(Context ctx,
SettingsManager settingsManager,
Executor dbExecutor) {
this.ctx = ctx;
this.settingsManager = settingsManager;
this.dbExecutor = dbExecutor;
}
void updateSettings(Settings settings) {
notifyPrivateMessages.postValue(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.postValue(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.postValue(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.postValue(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.postValue(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
ringtoneName = settings.get(PREF_NOTIFY_RINGTONE_NAME);
ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
notifySound.postValue(settings.getBoolean(PREF_NOTIFY_SOUND, true));
}
void onRingtoneSet(@Nullable Uri uri) {
Settings s = new Settings();
if (uri == null) {
// The user chose silence
s.putBoolean(PREF_NOTIFY_SOUND, false);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else if (RingtoneManager.isDefault(uri)) {
// The user chose the default
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else {
// The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(ctx, uri);
if (r == null || "file".equals(uri.getScheme())) {
Toast.makeText(ctx, R.string.cannot_load_ringtone, LENGTH_SHORT)
.show();
} else {
String name = r.getTitle(ctx);
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
}
}
dbExecutor.execute(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, SETTINGS_NAMESPACE);
logDuration(LOG, "Merging notification settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
LiveData<Boolean> getNotifyPrivateMessages() {
return notifyPrivateMessages;
}
LiveData<Boolean> getNotifyGroupMessages() {
return notifyGroupMessages;
}
LiveData<Boolean> getNotifyForumPosts() {
return notifyForumPosts;
}
LiveData<Boolean> getNotifyBlogPosts() {
return notifyBlogPosts;
}
LiveData<Boolean> getNotifyVibration() {
return notifyVibration;
}
@NonNull
LiveData<Boolean> getNotifySound() {
return notifySound;
}
String getRingtoneName() {
return ringtoneName;
}
String getRingtoneUri() {
return ringtoneUri;
}
}

View File

@@ -0,0 +1,112 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
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.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SecurityFragment extends PreferenceFragmentCompat {
public static final String PREF_SCREEN_LOCK = "pref_key_lock";
public static final String PREF_SCREEN_LOCK_TIMEOUT =
"pref_key_lock_timeout";
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private SwitchPreferenceCompat screenLock;
private ListPreference screenLockTimeout;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_security);
getPreferenceManager().setPreferenceDataStore(viewModel.settingsStore);
screenLock = findPreference(PREF_SCREEN_LOCK);
screenLockTimeout =
requireNonNull(findPreference(PREF_SCREEN_LOCK_TIMEOUT));
screenLockTimeout.setSummaryProvider(preference -> {
CharSequence timeout = screenLockTimeout.getValue();
String never = getString(R.string.pref_lock_timeout_value_never);
if (timeout.equals(never)) {
return getString(R.string.pref_lock_timeout_never_summary);
} else {
return getString(R.string.pref_lock_timeout_summary,
screenLockTimeout.getEntry());
}
});
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (SDK_INT < 21) {
screenLock.setVisible(false);
screenLockTimeout.setVisible(false);
} else {
// timeout depends on screenLock and gets disabled automatically
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
viewModel.getScreenLockTimeout().observe(lifecycleOwner, value -> {
screenLockTimeout.setValue(value);
enableAndPersist(screenLockTimeout);
});
}
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.security_settings_title);
checkScreenLock();
}
private void checkScreenLock() {
if (SDK_INT < 21) return;
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
viewModel.getScreenLockEnabled().removeObservers(lifecycleOwner);
if (hasScreenLock(requireActivity())) {
viewModel.getScreenLockEnabled().observe(lifecycleOwner, on -> {
screenLock.setChecked(on);
enableAndPersist(screenLock);
});
screenLock.setSummary(R.string.pref_lock_summary);
} else {
screenLock.setEnabled(false);
screenLock.setPersistent(false);
screenLock.setChecked(false);
screenLock.setSummary(R.string.pref_lock_disabled_summary);
}
}
}

View File

@@ -1,41 +1,45 @@
package org.briarproject.briar.android.settings;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.FeatureFlags;
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.util.UiUtils;
import org.briarproject.briar.android.view.AuthorView;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import de.hdodenhof.circleimageview.CircleImageView;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback;
import static android.widget.Toast.LENGTH_LONG;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_AVATAR_IMAGE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SettingsActivity extends BriarActivity
implements OnPreferenceStartFragmentCallback {
public class SettingsActivity extends BriarActivity {
static final String EXTRA_THEME_CHANGE = "themeChange";
@Inject
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel settingsViewModel;
@Inject
FeatureFlags featureFlags;
private SettingsViewModel viewModel;
@Override
public void onCreate(Bundle bundle) {
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void onCreate(@Nullable Bundle bundle) {
super.onCreate(bundle);
ActionBar actionBar = getSupportActionBar();
@@ -44,43 +48,24 @@ public class SettingsActivity extends BriarActivity {
actionBar.setDisplayHomeAsUpEnabled(true);
}
// show display fragment after theme change
Bundle extras = getIntent().getExtras();
if (bundle == null && extras != null &&
extras.getBoolean(EXTRA_THEME_CHANGE, false)) {
FragmentManager fragmentManager = getSupportFragmentManager();
showNextFragment(fragmentManager, new DisplayFragment());
}
setContentView(R.layout.activity_settings);
if (featureFlags.shouldEnableProfilePictures()) {
ViewModelProvider provider =
new ViewModelProvider(this, viewModelFactory);
settingsViewModel = provider.get(SettingsViewModel.class);
ViewModelProvider provider =
new ViewModelProvider(this, viewModelFactory);
viewModel = provider.get(SettingsViewModel.class);
TextView textViewUserName = findViewById(R.id.username);
CircleImageView imageViewAvatar =
findViewById(R.id.avatarImage);
settingsViewModel.getOwnIdentityInfo().observe(this, us -> {
textViewUserName.setText(us.getLocalAuthor().getName());
AuthorView.setAvatar(imageViewAvatar,
us.getLocalAuthor().getId(), us.getAuthorInfo());
});
settingsViewModel.getSetAvatarFailed()
.observeEvent(this, failed -> {
if (failed) {
Toast.makeText(this,
R.string.change_profile_picture_failed_message,
LENGTH_LONG).show();
}
});
View avatarGroup = findViewById(R.id.avatarGroup);
avatarGroup.setOnClickListener(e -> selectAvatarImage());
} else {
View view = findViewById(R.id.avatarGroup);
view.setVisibility(View.GONE);
}
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel.getLanguageChange().observeEvent(this, b -> {
signOut(false, false);
finishAffinity();
});
}
@Override
@@ -92,30 +77,40 @@ public class SettingsActivity extends BriarActivity {
return false;
}
private void selectAvatarImage() {
Intent intent = UiUtils.createSelectImageIntent(false);
startActivityForResult(intent, REQUEST_AVATAR_IMAGE);
@Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller,
Preference pref) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentFactory fragmentFactory = fragmentManager.getFragmentFactory();
Fragment fragment = fragmentFactory
.instantiate(getClassLoader(), pref.getFragment());
fragment.setTargetFragment(caller, 0);
// Replace the existing Fragment with the new Fragment
showNextFragment(fragmentManager, fragment);
return true;
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
private void showNextFragment(FragmentManager fragmentManager, Fragment f) {
fragmentManager.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)
.addToBackStack(null)
.commit();
}
if (request == REQUEST_AVATAR_IMAGE && result == RESULT_OK) {
onAvatarImageReceived(data);
/**
* If the preference is not yet enabled, this enables the preference
* and makes it persist changed values.
* Call this after setting the initial value
* to prevent this change from getting persisted in the DB unnecessarily.
*/
static void enableAndPersist(Preference pref) {
if (!pref.isEnabled()) {
pref.setEnabled(true);
pref.setPersistent(true);
}
}
private void onAvatarImageReceived(@Nullable Intent resultData) {
if (resultData == null) return;
Uri uri = resultData.getData();
if (uri == null) return;
ConfirmAvatarDialogFragment dialog =
ConfirmAvatarDialogFragment.newInstance(uri);
dialog.show(getSupportFragmentManager(),
ConfirmAvatarDialogFragment.TAG);
}
}

View File

@@ -1,761 +1,120 @@
package org.briarproject.briar.android.settings;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.util.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.core.text.TextUtilsCompat;
import androidx.preference.ListPreference;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroup;
import androidx.preference.SwitchPreference;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.media.RingtoneManager.ACTION_RINGTONE_PICKER;
import static android.media.RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_EXISTING_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_PICKED_URI;
import static android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TITLE;
import static android.media.RingtoneManager.EXTRA_RINGTONE_TYPE;
import static android.media.RingtoneManager.TYPE_NOTIFICATION;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.SIGN_OUT_URI;
import static org.briarproject.briar.android.util.UiUtils.getCountryDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_AVATAR_IMAGE;
import static org.briarproject.briar.android.util.UiUtils.createSelectImageIntent;
import static org.briarproject.briar.android.util.UiUtils.triggerFeedback;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.GROUP_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_BLOG;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_FORUM;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_GROUP;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_PRIVATE;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_NAME;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_RINGTONE_URI;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_SOUND;
import static org.briarproject.briar.api.android.AndroidNotificationManager.PREF_NOTIFY_VIBRATION;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class SettingsFragment extends PreferenceFragmentCompat
implements EventListener, OnPreferenceChangeListener {
public class SettingsFragment extends PreferenceFragmentCompat {
public static final String SETTINGS_NAMESPACE = "android-ui";
public static final String LANGUAGE = "pref_key_language";
public static final String PREF_SCREEN_LOCK = "pref_key_lock";
public static final String PREF_SCREEN_LOCK_TIMEOUT =
"pref_key_lock_timeout";
public static final String NOTIFY_SIGN_IN = "pref_key_notify_sign_in";
private static final String BT_NAMESPACE =
BluetoothConstants.ID.getString();
private static final String BT_ENABLE = "pref_key_bluetooth";
private static final String WIFI_NAMESPACE = LanTcpConstants.ID.getString();
private static final String WIFI_ENABLE = "pref_key_wifi";
private static final String TOR_NAMESPACE = TorConstants.ID.getString();
private static final String TOR_ENABLE = "pref_key_tor_enable";
private static final String TOR_NETWORK = "pref_key_tor_network";
private static final String TOR_MOBILE = "pref_key_tor_mobile_data";
private static final String TOR_ONLY_WHEN_CHARGING =
"pref_key_tor_only_when_charging";
private static final Logger LOG =
Logger.getLogger(SettingsFragment.class.getName());
private SettingsActivity listener;
private ListPreference language;
private SwitchPreference enableBluetooth;
private SwitchPreference enableWifi;
private SwitchPreference enableTor;
private ListPreference torNetwork;
private SwitchPreference torMobile;
private SwitchPreference torOnlyWhenCharging;
private SwitchPreference screenLock;
private ListPreference screenLockTimeout;
private SwitchPreference notifyPrivateMessages;
private SwitchPreference notifyGroupMessages;
private SwitchPreference notifyForumPosts;
private SwitchPreference notifyBlogPosts;
private SwitchPreference notifyVibration;
private Preference notifySound;
// Fields that are accessed from background threads must be volatile
private volatile Settings settings, btSettings, wifiSettings, torSettings;
private volatile boolean settingsLoaded = false;
private static final String PREF_KEY_AVATAR = "pref_key_avatar";
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";
@Inject
volatile SettingsManager settingsManager;
@Inject
volatile EventBus eventBus;
@Inject
LocationUtils locationUtils;
@Inject
CircumventionProvider circumventionProvider;
ViewModelProvider.Factory viewModelFactory;
private SettingsViewModel viewModel;
private AvatarPreference prefAvatar;
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
listener = (SettingsActivity) context;
listener.getActivityComponent().inject(this);
getAndroidComponent(context).inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(SettingsViewModel.class);
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings);
language = findPreference(LANGUAGE);
setLanguageEntries();
ListPreference theme = findPreference("pref_key_theme");
enableBluetooth = findPreference(BT_ENABLE);
enableWifi = findPreference(WIFI_ENABLE);
enableTor = findPreference(TOR_ENABLE);
torNetwork = findPreference(TOR_NETWORK);
torMobile = findPreference(TOR_MOBILE);
torOnlyWhenCharging = findPreference(TOR_ONLY_WHEN_CHARGING);
screenLock = findPreference(PREF_SCREEN_LOCK);
screenLockTimeout = findPreference(PREF_SCREEN_LOCK_TIMEOUT);
notifyPrivateMessages =
findPreference("pref_key_notify_private_messages");
notifyGroupMessages = findPreference("pref_key_notify_group_messages");
notifyForumPosts = findPreference("pref_key_notify_forum_posts");
notifyBlogPosts = findPreference("pref_key_notify_blog_posts");
notifyVibration = findPreference("pref_key_notify_vibration");
notifySound = findPreference("pref_key_notify_sound");
language.setOnPreferenceChangeListener(this);
theme.setOnPreferenceChangeListener((preference, newValue) -> {
if (getActivity() != null) {
// activate new theme
UiUtils.setTheme(getActivity(), (String) newValue);
// bring up parent activity, so it can change its theme as well
// upstream bug: https://issuetracker.google.com/issues/38352704
Intent intent = new Intent(getActivity(), ENTRY_ACTIVITY);
intent.setFlags(
FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
// bring this activity back to the foreground
intent = new Intent(getActivity(), getActivity().getClass());
startActivity(intent);
getActivity().finish();
}
return true;
});
enableBluetooth.setOnPreferenceChangeListener(this);
enableWifi.setOnPreferenceChangeListener(this);
enableTor.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
torMobile.setOnPreferenceChangeListener(this);
torOnlyWhenCharging.setOnPreferenceChangeListener(this);
screenLock.setOnPreferenceChangeListener(this);
screenLockTimeout.setOnPreferenceChangeListener(this);
prefAvatar = requireNonNull(findPreference(PREF_KEY_AVATAR));
if (viewModel.shouldEnableProfilePictures()) {
prefAvatar.setOnPreferenceClickListener(preference -> {
Intent intent = createSelectImageIntent(false);
startActivityForResult(intent, REQUEST_AVATAR_IMAGE);
return true;
});
} else {
prefAvatar.setVisible(false);
}
Preference prefFeedback =
requireNonNull(findPreference("pref_key_send_feedback"));
requireNonNull(findPreference(PREF_KEY_FEEDBACK));
prefFeedback.setOnPreferenceClickListener(preference -> {
triggerFeedback(requireContext());
return true;
});
if (SDK_INT < 27) {
// remove System Default Theme option from preference entries
// as it is not functional on this API anyway
List<CharSequence> entries =
new ArrayList<>(Arrays.asList(theme.getEntries()));
entries.remove(getString(R.string.pref_theme_system));
theme.setEntries(entries.toArray(new CharSequence[0]));
// also remove corresponding value
List<CharSequence> values =
new ArrayList<>(Arrays.asList(theme.getEntryValues()));
values.remove(getString(R.string.pref_theme_system_value));
theme.setEntryValues(values.toArray(new CharSequence[0]));
}
Preference explode = requireNonNull(findPreference("pref_key_explode"));
Preference explode = requireNonNull(findPreference(PREF_KEY_EXPLODE));
if (IS_DEBUG_BUILD) {
explode.setOnPreferenceClickListener(preference -> {
throw new RuntimeException("Boom!");
});
} else {
explode.setVisible(false);
findPreference("pref_key_test_data").setVisible(false);
PreferenceGroup testing = explode.getParent();
if (testing == null) throw new AssertionError();
testing.setVisible(false);
PreferenceGroup dev = requireNonNull(findPreference(PREF_KEY_DEV));
dev.setVisible(false);
}
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
ColorDrawable divider = new ColorDrawable(
ContextCompat.getColor(requireContext(), R.color.divider));
setDivider(divider);
return view;
super.onViewCreated(view, savedInstanceState);
viewModel.getOwnIdentityInfo().observe(getViewLifecycleOwner(), us ->
prefAvatar.setOwnIdentityInfo(us)
);
}
@Override
public void onStart() {
super.onStart();
eventBus.addListener(this);
setSettingsEnabled(false);
loadSettings();
requireActivity().setTitle(R.string.settings_button);
}
@Override
public void onStop() {
super.onStop();
eventBus.removeListener(this);
}
private void setLanguageEntries() {
CharSequence[] tags = language.getEntryValues();
List<CharSequence> entries = new ArrayList<>(tags.length);
List<CharSequence> entryValues = new ArrayList<>(tags.length);
for (CharSequence cs : tags) {
String tag = cs.toString();
if (tag.equals("default")) {
entries.add(getString(R.string.pref_language_default));
entryValues.add(tag);
continue;
}
Locale locale = Localizer.getLocaleFromTag(tag);
if (locale == null)
throw new IllegalStateException();
// Exclude RTL locales on API < 17, they won't be laid out correctly
if (SDK_INT < 17 && !isLeftToRight(locale)) {
if (LOG.isLoggable(INFO))
LOG.info("Skipping RTL locale " + tag);
continue;
}
String nativeName = locale.getDisplayName(locale);
// Fallback to English if the name is unknown in both native and
// current locale.
if (nativeName.equals(tag)) {
String tmp = locale.getDisplayLanguage(Locale.ENGLISH);
if (!tmp.isEmpty() && !tmp.equals(nativeName))
nativeName = tmp;
}
// Prefix with LRM marker to prevent any RTL direction
entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase()
+ nativeName.substring(1));
entryValues.add(tag);
}
language.setEntries(entries.toArray(new CharSequence[0]));
language.setEntryValues(entryValues.toArray(new CharSequence[0]));
}
private boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
private void setTorNetworkSummary(int torNetworkSetting) {
if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) {
torNetwork.setSummary("%s"); // use setting value
return;
}
// Look up country name in the user's chosen language if available
String country = locationUtils.getCurrentCountry();
String countryName = getCountryDisplayName(country);
boolean blocked =
circumventionProvider.isTorProbablyBlocked(country);
boolean useBridges = circumventionProvider.doBridgesWork(country);
String setting =
getString(R.string.tor_network_setting_without_bridges);
if (blocked && useBridges) {
setting = getString(R.string.tor_network_setting_with_bridges);
} else if (blocked) {
setting = getString(R.string.tor_network_setting_never);
}
torNetwork.setSummary(
getString(R.string.tor_network_setting_summary, setting,
countryName));
}
private void loadSettings() {
listener.runOnDbThread(() -> {
try {
long start = now();
settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
btSettings = settingsManager.getSettings(BT_NAMESPACE);
wifiSettings = settingsManager.getSettings(WIFI_NAMESPACE);
torSettings = settingsManager.getSettings(TOR_NAMESPACE);
settingsLoaded = true;
logDuration(LOG, "Loading settings", start);
displaySettings();
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
// TODO: Remove after a reasonable migration period (added 2020-06-25)
private Settings migrateTorSettings(Settings s) {
int network = s.getInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
if (network == PREF_TOR_NETWORK_NEVER) {
s.putInt(PREF_TOR_NETWORK, DEFAULT_PREF_TOR_NETWORK);
s.putBoolean(PREF_PLUGIN_ENABLE, false);
// We don't need to save the migrated settings - the Tor plugin is
// responsible for that. This code just handles the case where the
// settings are loaded before the plugin migrates them.
}
return s;
}
private void displaySettings() {
listener.runOnUiThreadUnlessDestroyed(() -> {
// due to events, we might try to display before a load completed
if (!settingsLoaded) return;
boolean btEnabledSetting = btSettings.getBoolean(PREF_PLUGIN_ENABLE,
BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE);
enableBluetooth.setChecked(btEnabledSetting);
boolean wifiEnabledSetting =
wifiSettings.getBoolean(PREF_PLUGIN_ENABLE,
LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE);
enableWifi.setChecked(wifiEnabledSetting);
boolean torEnabledSetting =
torSettings.getBoolean(PREF_PLUGIN_ENABLE,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
enableTor.setChecked(torEnabledSetting);
int torNetworkSetting = torSettings.getInt(PREF_TOR_NETWORK,
DEFAULT_PREF_TOR_NETWORK);
torNetwork.setValue(Integer.toString(torNetworkSetting));
setTorNetworkSummary(torNetworkSetting);
boolean torMobileSetting = torSettings.getBoolean(PREF_TOR_MOBILE,
DEFAULT_PREF_TOR_MOBILE);
torMobile.setChecked(torMobileSetting);
boolean torChargingSetting =
torSettings.getBoolean(PREF_TOR_ONLY_WHEN_CHARGING,
DEFAULT_PREF_TOR_ONLY_WHEN_CHARGING);
torOnlyWhenCharging.setChecked(torChargingSetting);
displayScreenLockSetting();
if (SDK_INT < 26) {
notifyPrivateMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_PRIVATE, true));
notifyGroupMessages.setChecked(settings.getBoolean(
PREF_NOTIFY_GROUP, true));
notifyForumPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_FORUM, true));
notifyBlogPosts.setChecked(settings.getBoolean(
PREF_NOTIFY_BLOG, true));
notifyVibration.setChecked(settings.getBoolean(
PREF_NOTIFY_VIBRATION, true));
notifyPrivateMessages.setOnPreferenceChangeListener(this);
notifyGroupMessages.setOnPreferenceChangeListener(this);
notifyForumPosts.setOnPreferenceChangeListener(this);
notifyBlogPosts.setOnPreferenceChangeListener(this);
notifyVibration.setOnPreferenceChangeListener(this);
notifySound.setOnPreferenceClickListener(
pref -> onNotificationSoundClicked());
String text;
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
String ringtoneName =
settings.get(PREF_NOTIFY_RINGTONE_NAME);
if (StringUtils.isNullOrEmpty(ringtoneName)) {
text = getString(R.string.notify_sound_setting_default);
} else {
text = ringtoneName;
}
} else {
text = getString(R.string.notify_sound_setting_disabled);
}
notifySound.setSummary(text);
} else {
setupNotificationPreference(notifyPrivateMessages,
CONTACT_CHANNEL_ID,
R.string.notify_private_messages_setting_summary_26);
setupNotificationPreference(notifyGroupMessages,
GROUP_CHANNEL_ID,
R.string.notify_group_messages_setting_summary_26);
setupNotificationPreference(notifyForumPosts, FORUM_CHANNEL_ID,
R.string.notify_forum_posts_setting_summary_26);
setupNotificationPreference(notifyBlogPosts, BLOG_CHANNEL_ID,
R.string.notify_blog_posts_setting_summary_26);
notifyVibration.setVisible(false);
notifySound.setVisible(false);
}
setSettingsEnabled(true);
});
}
private void setSettingsEnabled(boolean enabled) {
// preferences not needed here, because handled by SharedPreferences:
// - pref_key_theme
// - pref_key_notify_sign_in
// preferences partly needed here, because they have their own logic
// - pref_key_lock (screenLock -> displayScreenLockSetting())
// - pref_key_lock_timeout (screenLockTimeout)
enableBluetooth.setEnabled(enabled);
enableWifi.setEnabled(enabled);
enableTor.setEnabled(enabled);
torNetwork.setEnabled(enabled);
torMobile.setEnabled(enabled);
torOnlyWhenCharging.setEnabled(enabled);
if (!enabled) screenLock.setEnabled(false);
notifyPrivateMessages.setEnabled(enabled);
notifyGroupMessages.setEnabled(enabled);
notifyForumPosts.setEnabled(enabled);
notifyBlogPosts.setEnabled(enabled);
notifyVibration.setEnabled(enabled);
notifySound.setEnabled(enabled);
}
private void displayScreenLockSetting() {
if (SDK_INT < 21) {
screenLock.setVisible(false);
screenLockTimeout.setVisible(false);
} else {
if (getActivity() != null && hasScreenLock(getActivity())) {
screenLock.setEnabled(true);
screenLock.setChecked(
settings.getBoolean(PREF_SCREEN_LOCK, false));
screenLock.setSummary(R.string.pref_lock_summary);
} else {
screenLock.setEnabled(false);
screenLock.setChecked(false);
screenLock.setSummary(R.string.pref_lock_disabled_summary);
}
// timeout depends on screenLock and gets disabled automatically
int timeout = settings.getInt(PREF_SCREEN_LOCK_TIMEOUT,
Integer.valueOf(getString(
R.string.pref_lock_timeout_value_default)));
String newValue = String.valueOf(timeout);
screenLockTimeout.setValue(newValue);
setScreenLockTimeoutSummary(newValue);
}
}
private void setScreenLockTimeoutSummary(String timeout) {
String never = getString(R.string.pref_lock_timeout_value_never);
if (timeout.equals(never)) {
screenLockTimeout
.setSummary(R.string.pref_lock_timeout_never_summary);
} else {
screenLockTimeout
.setSummary(R.string.pref_lock_timeout_summary);
}
}
@TargetApi(26)
private void setupNotificationPreference(SwitchPreference pref,
String channelId, @StringRes int summary) {
pref.setWidgetLayoutResource(0);
pref.setSummary(summary);
pref.setOnPreferenceClickListener(clickedPref -> {
String packageName = requireContext().getPackageName();
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, packageName)
.putExtra(EXTRA_CHANNEL_ID, channelId);
Context ctx = requireContext();
if (intent.resolveActivity(ctx.getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_SHORT)
.show();
}
return true;
});
}
private boolean onNotificationSoundClicked() {
String title = getString(R.string.choose_ringtone_title);
Intent i = new Intent(ACTION_RINGTONE_PICKER);
i.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
i.putExtra(EXTRA_RINGTONE_TITLE, title);
i.putExtra(EXTRA_RINGTONE_DEFAULT_URI,
DEFAULT_NOTIFICATION_URI);
i.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
if (settings.getBoolean(PREF_NOTIFY_SOUND, true)) {
Uri uri;
String ringtoneUri =
settings.get(PREF_NOTIFY_RINGTONE_URI);
if (StringUtils.isNullOrEmpty(ringtoneUri))
uri = DEFAULT_NOTIFICATION_URI;
else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
}
if (i.resolveActivity(requireActivity().getPackageManager()) != null) {
startActivityForResult(i, REQUEST_RINGTONE);
} else {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
}
return true;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (preference == language) {
if (!language.getValue().equals(newValue))
languageChanged((String) newValue);
return false;
} else if (preference == enableBluetooth) {
boolean btSetting = (Boolean) newValue;
storeBluetoothSetting(btSetting);
} else if (preference == enableWifi) {
boolean wifiSetting = (Boolean) newValue;
storeWifiSetting(wifiSetting);
} else if (preference == enableTor) {
boolean torEnabledSetting = (Boolean) newValue;
storeTorEnabledSetting(torEnabledSetting);
} else if (preference == torNetwork) {
int torNetworkSetting = Integer.valueOf((String) newValue);
storeTorNetworkSetting(torNetworkSetting);
setTorNetworkSummary(torNetworkSetting);
} else if (preference == torMobile) {
boolean torMobileSetting = (Boolean) newValue;
storeTorMobileSetting(torMobileSetting);
} else if (preference == torOnlyWhenCharging) {
boolean torChargingSetting = (Boolean) newValue;
storeTorChargingSetting(torChargingSetting);
} else if (preference == screenLock) {
Settings s = new Settings();
s.putBoolean(PREF_SCREEN_LOCK, (Boolean) newValue);
storeSettings(s);
} else if (preference == screenLockTimeout) {
Settings s = new Settings();
String value = (String) newValue;
s.putInt(PREF_SCREEN_LOCK_TIMEOUT, Integer.valueOf(value));
storeSettings(s);
setScreenLockTimeoutSummary(value);
} else if (preference == notifyPrivateMessages) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyGroupMessages) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_GROUP, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyForumPosts) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_FORUM, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyBlogPosts) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_BLOG, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyVibration) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_VIBRATION, (Boolean) newValue);
storeSettings(s);
}
return true;
}
private void languageChanged(String newValue) {
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.pref_language_title);
builder.setMessage(R.string.pref_language_changed);
builder.setPositiveButton(R.string.sign_out_button,
(dialogInterface, i) -> {
language.setValue(newValue);
Intent intent = new Intent(getContext(), ENTRY_ACTIVITY);
intent.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
intent.setData(SIGN_OUT_URI);
requireActivity().startActivity(intent);
requireActivity().finish();
});
builder.setNegativeButton(R.string.cancel, null);
builder.setCancelable(false);
builder.show();
}
private void storeTorEnabledSetting(boolean torEnabledSetting) {
Settings s = new Settings();
s.putBoolean(PREF_PLUGIN_ENABLE, torEnabledSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeTorNetworkSetting(int torNetworkSetting) {
Settings s = new Settings();
s.putInt(PREF_TOR_NETWORK, torNetworkSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeTorMobileSetting(boolean torMobileSetting) {
Settings s = new Settings();
s.putBoolean(PREF_TOR_MOBILE, torMobileSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeTorChargingSetting(boolean torChargingSetting) {
Settings s = new Settings();
s.putBoolean(PREF_TOR_ONLY_WHEN_CHARGING, torChargingSetting);
mergeSettings(s, TOR_NAMESPACE);
}
private void storeBluetoothSetting(boolean btSetting) {
Settings s = new Settings();
s.putBoolean(PREF_PLUGIN_ENABLE, btSetting);
mergeSettings(s, BT_NAMESPACE);
}
private void storeWifiSetting(boolean wifiSetting) {
Settings s = new Settings();
s.putBoolean(PREF_PLUGIN_ENABLE, wifiSetting);
mergeSettings(s, WIFI_NAMESPACE);
}
private void storeSettings(Settings s) {
mergeSettings(s, SETTINGS_NAMESPACE);
}
private void mergeSettings(Settings s, String namespace) {
listener.runOnDbThread(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, namespace);
logDuration(LOG, "Merging settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@Override
public void onActivityResult(int request, int result, Intent data) {
public void onActivityResult(int request, int result,
@Nullable Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_RINGTONE && result == RESULT_OK) {
Settings s = new Settings();
Uri uri = data.getParcelableExtra(EXTRA_RINGTONE_PICKED_URI);
if (uri == null) {
// The user chose silence
s.putBoolean(PREF_NOTIFY_SOUND, false);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else if (RingtoneManager.isDefault(uri)) {
// The user chose the default
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, "");
s.put(PREF_NOTIFY_RINGTONE_URI, "");
} else {
// The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(getContext(), uri);
if (r == null || "file".equals(uri.getScheme())) {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
} else {
String name = r.getTitle(getContext());
s.putBoolean(PREF_NOTIFY_SOUND, true);
s.put(PREF_NOTIFY_RINGTONE_NAME, name);
s.put(PREF_NOTIFY_RINGTONE_URI, uri.toString());
}
}
storeSettings(s);
}
}
if (request == REQUEST_AVATAR_IMAGE && result == RESULT_OK) {
if (data == null) return;
Uri uri = data.getData();
if (uri == null) return;
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
String namespace = s.getNamespace();
if (namespace.equals(SETTINGS_NAMESPACE)) {
LOG.info("Settings updated");
settings = s.getSettings();
displaySettings();
} else if (namespace.equals(BT_NAMESPACE)) {
LOG.info("Bluetooth settings updated");
btSettings = s.getSettings();
displaySettings();
} else if (namespace.equals(WIFI_NAMESPACE)) {
LOG.info("Wifi settings updated");
wifiSettings = s.getSettings();
displaySettings();
} else if (namespace.equals(TOR_NAMESPACE)) {
LOG.info("Tor settings updated");
torSettings = migrateTorSettings(s.getSettings());
displaySettings();
}
DialogFragment dialog =
ConfirmAvatarDialogFragment.newInstance(uri);
dialog.show(getParentFragmentManager(),
ConfirmAvatarDialogFragment.TAG);
}
}

View File

@@ -0,0 +1,80 @@
package org.briarproject.briar.android.settings;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceDataStore;
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.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
/**
* A custom PreferenceDataStore that stores settings in Briar's encrypted DB.
*/
@NotNullByDefault
class SettingsStore extends PreferenceDataStore {
private final static Logger LOG = getLogger(SettingsStore.class.getName());
private final SettingsManager settingsManager;
private final Executor dbExecutor;
private final String namespace;
SettingsStore(SettingsManager settingsManager,
Executor dbExecutor,
String namespace) {
this.settingsManager = settingsManager;
this.dbExecutor = dbExecutor;
this.namespace = namespace;
}
@Override
public void putBoolean(String key, boolean value) {
if (LOG.isLoggable(INFO))
LOG.info("Store bool setting: " + key + "=" + value);
Settings s = new Settings();
s.putBoolean(key, value);
storeSettings(s);
}
@Override
public void putInt(String key, int value) {
if (LOG.isLoggable(INFO))
LOG.info("Store int setting: " + key + "=" + value);
Settings s = new Settings();
s.putInt(key, value);
storeSettings(s);
}
@Override
public void putString(String key, @Nullable String value) {
if (LOG.isLoggable(INFO))
LOG.info("Store string setting: " + key + "=" + value);
Settings s = new Settings();
s.put(key, value);
storeSettings(s);
}
private void storeSettings(Settings s) {
dbExecutor.execute(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, namespace);
logDuration(LOG, "Merging " + namespace + " settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
}

View File

@@ -3,15 +3,34 @@ package org.briarproject.briar.android.settings;
import android.app.Application;
import android.content.ContentResolver;
import android.net.Uri;
import android.widget.Toast;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.BluetoothConstants;
import org.briarproject.bramble.api.plugin.LanTcpConstants;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.R;
import org.briarproject.briar.android.attachment.UnsupportedMimeTypeException;
import org.briarproject.briar.android.attachment.media.ImageCompressor;
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.avatar.AvatarManager;
@@ -25,66 +44,129 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.lifecycle.AndroidViewModel;
import androidx.annotation.AnyThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.Arrays.asList;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SecurityFragment.PREF_SCREEN_LOCK_TIMEOUT;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
@NotNullByDefault
class SettingsViewModel extends AndroidViewModel {
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class SettingsViewModel extends DbViewModel implements EventListener {
private final static Logger LOG =
getLogger(SettingsViewModel.class.getName());
static final String BT_NAMESPACE =
BluetoothConstants.ID.getString();
static final String WIFI_NAMESPACE = LanTcpConstants.ID.getString();
static final String TOR_NAMESPACE = TorConstants.ID.getString();
private final SettingsManager settingsManager;
private final IdentityManager identityManager;
private final EventBus eventBus;
private final AvatarManager avatarManager;
private final AuthorManager authorManager;
private final ImageCompressor imageCompressor;
@IoExecutor
private final Executor ioExecutor;
@DatabaseExecutor
private final Executor dbExecutor;
private final FeatureFlags featureFlags;
final SettingsStore settingsStore;
final TorSummaryProvider torSummaryProvider;
final ConnectionsManager connectionsManager;
final NotificationsManager notificationsManager;
private volatile Settings settings;
private final MutableLiveData<OwnIdentityInfo> ownIdentityInfo =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> setAvatarFailed =
private final MutableLiveData<Boolean> screenLockEnabled =
new MutableLiveData<>();
private final MutableLiveData<String> screenLockTimeout =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> languageChanged =
new MutableLiveEvent<>();
@Inject
SettingsViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
SettingsManager settingsManager,
IdentityManager identityManager,
EventBus eventBus,
AvatarManager avatarManager,
AuthorManager authorManager,
ImageCompressor imageCompressor,
LocationUtils locationUtils,
CircumventionProvider circumventionProvider,
@IoExecutor Executor ioExecutor,
@DatabaseExecutor Executor dbExecutor) {
super(application);
FeatureFlags featureFlags) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.settingsManager = settingsManager;
this.identityManager = identityManager;
this.eventBus = eventBus;
this.imageCompressor = imageCompressor;
this.avatarManager = avatarManager;
this.authorManager = authorManager;
this.ioExecutor = ioExecutor;
this.dbExecutor = dbExecutor;
this.featureFlags = featureFlags;
settingsStore = new SettingsStore(settingsManager, dbExecutor,
SETTINGS_NAMESPACE);
torSummaryProvider = new TorSummaryProvider(getApplication(),
locationUtils, circumventionProvider);
connectionsManager =
new ConnectionsManager(settingsManager, dbExecutor);
notificationsManager = new NotificationsManager(getApplication(),
settingsManager, dbExecutor);
loadOwnIdentityInfo();
eventBus.addListener(this);
loadSettings();
if (shouldEnableProfilePictures()) loadOwnIdentityInfo();
}
LiveData<OwnIdentityInfo> getOwnIdentityInfo() {
return ownIdentityInfo;
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
}
LiveEvent<Boolean> getSetAvatarFailed() {
return setAvatarFailed;
private void loadSettings() {
runOnDbThread(() -> {
try {
long start = now();
settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
updateSettings(settings);
connectionsManager.updateBtSetting(
settingsManager.getSettings(BT_NAMESPACE));
connectionsManager.updateWifiSettings(
settingsManager.getSettings(WIFI_NAMESPACE));
connectionsManager.updateTorSettings(
settingsManager.getSettings(TOR_NAMESPACE));
logDuration(LOG, "Loading settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
boolean shouldEnableProfilePictures() {
return featureFlags.shouldEnableProfilePictures();
}
private void loadOwnIdentityInfo() {
dbExecutor.execute(() -> {
runOnDbThread(() -> {
try {
LocalAuthor localAuthor = identityManager.getLocalAuthor();
AuthorInfo authorInfo = authorManager.getMyAuthorInfo();
@@ -96,13 +178,47 @@ class SettingsViewModel extends AndroidViewModel {
});
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
String namespace = s.getNamespace();
if (namespace.equals(SETTINGS_NAMESPACE)) {
LOG.info("Settings updated");
settings = s.getSettings();
updateSettings(settings);
} else if (namespace.equals(BT_NAMESPACE)) {
LOG.info("Bluetooth settings updated");
connectionsManager.updateBtSetting(s.getSettings());
} else if (namespace.equals(WIFI_NAMESPACE)) {
LOG.info("Wifi settings updated");
connectionsManager.updateWifiSettings(s.getSettings());
} else if (namespace.equals(TOR_NAMESPACE)) {
LOG.info("Tor settings updated");
connectionsManager.updateTorSettings(s.getSettings());
}
}
}
@AnyThread
private void updateSettings(Settings settings) {
screenLockEnabled.postValue(settings.getBoolean(PREF_SCREEN_LOCK,
false));
int defaultTimeout = Integer.parseInt(getApplication()
.getString(R.string.pref_lock_timeout_value_default));
screenLockTimeout.postValue(String.valueOf(
settings.getInt(PREF_SCREEN_LOCK_TIMEOUT, defaultTimeout)
));
notificationsManager.updateSettings(settings);
}
void setAvatar(Uri uri) {
ioExecutor.execute(() -> {
try {
trySetAvatar(uri);
} catch (IOException e) {
logException(LOG, WARNING, e);
setAvatarFailed.postEvent(true);
onSetAvatarFailed();
}
});
}
@@ -120,15 +236,42 @@ class SettingsViewModel extends AndroidViewModel {
"ContentResolver returned null when opening InputStream");
InputStream compressed = imageCompressor.compressImage(is, contentType);
dbExecutor.execute(() -> {
runOnDbThread(() -> {
try {
avatarManager.addAvatar(ImageCompressor.MIME_TYPE, compressed);
loadOwnIdentityInfo();
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
setAvatarFailed.postEvent(true);
onSetAvatarFailed();
}
});
}
@AnyThread
private void onSetAvatarFailed() {
androidExecutor.runOnUiThread(() -> Toast.makeText(getApplication(),
R.string.change_profile_picture_failed_message, LENGTH_LONG)
.show());
}
void languageChanged() {
languageChanged.setEvent(true);
}
LiveData<OwnIdentityInfo> getOwnIdentityInfo() {
return ownIdentityInfo;
}
LiveData<Boolean> getScreenLockEnabled() {
return screenLockEnabled;
}
LiveData<String> getScreenLockTimeout() {
return screenLockTimeout;
}
LiveEvent<Boolean> getLanguageChange() {
return languageChanged;
}
}

View File

@@ -0,0 +1,57 @@
package org.briarproject.briar.android.settings;
import android.content.Context;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.R;
import androidx.preference.ListPreference;
import androidx.preference.Preference.SummaryProvider;
import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC;
import static org.briarproject.briar.android.util.UiUtils.getCountryDisplayName;
@NotNullByDefault
class TorSummaryProvider implements SummaryProvider<ListPreference> {
private final Context ctx;
private final LocationUtils locationUtils;
private final CircumventionProvider circumventionProvider;
TorSummaryProvider(Context ctx,
LocationUtils locationUtils,
CircumventionProvider circumventionProvider) {
this.ctx = ctx;
this.locationUtils = locationUtils;
this.circumventionProvider = circumventionProvider;
}
@Override
public CharSequence provideSummary(ListPreference preference) {
int torNetworkSetting = Integer.parseInt(preference.getValue());
if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) {
return preference.getEntry(); // use setting value
}
// Look up country name in the user's chosen language if available
String country = locationUtils.getCurrentCountry();
String countryName = getCountryDisplayName(country);
boolean blocked =
circumventionProvider.isTorProbablyBlocked(country);
boolean useBridges = circumventionProvider.doBridgesWork(country);
String setting =
ctx.getString(R.string.tor_network_setting_without_bridges);
if (blocked && useBridges) {
setting = ctx.getString(R.string.tor_network_setting_with_bridges);
} else if (blocked) {
setting = ctx.getString(R.string.tor_network_setting_never);
}
return ctx.getString(R.string.tor_network_setting_summary, setting,
countryName);
}
}

View File

@@ -34,7 +34,6 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.reporting.FeedbackActivity;
import org.briarproject.briar.android.view.ArticleMovementMethod;
import org.briarproject.briar.android.widget.LinkDialogFragment;
import java.util.Locale;
@@ -49,8 +48,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.text.HtmlCompat;
import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -198,8 +197,7 @@ public class UiUtils {
}
public static void makeLinksClickable(TextView v,
@Nullable FragmentManager fm) {
if (fm == null) return;
Consumer<String> onLinkClicked) {
SpannableStringBuilder ssb = new SpannableStringBuilder(v.getText());
URLSpan[] spans = ssb.getSpans(0, ssb.length(), URLSpan.class);
for (URLSpan span : spans) {
@@ -210,8 +208,7 @@ public class UiUtils {
ClickableSpan cSpan = new ClickableSpan() {
@Override
public void onClick(View v2) {
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
f.show(fm, f.getUniqueTag());
onLinkClicked.accept(url);
}
};
ssb.setSpan(cSpan, start, end, 0);

View File

@@ -135,9 +135,10 @@ public class AuthorView extends ConstraintLayout {
}
public void setAuthorNotClickable() {
setClickable(false);
setBackgroundResource(0);
setOnClickListener(null);
setClickable(false);
setFocusable(false);
setBackgroundResource(0);
}
/**

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.viewmodel;
import android.app.Application;
import android.widget.Toast;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbCallable;
@@ -11,8 +12,10 @@ import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.Executor;
@@ -20,6 +23,7 @@ import java.util.logging.Logger;
import javax.annotation.concurrent.Immutable;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -27,8 +31,10 @@ import androidx.arch.core.util.Function;
import androidx.core.util.Consumer;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.RecyclerView;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -64,7 +70,7 @@ public abstract class DbViewModel extends AndroidViewModel {
* <p>
* If you need a list of items to be displayed in a
* {@link RecyclerView.Adapter},
* use {@link #loadList(DbCallable, UiConsumer)} instead.
* use {@link #loadFromDb(DbCallable, UiConsumer)} instead.
*/
protected void runOnDbThread(Runnable task) {
dbExecutor.execute(() -> {
@@ -84,7 +90,7 @@ public abstract class DbViewModel extends AndroidViewModel {
* <p>
* If you need a list of items to be displayed in a
* {@link RecyclerView.Adapter},
* use {@link #loadList(DbCallable, UiConsumer)} instead.
* use {@link #loadFromDb(DbCallable, UiConsumer)} instead.
*/
protected void runOnDbThread(boolean readOnly,
DbRunnable<Exception> task, Consumer<Exception> err) {
@@ -102,21 +108,20 @@ public abstract class DbViewModel extends AndroidViewModel {
}
/**
* Loads a list of items on the {@link DatabaseExecutor} within a single
* Loads a data on the {@link DatabaseExecutor} within a single
* {@link Transaction} and publishes it as a {@link LiveResult}
* to the {@link UiThread}.
* <p>
* Use this to ensure that modifications to your local list do not get
* Use this to ensure that modifications to your local UI data do not get
* overridden by database loads that were in progress while the modification
* was made.
* E.g. An event about the removal of a message causes the message item to
* be removed from the local list while all messages are reloaded.
* be removed from the local data set while all messages are reloaded.
* This method ensures that those operations can be processed on the
* UiThread in the correct order so that the removed message will not be
* re-added when the re-load completes.
*/
protected <T extends List<?>> void loadList(
DbCallable<T, DbException> task,
protected <T> void loadFromDb(DbCallable<T, DbException> task,
UiConsumer<LiveResult<T>> uiConsumer) {
dbExecutor.execute(() -> {
try {
@@ -143,25 +148,46 @@ public abstract class DbViewModel extends AndroidViewModel {
}
/**
* Creates a copy of the list available in the given LiveData
* and replaces items where the given test function returns true.
* Creates a copy of the given list and adds the given item to the copy.
*
* @return a copy of the list in the LiveData with item(s) replaced
* or null when the
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function did return false for all items in the list
* </ul>
* @return an updated copy of the list, or null if the list is null
*/
@Nullable
protected <T> List<T> updateListItems(
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test,
Function<T, T> replacer) {
List<T> items = getListCopy(liveData);
if (items == null) return null;
protected <T> List<T> addListItem(@Nullable List<T> list, T item) {
if (list == null) return null;
List<T> copy = new ArrayList<>(list);
copy.add(item);
return copy;
}
ListIterator<T> iterator = items.listIterator();
/**
* Creates a copy of the given list and adds the given items to the copy.
*
* @return an updated copy of the list, or null if the list is null
*/
@Nullable
protected <T> List<T> addListItems(@Nullable List<T> list,
Collection<T> items) {
if (list == null) return null;
List<T> copy = new ArrayList<>(list);
copy.addAll(items);
return copy;
}
/**
* Creates a copy of the given list, replacing items where the given test
* function returns true.
*
* @return an updated copy of the list, or null if either the list is null
* or the test function returns false for all items
*/
@Nullable
protected <T> List<T> updateListItems(@Nullable List<T> list,
Function<T, Boolean> test, Function<T, T> replacer) {
if (list == null) return null;
List<T> copy = new ArrayList<>(list);
ListIterator<T> iterator = copy.listIterator();
boolean changed = false;
while (iterator.hasNext()) {
T item = iterator.next();
@@ -170,28 +196,23 @@ public abstract class DbViewModel extends AndroidViewModel {
iterator.set(replacer.apply(item));
}
}
return changed ? items : null;
return changed ? copy : null;
}
/**
* Creates a copy of the list available in the given LiveData
* and removes the items from it where the given test function returns true.
* Creates a copy of the given list, removing items from it where the given
* test function returns true.
*
* @return a copy of the list in the LiveData with item(s) removed
* or null when the
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function did return false for all items in the list
* </ul>
* @return an updated copy of the list, or null if either the list is null
* or the test function returns false for all items
*/
@Nullable
protected <T> List<T> removeListItems(
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test) {
List<T> items = getListCopy(liveData);
if (items == null) return null;
protected <T> List<T> removeListItems(@Nullable List<T> list,
Function<T, Boolean> test) {
if (list == null) return null;
List<T> copy = new ArrayList<>(list);
ListIterator<T> iterator = items.listIterator();
ListIterator<T> iterator = copy.listIterator();
boolean changed = false;
while (iterator.hasNext()) {
T item = iterator.next();
@@ -200,21 +221,58 @@ public abstract class DbViewModel extends AndroidViewModel {
iterator.remove();
}
}
return changed ? items : null;
return changed ? copy : null;
}
/**
* Retrieves a copy of the list of items from the given LiveData
* or null if it is not available.
* The list copy can be safely mutated.
* Updates the given LiveData with a copy of its list
* with the items removed where the given test function returns true.
* <p>
* Nothing is updated, if the
* <ul>
* <li> LiveData does not have a value
* <li> LiveResult in the LiveData has an error
* <li> test function returned false for all items in the list
* </ul>
*/
@UiThread
protected <T> void removeAndUpdateListItems(
MutableLiveData<LiveResult<List<T>>> liveData,
Function<T, Boolean> test) {
List<T> copy = removeListItems(getList(liveData), test);
if (copy != null) liveData.setValue(new LiveResult<>(copy));
}
/**
* Returns the list of items from the given LiveData, or null if no list is
* available.
*/
@Nullable
private <T> List<T> getListCopy(LiveData<LiveResult<List<T>>> liveData) {
protected <T> List<T> getList(LiveData<LiveResult<List<T>>> liveData) {
LiveResult<List<T>> value = liveData.getValue();
if (value == null) return null;
List<T> list = value.getResultOrNull();
if (list == null) return null;
return new ArrayList<>(list);
return value.getResultOrNull();
}
/**
* Logs the exception and shows a Toast to the user.
* <p>
* Errors that are likely or expected to happen should not use this method
* and show proper error states in UI.
*/
@AnyThread
protected void handleException(Exception e) {
logException(LOG, WARNING, e);
androidExecutor.runOnUiThread(() -> {
String msg = "Error: " + e.getClass().getSimpleName();
if (!StringUtils.isNullOrEmpty(e.getMessage())) {
msg += " " + e.getMessage();
}
if (e.getCause() != null) {
msg += " caused by " + e.getCause().getClass().getSimpleName();
}
Toast.makeText(getApplication(), msg, LENGTH_LONG).show();
});
}
}

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M11,14H9c0,-4.97 4.03,-9 9,-9v2C14.13,7 11,10.13 11,14zM18,11V9c-2.76,0 -5,2.24 -5,5h2C15,12.34 16.34,11 18,11zM7,4c0,-1.11 -0.89,-2 -2,-2S3,2.89 3,4s0.89,2 2,2S7,5.11 7,4zM11.45,4.5h-2C9.21,5.92 7.99,7 6.5,7h-3C2.67,7 2,7.67 2,8.5V11h6V8.74C9.86,8.15 11.25,6.51 11.45,4.5zM19,17c1.11,0 2,-0.89 2,-2s-0.89,-2 -2,-2s-2,0.89 -2,2S17.89,17 19,17zM20.5,18h-3c-1.49,0 -2.71,-1.08 -2.95,-2.5h-2c0.2,2.01 1.59,3.65 3.45,4.24V22h6v-2.5C22,18.67 21.33,18 20.5,18z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM13,14h-2v-2h2v2zM13,10h-2L11,6h2v4z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24"
tools:ignore="NewApi">
<path
android:fillColor="@android:color/white"
android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02zM8,16h2.5l1.5,1.5 1.5,-1.5L16,16v-2.5l1.5,-1.5 -1.5,-1.5L16,8h-2.5L12,6.5 10.5,8L8,8v2.5L6.5,12 8,13.5L8,16zM12,9c1.66,0 3,1.34 3,3s-1.34,3 -3,3L12,9z" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
tools:ignore="NewApi">
<path
android:fillColor="#FF000000"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>

View File

@@ -1,78 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragmentContainer"
android:name="org.briarproject.briar.android.settings.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/avatarGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarImage"
style="@style/BriarAvatar"
android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher_round" />
<TextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:textColor="@color/briar_text_primary_inverse"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toTopOf="@+id/avatarExplanation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="username" />
<TextView
android:id="@+id/avatarExplanation"
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:layout_marginBottom="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:text="@string/change_profile_picture"
android:textColor="@color/briar_text_secondary_inverse"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toBottomOf="@+id/username" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="@+id/fragment"
android:name="org.briarproject.briar.android.settings.SettingsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

View File

@@ -16,27 +16,18 @@
android:id="@+id/rebloggerView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/listitem_vertical_margin"
android:layout_marginLeft="@dimen/listitem_vertical_margin"
android:layout_marginTop="@dimen/listitem_vertical_margin"
android:layout_marginEnd="@dimen/listitem_vertical_margin"
android:layout_marginRight="@dimen/listitem_vertical_margin"
android:layout_marginBottom="@dimen/listitem_horizontal_margin"
android:padding="@dimen/listitem_vertical_margin"
app:layout_constraintEnd_toStartOf="@+id/commentView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:persona="reblogger" />
app:persona="reblogger"
tools:visibility="visible" />
<org.briarproject.briar.android.view.AuthorView
android:id="@+id/authorView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/listitem_vertical_margin"
android:layout_marginLeft="@dimen/listitem_vertical_margin"
android:layout_marginTop="@dimen/listitem_vertical_margin"
android:layout_marginEnd="@dimen/listitem_vertical_margin"
android:layout_marginRight="@dimen/listitem_vertical_margin"
android:layout_marginBottom="@dimen/listitem_horizontal_margin"
android:padding="@dimen/listitem_vertical_margin"
app:layout_constraintEnd_toStartOf="@+id/commentView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rebloggerView" />

View File

@@ -44,7 +44,6 @@
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"
android:widgetLayout="@layout/preference_switch_compat"
tools:checked="true"
tools:text="@string/tor_enable_title" />

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout 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="wrap_content"
tools:layout_width="800dp"
tools:layout_height="75dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/avatarGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarImage"
style="@style/BriarAvatar"
android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher_round" />
<TextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:textColor="@color/briar_text_primary_inverse"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toTopOf="@+id/avatarExplanation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="username" />
<TextView
android:id="@+id/avatarExplanation"
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:layout_marginBottom="16dp"
android:paddingStart="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:text="@string/change_profile_picture"
android:textColor="@color/briar_text_secondary_inverse"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/avatarImage"
app:layout_constraintTop_toBottomOf="@+id/username" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Needed for SwitchPreference on Android 4 (API < 21)-->
<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
tools:targetApi="n" />

View File

@@ -171,7 +171,7 @@
<string name="you">Du</string>
<string name="save_image">Bild speichern</string>
<string name="dialog_title_save_image">Bild speichern?</string>
<string name="dialog_message_save_image">Gespeicherte Bilder können von vielen anderen Apps eingesehen werden.\n\nBist du sicher, dass du das Bild speichern möchtest?</string>
<string name="dialog_message_save_image">Durch das Speichern dieses Bildes können andere Apps auf das Bild zugreifen.\n\nBist du sicher, dass du das Bild speichern möchtest?</string>
<string name="save_image_success">Bild wurde gespeichert</string>
<string name="save_image_error">Bild konnte nicht gespeichert werden</string>
<string name="dialog_title_no_image_support">Bilder nicht verfügbar</string>
@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Tippe, um dein Profilbild zu ändern</string>
<string name="dialog_confirm_profile_picture_title">Profilbild ändern</string>
<string name="dialog_confirm_profile_picture_remark">Nur deine Kontakte können dein Profilbild sehen</string>
<string name="dialog_confirm_profile_picture_remark">Nur deine Kontakte können dieses Bild sehen</string>
<string name="change_profile_picture_failed_message">Es tut uns leid, aber beim Aktualisieren deines Profilbildes ist ein Fehler aufgetreten</string>
<!--Settings Display-->
<string name="pref_language_title">Sprache &amp; Region</string>
@@ -536,6 +536,7 @@
<string name="optional_contact_email">Deine E-Mail-Adresse (optional)</string>
<string name="include_debug_report_crash">Anonymisierte Daten über den Absturz anhängen</string>
<string name="include_debug_report_feedback">Anonymisierte Daten über dieses Gerät anhängen</string>
<string name="dev_report_user_info">Benutzerinformation</string>
<string name="dev_report_basic_info">Basisinformationen</string>
<string name="dev_report_device_info">Geräteinformationen</string>
<string name="dev_report_stacktrace">Stacktrace</string>

View File

@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Pulsar para cambiar la imagen de tu perfil</string>
<string name="dialog_confirm_profile_picture_title">Cambiar imagen de perfil</string>
<string name="dialog_confirm_profile_picture_remark">Solo tus contactos pueden ver la imagen de tu perfil</string>
<string name="dialog_confirm_profile_picture_remark">Solo tus contactos pueden ver esta imagen</string>
<string name="change_profile_picture_failed_message">Lo sentimos, pero algo falló mientras se estaba actualizando la imagen de tu perfil</string>
<!--Settings Display-->
<string name="pref_language_title">Lenguaje &amp; región</string>
@@ -536,6 +536,7 @@
<string name="optional_contact_email">Tu correo electrónico (opcional)</string>
<string name="include_debug_report_crash">Incluir datos anónimos sobre la falla</string>
<string name="include_debug_report_feedback">Incluir datos anónimos sobre este dispositivo</string>
<string name="dev_report_user_info">Información de usuario</string>
<string name="dev_report_basic_info">Información básica</string>
<string name="dev_report_device_info">Información del dispositivo</string>
<string name="dev_report_stacktrace">Traza de pila</string>

View File

@@ -467,7 +467,7 @@
<!--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="dialog_confirm_profile_picture_remark">تنها مخاطبین شما می‌توانند این تصویر را مشاهده کنند.</string>
<string name="change_profile_picture_failed_message">تاسفیم اما هنگام بروزرسانی تصویر نمایه شما مشکلی رخ داد.</string>
<!--Settings Display-->
<string name="pref_language_title">زبان و منطقه</string>
@@ -576,6 +576,7 @@
<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">Stacktrace</string>

View File

@@ -213,7 +213,7 @@
<string name="your_link">Donnez ce lien au contact que vous souhaitez ajouter</string>
<string name="link_clip_label">Lien Briar</string>
<string name="link_copied_toast">Le lien a été copié</string>
<string name="adding_contact_error">Une erreur est survenue lors de lajout du contact</string>
<string name="adding_contact_error">Une erreur est survenue lors de lajout du contact.</string>
<string name="pending_contact_requests_snackbar">Des demandes de contact sont en attente</string>
<string name="pending_contact_requests">Demandes de contact en attente</string>
<string name="no_pending_contacts">Il ny a aucun contact en attente</string>
@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Touchez pour changer votre photo de profil</string>
<string name="dialog_confirm_profile_picture_title">Changer la photo de profil</string>
<string name="dialog_confirm_profile_picture_remark">Seuls vos contacts peuvent voir votre photo de profil</string>
<string name="dialog_confirm_profile_picture_remark">Seuls vos contacts peuvent voir cette photo</string>
<string name="change_profile_picture_failed_message">Nous sommes désolés, mais un problème est survenu lors de la mise à jour de votre photo de profil</string>
<!--Settings Display-->
<string name="pref_language_title">Langue et région</string>
@@ -536,6 +536,7 @@
<string name="optional_contact_email">Votre adresse courriel (facultative)</string>
<string name="include_debug_report_crash">Inclure des données anonymes concernant le plantage</string>
<string name="include_debug_report_feedback">Inclure des données anonymes concernant cet appareil</string>
<string name="dev_report_user_info">Renseignements sur lutilisateur</string>
<string name="dev_report_basic_info">Renseignements de base</string>
<string name="dev_report_device_info">Renseignements sur lappareil</string>
<string name="dev_report_stacktrace">Trace dappels</string>

View File

@@ -2,50 +2,50 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Setup-->
<string name="setup_title">Benvida a Briar</string>
<string name="setup_name_explanation">O seu alcume mostrarase xunto a todas as mensaxes que publique. Pode cambialo tras crear a súa conta.</string>
<string name="setup_name_explanation">O teu alcume mostrarase xunto a todas as mensaxes que publiques. Podes cambialo tras crear a túa conta.</string>
<string name="setup_next">Seguinte</string>
<string name="setup_password_intro">Escolla unha Clave</string>
<string name="setup_password_explanation">A súa conta en Briar gárdase cifrada no seu dispositivo, non na nube. Si esquece o contrasinal ou desinstala Briar, non haberá xeito de recuperar a súa conta.\n\nEscolla unha clave longa que sexa difícil de adiviñar, algo como catro palabras ao chou, ou dez letras aleatorias, números e símbolos.</string>
<string name="setup_password_intro">Elixe un Contrasinal</string>
<string name="setup_password_explanation">A túa conta en Briar gárdase cifrada no teu dispositivo, non na nube. Se esqueces o contrasinal ou desinstalas Briar, non haberá xeito de recuperar a túa conta.\n\nEscolle un contrasinal longo que sexa difícil de adiviñar, algo como catro palabras ao chou, ou dez letras aleatorias, números e símbolos.</string>
<string name="setup_doze_title">Conexións en segundo plano</string>
<string name="setup_doze_intro">Para recibir mensaxes, Briar precisa estar conectada en segundo plano.</string>
<string name="setup_doze_explanation">Para recibir mensaxes, Briar precisa estar conectada en segundo plano. Por favor desactive as optimizacións de batería para que Briar poida permanecer conectada.</string>
<string name="setup_doze_explanation">Para recibir mensaxes, Briar precisa estar conectada en segundo plano. Por favor desactiva as optimizacións de batería para que Briar poida permanecer conectada.</string>
<string name="setup_doze_button">Permitir conexións</string>
<string name="choose_nickname">Escolle o teu alcume</string>
<string name="choose_password">Escolle a túa clave</string>
<string name="confirm_password">Confirma a túa clave</string>
<string name="choose_password">Elixe o teu contrasinal</string>
<string name="confirm_password">Confirma o teu contrasinal</string>
<string name="name_too_long">O nome é demasiado longo</string>
<string name="password_too_weak">A clave é demasiado débil</string>
<string name="passwords_do_not_match">As claves non coinciden</string>
<string name="create_account_button">Crea a conta</string>
<string name="password_too_weak">O contrasinal é demasiado débil</string>
<string name="passwords_do_not_match">Os contrasinais non concordan</string>
<string name="create_account_button">Crea unha conta</string>
<string name="more_info">Máis información</string>
<string name="don_t_ask_again">Non preguntar de novo</string>
<string name="setup_huawei_text">Por favor toque o botón inferior e asegúrese de que Briar está protexida na pantalla \"Apps Protexidas\"</string>
<string name="setup_huawei_text">Por favor toca o botón inferior e asegúrate de que Briar está protexida na pantalla \"Apps Protexidas\"</string>
<string name="setup_huawei_button">Protexer Briar</string>
<string name="setup_huawei_help">Si Briar non se engade ao listado de apps protexidas, non poderá funcionar en segundo plano.</string>
<string name="setup_huawei_help">Se Briar non se engade ao listado de apps protexidas, non poderá funcionar en segundo plano.</string>
<string name="warning_dozed">%s non foi quen de funcionar en segundo plano</string>
<!--Login-->
<string name="enter_password">Clave</string>
<string name="try_again">Clave incorrecta, tenteo de novo</string>
<string name="enter_password">Contrasinal</string>
<string name="try_again">Contrasinal incorrecto, tenteo de novo</string>
<string name="dialog_title_cannot_check_password">Non se comprobou o contrasinal</string>
<string name="dialog_message_cannot_check_password">Briar non pode comprobar o contrasinal. Intenta reiniciar o dispositivo para solucionar o problema.</string>
<string name="sign_in_button">Iniciar sesión</string>
<string name="forgotten_password">Esquecín a miña clave</string>
<string name="dialog_title_lost_password">Clave perdida</string>
<string name="dialog_message_lost_password">Briar almacena a súa configuración encriptada no dispositivo, non na nube, así que non podemos restabelecer a súa clave. Quererías borrar a túa conta e empezar de novo?\n\nPrecaución: As túas identidades, contactos e mensaxes serán eliminadas de forma permanente.</string>
<string name="dialog_title_lost_password">Contrasinal perdido</string>
<string name="dialog_message_lost_password">Briar almacena a túa configuración cifrada no dispositivo, non na nube, así que non podemos restabelecer o teu contrasinal. Quererías borrar a túa conta e empezar de novo?\n\nPrecaución: As túas identidades, contactos e mensaxes serán eliminadas de forma permanente.</string>
<string name="startup_failed_notification_title">Briar non puido iniciarse</string>
<string name="startup_failed_notification_text">Toque para máis información.</string>
<string name="startup_failed_notification_text">Toca para máis información.</string>
<string name="startup_failed_activity_title">Fallo de Inicio de Briar</string>
<string name="startup_failed_db_error">Por algún motivo, a súa base de datos de Briar está defectuosa sen remedio. A súa conta, os seus datos e contactos perdéronse. Desgraciadamente, debe reinstalar Briar ou crear unha nova conta escollendo \'Esquecín o meu contrasinal\' cando se lle solicite a clave.</string>
<string name="startup_failed_data_too_old_error">A súa conta foi creada cunha versión anterior da aplicación e non se pode abrir con esta versión. Deberá reinstalar a versión anterior ou ben crear unha nova conta escollendo \'Esquecín o meu contrasinal\' cando se lle solicite o contrasinal.</string>
<string name="startup_failed_data_too_new_error">Esta versión da app é moi antiga. Actualice por favor á última versión e inténteo de novo.</string>
<string name="startup_failed_service_error"> Briar non puido iniciar un complemento necesario. Xeralmente reinstalar Briar resolve este problema. Teña en conta que entón perderá a súa conta e todos os datos asociados a esta pois Briar non está a utilizar servidores centrais para almacenar os seus datos.</string>
<string name="startup_failed_db_error">Por algún motivo, a túa base de datos de Briar está defectuosa sen remedio. A túa conta, os teus datos e contactos perdéronse. Desgraciadamente, debes reinstalar Briar ou crear unha nova conta escollendo \'Esquecín o meu contrasinal\' cando se che solicite o contrasinal.</string>
<string name="startup_failed_data_too_old_error">A túa conta foi creada cunha versión anterior da aplicación e non se pode abrir con esta versión. Deberás reinstalar a versión anterior ou ben crear unha nova conta escollendo \'Esquecín o meu contrasinal\' cando se che solicite o contrasinal.</string>
<string name="startup_failed_data_too_new_error">Esta versión da app é moi antiga. Actualiza por favor á última versión e inténtao de novo.</string>
<string name="startup_failed_service_error">Briar non puido iniciar un complemento necesario. Normalmente ao reinstalar Briar solucionase este problema. Ten en conta que entón perderás a túa conta e todos os datos asociados a esta pois Briar non está a utilizar servidores centrais para almacenar os teus datos.</string>
<plurals name="expiry_warning">
<item quantity="one">Esta versión de Briar é para probas. A conta caducará en %d día e non se pode anovar.</item>
<item quantity="other">Esta é unha versión de proba de Briar. A conta caducará en %d días e non se pode anovar.</item>
</plurals>
<string name="expiry_date_reached">Este software caducou.\nGrazas por probalo!</string>
<string name="download_briar">Para seguir utilizando Briar, descarga por favor a última versión.</string>
<string name="create_new_account">Precisa crear unha nova conta, pero pode utilizar o mesmo alcume.</string>
<string name="create_new_account">Precisas crear unha nova conta, pero podes utilizar o mesmo alcume.</string>
<string name="download_briar_button">Descargar Última Versión</string>
<string name="startup_open_database">Descifrando a Base de datos...</string>
<string name="startup_migrate_database">Actualizando a Base de datos...</string>
@@ -92,12 +92,12 @@
<string name="bt_plugin_status_inactive">Briar non pode conectar por Bluetooth</string>
<string name="bt_plugin_status_disabled">Briar está configurada para non usar Bluetooth</string>
<!--Notifications-->
<string name="reminder_notification_title">Desconectou de Briar</string>
<string name="reminder_notification_text">Toque para voltar a conectar</string>
<string name="reminder_notification_title">Desconectaches de Briar</string>
<string name="reminder_notification_text">Toca para volver a conectar</string>
<string name="reminder_notification_channel_title">Recordatorio para conectar a Briar</string>
<string name="reminder_notification_dismiss">Desbotar</string>
<string name="ongoing_notification_title">Conectado a Briar</string>
<string name="ongoing_notification_text">Toque para abrir Briar</string>
<string name="ongoing_notification_title">Conectada a Briar</string>
<string name="ongoing_notification_text">Toca para abrir Briar</string>
<plurals name="private_message_notification_text">
<item quantity="one">Nova mensaxe privada.</item>
<item quantity="other">%d novas mensaxes privadas.</item>
@@ -136,15 +136,15 @@
<string name="show_onboarding">Amosar xanela de axuda</string>
<string name="fix">Arranxar</string>
<string name="help">Axuda</string>
<string name="sorry">Desculpe</string>
<string name="sorry">Desculpa</string>
<string name="error_start_activity">Non dispoñible para o teu sistema</string>
<string name="status_heading">Estado</string>
<string name="status_heading">Estado:</string>
<!--Contacts and Private Conversations-->
<string name="no_contacts">Sen contactos para amosar</string>
<string name="no_contacts_action">Toque a icona + para engadir un contacto</string>
<string name="no_contacts">Sen contactos que amosar</string>
<string name="no_contacts_action">Toca na icona + para engadir un contacto</string>
<string name="date_no_private_messages">Sen mensaxes</string>
<string name="no_private_messages">Sen mensaxes que amosar</string>
<string name="message_hint">Esciba unha mensaxe</string>
<string name="message_hint">Escibe unha mensaxe</string>
<string name="image_caption_hint">Engadir un comentario (optativo)</string>
<string name="image_attach">Anexar imaxe</string>
<string name="image_attach_error">Non se anexaron imaxe(s)</string>
@@ -152,11 +152,11 @@
<string name="image_attach_error_invalid_mime_type">Formato non admitido: %s</string>
<string name="set_contact_alias">Cambiar o nome do contacto</string>
<string name="set_contact_alias_hint">Nome do contacto</string>
<string name="delete_all_messages">Borrar todas as mensaxes</string>
<string name="delete_all_messages">Borrar tódalas mensaxes</string>
<string name="dialog_title_delete_all_messages">Confirmar borrado de mensaxes</string>
<string name="dialog_message_delete_all_messages">Seguro que queres borrar todas as mensaxes?</string>
<string name="dialog_title_not_all_messages_deleted">Non se puideron borrar todas as mensaxes</string>
<string name="dialog_message_not_deleted_ongoing_both">As mensaxes relacionadas con convites actuais e presentacións non se poden borrar ata que conclúan.</string>
<string name="dialog_message_delete_all_messages">Seguro que queres borrar tódalas mensaxes?</string>
<string name="dialog_title_not_all_messages_deleted">Non se puideron borrar todalas mensaxes</string>
<string name="dialog_message_not_deleted_ongoing_both">As mensaxes relacionadas con convites e presentacións actuais non se poden borrar ata que conclúan.</string>
<string name="dialog_message_not_deleted_ongoing_introductions">As mensaxes relacionadas con presentacións en curso non se poden borrar ata que conclúan.</string>
<string name="dialog_message_not_deleted_ongoing_invitations">As mensaxes relacionadas con convites en curso non se poden borrar ata que conclúan.</string>
<string name="dialog_message_not_deleted_partly_downloaded">As mensaxes parcialmente descargadas non se poden borrar ata que rematen de descargarse.</string>
@@ -164,71 +164,71 @@
<string name="dialog_message_not_deleted_not_all_selected_introductions">Para borrar unha presentación, debes seleccionar a solicitude e a resposta.</string>
<string name="dialog_message_not_deleted_not_all_selected_invitations">Para borrar un convite, debes seleccionar a solicitude e a resposta.</string>
<string name="delete_contact">Eliminar contacto</string>
<string name="dialog_title_delete_contact">Confirme a eliminación do contacto</string>
<string name="dialog_title_delete_contact">Confirma a eliminación do contacto</string>
<string name="dialog_message_delete_contact">Segura de querer eliminar este contacto e todas as mensaxes que intercambiaron?</string>
<string name="contact_deleted_toast">Contacto eliminado</string>
<!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">Vostede</string>
<string name="you">Ti</string>
<string name="save_image">Gardar imaxe</string>
<string name="dialog_title_save_image">Gardar imaxe?</string>
<string name="dialog_message_save_image">Ao gardar esta imaxe permitirá que outras apps teñan acceso a ela.\n\nSeguro que a quere gardar?</string>
<string name="dialog_message_save_image">Ao gardar esta imaxe permitirás que outras apps teñan acceso a ela.\n\nSeguro que a queres gardar?</string>
<string name="save_image_success">Gardouse a imaxe</string>
<string name="save_image_error">Non se gardou a imaxe</string>
<string name="dialog_title_no_image_support">Imaxes non dispoñibles</string>
<string name="dialog_message_no_image_support">O cliente do seu contacto Briar non admite anexos de imaxes. Unha vez actualice verá unha icona diferente.</string>
<string name="dialog_title_image_support">Xa pode enviar imaxes a este contacto</string>
<string name="dialog_message_image_support">Toque en esta icona para anexar imaxes.</string>
<string name="dialog_message_no_image_support">O cliente do teu contacto Briar non admite anexos de imaxes. Unha vez actualice verá unha icona diferente.</string>
<string name="dialog_title_image_support">Xa podes enviar imaxes a este contacto</string>
<string name="dialog_message_image_support">Toque nesta icona para anexar imaxes.</string>
<string name="messaging_too_many_attachments_toast">Só se enviarán as primeiras %d imaxes</string>
<!--Adding Contacts-->
<string name="add_contact_title">Engadir contacto próximo</string>
<string name="face_to_face">Debe atoparse coa persoa que quere engadir como contacto.\n\Isto evitará que calquera poida suplantala ou ler as súas mensaxes no futuro.</string>
<string name="face_to_face">Debes verte coa persoa que queres engadir como contacto.\n\Isto evitará que calquera poida suplantarte ou ler as túas mensaxes no futuro.</string>
<string name="continue_button">Continuar</string>
<string name="try_again_button">Tenteo de novo</string>
<string name="try_again_button">Inténtao de novo</string>
<string name="waiting_for_contact_to_scan">Agardando polo contacto para escanear e conectar\u2026</string>
<string name="exchanging_contact_details">Intercambiando detalles do contacto\u2026</string>
<string name="contact_added_toast">Contacto engadido: %s</string>
<string name="contact_already_exists">O contacto %s xa existe</string>
<string name="qr_code_invalid">O código QR non é válido</string>
<string name="qr_code_too_old">O código QR que escaneou procede de unha versión anterior de %s.\n\nPor favor, solicite ao seu contacto actualizar a nova versión e inténteo de novo.</string>
<string name="qr_code_too_new">O código QR que escaneou procede de unha nova versión de %s.\n\nPor favor, actualice a última versión e inténteo de novo.</string>
<string name="qr_code_too_old">O código QR que escaneaches procede dunha versión anterior de %s.\n\nPor favor, solicite ao teu contacto actualizar a nova versión e inténtao de novo.</string>
<string name="qr_code_too_new">O código QR que escaneaches procede dunha nova versión de %s.\n\nPor favor, actualiza a última versión e inténteo de novo.</string>
<string name="camera_error">Fallo na cámara</string>
<string name="connecting_to_device">Conectando co dispositivo\u2026</string>
<string name="authenticating_with_device">Autenticándose co dispositivo\u2026</string>
<string name="connection_error_title">Non se puido conectar co contacto</string>
<string name="connection_error_feedback">Si persiste o problema,<a href="feedback">envíe un informe</a> para axudarnos a mellorar a app.</string>
<string name="connection_error_feedback">Se persiste o problema,<a href="feedback">envía un informe</a> para axudarnos a mellorar a app.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Engadir contacto a distancia</string>
<string name="add_contact_nearby_title">Engadir contacto próximo</string>
<string name="add_contact_remotely_title">Engadir contacto a distancia</string>
<string name="contact_link_intro">Introducir aquí a ligazón do seu contacto</string>
<string name="contact_link_intro">Introducir aquí a ligazón do teu contacto</string>
<string name="contact_link_hint">Ligazón do contacto</string>
<string name="paste_button">Pegar</string>
<string name="add_contact_button">Engadir contacto</string>
<string name="copy_button">Copiar</string>
<string name="share_button">Compartir</string>
<string name="send_link_title">Intercambiar ligazóns</string>
<string name="add_contact_choose_nickname">Escoller alcume</string>
<string name="add_contact_choose_a_nickname">Introducir alcume</string>
<string name="nickname_intro">Déalle un alcume ao contacto. Só vostede pode velo.</string>
<string name="your_link">Envíe esta ligazón ao contacto que quere engadir</string>
<string name="add_contact_choose_nickname">Elexir alcume</string>
<string name="add_contact_choose_a_nickname">Escribe un alcume</string>
<string name="nickname_intro">Dálle un alcume ao contacto. Só ti pode velo.</string>
<string name="your_link">Envía esta ligazón ao contacto que queres engadir</string>
<string name="link_clip_label">Ligazón Briar</string>
<string name="link_copied_toast">Ligazón copiada</string>
<string name="adding_contact_error">Algo fallou ao engadir o contacto.</string>
<string name="pending_contact_requests_snackbar">Existen solicitudes de contactos pendentes</string>
<string name="pending_contact_requests">Solicitudes Pendentes de Contactos</string>
<string name="pending_contact_requests">Solicitudes de Contacto pendentes</string>
<string name="no_pending_contacts">Sen contactos pendentes</string>
<string name="waiting_for_contact_to_come_online">Agardando a que o contacto se conecte...</string>
<string name="connecting">Conectando...</string>
<string name="adding_contact">Engadindo contacto...</string>
<string name="adding_contact_failed">Fallou engadir contacto</string>
<string name="dialog_title_remove_pending_contact">Confirme a eliminación</string>
<string name="dialog_message_remove_pending_contact">Este contacto aínda está a ser engadido. Se o elimina agora, non será engadido.</string>
<string name="dialog_title_remove_pending_contact">Confirma a eliminación</string>
<string name="dialog_message_remove_pending_contact">Este contacto aínda está a ser engadido. Se o eliminas agora, non será engadido.</string>
<string name="own_link_error">Introduza a ligazón do contacto, non o propio</string>
<string name="nickname_missing">Por favor introduza un alcume</string>
<string name="nickname_missing">Por favor escribe un alcume</string>
<string name="invalid_link">Ligazón non válida</string>
<string name="unsupported_link">Esta ligazón chega desde unha versión nova de Briar. Por favor, actualice a nova versión e inténteo de novo.</string>
<string name="unsupported_link">Esta ligazón chega desde unha versión nova de Briar. Por favor, actualiza a nova versión e inténtao de novo.</string>
<string name="intent_own_link">Abreu a súa propia ligazón. Utilice a do contacto que quere engadir!</string>
<string name="missing_link">Por favor introduza ligazón</string>
<string name="missing_link">Por favor escribe unha ligazón</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-->
@@ -239,7 +239,7 @@
</plurals>
<string name="offline_state">Sen conexión a internet</string>
<string name="duplicate_link_dialog_title">Duplicar Ligazón</string>
<string name="duplicate_link_dialog_text_1">Xa ten un contacto pendente con esta ligazón: %s</string>
<string name="duplicate_link_dialog_text_1">Xa tes un contacto pendente con esta ligazón: %s</string>
<string name="duplicate_link_dialog_text_1_contact">Xa tes un contacto con esta ligazón: %s</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<string name="duplicate_link_dialog_text_2">Son %s e %s a mesma persoa?</string>
@@ -251,65 +251,65 @@
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">Diferente Persoa</string>
<string name="duplicate_link_dialog_text_3">%s e %s enviaronlle a mesma ligazón.\n\nUnha delas podería estar a intentar descubrir os seus contactos.\n\nNon lles diga que recibeu a mesma ligazón de alguén máis.</string>
<string name="duplicate_link_dialog_text_3">%s e %s enviáronche a mesma ligazón.\n\nUnha delas podería estar a intentar descubrir os teus contactos.\n\nNon lles digas que recibiches a mesma ligazón de alguén máis.</string>
<string name="pending_contact_updated_toast">Contacto pendente actualizado</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Presente aos seus contactos</string>
<string name="introduction_onboarding_text">Pode presentar aos seus contactos, así non precisan encontrarse en persoa para conectar a través de Briar.</string>
<string name="introduction_menu_item">Preséntese</string>
<string name="introduction_activity_title">Escolla contacto</string>
<string name="introduction_not_possible">Xa ten unha presentación en progreso con estes contactos. Por favor, deixe que remate o proceso. Isto podería levar algún tempo se vostede ou os contactos raramente se conectan.</string>
<string name="introduction_message_title">Introducir Contactos</string>
<string name="introduction_onboarding_title">Presenta aos seus contactos</string>
<string name="introduction_onboarding_text">Podes presentar aos teus contactos, así non precisan encontrarse en persoa para conectar a través de Briar.</string>
<string name="introduction_menu_item">Preséntate</string>
<string name="introduction_activity_title">Elixe Contacto</string>
<string name="introduction_not_possible">Xa tes unha presentación en progreso con estes contactos. Por favor, deixa que remate o proceso. Isto podería levar algún tempo se ti ou os contactos raramente se conectan.</string>
<string name="introduction_message_title">Presentar Contactos</string>
<string name="introduction_message_hint">Engadir unha mensaxe (opcional)</string>
<string name="introduction_button">Preséntese</string>
<string name="introduction_sent">Enviouse a súa presentación.</string>
<string name="introduction_button">Preséntate</string>
<string name="introduction_sent">Enviouse a túa presentación.</string>
<string name="introduction_error">Algo fallou ao enviar a presentación.</string>
<string name="introduction_request_sent">Solicitou presentar %1$s a %2$s.</string>
<string name="introduction_request_received">%1$s solicitou presentala a %2$s. Quere engadir a %2$s ao seu listado de contactos?</string>
<string name="introduction_request_exists_received">%1$s solicitou presentala a %2$s, pero %2$s xa está no seu listado de contactos. Xa que %1$s podería non sabelo, pode responder igualmente:</string>
<string name="introduction_request_answered_received">%1$s solicitou presentala a %2$s.</string>
<string name="introduction_response_accepted_sent">Aceptou a presentación a %1$s.</string>
<string name="introduction_response_accepted_sent_info">Antes de engadir %1$s aos seus contactos, eles precisan aceptar a presentación tamén. Esto podería levar algún tempo.</string>
<string name="introduction_response_declined_sent">Vostede rexeitou a presentación a %1$s.</string>
<string name="introduction_request_sent">Solicitaches presentar %1$s a %2$s.</string>
<string name="introduction_request_received">%1$s solicitou presentarche a %2$s. Queres engadir a %2$s á túa lista de contactos?</string>
<string name="introduction_request_exists_received">%1$s solicitou presentarte a %2$s, pero %2$s xa estás na súa lista de contactos. Xa que %1$s podería non sabelo, podes responder igualmente:</string>
<string name="introduction_request_answered_received">%1$s solicitou presentarte a %2$s.</string>
<string name="introduction_response_accepted_sent">Aceptaches a presentación a %1$s.</string>
<string name="introduction_response_accepted_sent_info">Antes de engadir a %1$s aos teus contactos, eles precisan aceptar a presentación tamén. Esto podería levar algún tempo.</string>
<string name="introduction_response_declined_sent">Rexeitaches a presentación a %1$s.</string>
<string name="introduction_response_accepted_received">%1$s aceptou a presentación a %2$s.</string>
<string name="introduction_response_declined_received">%1$s rexeitou a presentación a %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s di que %2$srexeitou a presentación.</string>
<!--Private Groups-->
<string name="groups_list_empty">Sen grupos que amosar</string>
<string name="groups_list_empty_action">Toque a icona + para crear un grupo, ou solicite aos contactos que compartan grupos con vostede</string>
<string name="groups_list_empty_action">Toca a icona + para crear un grupo, ou solicita aos contactos que compartan grupos contigo</string>
<string name="groups_created_by">Creado por %s</string>
<plurals name="messages">
<item quantity="one">%d mensaxe</item>
<item quantity="other">%d mensaxes</item>
</plurals>
<string name="groups_group_is_empty">Este grupo está valeiro</string>
<string name="groups_group_is_empty">Este grupo está baleiro</string>
<string name="groups_group_is_dissolved">Este grupo foi disolto</string>
<string name="groups_remove">Eliminar</string>
<string name="groups_create_group_title">Crear Grupo Privado</string>
<string name="groups_create_group_button">Crear Grupo</string>
<string name="groups_create_group_invitation_button">Enviar Convite</string>
<string name="groups_create_group_hint">Escolla un nome para o seu grupo privado</string>
<string name="groups_create_group_hint">Escolle un nome para o teu grupo privado</string>
<string name="groups_invitation_sent">Enviouse o convite de grupo</string>
<string name="groups_member_list">Lista de Membros</string>
<string name="groups_invite_members">Convidar a Membros</string>
<string name="groups_member_created_you">Vostede creou o grupo</string>
<string name="groups_member_created_you">Creaches o grupo</string>
<string name="groups_member_created">%s creou o grupo</string>
<string name="groups_member_joined_you">Vostede ingresou no grupo</string>
<string name="groups_member_joined_you">Entraches no grupo</string>
<string name="groups_member_joined">%s uníuse ao grupo</string>
<string name="groups_leave">Deixar Grupo</string>
<string name="groups_leave_dialog_title">Confirme que deixa o Grupo</string>
<string name="groups_leave_dialog_message">Está certo de que quere deixar este grupo?</string>
<string name="groups_leave_dialog_title">Confirma que deixas o Grupo</string>
<string name="groups_leave_dialog_message">Tes a certeza de querer deixar este grupo?</string>
<string name="groups_dissolve">Desfacer o grupo</string>
<string name="groups_dissolve_dialog_title">Confirme a disolución do grupo</string>
<string name="groups_dissolve_dialog_title">Confirma a disolución do grupo</string>
<string name="groups_dissolve_dialog_message">Segura de querer desfacer o grupo?\n\nOs restantes membros non poderán continuar as súas conversas e poderían non recibir as últimas mensaxes.</string>
<string name="groups_dissolve_button">Desfacer</string>
<string name="groups_dissolved_dialog_title">O grupo foi desfeito</string>
<string name="groups_dissolved_dialog_message">A persoa creadora do grupo desfíxoo.\n\nXa non poderá escribir mensaxes ao grupo e podería non ter recibido todas as mensaxes que foran escritas.</string>
<string name="groups_dissolved_dialog_title">O grupo foi destruído</string>
<string name="groups_dissolved_dialog_message">A persoa creadora do grupo desfíxoo.\n\nXa non poderás escribir mensaxes ao grupo e poderías non ter recibido tódalas mensaxes que foran escritas.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">Convites de Grupo</string>
<string name="groups_invitations_invitation_sent">Convidou a %1$s a unirse ao grupo \"%2$s\".</string>
<string name="groups_invitations_invitation_received">%1$s convidouna a unirse ao grupo \"%2$s\".</string>
<string name="groups_invitations_joined">Xa está no grupo</string>
<string name="groups_invitations_invitation_sent">Convidaches a %1$s a unirse ao grupo \"%2$s\".</string>
<string name="groups_invitations_invitation_received">%1$s convidoute a unirte ao grupo \"%2$s\".</string>
<string name="groups_invitations_joined">Xa estás no grupo</string>
<string name="groups_invitations_declined">Rexeitou o convite ao grupo</string>
<plurals name="groups_invitations_open">
<item quantity="one">%d convite de grupo aberto</item>
@@ -322,16 +322,16 @@
<string name="sharing_status_groups">Só a persoa creadora poder convidar a novos membros ao grupo. Abaixo está a membresía do grupo.</string>
<!--Private Groups Revealing Contacts-->
<string name="groups_reveal_contacts">Revelar contactos</string>
<string name="groups_reveal_dialog_message">Pode escoller si revela os contactos a todos os compoñentes actuais e futuros do grupo.\n\Si revela os contactos a conexión do grupo será máis rápida e fiable, xa que poderá comunicarse cos contactos revelados incluso si a creadora do grupo non está en liña.</string>
<string name="groups_reveal_visible">A relación do contacto é visible para o grupo</string>
<string name="groups_reveal_visible_revealed_by_us">A relación co contacto é visible para o grupo (revelada por vostede)</string>
<string name="groups_reveal_dialog_message">Podes escoller se revelas os contactos a todos os compoñentes actuais e futuros do grupo.\n\Se revelas os contactos a conexión do grupo será máis rápida e fiable, xa que poderá comunicarse cos contactos revelados incluso se a creadora do grupo non está en liña.</string>
<string name="groups_reveal_visible">As relacións dos contactos son visibles para o grupo</string>
<string name="groups_reveal_visible_revealed_by_us">A relación co contacto é visible para o grupo (revelada por ti)</string>
<string name="groups_reveal_visible_revealed_by_contact">A relación con contacto é visible para o grupo (revelada por %s)</string>
<string name="groups_reveal_invisible">A relación co contacto non é visible para o grupo</string>
<!--Forums-->
<string name="no_forums">Sen foros que mostrar</string>
<string name="no_forums_action">Toque na icona + para crear un foro, ou pida aos contactos que compartan foros con vostede</string>
<string name="no_forums_action">Toca na icona + para crear un foro, ou pídelle aos contactos que compartan foros contigo</string>
<string name="create_forum_title">Crear Foro</string>
<string name="choose_forum_hint">Escolla un nome para o seu foro</string>
<string name="choose_forum_hint">Escolle un nome para o teu foro</string>
<string name="create_forum_button">Crear Foro</string>
<string name="forum_created_toast">Foro creado</string>
<string name="no_forum_posts">Sen publicacións que mostrar</string>
@@ -342,35 +342,35 @@
</plurals>
<string name="forum_new_message_hint">Nova publicación</string>
<string name="forum_message_reply_hint">Nova Resposta</string>
<string name="btn_reply">Respostar</string>
<string name="btn_reply">Responder</string>
<string name="forum_leave">Deixar foro</string>
<string name="dialog_title_leave_forum">Confirme a saída do foro</string>
<string name="dialog_message_leave_forum">Segura de querer deixar este foro?\n\nTodos os contactos cos que comparteu este foro poderían deixar de recibir actualizacións.</string>
<string name="dialog_title_leave_forum">Confirma a saída do foro</string>
<string name="dialog_message_leave_forum">Segura de querer deixar este foro?\n\nTodos os contactos cos que compartiches este foro poderían deixar de recibir actualizacións.</string>
<string name="dialog_button_leave">Saír</string>
<string name="forum_left_toast">Saír do foro</string>
<!--Forum Sharing-->
<string name="forum_share_button">Compartir Foro</string>
<string name="contacts_selected">Contactos selecionados</string>
<string name="activity_share_toolbar_header">Escolla Contactos</string>
<string name="no_contacts_selector">Sen contactos a mostrar</string>
<string name="no_contacts_selector_action">Volte aquí tras engadir un contacto</string>
<string name="activity_share_toolbar_header">Elixe Contactos</string>
<string name="no_contacts_selector">Sen contactos que amosar</string>
<string name="no_contacts_selector_action">Volve aquí tras engadir un contacto</string>
<string name="forum_shared_snackbar">Foro compartido cos contactos escollidos</string>
<string name="forum_share_message">Engadir unha mensaxe (opcional)</string>
<string name="forum_share_error">Algo fallou ao compartir este foro.</string>
<string name="forum_invitation_received">%1$s compartiu este foro \"%2$s\" con vostede.</string>
<string name="forum_invitation_received">%1$s compartiu este foro \"%2$s\" contigo.</string>
<string name="forum_invitation_sent">Compartiu este foro \"%1$s\" con %2$s.</string>
<string name="forum_invitations_title">Convites a foros</string>
<string name="forum_invitation_exists">Xa aceptara o convite a este foro\n\nAceptando máis convites fará a súa conexión ao foro máis rápida e fiable.</string>
<string name="forum_joined_toast">Uniuse ao foro</string>
<string name="forum_declined_toast">Rexeitou o convite</string>
<string name="forum_invitation_exists">Xa aceptaras o convite a este foro\n\nAceptando máis convites farás a túa conexión ao foro máis rápida e fiable.</string>
<string name="forum_joined_toast">Unícheste ao foro</string>
<string name="forum_declined_toast">Rexeitaches o convite</string>
<string name="shared_by_format">Compartido por %s</string>
<string name="forum_invitation_already_sharing">Xa compartindo</string>
<string name="forum_invitation_response_accepted_sent">Aceptou o convite de %s ao foro.</string>
<string name="forum_invitation_response_declined_sent">Rexeitou o convite de %s ao foro.</string>
<string name="forum_invitation_response_accepted_sent">Aceptaches o convite de %s ao foro.</string>
<string name="forum_invitation_response_declined_sent">Rexeitaches o convite de %s ao foro.</string>
<string name="forum_invitation_response_accepted_received">%s aceptou o convite ao foro.</string>
<string name="forum_invitation_response_declined_received">%s rexeitou o convite ao foro.</string>
<string name="sharing_status">Estado do compartido</string>
<string name="sharing_status_forum">Calquera compoñente do foro pode compartilo cos seus contactos. Vostede pode compartir este foro cos seguintes contactos. Pode haber outras persoas que vostede non pode ver.</string>
<string name="sharing_status_forum">Calquera compoñente do foro pode compartilo cos seus contactos. Tie pode compartir este foro cos seguintes contactos. Pode haber outras persoas que non podes ver.</string>
<string name="shared_with">Compartido con %1$d (%2$d en liña)</string>
<plurals name="forums_shared">
<item quantity="one">%d foro compartido por contactos</item>
@@ -381,15 +381,15 @@
<string name="blogs_other_blog_empty_state">Sen publicacións que mostrar</string>
<string name="read_more">ler mais</string>
<string name="blogs_write_blog_post">Escribir entrada de Blog</string>
<string name="blogs_write_blog_post_body_hint">Escriba a súa entrada no blog</string>
<string name="blogs_write_blog_post_body_hint">Escribe o artigo no blog</string>
<string name="blogs_publish_blog_post">Publicar</string>
<string name="blogs_blog_post_created">Entrada no blog creada</string>
<string name="blogs_blog_post_received">Nova entrada de blog recibida</string>
<string name="blogs_blog_post_scroll_to">Desplazarse a</string>
<string name="blogs_blog_post_scroll_to">Desprazarse a</string>
<string name="blogs_feed_empty_state">Sen publicacións que mostrar</string>
<string name="blogs_feed_empty_state_action">Publicacións dos contactos e blogs aos que se suscriba aparecerán aquí\n\nToque na icona do lápis para encribir unha entrada</string>
<string name="blogs_feed_empty_state_action">Publicacións dos contactos e blogs aos que te suscribas aparecerán aquí\n\nToca na icona do lapis para encribir unha entrada</string>
<string name="blogs_remove_blog">Eliminar Blog</string>
<string name="blogs_remove_blog_dialog_message">Segura de que quere eliminar este blog?\n\nAs entradas eliminaranse do seu dispositivo pero non dos dispositivos de outras persoas.\n\nCalquera contacto co que compartira este blog podería deixar de recibir actualizacións.</string>
<string name="blogs_remove_blog_dialog_message">Tes a certeza de querer eliminar este blog?\n\nAs entradas eliminaranse do teu dispositivo pero non dos dispositivos doutras persoas.\n\nCalquera contacto co que compartira este blog podería deixar de recibir actualizacións.</string>
<string name="blogs_remove_blog_ok">Eliminar</string>
<string name="blogs_blog_removed">Blog eliminado</string>
<string name="blogs_reblog_comment_hint">Engadir un comentario (optativo)</string>
@@ -399,46 +399,46 @@
<string name="blogs_sharing_error">Algo fallou ao compartir o blog</string>
<string name="blogs_sharing_button">Compartir blog</string>
<string name="blogs_sharing_snackbar">Blog compartido cos contactos escollidos</string>
<string name="blogs_sharing_response_accepted_sent">Aceptou o convite ao blog de %s.</string>
<string name="blogs_sharing_response_declined_sent">Rexeitou o convite ao blog de %s.</string>
<string name="blogs_sharing_response_accepted_sent">Aceptaches o convite ao blog de %s.</string>
<string name="blogs_sharing_response_declined_sent">Rexeitaches o convite ao blog de %s.</string>
<string name="blogs_sharing_response_accepted_received">%s aceptou o convite ao blog.</string>
<string name="blogs_sharing_response_declined_received">%s rexeitou o convite ao blog.</string>
<string name="blogs_sharing_invitation_received">%1$s compartiu o blog \"%2$s\" con vostede.</string>
<string name="blogs_sharing_invitation_sent">Vostede compartiu o blog \"%1$s\" con %2$s.</string>
<string name="blogs_sharing_invitation_received">%1$s compartiu o blog \"%2$s\" contigo.</string>
<string name="blogs_sharing_invitation_sent">Compartiches o blog \"%1$s\" con %2$s.</string>
<string name="blogs_sharing_invitations_title">Convites a Blog</string>
<string name="blogs_sharing_joined_toast">Subscrita ao blog</string>
<string name="blogs_sharing_declined_toast">Rexeitou o convite</string>
<string name="sharing_status_blog">Calquera que se subscribe a un blog pode compartilo cos seus contactos. Pode compartir este blog cos seguintes contactos. Podería haber outras subscritoras que vostede non pode ver.</string>
<string name="blogs_sharing_declined_toast">Rexeitaches o convite</string>
<string name="sharing_status_blog">Calquera que se subscribe a un blog pode compartilo cos seus contactos. Podes compartir este blog cos seguintes contactos. Podería haber outras subscritoras que ti non podes ver.</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import">Importar fonte RSS</string>
<string name="blogs_rss_feeds_import_button">Importar</string>
<string name="blogs_rss_feeds_import_hint">Introduza o URL da fonte RSS</string>
<string name="blogs_rss_feeds_import_hint">Escribe o URL da fonte RSS</string>
<string name="blogs_rss_feeds_import_error">Lamentámolo! Algo fallou ao importar a fonte.</string>
<string name="blogs_rss_feeds_manage">Xestionar Fontes RSS</string>
<string name="blogs_rss_feeds_manage_imported">Importado:</string>
<string name="blogs_rss_feeds_manage_author">Autor/a:</string>
<string name="blogs_rss_feeds_manage_updated">Última actualización:</string>
<string name="blogs_rss_remove_feed">Eliminar fonte</string>
<string name="blogs_rss_remove_feed_dialog_message">Está segura de que quere eliminar esta fonte?\n\nAs entradas eliminaranse do seu dispositivo pero non dos dispositivos de outras persoas\n\nTodas as persoas coas que compartiu esta fonte poderían deixar de recibir actualizacións.</string>
<string name="blogs_rss_remove_feed_dialog_message">Tes a certeza de querer eliminar esta fonte?\n\nAs entradas eliminaranse do teu dispositivo pero non dos dispositivos doutras persoas\n\nTodas as persoas coas que compartiches esta fonte poderían deixar de recibir actualizacións.</string>
<string name="blogs_rss_remove_feed_ok">Eliminar</string>
<string name="blogs_rss_feeds_manage_delete_error">Non se puido eliminar a fonte!</string>
<string name="blogs_rss_feeds_manage_empty_state">Sen fontes RSS que mostrar\n\nToque na icona + para importar unha fonte</string>
<string name="blogs_rss_feeds_manage_error">Aconteceu un problema ao cargar as súas fontes. Por favor, inténteo máis tarde.</string>
<string name="blogs_rss_feeds_manage_error">Aconteceu un problema ao cargar as túas fontes. Por favor, inténtao máis tarde.</string>
<!--Settings Profile Picture-->
<string name="change_profile_picture">Toca para cambiar a túa imaxe de perfil</string>
<string name="dialog_confirm_profile_picture_title">Mudar imaxe de perfil</string>
<string name="dialog_confirm_profile_picture_remark">Só os teus contactos poden ver a túa imaxe de perfil</string>
<string name="dialog_confirm_profile_picture_remark">Só os teus contactos poden ver esta foto</string>
<string name="change_profile_picture_failed_message">Lamentámolo, pero algo fallou cando intentamos actualizar a túa imaxe de pefil</string>
<!--Settings Display-->
<string name="pref_language_title">Idioma &amp; rexión</string>
<string name="pref_language_changed">Este axuste terá efecto cando reinicie Briar. Por favor desconecte e volte a iniciar Briar.</string>
<string name="pref_language_default">Por omisión do sistema</string>
<string name="pref_language_changed">Este axuste terá efecto cando reinicies Briar. Pecha e reinicia Briar.</string>
<string name="pref_language_default">Por defecto no sistema</string>
<string name="display_settings_title">Pantalla</string>
<string name="pref_theme_title">Decorado</string>
<string name="pref_theme_light">Claro</string>
<string name="pref_theme_dark">Oscuro</string>
<string name="pref_theme_dark">Escuro</string>
<string name="pref_theme_auto">Automático (no día)</string>
<string name="pref_theme_system">Por omisión do sistema</string>
<string name="pref_theme_system">Por defecto no sistema</string>
<!--Settings Connections-->
<string name="network_settings_title">Conexións</string>
<string name="bluetooth_setting">Conectar cos contactos vía Bluetooth</string>
@@ -458,11 +458,11 @@
<!--Settings Security and Panic-->
<string name="security_settings_title">Seguridade</string>
<string name="pref_lock_title">Bloquear app</string>
<string name="pref_lock_summary">Utilizar o bloqueo de pantalla do dispositiov para protexer Briar mentras está conectada</string>
<string name="pref_lock_disabled_summary">Para utilizar esta función, axuste o bloqueo de pantalla do dispositivo</string>
<string name="pref_lock_summary">Utilizar o bloqueo de pantalla do dispositio para protexer Briar mentras estás conectada</string>
<string name="pref_lock_disabled_summary">Para utilizar esta función, axusta o bloqueo de pantalla do dispositivo</string>
<string name="pref_lock_timeout_title">Tempo de espera en inactividade para o bloqueo</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<string name="pref_lock_timeout_summary">Cando non utilice Briar, bloquear automáticamente tras %s</string>
<string name="pref_lock_timeout_summary">Cando non uses Briar, bloquea automáticamente tras %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 minuto</string>
<!--Will be shown in a list of lock times. Should fit into the %s of "automatically lock it after %s"-->
@@ -475,25 +475,25 @@
<string name="pref_lock_timeout_60">1 hora</string>
<string name="pref_lock_timeout_never">Nunca</string>
<string name="pref_lock_timeout_never_summary">Nunca bloquear Briar automáticamente</string>
<string name="change_password">Trocar a clave</string>
<string name="current_password">Clave actual</string>
<string name="choose_new_password">Nova clave</string>
<string name="confirm_new_password">Confirme a nova clave</string>
<string name="password_changed">Trocouse a clave</string>
<string name="change_password">Trocar o contrasinal</string>
<string name="current_password">Contrasinal actual</string>
<string name="choose_new_password">Novo contrasinal</string>
<string name="confirm_new_password">Confirma o novo contrasinal</string>
<string name="password_changed">Cambiaches o contrasinal</string>
<string name="panic_setting">Axustes do botón do pánico</string>
<string name="panic_setting_title">Botón do pánico</string>
<string name="panic_setting_hint">Axuste como debe reaccionar Briar cando utilice a app botón do pánico</string>
<string name="panic_setting_hint">Axuste como debe reaccionar Briar cando utilices a app botón do pánico</string>
<string name="panic_app_setting_title">App Botón do pánico</string>
<string name="unknown_app">unha aplicación descoñecida</string>
<string name="panic_app_setting_summary">Non se estableceu unha app</string>
<string name="panic_app_setting_none">Ningún</string>
<string name="dialog_title_connect_panic_app">Confirme a App do pánico</string>
<string name="dialog_message_connect_panic_app">Está segura de querer permitir a %1$s activar accións destrutivas do botón do pánico?</string>
<string name="panic_setting_destructive_action">Accións destructivas</string>
<string name="panic_setting_signout_title">Finalizar sesión</string>
<string name="panic_setting_signout_summary">Desconecte de Briar si o botón do pánico se preme</string>
<string name="dialog_title_connect_panic_app">Confirma a App do pánico</string>
<string name="dialog_message_connect_panic_app">Tes a certeza de querer permitir a %1$s activar accións destrutivas do botón do pánico?</string>
<string name="panic_setting_destructive_action">Accións destrutivas</string>
<string name="panic_setting_signout_title">Pechar sesión</string>
<string name="panic_setting_signout_summary">Desconecta de Briar se o botón do pánico se preme</string>
<string name="purge_setting_title">Eliminar conta</string>
<string name="purge_setting_summary">Elimina a súa conta en Briar si se preme o botón do pánico. Coidado: Isto eliminará permanentemente as súas identidades, contactos e mensaxes</string>
<string name="purge_setting_summary">Elimina a túa conta en Briar se se preme o botón do pánico. Coidado: Isto eliminará permanentemente as túas identidades, contactos e mensaxes</string>
<!--Settings Notifications-->
<string name="notification_settings_title">Notificacións</string>
<string name="notify_sign_in_title">Lembrarme conectar</string>
@@ -512,28 +512,28 @@
<string name="notify_blog_posts_setting_summary_26">Configurar alertas para entradas no blog</string>
<string name="notify_vibration_setting">Vibrar</string>
<string name="notify_sound_setting">Son</string>
<string name="notify_sound_setting_default">Ton por omisión</string>
<string name="notify_sound_setting_default">Ton por defecto</string>
<string name="notify_sound_setting_disabled">Ningún</string>
<string name="choose_ringtone_title">Escolla ton</string>
<string name="choose_ringtone_title">Escolle ton</string>
<string name="cannot_load_ringtone">Non se cargou o ton</string>
<!--Settings Feedback-->
<string name="feedback_settings_title">Avaliación</string>
<string name="send_feedback">Envíe a avaliación</string>
<string name="send_feedback">Envía a avaliación</string>
<!--Link Warning-->
<string name="link_warning_title">Aviso de ligazón</string>
<string name="link_warning_intro">Vai abrir a seguinte ligazón nunha aplicación externa.</string>
<string name="link_warning_text">Esto pódese utilizar para identificala. Pense ben se confía na persoa que lle envía a ligazón e considere abrila co Navegador Tor.</string>
<string name="link_warning_intro">Vas abrir a seguinte ligazón nunha aplicación externa.</string>
<string name="link_warning_text">Esto pódese utilizar para identificarte. Pensa ben se confías na persoa que che envía a ligazón e considera abrila co Navegador Tor.</string>
<string name="link_warning_open_link">Abrir ligazón</string>
<!--Crash Reporter-->
<string name="crash_report_title">Informe de fallo de Briar</string>
<string name="briar_crashed">Lamentámolo, Briar fallou.</string>
<string name="not_your_fault">Non é culpa súa.</string>
<string name="please_send_report">Axúdenos por favor a mellorar Briar enviándonos un informe do fallo.</string>
<string name="not_your_fault">Non é culpa túa.</string>
<string name="please_send_report">Axúdanos por favor a mellorar Briar enviándonos un informe do fallo.</string>
<string name="report_is_encrypted">Prometemos que o informe está cifrado e enviado con seguridade.</string>
<string name="feedback_title">Avaliación</string>
<string name="describe_crash">Describa que aconteceu (optativo)</string>
<string name="enter_feedback">Escriba a súa avaliación</string>
<string name="optional_contact_email">O seu enderezo e-mail (optativo)</string>
<string name="describe_crash">Describe que aconteceu (optativo)</string>
<string name="enter_feedback">Escribe a túa avaliación</string>
<string name="optional_contact_email">O teu enderezo e-mail (optativo)</string>
<string name="include_debug_report_crash">Incluír datos anónimos sobre o fallo</string>
<string name="include_debug_report_feedback">Incluír datos anónimos sobre este dispositivo</string>
<string name="dev_report_basic_info">Información básica</string>
@@ -550,13 +550,13 @@
<string name="close">Pechar</string>
<string name="dev_report_sending">Enviando comentarios...</string>
<string name="dev_report_sent">Comentarios enviados</string>
<string name="dev_report_saved">Informe gardado. Enviarase a seguinte vez que se conecte con Briar.</string>
<string name="dev_report_saved">Informe gardado. Enviarase a seguinte vez que te conectes con Briar.</string>
<string name="dev_report_error">Erro: fallou o envío do informe</string>
<!--Sign Out-->
<string name="progress_title_logout">Desconectando de Briar...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Detectouse unha sobreescrita da pantalla</string>
<string name="screen_filter_body">Outra aplicación estase a amosar enriba de Briar. Para protexer a súa seguridade, Briar non responderá a toques cando outra aplicación está debuxando enriba.\n\nAs seguintes aplicacións poderían estar debuxando enriba:\n\n%1$s</string>
<string name="screen_filter_body">Outra aplicación estase a amosar enriba de Briar. Para protexer a túa seguridade, Briar non responderá a toques cando outra aplicación está debuxando enriba.\n\nAs seguintes aplicacións poderían estar debuxando enriba:\n\n%1$s</string>
<string name="screen_filter_body_api_30">Outra app ten acceso a ver a pantalla enriba de Briar. Para protexer a túa seguridade, Briar non vai responder aos toques cando outra app ten acceso a pantalla.\n\nRevisa as app de abaixo para atopara a resposable.</string>
<string name="screen_filter_allow">Permitir a estas aplicación amosarse enriba</string>
<string name="screen_filter_review_apps">Revisar apps</string>
@@ -564,20 +564,20 @@
<string name="permission_camera_title">Permiso da cámara</string>
<string name="permission_camera_request_body">Para escanear códigos QR, Briar precisa acceso a cámara.</string>
<string name="permission_location_title">Permiso de localización</string>
<string name="permission_location_request_body">Para descubrir dispositivos Bluetooth, Briar precisa permiso para acceder a súa localización.\n\nBriar non garda a súa localización nin a comparte con ninguén.</string>
<string name="permission_location_request_body">Para descubrir dispositivos Bluetooth, Briar precisa permiso para acceder a túa localización.\n\nBriar non garda a túa localización nin a comparte con ninguén.</string>
<string name="permission_camera_location_title">Cámara e localización</string>
<string name="permission_camera_location_request_body">Para escanear o código QR, Briar precisa acceso a cámara.\n\nPara descubrir dispositivos Bluetooth, Briar precisa permiso a súa localización.\n\nBriar non garda a súa localización nin a comparte con ninguén.</string>
<string name="permission_camera_denied_body">Denegou o permiso de acceso a cámara, pero é necesario para engadir contactos.\n\nPor favor, considere conceder o permiso.</string>
<string name="permission_camera_location_request_body">Para escanear o código QR, Briar precisa acceso a cámara.\n\nPara descubrir dispositivos Bluetooth, Briar precisa permiso a túa localización.\n\nBriar non garda a túa localización nin a comparte con ninguén.</string>
<string name="permission_camera_denied_body">Denegaches o permiso de acceso a cámara, pero é necesario para engadir contactos.\n\nPor favor, considera conceder o permiso.</string>
<string name="permission_location_denied_body">Denegache o acceso á localización, mais Briar precisa este permiso para descubrir dispositivos Bluetooth.\n\nConsidera conceder o permiso.</string>
<string name="qr_code">Código QR</string>
<string name="show_qr_code_fullscreen">Amosar o código QR a pantalla completa</string>
<!--App Locking-->
<string name="lock_unlock">Desbloquear Briar</string>
<string name="lock_unlock_verbose">Introduza o PIN do dispositivo, patrón ou contrasinal para desbloquear Briar</string>
<string name="lock_unlock_verbose">Escribe o PIN do dispositivo, patrón ou contrasinal para desbloquear Briar</string>
<string name="lock_unlock_fingerprint_description">Toca o sensor de impresións dixitais co dedo rexistrado para continuar</string>
<string name="lock_unlock_password">Utilizar contrasinal</string>
<string name="lock_is_locked">Briar está bloqueada</string>
<string name="lock_tap_to_unlock">Toque para desbloquear</string>
<string name="lock_tap_to_unlock">Toca para desbloquear</string>
<!--Connections Screen-->
<string name="transports_help_text">Briar pode conectar cos teus contactos a través de Internet, Wi-Fi ou Bluetooth.\n\nTodas as conexións a internet pasan a través da rede Tor para máis privacidade.\n\nSe un contacto é accesible de múltiples xeitos, Briar usaráos en paralelo.</string>
<!--Screenshots-->

View File

@@ -1,25 +1,25 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Setup-->
<string name="setup_title">ברוך הבא אל Briar</string>
<string name="setup_title">ברוך בואך אל Briar</string>
<string name="setup_name_explanation">כינויך יוראה ליד תוכן כלשהו שתכתוב. אינך יכול לשנות אותו לאחר יצירת חשבונך.</string>
<string name="setup_next">הבא</string>
<string name="setup_password_intro">בחר סיסמה</string>
<string name="setup_password_intro">נא לבחור סיסמה</string>
<string name="setup_password_explanation">חשבון Briar שלך מאוחסן באופן מוצפן במכשירך, לא בענן. אם תשכח את סיסמתך או תסיר את Briar, אין דרך להשיב את חשבונך.\n\nבחר סיסמה ארוכה שקשה לנחש אותה, כגון ארבע מילים אקראיות, או עשרה תווים אקראיים של אותיות, מספרים וסמלים.</string>
<string name="setup_doze_title">חיבורי רקע</string>
<string name="setup_doze_intro">כדי לקבל הודעות, Briar צריך להישאר מחובר ברקע.</string>
<string name="setup_doze_explanation">כדי לקבל הודעות, Briar צריך להישאר מחובר ברקע. אנא השבת מיטובי סוללה כך ש־Briar יוכל להישאר מחובר.</string>
<string name="setup_doze_button">התר חיבורים</string>
<string name="choose_nickname">בחר את הכינוי שלך</string>
<string name="choose_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="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="warning_dozed">%s לא היה יכול לרוץ ברקע</string>
@@ -28,12 +28,12 @@
<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="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="startup_failed_notification_title">Briar לא היה ניתן להתחיל</string>
<string name="startup_failed_notification_text">הקש לעוד מידע.</string>
<string name="startup_failed_notification_text">יש להקיש למידע נוסף.</string>
<string name="startup_failed_activity_title">כישלון הזנק Briar</string>
<string name="startup_failed_db_error">מסיבה כלשהי, מסד נתונים Briar שלך פגום ללא יכולת תיקון. החשבון שלך, הנתונים שלך וכל אנשי הקשר שלך אבדו. למרבה הצער, אתה צריך להתקין מחדש את Briar או להגדיר חשבון חדש ע״י בחירה באפשרות \'שכחתי את הסיסמה שלי\' בתזכיר הסיסמה.</string>
<string name="startup_failed_data_too_old_error">החשבון שלך נוצר עם גרסה ישנה של יישום זה ואינו יכול להיפתח עם גרסה זו. אתה חייב להתקין מחדש את הגרסה הישנה או להגדיר חשבון חדש ע״י בחירה באפשרות \'שכחתי את הסיסמה שלי\' בתזכיר הסיסמה.</string>
@@ -47,7 +47,7 @@
</plurals>
<string name="expiry_date_reached">תוכנה זו פגה.\nתודה על הבדיקה!</string>
<string name="download_briar">כדי להמשיך להשתמש ב־Briar, אנא הורד את השחרור האחרון.</string>
<string name="create_new_account">תיצטרך ליצור חשבון חדש, אבל אתה יכול להשתמש באותו כינוי.</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>
@@ -62,13 +62,13 @@
<!--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="sign_out_button">יציאה</string>
<string name="transports_onboarding_text">הקש כאן כדי לשלוט איך Briar מתחבר אל אנשי הקשר שלך.</string>
<!--Transports: Tor-->
<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_device_status_online_wifi">לטלפון שלך גישה לאינטרנט באמצעות רשת אלחוטית</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>
@@ -77,14 +77,14 @@
<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">אותה רשת Wi-Fi</string>
<string name="lan_device_status_on">הטלפון שלך מחובר אל Wi-Fi</string>
<string name="lan_device_status_off">הטלפון שלך אינו מחובר אל Wi-Fi</string>
<string name="lan_plugin_status_enabling">Briar מתחבר אל רשת ה־Wi-Fi</string>
<string name="lan_plugin_status_active">Briar מחובר אל רשת ה־Wi-Fi</string>
<string name="lan_plugin_status_inactive">Briar אינו יכול להתחבר אל רשת ה־Wi-Fi</string>
<string name="lan_plugin_status_disabled">Briar מתוצר לא להשתמש ברשת ה־Wi-Fi</string>
<string name="transport_lan">רשת אלחוטית</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>
@@ -128,25 +128,25 @@
<string name="now">עכשיו</string>
<string name="show">הראה</string>
<string name="hide">הסתר</string>
<string name="ok">אשר</string>
<string name="cancel">בטל</string>
<string name="ok">אישור</string>
<string name="cancel">ביטול</string>
<string name="got_it">הבנתי</string>
<string name="delete">מחק</string>
<string name="delete">מחיקה</string>
<string name="accept">קבל</string>
<string name="decline">סרב</string>
<string name="online">מקוון</string>
<string name="online">מחובר</string>
<string name="offline">לא מקוון</string>
<string name="send">שלח</string>
<string name="send">שליחה</string>
<string name="allow">התר</string>
<string name="open">פתח</string>
<string name="change">שנה</string>
<string name="open">פתיחה</string>
<string name="change">שינוי</string>
<string name="no_data">אין נתונים</string>
<string name="ellipsis"></string>
<string name="text_too_long">הטקסט המוכנס ארוך מדי</string>
<string name="show_onboarding">הראה דו־שיח עזרה</string>
<string name="show_onboarding">הצגת דו־שיח עזרה</string>
<string name="fix">תקן</string>
<string name="help">עזרה</string>
<string name="sorry">סליחה</string>
<string name="sorry">עמך הסליחה</string>
<string name="error_start_activity">בלתי זמין במערכת שלך</string>
<string name="status_heading">מצב</string>
<!--Contacts and Private Conversations-->
@@ -162,41 +162,41 @@
<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="delete_all_messages">מחק את כל ההודעות</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_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_partly_downloaded">הודעות שירדו חלקית אינן יכולות להימחק עד שהן יסיימו לרדת.</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="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="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="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="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">Briar של איש הקשר שלך אינו תומך בצרופות תמונה. ברגע שהוא ישדרג, תראה צלמית שונה.</string>
<string name="dialog_title_image_support">אתה יכול כעת לשלוח תמונות אל איש קשר זה</string>
<string name="dialog_title_image_support">כעת אפשר לשלוח תמונות אל איש קשר זה</string>
<string name="dialog_message_image_support">הקש על צלמית זו כדי לצרף תמונות.</string>
<string name="messaging_too_many_attachments_toast">רק %d התמונות הראשונות יישלחו</string>
<!--Adding Contacts-->
<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="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>
@@ -215,19 +215,19 @@
<string name="paste_button">הדבק</string>
<string name="add_contact_button">הוסף איש קשר</string>
<string name="copy_button">העתק</string>
<string name="share_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="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="waiting_for_contact_to_come_online">ממתין להתחברות איש הקשר…</string>
<string name="connecting">מתחבר…</string>
<string name="adding_contact">מוסיף איש קשר…</string>
<string name="adding_contact_failed">הוספת איש קשר נכשלה</string>
@@ -237,7 +237,7 @@
<string name="nickname_missing">אנא הכנס כינוי</string>
<string name="invalid_link">קישור בלתי תקף</string>
<string name="unsupported_link">קישור זה מגיע מגרסה חדשה יותר של Briar. אנא שדרג אל הגרסה האחרונה ונסה שוב.</string>
<string name="intent_own_link">פתחת את הקישור של עצמך. השתמש באחד של איש הקשר שאתה רוצה להוסיף!</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>
@@ -267,10 +267,10 @@
<string name="pending_contact_updated_toast">איש קשר ממתין עודכן</string>
<!--Introductions-->
<string name="introduction_onboarding_title">הכר בין אנשי הקשר שלך</string>
<string name="introduction_onboarding_text">אתה יכול להכיר בין אנשי הקשר שלך אחד עם השני, כך שהם לא צריכים להיפגש פנים מול פנים כדי להתחבר ב־Briar.</string>
<string name="introduction_onboarding_text">אפשר להכיר בין אנשי הקשר שלך אחד עם השני, כך שהם לא צריכים להיפגש פנים מול פנים כדי להתחבר ב־Briar.</string>
<string name="introduction_menu_item">בצע היכרות</string>
<string name="introduction_activity_title">בחר איש קשר</string>
<string name="introduction_not_possible">יש לך כבר היכרות אחת בתהליך עם אנשי קשר אלו. אנא התר לה תחילה לסיים. אם אתה או אנשי הקשר שלך לעיתים נדירות מקוונים, זה עשוי לקחת זמן מה.</string>
<string name="introduction_not_possible">יש לך כבר היכרות אחת בתהליך עם אנשי קשר אלו. אנא התר לה תחילה לסיים. אם אתה או אנשי הקשר שלך לעיתים נדירות מחוברים, זה עשוי לקחת זמן מה.</string>
<string name="introduction_message_title">הכר בין אנשי קשר</string>
<string name="introduction_message_hint">הוסף הודעה (רשותי)</string>
<string name="introduction_button">בצע היכרות</string>
@@ -278,7 +278,7 @@
<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_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>
@@ -302,7 +302,7 @@
<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_create_group_hint">נא לבחור שם לקבוצה הפרטית שלך</string>
<string name="groups_invitation_sent">הזמנה קבוצתית נשלחה</string>
<string name="groups_member_list">רשימת חברי קבוצה</string>
<string name="groups_invite_members">הזמן חברי קבוצה</string>
@@ -312,13 +312,13 @@
<string name="groups_member_joined">%s הצטרף אל הקבוצה</string>
<string name="groups_leave">עזוב קבוצה</string>
<string name="groups_leave_dialog_title">אשר עזיבת קבוצה</string>
<string name="groups_leave_dialog_message">האם אתה בטוח שאתה רוצה לעזוב קבוצה זו?</string>
<string name="groups_leave_dialog_message">האם אכן ברצונך לעזוב קבוצה זו?</string>
<string name="groups_dissolve">פרק קבוצה</string>
<string name="groups_dissolve_dialog_title">אשר פירוק קבוצה</string>
<string name="groups_dissolve_dialog_message">האם אתה בטוח שאתה רוצה לפרק את הקבוצה הזאתו?\n\nכל חברי הקבוצה האחרים לא יוכלו להמשיך בשיחתם וכנראה לא יקבלו את ההודעות האחרונות.</string>
<string name="groups_dissolve_dialog_message">האם אכן ברצונך לפרק את הקבוצה הזאת?\n\nכל חברי הקבוצה האחרים לא יוכלו להמשיך בשיחתם וכנראה לא יקבלו את ההודעות האחרונות.</string>
<string name="groups_dissolve_button">פרק</string>
<string name="groups_dissolved_dialog_title">הקבוצה פורקה</string>
<string name="groups_dissolved_dialog_message">היוצר של הקבוצה הזאת פירק אותה.\n\nאתה לא יכול עוד לכתוב הודעות אל הקבוצה וכנראה שלא תקבל את כל הרשומות שנכתבו.</string>
<string name="groups_dissolved_dialog_message">היוצר של הקבוצה הזאת פירק אותה.\n\nאי אפשר לכתוב הודעות לקבוצה יותר וכנראה שלא ייתקבלו כל הרשומות שנכתבו.</string>
<!--Private Group Invitations-->
<string name="groups_invitations_title">הזמנות לקבוצה</string>
<string name="groups_invitations_invitation_sent">הזמנת את %1$s להצטרף אל הקבוצה \"%2$s\".</string>
@@ -365,8 +365,8 @@
<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="dialog_message_leave_forum">האם אכן ברצונך לעזוב פורום זה?\n\nכל אנשי הקשר ששיתפת איתם פורום זה עשויים להפסיק לקבל עדכונים.</string>
<string name="dialog_button_leave">עזיבה</string>
<string name="forum_left_toast">עזב פורום</string>
<!--Forum Sharing-->
<string name="forum_share_button">שתף פורום</string>
@@ -391,7 +391,7 @@
<string name="forum_invitation_response_declined_received">%s סירב אל ההזמנה לפורום.</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="two">%d פורומים משותפים ע״י אנשי קשר</item>
@@ -404,7 +404,7 @@
<string name="read_more">קרא עוד</string>
<string name="blogs_write_blog_post">כתוב רשומת בלוג</string>
<string name="blogs_write_blog_post_body_hint">הקלד את רשומת הבלוג שלך</string>
<string name="blogs_publish_blog_post">פרסם</string>
<string name="blogs_publish_blog_post">פרסום</string>
<string name="blogs_blog_post_created">רשומת בלוג נוצרה</string>
<string name="blogs_blog_post_received">רשומת בלוג חדשה התקבלה</string>
<string name="blogs_blog_post_scroll_to">גלול אל</string>
@@ -416,7 +416,7 @@
<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_comment_hint">הוספת תגובה (רשות)</string>
<string name="blogs_reblog_button">פרסם מחדש</string>
<!--Blog Sharing-->
<string name="blogs_sharing_share">שתף בלוג</string>
@@ -443,7 +443,7 @@
<string name="blogs_rss_feeds_manage_author">מחבר:</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_dialog_message">האם אכן ברצונך להסיר הזנה זו?\n\nרשומות יוסרו ממכשירך אבל לא ממכשירים של אנשים אחרים.\n\nאנשי קשר כלשהם ששיתפת איתם הזנה זו עלולים להפסיק לקבל עדכונים.</string>
<string name="blogs_rss_remove_feed_ok">הסר</string>
<string name="blogs_rss_feeds_manage_delete_error">ההזנה לא יכלה להימחק!</string>
<string name="blogs_rss_feeds_manage_empty_state">אין הזנות RSS להראות\n\nהקש על הצלמית + כדי לייבא הזנה</string>
@@ -451,7 +451,7 @@
<!--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="dialog_confirm_profile_picture_remark">רק אנשי הקשר שלך יכולים לראות את התמונה הזו</string>
<string name="change_profile_picture_failed_message">אנו מצטערים משהו השתבש בעת עדכון תמונת הפרופיל שלך</string>
<!--Settings Display-->
<string name="pref_language_title">שפה ואזור</string>
@@ -466,7 +466,7 @@
<!--Settings Connections-->
<string name="network_settings_title">חיבורים</string>
<string name="bluetooth_setting">התחבר אל אנשי קשר באמצעות Bluetooth</string>
<string name="wifi_setting">התחבר אל אנשי קשר באותה רשת Wi-Fi</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>
@@ -499,7 +499,7 @@
<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="change_password">שינוי הסיסמה</string>
<string name="current_password">סיסמה נוכחית</string>
<string name="choose_new_password">סיסמה חדשה</string>
<string name="confirm_new_password">אשר סיסמה חדשה</string>
@@ -545,18 +545,18 @@
<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.</string>
<string name="link_warning_intro">הקישור הבא עומד להיפתח עם יישום חיצוני.</string>
<string name="link_warning_text">זה יכול לשמש כדי לזהות אותך. נא לחשוב האם הנך בוטח באיש ששלח לך את הקישור הזה ולשקול לפתוח את הקישור עם דפדפן Tor.</string>
<string name="link_warning_open_link">פתח קישור</string>
<!--Crash Reporter-->
<string name="crash_report_title">דוח קריסת Briar</string>
<string name="briar_crashed">סליחה, Briar קרס.</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="enter_feedback">נא לתת את המשוב שלך</string>
<string name="optional_contact_email">כתובת הדוא״ל שלך (רשותי)</string>
<string name="include_debug_report_crash">כלול נתונים אלמוניים לגבי הקריסה</string>
<string name="include_debug_report_feedback">כלול נתונים אלמוניים לגבי מכשיר זה</string>
@@ -571,13 +571,13 @@
<string name="dev_report_logcat">יומן יישום</string>
<string name="dev_report_device_features">מאפייני מכשיר</string>
<string name="send_report">שלח דוח</string>
<string name="close">סגור</string>
<string name="close">סגירה</string>
<string name="dev_report_sending">שולח משוב…</string>
<string name="dev_report_sent">משוב נשלח</string>
<string name="dev_report_saved">הדוח נשמר. הוא יישלח בפעם הבאה שתתחבר אל Briar.</string>
<string name="dev_report_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>
@@ -601,20 +601,20 @@
<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>
<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>
<string name="transports_help_text">Briar יכול להתחבר אל אנשי הקשר שלך באמצעות האינטרנט, הרשת האלחוטית או 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>
<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>
<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>
<string name="screenshot_message_2">היי נועה! תודה שסיפרת לי על Briar!</string>
<!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">על לא דבר, אני מקווה שאתה אוהב אותו 😀</string>
<string name="screenshot_message_3">על לא דבר, מקווה שאתה אוהב אותו 😀</string>
</resources>

View File

@@ -433,6 +433,10 @@ Kapcsolatai, akivel megosztotta ezt a blogot, lehet nem kapnak többé frissít
<string name="blogs_rss_feeds_manage_empty_state">Nincs megjelenítendő</string>
<string name="blogs_rss_feeds_manage_error">Hiba történt a feed-jei betöltésével. Kérjük próbálja újra később.</string>
<!--Settings Profile Picture-->
<string name="change_profile_picture">Érintse meg a profilképe cseréjéhez</string>
<string name="dialog_confirm_profile_picture_title">Profil kép cseréje</string>
<string name="dialog_confirm_profile_picture_remark">Csak a kapcsolataid láthatják ezt a képet</string>
<string name="change_profile_picture_failed_message">Sajnáljuk, de valami hiba történt a profil kép frissítése során</string>
<!--Settings Display-->
<string name="pref_language_title">Nyelv és régió</string>
<string name="pref_language_changed">Ez a beállítás a Briar újraindítása után lép életbe. Kérjük lépjen ki és indítsa újra a Briar-t.</string>

View File

@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Ýttu til að skipta um auðkennismyndina þína</string>
<string name="dialog_confirm_profile_picture_title">Skipta um auðkennismynd</string>
<string name="dialog_confirm_profile_picture_remark">Einungis tengiliðirnir þínir geta séð auðkennismyndina þína</string>
<string name="dialog_confirm_profile_picture_remark">Einungis tengiliðirnir þínir geta séð þessa mynd</string>
<string name="change_profile_picture_failed_message">Því miður, eitthvað fór úrskeiðis við að uppfæra auðkennismyndina þína.</string>
<!--Settings Display-->
<string name="pref_language_title">Tungumál og landsvæði</string>

View File

@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Tocca per cambiare l\'immagine del profilo</string>
<string name="dialog_confirm_profile_picture_title">Cambia immagine profilo</string>
<string name="dialog_confirm_profile_picture_remark">Solo i tuoi contatti possono vedere l\'immagine del profilo</string>
<string name="dialog_confirm_profile_picture_remark">Solo i tuoi contatti possono vedere questa immagine</string>
<string name="change_profile_picture_failed_message">Spiacenti, qualcosa è andato storto aggiornando la tua foto del profilo.</string>
<!--Settings Display-->
<string name="pref_language_title">Lingua &amp; regione</string>

View File

@@ -472,6 +472,7 @@
<string name="include_debug_report_crash">クラッシュに関する匿名のデータを添付する</string>
<string name="include_debug_report_feedback">このデバイスに関する匿名のデータを添付する</string>
<string name="dev_report_basic_info">基本情報</string>
<string name="dev_report_memory">メモリー</string>
<string name="dev_report_storage">ストレージ</string>
<string name="dev_report_connectivity">接続</string>
<string name="send_report">レポートを送信</string>

View File

@@ -447,7 +447,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Bakstelėkite norėdami pasikeisti profilio paveikslą</string>
<string name="dialog_confirm_profile_picture_title">Keisti profilio paveikslą</string>
<string name="dialog_confirm_profile_picture_remark">Jūsų profilio paveikslą gali matyti tik jūsų adresatai</string>
<string name="dialog_confirm_profile_picture_remark">Šį paveikslą gali matyti tik jūsų adresatai</string>
<string name="change_profile_picture_failed_message">Atleiskite, bet atnaujinant jūsų profilio paveikslą kažkas nutiko</string>
<!--Settings Display-->
<string name="pref_language_title">Kalba ir regionas</string>
@@ -556,6 +556,7 @@
<string name="optional_contact_email">Jūsų el. pašto adresas (nebūtina)</string>
<string name="include_debug_report_crash">Įtraukti anoniminius duomenis apie strigtį</string>
<string name="include_debug_report_feedback">Įtraukti anoniminius duomenis apie šį įrenginį</string>
<string name="dev_report_user_info">Naudotojo informacija</string>
<string name="dev_report_basic_info">Pagrindinė informacija</string>
<string name="dev_report_device_info">Įrenginio informacija</string>
<string name="dev_report_stacktrace">Dėklo pėdsakas</string>

View File

@@ -0,0 +1,566 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--Setup-->
<string name="setup_title">ဘရိုင်ယာမှ ကြိုဆိုပါသည်</string>
<string name="setup_name_explanation">သင့်နာမည်ပြောင်သည် သင်တင်ထားသမျှအရာ၏ဘေးတွင် ပေါ်နေပါလိမ့်မည်။ သို့ပါ၍ သင့်နာမည်ပြောင်အား အကောင့်ဖွင့်ပြီးနောက် ပြောင်း၍မရတော့ပါ။</string>
<string name="setup_next">ရှေ့သို့</string>
<string name="setup_password_intro">စကားဝှက်တစ်ခု ရွေးပါ</string>
<string name="setup_password_explanation">သင့်ဘရိုင်ယာအကောင့်ကို ကလောက်တွင်မဟုတ်ဘဲ သင့်ကိရိယာတွင်သာ လျှို့ဝှက်ကုဒ်ပြောင်းသိမ်းဆည်းထားပါသည်။ သင့်စကားဝှက်ကို မေ့သွားလျှင် (သို့) ဘရိုင်ယာကို ဖြုတ်လိုက်လျှင် သင့်အကောင့်ကို မည်သည့်နည်းနှင့်မှ ပြန်မရယူနိုင်တော့ပါ။\n\nကြုံရာစကားလုံးလေးလုံး (သို့) ကြုံရာအက္ခရာဆယ်လုံး၊ နံပါတ်များနှင့် သင်္ကေတများကဲ့သို့ ခန့်မှန်းရခက်သည့် ရှည်လျားသောစကားဝှက်ကို ရွေးချယ်ပါ။</string>
<string name="setup_doze_title">နောက်ခံချိတ်ဆက်မှုများ</string>
<string name="setup_doze_intro">မက်ဆေ့ချ်များလက်ခံရယူနိုင်ရန် ဘရိုင်ယာသည် နောက်ခံချိတ်ဆက်နေရန် လိုအပ်သည်။</string>
<string name="setup_doze_explanation">မက်ဆေ့ချ်များလက်ခံရယူနိုင်ရန် ဘရိုင်ယာသည် နောက်ခံချိတ်ဆက်နေရန် လိုအပ်သည်။ ဘရိုင်ယာအနေဖြင့် ချိတ်ဆက်မှုမပြတ်ရှိနေစေရန် ဘတ္ထရီအားအကောင်းဆုံးထိန်းညှိခြင်းကို ကျေးဇူးပြု၍ ပိတ်ထားပေးပါ။</string>
<string name="setup_doze_button">ချိတ်ဆက်မှုများ ခွင့်ပြုမည်</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">အောက်ပါခလုတ်ကိုနှိပ်၍ \"ကာကွယ်မှုပေးထားသောအက်ပ်များ\" စခရင်တွင် ဘရိုင်ယာကို ကာကွယ်ထားကြောင်း သေချာစေပါ။</string>
<string name="setup_huawei_button">ဘရိုင်ယာကို ကာကွယ်မည်</string>
<string name="setup_huawei_help">ကာကွယ်မှုပေးထားသောအက်ပ်များစာရင်းတွင် ထည့်မထားလျှင် ဘရိုင်ယာကို နောက်ကွယ်တွင် ဖွင့်ထားနိုင်မည်မဟုတ်ပါ။</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">ဘရိုင်ယာက သင့်စကားဝှက်ကို မစစ်ဆေးနိုင်ပါ။ ဤပြဿနာကို ဖြေရှင်းရန် သင့်ကိရိယာကို ပိတ်ပြီးပြန်ဖွင့်ကြည့်ပါ။</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">သင့်ဘရိုင်ယာအကောင့်ကို ကလောက်တွင်မဟုတ်ဘဲ သင့်ကိရိယာတွင်သာ လျှို့ဝှက်ကုဒ်ပြောင်းသိမ်းဆည်းထားသောကြောင့် သင့်စကားဝှက်ကို ပြန်လည်သတ်မှတ်မပေးနိုင်ပါ။ သင့်အကောင့်ကို ဖျက်ပစ်ပြီး အသစ်ပြန်ဖွင့်ချင်ပါသလား။\n\nသတိ - သင့်ကိုယ်ပိုင်အချက်အလက်များ၊ အဆက်အသွယ်များနှင့် မက်ဆေ့ချ်များအားလုံး အပြီးပျက်သွားပါလိမ့်မည်။</string>
<string name="startup_failed_notification_title">ဘရိုင်ယာ မစတင်နိုင်ပါ</string>
<string name="startup_failed_notification_text">အချက်အလက်များကို ပိုမိုကြည့်ရှုရန် နှိပ်ပါ။</string>
<string name="startup_failed_activity_title">ဘရိုင်ယာ မစတင်နိုင်ပါ</string>
<string name="startup_failed_db_error">အကြောင်းကြောင်းကြောင့် သင့်ဘရိုင်ယာအချက်အလက်အစုတစ်ခုလုံး လုံးဝပျက်စီးသွားပါသည်။ သင့်အကောင့်၊ သင့်အချက်အလက်နှင့် သင့်အဆက်အသွယ်အကုန်လုံး ပျောက်ဆုံးသွားပါသည်။ ကံမကောင်းစွာဘဲ သင့်အနေဖြင့် ဘရိုင်ယာကို ပြန်လည်ထည့်သွင်းရပါမည် (သို့) စကားဝှက်တောင်းသည့်နေရာတွင် \'ကျွန်ုပ်၏စကားဝှက်ကို မေ့သွားပါသည်\' ကို ရွေးချယ်ပြီး အကောင့်အသစ်တစ်ခု ဖွင့်နိုင်ပါသည်။</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">ဘရိုင်ယာသည် လိုအပ်သည့် ချိတ်ဆက်ပရိုဂရမ်တစ်ခုကို မဖွင့်နိုင်ပါ။ ဘရိုင်ယာကို ပြန်ထည့်သွင်းခြင်းက ဤပြဿနာကို ပြေလည်စေလေ့ရှိပါသည်။ သို့သော်လည်း ဘရိုင်ယာသည် သင့်အချက်အလက်များသိမ်းဆည်းရန် ဗဟိုပြုဆာဗာများကို အသုံးပြုမနေသောကြောင့် သင့်အကောင့်နှင့် ဆက်စပ်အချက်အလက်အားလုံးကို ဆုံးရှုံးသွားပါလိမ့်မည်။</string>
<plurals name="expiry_warning">
<item quantity="other">ဤအရာသည် Briar ၏ စမ်းသပ်ဆဲဗားရှင်းဖြစ်ပါသည်။ သင့်အကောင့်သည် %d ရက်နေ့တွင် သက်တမ်းကုန်ဆုံးမည်ဖြစ်ပြီး သက်တမ်းတိုး၍မရနိုင်ပါ။</item>
</plurals>
<string name="expiry_date_reached">ဤဆော့ဖ်ဝဲသည် သက်တမ်းကုန်သွားပါပြီ။\nစမ်းသပ်အသုံးပြုခြင်းအတွက် ကျေးဇူးတင်ပါသည်။</string>
<string name="download_briar">ဘရိုင်ယာကို ဆက်လက်အသုံးပြုလိုပါက နောက်ဆုံးထွက်ထားသည်ကို ဒေါင်းလုဒ်လုပ်ပါ။</string>
<string name="create_new_account">အကောင့်အသစ်ဖွင့်ရန် လိုအပ်သော်လည်း သုံးလက်စနာမည်ပြောင်ကို ဆက်သုံးနိုင်ပါသည်။</string>
<string name="download_briar_button">နောက်ဆုံးထွက်ထားသည်ကို ဒေါင်းလုဒ်လုပ်မည်</string>
<string name="startup_open_database">အချက်အလက်အစုကို ပြန်ဖြည်နေသည်…</string>
<string name="startup_migrate_database">အချက်အလက်အစုကို အဆင့်မြှင့်နေသည်…</string>
<string name="startup_compact_database">အချက်အလက်အစုကို သိပ်သည်းစေရန်လုပ်နေသည်…</string>
<!--Navigation Drawer-->
<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">ဘရိုင်ယာက သင့်အဆက်အသွယ်များနှင့် မည်သို့ချိတ်ဆက်သည်ကို ထိန်းချုပ်ရန် ဤနေရာကိုနှိပ်ပါ။</string>
<!--Transports: Tor-->
<string name="transport_tor">အင်တာနက်</string>
<string name="tor_device_status_online_wifi">သင့်ဖုန်းသည် ဝိုင်ဖိုင်မှတစ်ဆင့် အင်တာနက်ရရှိနေသည်</string>
<string name="tor_device_status_online_mobile">သင့်ဖုန်းသည် မိုဘိုင်းလ်ဒေတာမှတစ်ဆင့် အင်တာနက်ရရှိနေသည်</string>
<string name="tor_device_status_offline">သင့်ဖုန်းတွင် အင်တာနက်ရရှိမနေပါ</string>
<string name="tor_plugin_status_enabling">ဘရိုင်ယာသည် အင်တာနက်ချိတ်ဆက်နေပါသည်</string>
<string name="tor_plugin_status_active">ဘရိုင်ယာသည် အင်တာနက်ချိတ်ဆက်ထားသည်</string>
<string name="tor_plugin_status_inactive">ဘရိုင်ယာသည် အင်တာနက်မချိတ်ဆက်နိုင်ပါ</string>
<string name="tor_plugin_status_disabled">အင်တာနက်အသုံးမပြုရန် ဘရိုင်ယာကို ချိန်ညှိထားသည်</string>
<string name="tor_plugin_status_disabled_mobile_data">မိုဘိုင်းလ်ဒေတာအသုံးမပြုရန် ဘရိုင်ယာကို ချိန်ညှိထားသည်</string>
<string name="tor_plugin_status_disabled_battery">ဘတ္ထရီအသုံးပြုနေပါက အင်တာနက်အသုံးမပြုရန် ဘရိုင်ယာကို ချိန်ညှိထားသည်</string>
<string name="tor_plugin_status_disabled_country_blocked">ဤနိုင်ငံတွင် အင်တာနက်အသုံးမပြုရန် ဘရိုင်ယာကို ချိန်ညှိထားသည်</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">ဝိုင်ဖိုင်</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">ဘရိုင်ယာသည် ဝိုင်ဖိုင်ကွန်ရက်ကို ချိတ်ဆက်နေပါသည်</string>
<string name="lan_plugin_status_active">ဘရိုင်ယာသည် ဝိုင်ဖိုင်ကွန်ရက်ကို ချိတ်ဆက်ထားသည်</string>
<string name="lan_plugin_status_inactive">ဘရိုင်ယာသည် ဝိုင်ဖိုင်ကွန်ရက်ကို မချိတ်ဆက်နိုင်ပါ</string>
<string name="lan_plugin_status_disabled">ဝိုင်ဖိုင်ကွန်ရက်အသုံးမပြုရန် ဘရိုင်ယာကို ချိန်ညှိထားသည်</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">ဘလူးတုသ်</string>
<string name="bt_device_status_on">သင့်ဖုန်းဘလူးတုသ် ဖွင့်ထားသည်</string>
<string name="bt_device_status_off">သင့်ဖုန်းဘလူးတုသ် ပိတ်ထားသည်</string>
<string name="bt_plugin_status_enabling">ဘရိုင်ယာသည် ဘလူးတုသ်ချိတ်ဆက်နေသည်</string>
<string name="bt_plugin_status_active">ဘရိုင်ယာသည် ဘလူးတုသ်ချိတ်ဆက်ထားသည်</string>
<string name="bt_plugin_status_inactive">ဘရိုင်ယာသည် ဘလူးတုသ်မချိတ်ဆက်နိုင်ပါ</string>
<string name="bt_plugin_status_disabled">ဘလူးတုသ်အသုံးမပြုရန် ဘရိုင်ယာကို ချိန်ညှိထားသည်</string>
<!--Notifications-->
<string name="reminder_notification_title">ဘရိုင်ယာမှ အကောင့်ထွက်ထားသည်</string>
<string name="reminder_notification_text">အကောင့်ပြန်ဝင်ရန် နှိပ်ပါ။</string>
<string name="reminder_notification_channel_title">ဘရိုင်ယာအကောင့်ဝင်ရန် သတိပေးချက်</string>
<string name="reminder_notification_dismiss">ဖြုတ်မယ်</string>
<string name="ongoing_notification_title">ဘရိုင်ယာသို့ အကောင့်ဝင်ထားသည်</string>
<string name="ongoing_notification_text">ဘရိုင်ယာဖွင့်ရန် နှိပ်ပါ</string>
<plurals name="private_message_notification_text">
<item quantity="other">ကိုယ်ရေးမက်ဆေ့ချ်အသစ် %d ခု။</item>
</plurals>
<plurals name="group_message_notification_text">
<item quantity="other">အဖွဲ့မက်ဆေ့ချ်အသစ် %d ခု။</item>
</plurals>
<plurals name="forum_post_notification_text">
<item quantity="other">ဆွေးနွေးမှုဖိုရမ်ပို့စ်အသစ် %d ခု။</item>
</plurals>
<plurals name="blog_post_notification_text">
<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="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="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="image_caption_hint">စာတန်းထည့်ရန် (မဖြစ်မနေမဟုတ်ပါ)</string>
<string name="image_attach">ရုပ်ပုံတွဲမယ်</string>
<string name="image_attach_error">ရုပ်ပုံ(များ)အား တွဲ၍ မရနိုင်ပါ</string>
<string name="image_attach_error_too_big">ရုပ်ပုံဆိုဒ်ကြီး နေပါသည်။ %d MB ပဲဆံ့ပါသည်။</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="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_partly_downloaded">တစ်စိတ်တစ်ပိုင်းသာ ဒေါင်းလုပ်ပြုလုပ်ထားရသေးသော မက်ဆေ့ချ်များအား ဒေါင်းလုပ်လုံးလုံး မပြီးဆုံးခြင်းထိ ပယ်ဖျက်၍မရပါ။</string>
<string name="dialog_message_not_deleted_not_all_selected_both">ဖိတ်ကြားခြင်း သို့မဟုတ် မိတ်ဆက်ခြင်းအား ပယ်ဖျက်ရန် တောင်းဆိုချက်နှင့် တုံ့ပြန်ချက်အား သင်ရွေးထားရပါသည်။</string>
<string name="dialog_message_not_deleted_not_all_selected_introductions">မိတ်ဆက်ခြင်းအား ပယ်ဖျက်ရန် တောင်းဆိုချက်နှင့် တုံ့ပြန်ချက်အား သင်ရွေးထားရပါသည်။</string>
<string name="dialog_message_not_deleted_not_all_selected_invitations">ဖိတ်ကြားခြင်းအား ပယ်ဖျက်ရန် တောင်းဆိုချက်နှင့် တုံ့ပြန်ချက်အား သင်ရွေးထားရပါသည်။</string>
<string name="delete_contact">အဆက်အသွယ်ကို ဖျက်မည်</string>
<string name="dialog_title_delete_contact">အဆက်အသွယ်ကို အပြီးပယ်ဖျက်ခြင်းအား အတည်ပြုမည်</string>
<string name="dialog_message_delete_contact">ဤအဆက်အသွယ်အပြင် ၎င်းနှင့်ပြောထားသမျှမက်ဆေ့ချ်အားလုံးကို ဖယ်ရှားချင်တာ သေချာပါသလား?</string>
<string name="contact_deleted_toast">အဆက်အသွယ်ကို ဖျက်ပြီးပါပြီ</string>
<!--This is shown in the action bar when opening an image in fullscreen that the user sent-->
<string name="you">သင်</string>
<string name="save_image">ရုပ်ပုံသိမ်းဆည်းမယ်</string>
<string name="dialog_title_save_image">ရုပ်ပုံသိမ်းဆည်းမည်လား?</string>
<string name="dialog_message_save_image">ဤရုပ်ပုံသိမ်းဆည်းခြင်းဖြင့် ၎င်းအား အခြားအပ္ပလီကေးရှင်းများ အသုံးပြုနိုင်ပါသည်။​ \n\n သင်သိမ်းဆည်းမည်ဆိုတာ သေချာပါလား?</string>
<string name="save_image_success">ရုပ်ပုံအား သိမ်းဆည်းပြီး</string>
<string name="save_image_error">ရုပ်ပုံအား သိမ်းဆည်း၍ မရနိုင်ပါ</string>
<string name="dialog_title_no_image_support">ရုပ်ပုံများ မရှိပါ</string>
<string name="dialog_message_no_image_support">သင့် အဆက်အသွယ်၏ ဘရိုင်ယာသည် ရုပ်ပုံပူးတွဲဖိုင်များ မပံ့ပိုးပါ။ ၎င်းတို့မှ အဆင့်မြှင့်ခဲ့လျှင် သင်သည် အခြားအိုင်ကွန်တွေ့ရှိပါမည်။</string>
<string name="dialog_title_image_support">ရုပ်ပုံများကို ဤအဆက်အသွယ်လိပ်စာသို့ ပို့လို့ရပါပြီ</string>
<string name="dialog_message_image_support">ဤအိုင်ကွန်ကို နှိပ်ပြီး ရုပ်ပုံများကို ပူးတွဲပါ</string>
<string name="messaging_too_many_attachments_toast">ပထမဆုံး %d ရုပ်ပုံများသာလျှင် ပို့ပါမည်</string>
<!--Adding Contacts-->
<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="waiting_for_contact_to_scan">အဆက်အသွယ်မှ​ စကန်ဖတ်ချိတ်ဆက်ရန် စောင့်နေပါသည် \u2026</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="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">ဘရိုင်ယာလင့်ခ်</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">ဤလင့်ခ်သည် ဘရိုင်ယာဗားရှင်းအသစ်မှ ဖြစ်ပါသည်။ ကျေးဇူးပြု၍ နောက်ဆုံးပေါ်ဗားရှင်းထိ မြှင့်တင်ပြီး ပြန်၍ကြိုးစားကြည့်ပါ။</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"></string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2"></string>
<plurals name="contact_added_notification_text">
<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">သင့်ရဲ့အဆက်အသွယ်လိပ်စာများကို အချင်းချင်း မိတ်ဆက်နိုင်ပါသည်၊ ထိုမှသာ သူတို့အချင်းချင်း အပြင်မှာတွေ့စရာမလိုပဲ ဘရိုင်ယာပေါ်မှာ ချိတ်ဆက်လို့ရမှာဖြစ်ပါတယ်။</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_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">%2$s ကို သင်မှ မိတ်ဆက်ပေးရန် %1$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_accepted_received">%2$s နှင့် မိတ်ဆက်ခြင်းကို %1$s က လက်ခံခဲ့ပါသည်။</string>
<string name="introduction_response_declined_received">%2$s နှင့် မိတ်ဆက်ခြင်းကို %1$s က ငြင်းဆိုခဲ့ပါသည်။</string>
<string name="introduction_response_declined_received_by_introducee">%2$s မှ မိတ်ဆက်ခြင်းအား ငြင်းဆိုခဲ့သည်ကို %1$s မှ ပြောပါသည်။</string>
<!--Private Groups-->
<string name="groups_list_empty">ပြသစရာအဖွဲ့များမရှိ</string>
<string name="groups_list_empty_action">+ (အပေါင်းအိုင်ကွန်) အား နှိပ်၍ အဖွဲ့ဖန်တီးပါ သို့မဟုတ် သင့် အဆက်အသွယ်များကို သင့်စီသို့ အဖွဲ့များ ဝေမျှရန် တောင်းဆိုပါ</string>
<string name="groups_created_by">%s မှ ဖန်တီးခဲ့သည်</string>
<plurals name="messages">
<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_member_list">အဖွဲ့ဝင်စာရင်း</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">%s သည် အဖွဲ့သို့ ဝင်ရောက်ခဲ့သည်</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>
<!--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_joined">အဖွဲ့ထဲဝင်ပြီး</string>
<string name="groups_invitations_declined">အဖွဲ့လိုက် ဖိတ်ခေါ်မှုအား ငြင်းဆိုခံရသည်</string>
<plurals name="groups_invitations_open">
<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_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>
<!--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="forum_created_toast">ဖိုရမ် ဖန်တီးပြီးပါပြီ</string>
<string name="no_forum_posts">ပြစရာ ပို့စ်များမရှိ</string>
<string name="no_posts">ပို့စ်များမရှိ</string>
<plurals name="posts">
<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>
<!--Forum Sharing-->
<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">သင်မှ %2$s စီသို့ \"%1$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="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_accepted_received">%s သည် ဖိုရမ်ဖိတ်ကြားမှုအား လက်ခံခဲ့ပါသည်။</string>
<string name="forum_invitation_response_declined_received">%s သည် ဖိုရမ်ဖိတ်ကြားမှုအား ငြင်းဆိုခဲ့ပါသည်။</string>
<string name="sharing_status">မျှဝေခြင်း အခြေအနေ</string>
<string name="sharing_status_forum">ဖိုရမ်ထဲမှ မည်သူမဆို ၎င်းတို့၏ အဆက်အသွယ်များနှင့် မျှဝေနိုင်ပါသည်။ သင်သည် အောက်ပါအဆက်အသွယ်များနှင့် ဤဖိုရမ်အား မျှဝေထားပါသည်။​ သင် မတွေ့နိုင်သော အခြားအဖွဲ့ဝင်များလဲ ရှိနိုင်ပါသည်။</string>
<plurals name="forums_shared">
<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="blogs_publish_blog_post">ထုတ်ဝေမယ်</string>
<string name="blogs_blog_post_created">ဘလော့ဂ်ပို့စ် ဖန်တီးပြီး</string>
<string name="blogs_blog_post_received">ဘလော့ဂ်ပို့စ်အသစ် လက်ခံရယူပြီး</string>
<string name="blogs_feed_empty_state">ပြစရာ ပို့စ်များမရှိ</string>
<string name="blogs_feed_empty_state_action">သင့် အဆက်အသွယ်များနှင့် သင်စာရင်းသွင်းရှိထားသော ဘလော့ဂ်များမှ ပို့စ်များသည် ဤနေရာတွင် ပေါ်ပါမည်။​ \n\n ဘောပင်အိုင်ကွန်အားနှိပ်၍ ပို့စ်ရေးပါ</string>
<string name="blogs_remove_blog">ဘလော့ဂ်အား ဖယ်ရှားမယ်</string>
<string name="blogs_remove_blog_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>
<!--Blog Sharing-->
<string name="blogs_sharing_share">ဘလော့ဂ်အား မျှဝေမယ်</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_accepted_received">%s သည် ဘလော့ဂ်သို့ ဖိတ်ခေါ်မှုအား လက်ခံခဲ့ပါသည်။</string>
<string name="blogs_sharing_response_declined_received">%s သည် ဘလော့ဂ်သို့ ဖိတ်ခေါ်မှုအား ငြင်းဆိုခဲ့ပါသည်။</string>
<string name="blogs_sharing_invitations_title">လော့ဂ်သို့ ဖိတ်ခေါ်မှုများ</string>
<string name="blogs_sharing_joined_toast">ဗလော့ဂ်စီသို့ စာရင်းသွင်းထား</string>
<string name="blogs_sharing_declined_toast">ဖိတ်ကြားမှု ငြင်းဆိုခဲ့သည်</string>
<string name="sharing_status_blog">ဗလော့ဂ်စီသို့ စာရင်းသွင်းထားသူသည် ၎င်းတို့၏ အဆက်အသွယ်များဖြင့် မျှဝေလို့ရပါသည်။ သင်သည် အောက်ပါအဆက်အသွယ်များဖြင့် ဤဗလော့ဂ်အား မျှဝေထားနေပါသည်။ သင်မမြင်နိုင်သော အခြား စာရင်းသွင်းထားသူများလည်း ရှိနိုင်ပါသည်။</string>
<!--RSS Feeds-->
<string name="blogs_rss_feeds_import_button">တင်သွင်းမယ်</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_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>
<!--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">ဘာသာစကား &amp; ဒေသ</string>
<string name="pref_language_changed">ဘရိုင်ယာအား ပြန်လည်စတင်လျှင် ဤအပြင်အဆင်သည် အကျိုးသက်ရောက်ပါမည်။ အကောင့်မှ ထွက်ခွာ၍ ဘရိုင်ယာအား ပြန်လည်စတင်ပေးပါ။</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">ဘလူးတုသ်မှတစ်ဆင့် အဆက်အသွယ်များနှင့် ချိတ်ဆက်မည်</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_never">အင်တာနက်နှင့် မချိတ်ဆက်ပါ</string>
<!--How and when Briar will connect to Tor: E.g. "Don't connect to the Internet (in China)" or "Use Tor network with bridges (in Belarus)"-->
<string name="tor_network_setting_summary">အလိုအလျောက် - %1$s (%2$s တွင်းရှိလျှင်)</string>
<string name="tor_mobile_data_title">မိုဘိုင်းလ်ဒေတာ</string>
<string name="tor_only_when_charging_title">အားသွင်းနေမှသာ အင်တာနက်နှင့် ချိတ်ဆက်မယ်</string>
<string name="tor_only_when_charging_summary">ကိရိယာက ဘတ္ထရီအသုံးပြုနေလျှင် အင်တာနက်ချိတ်ဆက်မှုကို ပိတ်ပေးသည်</string>
<!--Settings Security and Panic-->
<string name="security_settings_title">လုံခြုံရေး</string>
<string name="pref_lock_title">အပ္ပလီကေးရှင်း သော့</string>
<string name="pref_lock_disabled_summary">ဤအင်္ဂါရပ်အားသုံးရန် သင့်စက်အတွက် စကင်သော့တပ်ဆင်ပေးပါ</string>
<!--The %s placeholder is replaced with the following time spans, e.g. 5 Minutes, 1 Hour-->
<!--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">၁ မိနစ်</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">၅ မိနစ်</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">၁၅ မိနစ်</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">၃၀ မိနစ်</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">၁ နာရီ</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">ထိတ်လန့်ခလုတ်အား နှိပ်လျှင် ဘရိုင်ယာမှ မည်ကဲ့သို့ လုပ်ဆောင်ရန် ပြင်ဆင်ချိန်ဆမယ်</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="panic_setting_signout_title">အကောင့်ထွက်မည်</string>
<string name="panic_setting_signout_summary">ထိတ်လန့်ခလုတ် အားနှိပ်ခံရလျှင် ဘရိုင်ယာအကောင့်မှ ထွက်ခွာမည်</string>
<string name="purge_setting_title">အကောင့် ဖျက်သိမ်းမယ်</string>
<string name="purge_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_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_vibration_setting">တုန်ခါမှု</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>
<!--Settings Feedback-->
<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 ဘရောင်ဇာနှင့် ဖွင့်ရန် စဥ်းစားပါ။</string>
<string name="link_warning_open_link">လင့်ခ်ဖွင့်မယ်</string>
<!--Crash Reporter-->
<string name="crash_report_title">ဘရိုင်ယာပျက်ယွင်းမှုတိုင်ကြားခြင်း</string>
<string name="briar_crashed">ဝမ်းနည်းပါတယ်၊ ဘရိုင်ယာ ပျက်သွားပါသည်။</string>
<string name="not_your_fault">သင့်အမှားမဟုတ်ပါ။</string>
<string name="please_send_report">ပျက်ကွက်မှု တိုင်ကြားစာ ပေးပို့၍ ကျွန်ုပ်တို့်အား ဘရိုင်ယာအပ္ပလီကေးရှင်း တိုးတက်ရန် ကူညီပေးပါ။</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_basic_info">အခြေခံ အချက်အလက်များ</string>
<string name="dev_report_device_info">စက်ပစ္စည်း အချက်အလက်များ</string>
<string name="dev_report_time_info">အချိန် အချက်အလက်များ</string>
<string name="dev_report_storage">သိမ်းဆည်းမှု</string>
<string name="dev_report_connectivity">ချိတ်ဆက်မှု</string>
<string name="dev_report_build_config">ပြင်ဆင်ချိန်ဆမှု တည်ဆောက်မယ်</string>
<string name="dev_report_logcat">အပ္ပလီကေးရှင်း မှတ်တမ်း</string>
<string name="dev_report_device_features">စက်ပစ္စည်း အင်္ဂါရပ်များ</string>
<string name="send_report">တိုင်ကြားစာ ပေးပို့မယ်</string>
<string name="close">ပိတ်မယ်</string>
<string name="dev_report_sending">အကြံပြုချက်ပို့နေပါသည်…</string>
<string name="dev_report_sent">တုံ့ပြန်ချက် ပေးပို့ပြီး</string>
<string name="dev_report_saved">တိုင်ကြားစာ သိမ်းဆည်းပြီး။ နောက်တစ်ကြိမ် သင် ဘရိုင်ယာ ထဲသို့ လော့အင်ဝင်လျှင် ပေးပို့လိုက်ပါမည်။</string>
<string name="dev_report_error">ပျက်ကွက်မှု - တိုင်ကြားစာ ပေးပို့ခြင်း မအောင်မြင်ပါ</string>
<!--Sign Out-->
<string name="progress_title_logout">ဘရိုင်ယာမှ အကောင့်ထွက်နေသည်…</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_allow">ဤအပ္ပလီကေးရှင်းများ အပေါ်မှ ဆွဲရန် ခွင့်ပြုမည်</string>
<string name="screen_filter_review_apps">အပ္ပလီကေးရှင်းများ သုံးသပ်မယ်</string>
<!--Permission Requests-->
<string name="permission_camera_title">ကင်မရာသုံးခွင့်</string>
<string name="permission_camera_request_body">QR ကုဒ်အား စကင်ဖတ်ရန် ဘရိုင်ယာမှ ကင်မရာသုံးခွင့်လိုအပ်ပါသည်။</string>
<string name="permission_location_title">တည်နေရာသုံးခွင့်</string>
<string name="permission_location_request_body">ဘလူးတုသ် စက်ပစ္စည်းများ ရှာဖွေရန် ဘရိုင်ယာမှ သင့် တည်နေရာသုံးခွင့် လိုအပ်ပါသည်။ \n\n ဘရိုင်ယာမှ သင့် တည်နေရာအား သိမ်းဆည်းခြင်းနှင့် မည်သူစီသို့ မျှဝေခြင်း မပြုလုပ်ပါ။</string>
<string name="permission_camera_location_title">ကင်မရာ နှင့် တည်နေရာ</string>
<string name="permission_camera_location_request_body">QR ကုဒ် စကင်ဖတ်ရန် ဘရိုင်ယာမှ ကင်မရာသုံးခွင့် လိုအပ်ပါသည်။ \n\n ဘလူးတုသ် စက်ပစ္စည်းများ ရှာဖွေရန် ဘရိုင်ယာမှ သင့် တည်နေရာသုံးခွင့် လိုအပ်ပါသည်။ \n\n ဘရိုင်ယာမှ သင့် တည်နေရာအား သိမ်းဆည်းခြင်းနှင့် မည်သူစီသို့ မျှဝေခြင်း မပြုလုပ်ပါ။</string>
<string name="permission_camera_denied_body">သင်မှ ကင်မရာသုံးခွင့် ငြင်းပယ်ခဲ့ပါသည်၊ သို့သော်လည်း အဆက်အသွယ်များပေါင်းထည့်ရန် ကင်မရာသုံးခွင့် လိုအပ်ပါသည်။ \n\n ကျေးဇူးပြု၍ သုံးခွင့်ပေးရန် စဥ်းစားပေးပါ။</string>
<string name="permission_location_denied_body">သင်မှ တည်နေရာသုံးခွင့် ငြင်းပယ်ခဲ့ပါသည်၊ သို့သော်လည်း ဘလူးတုသ်ရှာဖွေရန် ဤသုံးခွင့် လိုအပ်ပါသည်။ \n\n ကျေးဇူးပြု၍ သုံးခွင့်ပေးရန် စဥ်းစားပေးပါ။</string>
<string name="qr_code">QR ကုဒ်</string>
<string name="show_qr_code_fullscreen">စကရင်အပြည့်တွင် QR ကုဒ် ပြပါ</string>
<!--App Locking-->
<string name="lock_unlock">ဘရိုင်ယာ သော့ဖွင့်မယ်</string>
<string name="lock_unlock_verbose">ဘရိုင်ယာ သော့ဖွင့်ရန် သင့် စက်၏ ပင်နံပါတ်၊ ကုဒ်ပုံစံ သို့မဟုတ် စကားဝှက်အား ရိုက်ထည့်ပေးပါ</string>
<string name="lock_unlock_fingerprint_description">ဆက်လက်ရန် လက်ဗွေမှတ်ထားသော လက်ချောင်းဖြင့် လက်ဗွေမှတ်နေရာပေါ်နှိပ်ပါ</string>
<string name="lock_unlock_password">စကားဝှက်အား အသုံးပြုပါ</string>
<string name="lock_is_locked">ဘရိုင်ယာ အားသော့ခတ်ထားပါသည်</string>
<string name="lock_tap_to_unlock">နှိပ်၍ သော့ဖွင့်မယ်</string>
<!--Connections Screen-->
<string name="transports_help_text">ဘရိုင်ယာသည် သင့်အဆက်အသွယ်များကို အင်တာနက်၊ ဝိုင်ဖိုင်၊ သို့မဟုတ် ဘလူးတုသ် မှတစ်ဆင့် ချိတ်ဆက်နိုင်ပါသည်။ \n\n အင်တာနက်ချိတ်ဆက်မှုအားလုံးသည် လုံခြုံရေးအတွက် Tor ကွန်ယက်မှတစ်ဆင့် ဖြတ်သန်းပါသည်။ \n\n အဆက်အသွယ်တစ်ခုအား နည်းမျိုးစုံနှင့် ဆက်သွယ်နိုင်ခဲ့လျှင် ဘရိုင်ယာသည် နည်းမျိုးစုံအား တစ်ပြိုင်နက်တည်း အသုံးပြုနေပါသည်။</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alice</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_bob">Bob</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_carol">Carol</string>
<!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
<string name="screenshot_message_1">မင်္ဂလာပါ Bob!</string>
<!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
<string name="screenshot_message_2">မင်္ဂလာပါ Alice! Briar အကြောင်းပြောပြတဲ့အတွက် ကျေးဇူတင်ပါတယ်!</string>
<!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">ကိစ္စမရှိပါဘူး၊ မင်းကြိုက်မယ်လို့မျှော်လင့်ပါတယ်။</string>
</resources>

View File

@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Tik om je profielfoto te wijzigen</string>
<string name="dialog_confirm_profile_picture_title">Wijzig profielfoto</string>
<string name="dialog_confirm_profile_picture_remark">Alleen je contacten kunnen je profielfoto zien</string>
<string name="dialog_confirm_profile_picture_remark">Alleen je contacten kunnen deze afbeelding zien</string>
<string name="change_profile_picture_failed_message">Excuses, maar er is iets misgegaan met het updaten van je profielfoto</string>
<!--Settings Display-->
<string name="pref_language_title">Taal &amp; regio</string>

View File

@@ -437,7 +437,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Atingeți pentru a vă schimba poza de profil</string>
<string name="dialog_confirm_profile_picture_title">Schimbare poză de profil</string>
<string name="dialog_confirm_profile_picture_remark">Doar contactele vor vedea poza de contact</string>
<string name="dialog_confirm_profile_picture_remark">Doar contactele dumneavoastră pot vedea această poză</string>
<string name="change_profile_picture_failed_message">Ne pare rău, dar ceva nu a funcționat cum trebuie la actualizarea pozei de profil</string>
<!--Settings Display-->
<string name="pref_language_title">Limbă &amp; Regiune</string>

View File

@@ -132,7 +132,7 @@
<string name="hide">Скрыть</string>
<string name="ok">OK</string>
<string name="cancel">Отмена</string>
<string name="got_it">Понятно</string>
<string name="got_it">ОК</string>
<string name="delete">Удалить</string>
<string name="accept">Принять</string>
<string name="decline">Отклонить</string>
@@ -168,12 +168,12 @@
<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_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_partly_downloaded">Частично загруженные сообщения не могут быть удалены, пока они не загрузятся полностью.</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_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>
@@ -219,9 +219,9 @@
<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="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>
@@ -236,7 +236,7 @@
<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="nickname_missing">Пожалуйста, введите псевдоним</string>
<string name="invalid_link">Неправильная ссылка</string>
<string name="unsupported_link">Эта ссылка из более новой версии Briar. Пожалуйста, обновитесь до последней версии и попробуйте снова.</string>
<string name="intent_own_link">Вы открыли свою собственную ссылку. Используйте контакт, который вы хотите добавить!</string>
@@ -270,24 +270,24 @@
<!--Introductions-->
<string name="introduction_onboarding_title">Представление ваших контактов</string>
<string name="introduction_onboarding_text">Вы можете представить ваши контакты друг другу. Это позволит исключить личную встречу, чтобы связаться через Briar. </string>
<string name="introduction_menu_item">Сделать представление</string>
<string name="introduction_menu_item">Выполнить знакомство</string>
<string name="introduction_activity_title">Выберите контакт</string>
<string name="introduction_not_possible">Вы уже сделали представление этих контактов. Пожалуйста, подождите пока они ответят на него. Если вы или ваши контакты редко бываете в сети, это может занять некоторое время.</string>
<string name="introduction_not_possible">Вы уже познакомили эти контакты. Пожалуйста, подождите пока они ответят на него. Если вы или ваши контакты редко бываете в сети, это может занять некоторое время.</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_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_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_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_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>
<!--Private Groups-->
<string name="groups_list_empty">Нет групп для отображения</string>
<string name="groups_list_empty_action">Для создания группы нажмите значок + или попросите ваши контакты поделиться с вами группами</string>
@@ -449,7 +449,7 @@
<!--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="dialog_confirm_profile_picture_remark">Это изображение могут видеть только ваши контакты</string>
<string name="change_profile_picture_failed_message">Нам очень жаль, но что-то пошло не так во время обновления изображения вашего профиля.</string>
<!--Settings Display-->
<string name="pref_language_title">Язык и регион</string>
@@ -466,7 +466,7 @@
<string name="bluetooth_setting">Подключение к контактам через Bluetooth</string>
<string name="wifi_setting">Подключение к контактам в той же сети Wi-Fi</string>
<string name="tor_enable_title">Подключение к контактам через интернет</string>
<string name="tor_enable_summary">Все соединения проходят через сеть Tor для обеспечения конфиденциальности</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>
@@ -486,7 +486,7 @@
<!--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>
<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"-->
@@ -558,6 +558,7 @@
<string name="optional_contact_email">Ваш адрес 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>
@@ -601,7 +602,7 @@
<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>
<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>

View File

@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Që të ndryshoni foton e profilit tuaj, prekeni</string>
<string name="dialog_confirm_profile_picture_title">Ndryshoni foto profili</string>
<string name="dialog_confirm_profile_picture_remark">Figurën e profilit tuaj mund ta shohin vetëm kontaktet tuaj</string>
<string name="dialog_confirm_profile_picture_remark">Këtë foto mund ta shohin vetëm kontaktet tuaja</string>
<string name="change_profile_picture_failed_message">Na ndjeni, por diç shkoi ters me përditësimin e fotos së profilit tuaj</string>
<!--Settings Display-->
<string name="pref_language_title">Gjuhë &amp; rajon</string>
@@ -536,6 +536,7 @@
<string name="optional_contact_email">Adresa juaj email (në daçi)</string>
<string name="include_debug_report_crash">Përfshi të dhëna anonime rreth vithisjes</string>
<string name="include_debug_report_feedback">Përfshi të dhëna anonime rreth kësaj pajisjeje</string>
<string name="dev_report_user_info">Informacion mbi përdoruesin</string>
<string name="dev_report_basic_info">Të dhëna elementare</string>
<string name="dev_report_device_info">Të dhëna pajisjeje</string>
<string name="dev_report_time_info">Të dhëna kohe</string>

View File

@@ -425,6 +425,10 @@
<string name="blogs_rss_feeds_manage_empty_state">Inga RSS-flöden\n\nTryck plus-ikonen (+) för att importera ett flöde</string>
<string name="blogs_rss_feeds_manage_error">Något gick fel när dina flöden skulle laddas. Försök ingen senare.</string>
<!--Settings Profile Picture-->
<string name="change_profile_picture">Tryck för att ändra din profilbild</string>
<string name="dialog_confirm_profile_picture_title">Ändra profilbild</string>
<string name="dialog_confirm_profile_picture_remark">Bara din kontakter kan se denna bild</string>
<string name="change_profile_picture_failed_message">Tyvärr, något gick fel när din profilbild skulle ändras</string>
<!--Settings Display-->
<string name="pref_language_title">Språk &amp; region</string>
<string name="pref_language_changed">Den här inställningen träder i kraft när Briar startas om. Logga ur och starta om Briar.</string>

View File

@@ -427,7 +427,7 @@
<!--Settings Profile Picture-->
<string name="change_profile_picture">Profil resminizi değiştirmek için dokunun</string>
<string name="dialog_confirm_profile_picture_title">Profil resminizi değiştirin</string>
<string name="dialog_confirm_profile_picture_remark">Sadece bağlantılarınız profil resminizi görebilir</string>
<string name="dialog_confirm_profile_picture_remark">Sadece bağlantılarınız bu resmi görebilir</string>
<string name="change_profile_picture_failed_message">Özür dileriz, profil resminizi güncellerken bir şeyler ters gitti</string>
<!--Settings Display-->
<string name="pref_language_title">Dil ve Bölge</string>
@@ -536,6 +536,7 @@
<string name="optional_contact_email">E-posta adresiniz (isteğe bağlı)</string>
<string name="include_debug_report_crash">Çökme ile ilgili anonim verileri ekle</string>
<string name="include_debug_report_feedback">Bu aygıtla ilgili anonim verileri ekle</string>
<string name="dev_report_user_info">Kullanıcı bilgisi</string>
<string name="dev_report_basic_info">Temel bilgi</string>
<string name="dev_report_device_info">Aygıt bilgisi</string>
<string name="dev_report_stacktrace">Yığın izleme</string>

View File

@@ -415,9 +415,9 @@
<string name="blogs_rss_feeds_manage_empty_state">尚无订阅源可供显示\n\n轻按 + 号导入一个订阅源</string>
<string name="blogs_rss_feeds_manage_error">加载订阅源时出错。请稍候再试。</string>
<!--Settings Profile Picture-->
<string name="change_profile_picture">轻按更改你的个人资料图片</string>
<string name="change_profile_picture">轻按更改你的个人资料图片</string>
<string name="dialog_confirm_profile_picture_title">更改个人资料图片</string>
<string name="dialog_confirm_profile_picture_remark">只有你的联系人能看到你的个人资料图片</string>
<string name="dialog_confirm_profile_picture_remark">只有你的联系人能看到这张图</string>
<string name="change_profile_picture_failed_message">抱歉,更新你的个人资料图片时出了问题</string>
<!--Settings Display-->
<string name="pref_language_title">语言 &amp; 区域</string>
@@ -526,6 +526,7 @@
<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>
@@ -547,7 +548,7 @@
<!--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_body_api_30">另一款应用程序正显示在 Briar 的上层。为了保护您的安全Briar 在另一个应用程序显示在上层时不会响应屏幕触摸。\n\n查看下面的应用程序,寻找是哪个应用导致了此问题</string>
<string name="screen_filter_allow">允许这些应用覆盖在上方</string>
<string name="screen_filter_review_apps">查看应用程序</string>
<!--Permission Requests-->

View File

@@ -39,6 +39,7 @@
<item>lt</item>
<item>mk</item>
<item>ms</item>
<item>my</item>
<item>nb</item>
<item>nl</item>
<item>oc</item>

View File

@@ -468,7 +468,7 @@
<string name="pref_theme_light">Light</string>
<string name="pref_theme_dark">Dark</string>
<string name="pref_theme_auto">Automatic (Daytime)</string>
<string name="pref_theme_system">System Default</string>
<string name="pref_theme_system">System default</string>
<!-- Settings Connections -->
<string name="network_settings_title">Connections</string>
@@ -552,7 +552,6 @@
<string name="cannot_load_ringtone">Cannot load ringtone</string>
<!-- Settings Feedback -->
<string name="feedback_settings_title">Feedback</string>
<string name="send_feedback">Send feedback</string>
<!-- Link Warning -->
@@ -573,6 +572,7 @@
<string name="optional_contact_email">Your email address (optional)</string>
<string name="include_debug_report_crash">Include anonymous data about the crash</string>
<string name="include_debug_report_feedback">Include anonymous data about this device</string>
<string name="dev_report_user_info">User information</string>
<string name="dev_report_basic_info">Basic information</string>
<string name="dev_report_device_info">Device information</string>
<string name="dev_report_stacktrace">Stacktrace</string>

Some files were not shown because too many files have changed in this diff Show More