Compare commits

..

1 Commits

Author SHA1 Message Date
akwizgran
d715701eaa Disable image attachments for remote contacts alpha. 2019-06-18 17:49:17 +01:00
173 changed files with 1782 additions and 9024 deletions

View File

@@ -1,7 +1,16 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="100" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="ANNOTATION_PARAMETER_WRAP" value="1" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
@@ -68,6 +77,7 @@
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
@@ -80,8 +90,7 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
</AND>
</match>
</rule>
@@ -91,8 +100,7 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
<XML_NAMESPACE>Namespace:</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
@@ -103,7 +111,6 @@
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -114,7 +121,6 @@
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@@ -125,7 +131,6 @@
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -136,7 +141,6 @@
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -147,7 +151,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@@ -158,12 +161,64 @@
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<NAME>.*:layout_width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
@@ -171,7 +226,6 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>

View File

@@ -9,10 +9,10 @@ android {
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 10204
versionName "1.2.4"
minSdkVersion 14
targetSdkVersion 26
versionCode 10107
versionName "1.1.7"
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -30,8 +30,8 @@ configurations {
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
tor 'org.briarproject:tor-android:0.3.5.8-64@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip'
tor 'org.briarproject:tor-android:0.3.5.8@zip'
tor 'org.briarproject:obfs4proxy-android:0.0.9@zip'
annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1'
@@ -59,8 +59,6 @@ task unpackTorBinaries {
copy {
from configurations.tor.collect { zipTree(it) }
into torBinariesDir
// TODO: Remove after next Tor upgrade, which won't include non-PIE binaries
include 'geoip.zip', '*_pie.zip'
}
}
dependsOn cleanTorBinaries

View File

@@ -1,6 +1,7 @@
package org.briarproject.bramble.plugin.tor;
import android.content.Context;
import android.os.Build;
import org.briarproject.bramble.api.battery.BatteryManager;
import org.briarproject.bramble.api.event.EventBus;
@@ -88,15 +89,9 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
// Check that we have a Tor binary for this architecture
String architecture = null;
for (String abi : AndroidUtils.getSupportedArchitectures()) {
if (abi.startsWith("x86_64")) {
architecture = "x86_64";
break;
} else if (abi.startsWith("x86")) {
if (abi.startsWith("x86")) {
architecture = "x86";
break;
} else if (abi.startsWith("arm64")) {
architecture = "arm64";
break;
} else if (abi.startsWith("armeabi")) {
architecture = "arm";
break;
@@ -106,8 +101,8 @@ public class AndroidTorPluginFactory implements DuplexPluginFactory {
LOG.info("Tor is not supported on this architecture");
return null;
}
// Use position-independent executable
architecture += "_pie";
// Use position-independent executable for SDK >= 16
if (Build.VERSION.SDK_INT >= 16) architecture += "_pie";
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE);

View File

@@ -23,7 +23,6 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static android.content.Context.WIFI_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.Secure.ANDROID_ID;
@Immutable
@@ -75,7 +74,8 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
// Silence strict mode
StrictMode.ThreadPolicy tp = StrictMode.allowThreadDiskWrites();
super.writeSeed();
if (SDK_INT <= 18) applyOpenSslFix();
if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT <= 18)
applyOpenSslFix();
StrictMode.setThreadPolicy(tp);
}

View File

@@ -66,8 +66,8 @@ dependencyVerification {
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
'org.briarproject:obfs4proxy-android:0.0.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a',
'org.briarproject:tor-android:0.3.5.8-64:tor-android-0.3.5.8-64.zip:9f144088c0fe845d1cf3232cdc2b51c68e6f9a22660592009f43a5633fca8824',
'org.briarproject:obfs4proxy-android:0.0.9:obfs4proxy-android-0.0.9.zip:9b7e9181535ea8d8bbe8ae6338e08cf4c5fc1e357a779393e0ce49586d459ae0',
'org.briarproject:tor-android:0.3.5.8:tor-android-0.3.5.8.zip:42a13a6f185be1a62f42e3f30ce66a3c099ac5ec890a65e7593111b65b44a54a',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.groovy:groovy-all:2.4.15:groovy-all-2.4.15.jar:51d6c4e71782e85674239189499854359d380fb75e1a703756e3aaa5b98a5af0',

View File

@@ -7,5 +7,5 @@ public interface FeatureFlags {
boolean shouldEnableImageAttachments();
boolean shouldEnablePrivateMessageDeletion();
boolean shouldEnableRemoteContacts();
}

View File

@@ -4,10 +4,8 @@ import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.db.ContactExistsException;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.PendingContactExistsException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
@@ -119,14 +117,9 @@ public interface ContactManager {
* @throws FormatException If the link is invalid
* @throws GeneralSecurityException If the pending contact's handshake
* public key is invalid
* @throws ContactExistsException If a contact with the same handshake
* public key already exists
* @throws PendingContactExistsException If a pending contact with the same
* handshake public key already exists
*/
PendingContact addPendingContact(String link, String alias)
throws DbException, FormatException, GeneralSecurityException,
ContactExistsException, PendingContactExistsException;
throws DbException, FormatException, GeneralSecurityException;
/**
* Returns the pending contact with the given ID.

View File

@@ -83,7 +83,7 @@ public interface DatabaseComponent extends TransactionManager {
/**
* Stores a pending contact.
*/
void addPendingContact(Transaction txn, PendingContact p, AuthorId local)
void addPendingContact(Transaction txn, PendingContact p)
throws DbException;
/**

View File

@@ -1,21 +1,9 @@
package org.briarproject.bramble.api.db;
import org.briarproject.bramble.api.contact.PendingContact;
/**
* Thrown when a duplicate pending contact is added to the database. This
* exception may occur due to concurrent updates and does not indicate a
* database error.
*/
public class PendingContactExistsException extends DbException {
private final PendingContact pendingContact;
public PendingContactExistsException(PendingContact pendingContact) {
this.pendingContact = pendingContact;
}
public PendingContact getPendingContact() {
return pendingContact;
}
}

View File

@@ -139,8 +139,7 @@ class ContactManagerImpl implements ContactManager, EventListener {
pendingContactFactory.createPendingContact(link, alias);
Transaction txn = db.startTransaction(false);
try {
AuthorId local = identityManager.getLocalAuthor(txn).getId();
db.addPendingContact(txn, p, local);
db.addPendingContact(txn, p);
KeyPair ourKeyPair = identityManager.getHandshakeKeys(txn);
keyManager.addPendingContact(txn, p.getId(), p.getPublicKey(),
ourKeyPair);

View File

@@ -267,16 +267,6 @@ interface Database<T> {
*/
Collection<ContactId> getContacts(T txn, AuthorId local) throws DbException;
/**
* Returns the contact with the given {@code handshakePublicKey}
* for the given local pseudonym or {@code null} if none exists.
* <p/>
* Read-only.
*/
@Nullable
Contact getContact(T txn, PublicKey handshakePublicKey, AuthorId local)
throws DbException;
/**
* Returns the group with the given ID.
* <p/>

View File

@@ -291,17 +291,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
}
@Override
public void addPendingContact(Transaction transaction, PendingContact p,
AuthorId local) throws DbException {
public void addPendingContact(Transaction transaction, PendingContact p)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
Contact contact = db.getContact(txn, p.getPublicKey(), local);
if (contact != null)
throw new ContactExistsException(local, contact.getAuthor());
if (db.containsPendingContact(txn, p.getId())) {
PendingContact existing = db.getPendingContact(txn, p.getId());
throw new PendingContactExistsException(existing);
}
if (db.containsPendingContact(txn, p.getId()))
throw new PendingContactExistsException();
db.addPendingContact(txn, p);
transaction.attach(new PendingContactAddedEvent(p));
}

View File

@@ -1465,47 +1465,6 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
@Nullable
@Override
public Contact getContact(Connection txn, PublicKey handshakePublicKey,
AuthorId localAuthorId) throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT contactId, authorId, formatVersion, name,"
+ " alias, publicKey, verified"
+ " FROM contacts"
+ " WHERE handshakePublicKey = ? AND localAuthorId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, handshakePublicKey.getEncoded());
ps.setBytes(2, localAuthorId.getBytes());
rs = ps.executeQuery();
if (!rs.next()) {
rs.close();
ps.close();
return null;
}
ContactId contactId = new ContactId(rs.getInt(1));
AuthorId authorId = new AuthorId(rs.getBytes(2));
int formatVersion = rs.getInt(3);
String name = rs.getString(4);
String alias = rs.getString(5);
PublicKey publicKey = new SignaturePublicKey(rs.getBytes(6));
boolean verified = rs.getBoolean(7);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
Author author =
new Author(authorId, formatVersion, name, publicKey);
return new Contact(contactId, author, localAuthorId, alias,
handshakePublicKey, verified);
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override
public Group getGroup(Connection txn, GroupId g) throws DbException {
PreparedStatement ps = null;

View File

@@ -195,6 +195,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!assetsAreUpToDate()) installAssets();
if (cookieFile.exists() && !cookieFile.delete())
LOG.warning("Old auth cookie not deleted");
// Migrate old settings before having a chance to stop
migrateSettings();
// Start a new Tor process
LOG.info("Starting Tor");
String torPath = torFile.getAbsolutePath();
@@ -814,6 +816,21 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
}
// TODO remove when sufficient time has passed. Added 2018-08-15
private void migrateSettings() {
Settings sOld = callback.getSettings();
int oldNetwork = sOld.getInt("network", -1);
if (oldNetwork == -1) return;
Settings s = new Settings();
if (oldNetwork == 0) {
s.putInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_NEVER);
} else if (oldNetwork == 1) {
s.putBoolean(PREF_TOR_MOBILE, false);
}
s.putInt("network", -1);
callback.mergeSettings(s);
}
private static class ConnectionStatus {
// All of the following are locking: this

View File

@@ -34,7 +34,6 @@ import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.test.SettableClock;
import org.briarproject.bramble.test.TestDatabaseConfig;
import org.briarproject.bramble.test.TestMessageFactory;
import org.briarproject.bramble.test.TestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -1150,43 +1149,6 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close();
}
@Test
public void testGetContactsByHandshakePublicKey() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add an identity for a local author - no contacts should be
// associated
db.addIdentity(txn, identity);
PublicKey handshakePublicKey = TestUtils.getSignaturePublicKey();
Contact contact =
db.getContact(txn, handshakePublicKey, localAuthor.getId());
assertNull(contact);
// Add a contact associated with the local author
assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
handshakePublicKey, true));
contact = db.getContact(txn, handshakePublicKey, localAuthor.getId());
assertNotNull(contact);
assertEquals(contactId, contact.getId());
assertEquals(author, contact.getAuthor());
assertNull(contact.getAlias());
assertEquals(handshakePublicKey, contact.getHandshakePublicKey());
assertTrue(contact.isVerified());
assertEquals(author.getName(), contact.getAuthor().getName());
assertEquals(author.getPublicKey(), contact.getAuthor().getPublicKey());
assertEquals(author.getFormatVersion(),
contact.getAuthor().getFormatVersion());
// Ensure no contacts are returned after contact was deleted
db.removeContact(txn, contactId);
contact = db.getContact(txn, handshakePublicKey, localAuthor.getId());
assertNull(contact);
db.commitTransaction(txn);
db.close();
}
@Test
public void testOfferedMessages() throws Exception {
Database<Connection> db = open(false);

View File

@@ -26,7 +26,7 @@ public class BrambleCoreIntegrationTestModule {
}
@Override
public boolean shouldEnablePrivateMessageDeletion() {
public boolean shouldEnableRemoteContacts() {
return true;
}
};

View File

@@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = pt_BR: pt-rBR, nb_NO: nb, zh-Hans: zh-rCN, zh-Hant: zh-rTW
lang_map = pt_BR: pt-rBR, nb_NO: nb, zh-Hans: zh-rCN
[briar.stringsxml-5]
file_filter = src/main/res/values-<lang>/strings.xml

View File

@@ -20,10 +20,10 @@ android {
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 10204
versionName "1.2.4"
minSdkVersion 15
targetSdkVersion 26
versionCode 10107
versionName "1.1.7"
applicationId "org.briarproject.briar.android"
buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""
@@ -117,7 +117,7 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.google.zxing:core:3.3.3'
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0'
implementation 'com.vanniktech:emoji-google:0.6.0' // later versions already use androidx
implementation 'com.vanniktech:emoji-google:0.5.1'
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.1' // later versions already use androidx
def glideVersion = '4.9.0'
implementation("com.github.bumptech.glide:glide:$glideVersion") {

View File

@@ -5,10 +5,6 @@
# QR codes
-keep class com.google.zxing.Result
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# RSS libraries
-keep,includedescriptorclasses class com.rometools.rome.feed.synd.impl.** { *; }

View File

@@ -4,7 +4,6 @@ import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
import javax.inject.Singleton;
@@ -14,7 +13,6 @@ import dagger.Component;
@Singleton
@Component(modules = {
AppModule.class,
AttachmentModule.class,
BriarCoreModule.class,
BrambleAndroidModule.class,
BriarAccountModule.class,

View File

@@ -47,17 +47,15 @@ public class AttachmentRetrieverIntegrationTest {
);
private final MessageId msgId = new MessageId(getRandomId());
private final ImageHelper imageHelper = new ImageHelperImpl();
private final AttachmentRetriever retriever =
new AttachmentRetrieverImpl(null, dimensions, imageHelper,
new ImageSizeCalculator(imageHelper));
new AttachmentRetriever(null, dimensions);
@Test
public void testSmallJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(smallKitten);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(160, item.getWidth());
assertEquals(240, item.getHeight());
@@ -72,8 +70,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testBigJpegImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(originalKitten);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(1728, item.getWidth());
assertEquals(2592, item.getHeight());
@@ -88,8 +86,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallPngImage() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/png");
InputStream is = getUrlInputStream(pngKitten);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(msgId, item.getMessageId());
assertEquals(737, item.getWidth());
assertEquals(510, item.getHeight());
@@ -104,8 +102,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testUberGif() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(uberGif);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -119,8 +117,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testLottaPixels() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(lottaPixel);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(64250, item.getWidth());
assertEquals(64250, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -134,8 +132,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testImageIoCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(imageIoCrash);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1184, item.getWidth());
assertEquals(448, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -149,8 +147,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testGimpCrash() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(gimpCrash);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -164,8 +162,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testOptiPngAfl() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(optiPngAfl);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(32, item.getWidth());
assertEquals(32, item.getHeight());
assertEquals(dimensions.minHeight, item.getThumbnailWidth());
@@ -179,8 +177,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testLibrawError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getUrlInputStream(librawError);
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertTrue(item.hasError());
}
@@ -188,8 +186,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallAnimatedGifMaxDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated.gif");
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(65535, item.getWidth());
assertEquals(65535, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -203,8 +201,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallAnimatedGifHugeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("animated2.gif");
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(10000, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -218,8 +216,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testSmallGifLargeDimensions() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/gif");
InputStream is = getAssetInputStream("error_large.gif");
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(16384, item.getWidth());
assertEquals(16384, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());
@@ -233,8 +231,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testHighError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_high.jpg");
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1, item.getWidth());
assertEquals(10000, item.getHeight());
assertEquals(dimensions.minWidth, item.getThumbnailWidth());
@@ -248,8 +246,8 @@ public class AttachmentRetrieverIntegrationTest {
public void testWideError() throws Exception {
AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg");
InputStream is = getAssetInputStream("error_wide.jpg");
Attachment a = new Attachment(h, is);
AttachmentItem item = retriever.getAttachmentItem(a, true);
Attachment a = new Attachment(is);
AttachmentItem item = retriever.getAttachmentItem(h, a, true);
assertEquals(1920, item.getWidth());
assertEquals(1, item.getHeight());
assertEquals(dimensions.maxWidth, item.getThumbnailWidth());

View File

@@ -73,7 +73,7 @@
android:label="@string/crash_report_title"
android:launchMode="singleInstance"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden">
android:windowSoftInputMode="stateHidden">
</activity>
<activity
@@ -89,7 +89,7 @@
<activity
android:name="org.briarproject.briar.android.account.SetupActivity"
android:label="@string/setup_title"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
android:windowSoftInputMode="adjustResize">
</activity>
<activity
@@ -126,7 +126,7 @@
android:label="@string/app_name"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateUnchanged">
android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -145,7 +145,7 @@
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
android:label="@string/groups_create_group_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -174,7 +174,8 @@
<activity
android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity"
android:label="@string/groups_member_list"
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity">
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity"
android:windowSoftInputMode="adjustResize|stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/>
@@ -183,7 +184,8 @@
<activity
android:name="org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity"
android:label="@string/groups_reveal_contacts"
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity">
android:parentActivityName="org.briarproject.briar.android.privategroup.conversation.GroupActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/>
@@ -221,7 +223,7 @@
android:name="org.briarproject.briar.android.forum.CreateForumActivity"
android:label="@string/create_forum_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -290,7 +292,7 @@
android:name="org.briarproject.briar.android.blog.WriteBlogPostActivity"
android:label="@string/blogs_write_blog_post"
android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
android:windowSoftInputMode="stateVisible|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.blog.BlogActivity"/>
@@ -300,7 +302,7 @@
android:name="org.briarproject.briar.android.blog.ReblogActivity"
android:label="@string/blogs_reblog_button"
android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity"
android:windowSoftInputMode="adjustResize|stateHidden">
android:windowSoftInputMode="stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.blog.BlogActivity"/>
@@ -310,7 +312,7 @@
android:name="org.briarproject.briar.android.blog.RssFeedImportActivity"
android:label="@string/blogs_rss_feeds_import"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
android:windowSoftInputMode="stateVisible|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -339,7 +341,7 @@
android:name="org.briarproject.briar.android.introduction.IntroductionActivity"
android:label="@string/introduction_activity_title"
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
android:windowSoftInputMode="adjustResize|stateHidden">
android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.conversation.ConversationActivity"/>
@@ -367,8 +369,7 @@
<activity
android:name="org.briarproject.briar.android.login.ChangePasswordActivity"
android:label="@string/change_password"
android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.settings.SettingsActivity"/>
@@ -423,7 +424,7 @@
android:name=".android.contact.add.remote.AddContactActivity"
android:label="@string/add_contact_remotely_title_case"
android:theme="@style/BriarTheme"
android:windowSoftInputMode="adjustResize|stateHidden"/>
android:windowSoftInputMode="stateHidden|adjustResize"/>
<activity
android:name=".android.contact.add.remote.PendingContactListActivity"

View File

@@ -30,7 +30,6 @@ import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender;
@@ -69,8 +68,7 @@ import dagger.Component;
BriarCoreModule.class,
BrambleAndroidModule.class,
BriarAccountModule.class,
AppModule.class,
AttachmentModule.class
AppModule.class
})
public interface AndroidComponent
extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,

View File

@@ -347,10 +347,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (currentTime - lastSound > SOUND_DELAY) {
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (sound && !StringUtils.isNullOrEmpty(ringtoneUri)) {
Uri uri = Uri.parse(ringtoneUri);
if (!"file".equals(uri.getScheme())) b.setSound(uri);
}
if (sound && !StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
lastSound = currentTime;
}
@@ -361,8 +359,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
int defaults = DEFAULT_LIGHTS;
boolean sound = settings.getBoolean(PREF_NOTIFY_SOUND, true);
String ringtoneUri = settings.get(PREF_NOTIFY_RINGTONE_URI);
if (sound && (StringUtils.isNullOrEmpty(ringtoneUri) ||
"file".equals(Uri.parse(ringtoneUri).getScheme())))
if (sound && StringUtils.isNullOrEmpty(ringtoneUri))
defaults |= DEFAULT_SOUND;
if (settings.getBoolean(PREF_NOTIFY_VIBRATION, true))
defaults |= DEFAULT_VIBRATE;

View File

@@ -239,11 +239,11 @@ public class AppModule {
@Override
public boolean shouldEnableImageAttachments() {
return IS_DEBUG_BUILD;
return false;
}
@Override
public boolean shouldEnablePrivateMessageDeletion() {
public boolean shouldEnableRemoteContacts() {
return IS_DEBUG_BUILD;
}
};

View File

@@ -0,0 +1,54 @@
package org.briarproject.briar.android;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
class BackgroundMonitor implements ActivityLifecycleCallbacks {
private final AtomicInteger foregroundActivities = new AtomicInteger(0);
boolean isRunningInBackground() {
return foregroundActivities.get() == 0;
}
@Override
public void onActivityCreated(Activity a, @Nullable Bundle state) {
}
@Override
public void onActivityStarted(Activity a) {
foregroundActivities.incrementAndGet();
}
@Override
public void onActivityResumed(Activity a) {
}
@Override
public void onActivityPaused(Activity a) {
}
@Override
public void onActivityStopped(Activity a) {
foregroundActivities.decrementAndGet();
}
@Override
public void onActivitySaveInstanceState(Activity a,
@Nullable Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity a) {
}
}

View File

@@ -34,9 +34,9 @@ import java.util.logging.LogRecord;
import java.util.logging.Logger;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
@@ -82,9 +82,10 @@ public class BriarApplicationImpl extends Application
implements BriarApplication {
private static final Logger LOG =
getLogger(BriarApplicationImpl.class.getName());
Logger.getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler();
private final BackgroundMonitor backgroundMonitor = new BackgroundMonitor();
private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs;
@@ -107,16 +108,12 @@ public class BriarApplicationImpl extends Application
if (IS_DEBUG_BUILD) enableStrictMode();
Logger rootLogger = getLogger("");
Handler[] handlers = rootLogger.getHandlers();
// Disable the Android logger for release builds
for (Handler handler : handlers) rootLogger.removeHandler(handler);
if (IS_DEBUG_BUILD || IS_BETA_BUILD) {
// We can't set the level of the Android logger at runtime, so
// raise records to the logger's default level
rootLogger.addHandler(new LevelRaisingHandler(FINE, INFO));
// Restore the default handlers after the level raising handler
for (Handler handler : handlers) rootLogger.addHandler(handler);
Logger rootLogger = Logger.getLogger("");
if (!IS_DEBUG_BUILD && !IS_BETA_BUILD) {
// Remove default log handlers so system log is not used
for (Handler handler : rootLogger.getHandlers()) {
rootLogger.removeHandler(handler);
}
}
rootLogger.addHandler(logHandler);
rootLogger.setLevel(IS_DEBUG_BUILD || IS_BETA_BUILD ? FINE : INFO);
@@ -125,6 +122,9 @@ public class BriarApplicationImpl extends Application
applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider());
if (SDK_INT < 16)
registerActivityLifecycleCallbacks(backgroundMonitor);
}
protected AndroidComponent createApplicationComponent() {
@@ -186,8 +186,12 @@ public class BriarApplicationImpl extends Application
@Override
public boolean isRunningInBackground() {
RunningAppProcessInfo info = new RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND);
if (SDK_INT >= 16) {
RunningAppProcessInfo info = new RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND);
} else {
return backgroundMonitor.isRunningInBackground();
}
}
}

View File

@@ -238,6 +238,8 @@ public class BriarService extends Service {
} else if (level == TRIM_MEMORY_RUNNING_LOW) {
LOG.info("Trim memory: running low");
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) {
// This level may be received if SDK_INT < 16, although the
// constant isn't declared until API level 16
LOG.warning("Trim memory: running critically low");
// If we're not in the foreground, clear the UI to save memory
if (app.isRunningInBackground()) hideUi();

View File

@@ -1,42 +0,0 @@
package org.briarproject.briar.android;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.annotation.concurrent.Immutable;
/**
* Log handler that raises all records at or above a given source level to a
* given destination level. This affects the level seen by subsequent handlers.
*/
@Immutable
@NotNullByDefault
class LevelRaisingHandler extends Handler {
private final Level dest;
private final int srcInt, destInt;
LevelRaisingHandler(Level src, Level dest) {
this.dest = dest;
srcInt = src.intValue();
destInt = dest.intValue();
if (srcInt > destInt) throw new IllegalArgumentException();
}
@Override
public void publish(LogRecord record) {
int recordInt = record.getLevel().intValue();
if (recordInt >= srcInt && recordInt < destInt) record.setLevel(dest);
}
@Override
public void flush() {
}
@Override
public void close() {
}
}

View File

@@ -150,7 +150,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
// Get permissions
String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions == null) return false;
if (SDK_INT < 23) {
if (SDK_INT >= 16 && SDK_INT < 23) {
// Check whether the permission has been requested and granted
int[] flags = packageInfo.requestedPermissionsFlags;
for (int i = 0; i < requestedPermissions.length; i++) {

View File

@@ -3,7 +3,6 @@ package org.briarproject.briar.android.account;
import android.os.Bundle;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -11,15 +10,18 @@ import android.widget.Button;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import javax.annotation.Nullable;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
import static android.view.inputmethod.EditorInfo.IME_ACTION_NONE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
import static org.briarproject.briar.android.util.UiUtils.setError;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -62,6 +64,12 @@ public class AuthorNameFragment extends SetupFragment {
return TAG;
}
@Override
public void onResume() {
super.onResume();
showSoftKeyboard(authorNameInput);
}
@Override
protected String getHelpText() {
return getString(R.string.setup_name_explanation);
@@ -69,21 +77,20 @@ public class AuthorNameFragment extends SetupFragment {
@Override
public void onTextChanged(CharSequence authorName, int i, int i1, int i2) {
int authorNameLength = toUtf8(authorName.toString().trim()).length;
int authorNameLength = StringUtils.toUtf8(authorName.toString()).length;
boolean error = authorNameLength > MAX_AUTHOR_NAME_LENGTH;
setError(authorNameWrapper, getString(R.string.name_too_long), error);
boolean enabled = authorNameLength > 0 && !error;
authorNameInput
.setImeOptions(enabled ? IME_ACTION_NEXT : IME_ACTION_NONE);
authorNameInput.setOnEditorActionListener(enabled ? this : null);
nextButton.setEnabled(enabled);
}
@Override
public void onClick(View view) {
Editable text = authorNameInput.getText();
if (text != null) {
setupController.setAuthorName(text.toString().trim());
setupController.showPasswordFragment();
}
setupController.setAuthorName(authorNameInput.getText().toString());
setupController.showPasswordFragment();
}
}

View File

@@ -61,6 +61,7 @@ public class SetPasswordFragment extends SetupFragment {
strengthMeter = v.findViewById(R.id.strength_meter);
passwordEntryWrapper = v.findViewById(R.id.password_entry_wrapper);
passwordEntry = v.findViewById(R.id.password_entry);
passwordEntry.requestFocus();
passwordConfirmationWrapper =
v.findViewById(R.id.password_confirm_wrapper);
passwordConfirmation = v.findViewById(R.id.password_confirm);

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.activity;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
@@ -11,6 +12,7 @@ import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -42,10 +44,10 @@ import javax.inject.Inject;
import static android.arch.lifecycle.Lifecycle.State.STARTED;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
/**
* Warning: Some activities don't extend {@link BaseActivity}.
@@ -215,6 +217,17 @@ public abstract class BaseActivity extends AppCompatActivity
});
}
public void showSoftKeyboard(View view) {
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT);
}
public void hideSoftKeyboard(View view) {
IBinder token = view.getWindowToken();
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
}
@UiThread
public void handleDbException(DbException e) {
supportFinishAfterTransition();

View File

@@ -1,8 +1,6 @@
package org.briarproject.briar.android.attachment;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import android.support.annotation.Nullable;
@@ -14,17 +12,11 @@ import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.logging.Logger;
import static android.graphics.Bitmap.CompressFormat.JPEG;
import static android.graphics.BitmapFactory.decodeStream;
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.IoUtils.tryToClose;
@@ -32,7 +24,6 @@ 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.messaging.MessagingConstants.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreationTask {
@@ -40,11 +31,8 @@ class AttachmentCreationTask {
private static Logger LOG =
getLogger(AttachmentCreationTask.class.getName());
private static final int MAX_ATTACHMENT_DIMENSION = 1000;
private final MessagingManager messagingManager;
private final ContentResolver contentResolver;
private final ImageSizeCalculator imageSizeCalculator;
private final GroupId groupId;
private final Collection<Uri> uris;
private final boolean needsSize;
@@ -55,26 +43,24 @@ class AttachmentCreationTask {
AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver,
AttachmentCreator attachmentCreator,
ImageSizeCalculator imageSizeCalculator,
GroupId groupId, Collection<Uri> uris, boolean needsSize) {
AttachmentCreator attachmentCreator, GroupId groupId,
Collection<Uri> uris, boolean needsSize) {
this.messagingManager = messagingManager;
this.contentResolver = contentResolver;
this.imageSizeCalculator = imageSizeCalculator;
this.groupId = groupId;
this.uris = uris;
this.needsSize = needsSize;
this.attachmentCreator = attachmentCreator;
}
void cancel() {
public void cancel() {
canceled = true;
attachmentCreator = null;
}
@IoExecutor
void storeAttachments() {
for (Uri uri : uris) processUri(uri);
public void storeAttachments() {
for (Uri uri: uris) processUri(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (!canceled && attachmentCreator != null)
attachmentCreator.onAttachmentCreationFinished();
@@ -112,8 +98,6 @@ class AttachmentCreationTask {
}
InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
is = compressImage(is, contentType);
contentType = "image/jpeg";
long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
@@ -129,48 +113,4 @@ class AttachmentCreationTask {
return false;
}
private InputStream compressImage(InputStream is, String contentType)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
Bitmap bitmap = createBitmap(is, contentType);
for (int quality = 100; quality >= 0; quality -= 10) {
if (!bitmap.compress(JPEG, quality, out))
throw new IOException();
if (out.size() <= MAX_IMAGE_SIZE) {
if (LOG.isLoggable(INFO)) {
LOG.info("Compressed image to "
+ out.size() + " bytes, quality " + quality);
}
return new ByteArrayInputStream(out.toByteArray());
}
out.reset();
}
throw new IOException();
} finally {
tryToClose(is, LOG, WARNING);
}
}
private Bitmap createBitmap(InputStream is, String contentType)
throws IOException {
is = new BufferedInputStream(is);
Size size = imageSizeCalculator.getSize(is, contentType);
if (size.error) throw new IOException();
if (LOG.isLoggable(INFO))
LOG.info("Original image size: " + size.width + "x" + size.height);
int dimension = Math.max(size.width, size.height);
int inSampleSize = 1;
while (dimension > MAX_ATTACHMENT_DIMENSION) {
inSampleSize *= 2;
dimension /= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("Scaling attachment by factor of " + inSampleSize);
Options options = new Options();
options.inSampleSize = inSampleSize;
Bitmap bitmap = decodeStream(is, null, options);
if (bitmap == null) throw new IOException();
return bitmap;
}
}

View File

@@ -1,24 +1,82 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
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.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
public interface AttachmentCreator {
public class AttachmentCreator {
private static Logger LOG = getLogger(AttachmentCreator.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
private final MutableLiveData<AttachmentResult> result =
new MutableLiveData<>();
@Nullable
private AttachmentCreationTask task;
public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
}
@UiThread
LiveData<AttachmentResult> storeAttachments(LiveData<GroupId> groupId,
Collection<Uri> newUris);
public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || !uris.isEmpty())
throw new IllegalStateException();
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, id, uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
/**
* This should be only called after configuration changes.
@@ -26,10 +84,68 @@ public interface AttachmentCreator {
* They are already being created and returned by the existing LiveData.
*/
@UiThread
LiveData<AttachmentResult> getLiveAttachments();
public LiveData<AttachmentResult> getLiveAttachments() {
if (task == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(h, a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@IoExecutor
void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@IoExecutor
void onAttachmentCreationFinished() {
result.postValue(getResult(true));
}
@UiThread
List<AttachmentHeader> getAttachmentHeadersForSending();
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
/**
* Marks the attachments as sent and adds the items to the cache for display
@@ -37,24 +153,66 @@ public interface AttachmentCreator {
* @param id The MessageId of the sent message.
*/
@UiThread
void onAttachmentsSent(MessageId id);
public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
/**
* Needs to be called when created attachments will not be sent anymore.
*/
@UiThread
void cancel();
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
@UiThread
void deleteUnsentAttachments();
private void resetState() {
task = null;
uris.clear();
itemResults.clear();
result.setValue(null);
}
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize);
@UiThread
public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
@IoExecutor
void onAttachmentError(Uri uri, Throwable t);
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
@IoExecutor
void onAttachmentCreationFinished();
}
}

View File

@@ -1,233 +0,0 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
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.R;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreatorImpl implements AttachmentCreator {
private static Logger LOG =
getLogger(AttachmentCreatorImpl.class.getName());
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final ImageSizeCalculator imageSizeCalculator;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
@Nullable
private AttachmentCreationTask task;
@Nullable
private volatile MutableLiveData<AttachmentResult> result;
@Inject
AttachmentCreatorImpl(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever,
ImageSizeCalculator imageSizeCalculator) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
this.imageSizeCalculator = imageSizeCalculator;
}
@Override
@UiThread
public LiveData<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> newUris) {
if (task != null || result != null || !uris.isEmpty())
throw new IllegalStateException();
MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
this.result = result;
uris.addAll(newUris);
observeForeverOnce(groupId, id -> {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, imageSizeCalculator, id,
uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
return result;
}
@Override
@UiThread
public LiveData<AttachmentResult> getLiveAttachments() {
MutableLiveData<AttachmentResult> result = this.result;
if (task == null || result == null || uris.isEmpty())
throw new IllegalStateException();
// A task is already running. It will update the result LiveData.
// So nothing more to do here.
return result;
}
@Override
@IoExecutor
public void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = retriever.getMessageAttachment(h);
AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
if (item.hasError()) throw new IOException();
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, item);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@Override
@IoExecutor
public void onAttachmentError(Uri uri, Throwable t) {
// get error message
String errorMsg;
if (t instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
errorMsg = app.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (t instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = null; // generic error
}
AttachmentItemResult itemResult =
new AttachmentItemResult(uri, errorMsg);
itemResults.add(itemResult);
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(false));
// expect to receive a cancel from the UI
}
@Override
@IoExecutor
public void onAttachmentCreationFinished() {
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) result.postValue(getResult(true));
}
@Override
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
@Override
@UiThread
public void onAttachmentsSent(MessageId id) {
List<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
@Override
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
deleteUnsentAttachments();
resetState();
}
@UiThread
private void resetState() {
task = null;
uris.clear();
itemResults.clear();
MutableLiveData<AttachmentResult> result = this.result;
if (result != null) {
result.setValue(null);
this.result = null;
}
}
@Override
@UiThread
public void deleteUnsentAttachments() {
// Make a copy for the IoExecutor as we clear the itemResults soon
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
}

View File

@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
class AttachmentDimensions {
public class AttachmentDimensions {
final int defaultSize;
final int minWidth, maxWidth;
@@ -26,7 +26,7 @@ class AttachmentDimensions {
this.maxHeight = maxHeight;
}
static AttachmentDimensions getAttachmentDimensions(Resources res) {
public static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize(

View File

@@ -68,7 +68,7 @@ public class AttachmentItem implements Parcelable {
header = new AttachmentHeader(messageId, mimeType);
}
public AttachmentHeader getHeader() {
AttachmentHeader getHeader() {
return header;
}

View File

@@ -4,14 +4,12 @@ import android.arch.lifecycle.LiveData;
import android.net.Uri;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import java.util.Collection;
import java.util.List;
@UiThread
@NotNullByDefault
public interface AttachmentManager {
LiveData<AttachmentResult> storeAttachments(Collection<Uri> uri,

View File

@@ -1,43 +0,0 @@
package org.briarproject.briar.android.attachment;
import android.app.Application;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
@Module
public class AttachmentModule {
@Provides
ImageHelper provideImageHelper(ImageHelperImpl imageHelper) {
return imageHelper;
}
@Provides
ImageSizeCalculator provideImageSizeCalculator(ImageHelper imageHelper) {
return new ImageSizeCalculator(imageHelper);
}
@Provides
AttachmentDimensions provideAttachmentDimensions(Application app) {
return getAttachmentDimensions(app.getResources());
}
@Provides
@Singleton
AttachmentRetriever provideAttachmentRetriever(
AttachmentRetrieverImpl attachmentRetriever) {
return attachmentRetriever;
}
@Provides
@Singleton
AttachmentCreator provideAttachmentCreator(
AttachmentCreatorImpl attachmentCreator) {
return attachmentCreator;
}
}

View File

@@ -1,29 +1,276 @@
package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.media.ExifInterface;
import android.webkit.MimeTypeMap;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault
public interface AttachmentRetriever {
public class AttachmentRetriever {
void cachePut(MessageId messageId, List<AttachmentItem> attachments);
private static final Logger LOG =
getLogger(AttachmentRetriever.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
@VisibleForTesting
AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
public AttachmentRetriever(MessagingManager messagingManager,
AttachmentDimensions dimensions) {
this(messagingManager, dimensions, new ImageHelper() {
@Override
public DecodeResult decodeStream(InputStream is) {
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
});
}
public void cachePut(MessageId messageId, List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Nullable
List<AttachmentItem> cacheGet(MessageId messageId);
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
Attachment getMessageAttachment(AttachmentHeader h) throws DbException;
@DatabaseExecutor
public List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
List<AttachmentHeader> headers) throws DbException {
long start = now();
List<Pair<AttachmentHeader, Attachment>> attachments =
new ArrayList<>(headers.size());
for (AttachmentHeader h : headers) {
Attachment a = messagingManager.getAttachment(h.getMessageId());
attachments.add(new Pair<>(h, a));
}
logDuration(LOG, "Loading attachments", start);
return attachments;
}
@DatabaseExecutor
Attachment getMessageAttachment(AttachmentHeader h) throws DbException {
return messagingManager.getAttachment(h.getMessageId());
}
/**
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
* <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}.
*/
public List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1;
List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> a : attachments) {
AttachmentItem item =
getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
items.add(item);
}
return items;
}
/**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns.
*/
AttachmentItem getAttachmentItem(Attachment a, boolean needsSize);
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
boolean needsSize) {
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
Size size = new Size();
InputStream is = new MarkEnforcingInputStream(
new BufferedInputStream(a.getStream()));
is.mark(READ_LIMIT);
try {
// use exif to get size
if (h.getContentType().equals("image/jpeg")) {
size = getSizeFromExif(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
}
try {
// use BitmapFactory to get size
if (size.error) {
is.reset();
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
size = getSizeFromBitmap(is);
}
} catch (IOException e) {
logException(LOG, WARNING, e);
} finally {
tryToClose(is, LOG, WARNING);
}
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (!h.getContentType().equals(size.mimeType)) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Header has different mime type (" +
h.getContentType() + ") than image (" + size.mimeType +
").");
}
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
private static class Size {
private final int width;
private final int height;
private final String mimeType;
private final boolean error;
private Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
private Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}
}

View File

@@ -1,129 +0,0 @@
package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
@NotNullByDefault
class AttachmentRetrieverImpl implements AttachmentRetriever {
private static final Logger LOG =
getLogger(AttachmentRetrieverImpl.class.getName());
private final MessagingManager messagingManager;
private final ImageHelper imageHelper;
private final ImageSizeCalculator imageSizeCalculator;
private final int defaultSize;
private final int minWidth, maxWidth;
private final int minHeight, maxHeight;
private final Map<MessageId, List<AttachmentItem>> attachmentCache =
new ConcurrentHashMap<>();
@Inject
AttachmentRetrieverImpl(MessagingManager messagingManager,
AttachmentDimensions dimensions, ImageHelper imageHelper,
ImageSizeCalculator imageSizeCalculator) {
this.messagingManager = messagingManager;
this.imageHelper = imageHelper;
this.imageSizeCalculator = imageSizeCalculator;
defaultSize = dimensions.defaultSize;
minWidth = dimensions.minWidth;
maxWidth = dimensions.maxWidth;
minHeight = dimensions.minHeight;
maxHeight = dimensions.maxHeight;
}
@Override
public void cachePut(MessageId messageId,
List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Override
@Nullable
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
@Override
public Attachment getMessageAttachment(AttachmentHeader h)
throws DbException {
return messagingManager.getAttachment(h);
}
@Override
public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
AttachmentHeader h = a.getHeader();
if (!needsSize) {
String extension =
imageHelper.getExtensionFromMimeType(h.getContentType());
boolean hasError = false;
if (extension == null) {
extension = "";
hasError = true;
}
return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
}
InputStream is = new BufferedInputStream(a.getStream());
Size size = imageSizeCalculator.getSize(is, h.getContentType());
// calculate thumbnail size
Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
if (!size.error) {
thumbnailSize =
getThumbnailSize(size.width, size.height, size.mimeType);
}
// get file extension
String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
boolean hasError = extension == null || size.error;
if (!h.getContentType().equals(size.mimeType)) {
if (LOG.isLoggable(WARNING)) {
LOG.warning("Header has different mime type (" +
h.getContentType() + ") than image (" + size.mimeType +
").");
}
hasError = true;
}
if (extension == null) extension = "";
return new AttachmentItem(h, size.width, size.height, extension,
thumbnailSize.width, thumbnailSize.height, hasError);
}
private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height;
float scaleFactor = Math.min(widthPercentage, heightPercentage);
if (scaleFactor > 1) scaleFactor = 1f;
int thumbnailWidth = (int) (width * scaleFactor);
int thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
widthPercentage = minWidth / (float) width;
heightPercentage = minHeight / (float) height;
scaleFactor = Math.max(widthPercentage, heightPercentage);
thumbnailWidth = (int) (width * scaleFactor);
thumbnailHeight = (int) (height * scaleFactor);
if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
}
return new Size(thumbnailWidth, thumbnailHeight, mimeType);
}
}

View File

@@ -7,7 +7,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
@NotNullByDefault
public interface ImageHelper {
interface ImageHelper {
DecodeResult decodeStream(InputStream is);

View File

@@ -1,39 +0,0 @@
package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@NotNullByDefault
class ImageHelperImpl implements ImageHelper {
@Inject
ImageHelperImpl() {
}
@Override
public DecodeResult decodeStream(InputStream is) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
String mimeType = options.outMimeType;
if (mimeType == null) mimeType = "";
return new DecodeResult(options.outWidth, options.outHeight,
mimeType);
}
@Nullable
@Override
public String getExtensionFromMimeType(String mimeType) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
return mimeTypeMap.getExtensionFromMimeType(mimeType);
}
}

View File

@@ -1,94 +0,0 @@
package org.briarproject.briar.android.attachment;
import android.support.media.ExifInterface;
import com.bumptech.glide.util.MarkEnforcingInputStream;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
import static android.support.media.ExifInterface.TAG_ORIENTATION;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ImageSizeCalculator {
private static final Logger LOG =
getLogger(ImageSizeCalculator.class.getName());
private static final int READ_LIMIT = 1024 * 8192;
private final ImageHelper imageHelper;
ImageSizeCalculator(ImageHelper imageHelper) {
this.imageHelper = imageHelper;
}
Size getSize(InputStream is, String contentType) {
Size size = new Size();
is = new MarkEnforcingInputStream(is);
is.mark(READ_LIMIT);
if (contentType.equals("image/jpeg")) {
try {
// use exif to get size
size = getSizeFromExif(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
if (size.error) {
// need to mark again to re-add read limit
is.mark(READ_LIMIT);
try {
// use BitmapFactory to get size
size = getSizeFromBitmap(is);
is.reset();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
return size;
}
/**
* Gets the size of a JPEG {@link InputStream} if EXIF info is available.
*/
private Size getSizeFromExif(InputStream is) throws IOException {
ExifInterface exif = new ExifInterface(is);
// these can return 0 independent of default value
int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
if (width == 0 || height == 0) return new Size();
int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
if (orientation == ORIENTATION_ROTATE_90 ||
orientation == ORIENTATION_ROTATE_270 ||
orientation == ORIENTATION_TRANSVERSE ||
orientation == ORIENTATION_TRANSPOSE) {
//noinspection SuspiciousNameCombination
return new Size(height, width, "image/jpeg");
}
return new Size(width, height, "image/jpeg");
}
/**
* Gets the size of any image {@link InputStream}.
*/
private Size getSizeFromBitmap(InputStream is) {
DecodeResult result = imageHelper.decodeStream(is);
if (result.width < 1 || result.height < 1) return new Size();
return new Size(result.width, result.height, result.mimeType);
}
}

View File

@@ -1,23 +0,0 @@
package org.briarproject.briar.android.attachment;
class Size {
final int width;
final int height;
final String mimeType;
final boolean error;
Size(int width, int height, String mimeType) {
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.error = false;
}
Size() {
this.width = 0;
this.height = 0;
this.mimeType = "";
this.error = true;
}
}

View File

@@ -32,7 +32,6 @@ import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
public class RssFeedImportActivity extends BriarActivity {
@@ -78,6 +77,7 @@ public class RssFeedImportActivity extends BriarActivity {
if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
importButton.getVisibility() == VISIBLE) {
publish();
hideSoftKeyboard(urlInput);
return true;
}
return false;
@@ -123,7 +123,6 @@ public class RssFeedImportActivity extends BriarActivity {
// hide import button, show progress bar
importButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
hideSoftKeyboard(urlInput);
String url = validateAndNormaliseUrl(urlInput.getText().toString());
if (url == null) throw new AssertionError();

View File

@@ -3,8 +3,11 @@ package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.db.DbException;
@@ -41,7 +44,7 @@ import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_L
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class WriteBlogPostActivity extends BriarActivity
implements SendListener {
implements OnEditorActionListener, SendListener {
private static final Logger LOG =
Logger.getLogger(WriteBlogPostActivity.class.getName());
@@ -110,6 +113,12 @@ public class WriteBlogPostActivity extends BriarActivity
component.inject(this);
}
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
input.requestFocus();
return true;
}
@Override
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {

View File

@@ -14,6 +14,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
@@ -56,6 +57,7 @@ import javax.inject.Inject;
import io.github.kobakei.materialfabspeeddial.FabSpeedDial;
import io.github.kobakei.materialfabspeeddial.FabSpeedDial.OnMenuItemClickListener;
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
@@ -83,6 +85,8 @@ public class ContactListFragment extends BaseFragment implements EventListener,
EventBus eventBus;
@Inject
AndroidNotificationManager notificationManager;
@Inject
FeatureFlags featureFlags;
private ContactListAdapter adapter;
private BriarRecyclerView list;
@@ -122,7 +126,19 @@ public class ContactListFragment extends BaseFragment implements EventListener,
container, false);
FabSpeedDial speedDial = contentView.findViewById(R.id.speedDial);
speedDial.addOnMenuItemClickListener(this);
if (featureFlags.shouldEnableRemoteContacts()) {
speedDial.addOnMenuItemClickListener(this);
} else {
speedDial.setMenu(new FabSpeedDialMenu(contentView.getContext()));
speedDial.addOnStateChangeListener(open -> {
if (open) {
Intent intent = new Intent(getContext(),
ContactExchangeActivity.class);
startActivity(intent);
speedDial.closeMenu();
}
});
}
OnContactClickListener<ContactListItem> onContactClickListener =
(view, item) -> {
@@ -153,10 +169,9 @@ public class ContactListFragment extends BaseFragment implements EventListener,
startActivity(i);
}
};
adapter = new ContactListAdapter(requireContext(),
onContactClickListener);
adapter = new ContactListAdapter(getContext(), onContactClickListener);
list = contentView.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(requireContext()));
list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setAdapter(adapter);
list.setEmptyImage(R.drawable.ic_empty_state_contact_list);
list.setEmptyText(getString(R.string.no_contacts));
@@ -252,7 +267,7 @@ public class ContactListFragment extends BaseFragment implements EventListener,
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (contacts.isEmpty()) list.showData();
else adapter.replaceAll(contacts);
else adapter.addAll(contacts);
} else {
LOG.info("Concurrent update, reloading");
loadContacts();

View File

@@ -9,10 +9,8 @@ import android.support.annotation.Nullable;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchPendingContactException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
@@ -120,19 +118,4 @@ public class AddContactViewModel extends AndroidViewModel {
return addContactResult;
}
public void updatePendingContact(String name, PendingContact p) {
dbExecutor.execute(() -> {
try {
contactManager.removePendingContact(p.getId());
addContact(name);
} catch(NoSuchPendingContactException e) {
logException(LOG, WARNING, e);
// no error in UI as pending contact was converted into contact
} catch (DbException e) {
logException(LOG, WARNING, e);
addContactResult.postValue(new LiveResult<>(e));
}
});
}
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.android.contact.add.remote;
import android.animation.ObjectAnimator;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.ClipData;
@@ -12,9 +11,7 @@ import android.support.v4.app.ShareCompat.IntentBuilder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -37,8 +34,7 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class LinkExchangeFragment extends BaseFragment
implements OnGlobalLayoutListener {
public class LinkExchangeFragment extends BaseFragment {
private static final String TAG = LinkExchangeFragment.class.getName();
@@ -94,30 +90,9 @@ public class LinkExchangeFragment extends BaseFragment
observeOnce(viewModel.getHandshakeLink(), this,
this::onHandshakeLinkLoaded);
if (savedInstanceState == null) {
ScrollView scrollView = (ScrollView) v;
// we need to wait for views to be laid out to get the heights
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
return v;
}
@Override
public void onGlobalLayout() {
ScrollView scrollView = (ScrollView) requireNonNull(getView());
View layout = scrollView.getChildAt(0);
int scrollBy = layout.getHeight() - scrollView.getHeight();
if (scrollBy > 0) {
// smoothScrollTo() is too fast due to the transition animation
ObjectAnimator animator = ObjectAnimator
.ofInt(scrollView, "scrollY", scrollBy);
animator.setDuration(1000);
animator.start();
}
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
private void onHandshakeLinkLoaded(String link) {
View v = requireNonNull(getView());

View File

@@ -2,15 +2,10 @@ package org.briarproject.briar.android.contact.add.remote;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AlertDialog.Builder;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
@@ -20,10 +15,6 @@ import android.widget.ProgressBar;
import android.widget.Toast;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.db.ContactExistsException;
import org.briarproject.bramble.api.db.PendingContactExistsException;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
@@ -33,13 +24,9 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.support.v4.content.ContextCompat.getColor;
import static android.support.v4.content.ContextCompat.getDrawable;
import static android.support.v4.graphics.drawable.DrawableCompat.setTint;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
@@ -95,20 +82,19 @@ public class NicknameFragment extends BaseFragment {
@Nullable
private String getNicknameOrNull() {
Editable text = contactNameInput.getText();
if (text == null || text.toString().trim().length() == 0) {
Editable name = contactNameInput.getText();
if (name == null || name.toString().trim().length() == 0) {
contactNameLayout.setError(getString(R.string.nickname_missing));
contactNameInput.requestFocus();
return null;
}
String name = text.toString().trim();
if (utf8IsTooLong(name, MAX_AUTHOR_NAME_LENGTH)) {
if (utf8IsTooLong(name.toString(), MAX_AUTHOR_NAME_LENGTH)) {
contactNameLayout.setError(getString(R.string.name_too_long));
contactNameInput.requestFocus();
return null;
}
contactNameLayout.setError(null);
return name;
return name.toString().trim();
}
private void onAddButtonClicked() {
@@ -120,95 +106,23 @@ public class NicknameFragment extends BaseFragment {
viewModel.getAddContactResult().observe(this, result -> {
if (result == null) return;
if (result.hasError())
handleException(name, requireNonNull(result.getException()));
else
showPendingContactListActivity();
if (result.hasError()) {
int stringRes;
if (result
.getException() instanceof UnsupportedVersionException) {
stringRes = R.string.unsupported_link;
} else {
stringRes = R.string.adding_contact_error;
}
Toast.makeText(getContext(), stringRes, LENGTH_LONG).show();
} else {
Intent intent = new Intent(getActivity(),
PendingContactListActivity.class);
startActivity(intent);
}
finish();
});
viewModel.addContact(name);
}
private void showPendingContactListActivity() {
Intent intent = new Intent(getActivity(),
PendingContactListActivity.class);
startActivity(intent);
finish();
}
private void handleException(String name, Exception e) {
if (e instanceof ContactExistsException) {
ContactExistsException ce = (ContactExistsException) e;
handleExistingContact(name, ce.getRemoteAuthor());
} else if (e instanceof PendingContactExistsException) {
PendingContactExistsException pe =
(PendingContactExistsException) e;
handleExistingPendingContact(name, pe.getPendingContact());
} else if (e instanceof UnsupportedVersionException) {
int stringRes = R.string.unsupported_link;
Toast.makeText(getContext(), stringRes, LENGTH_LONG).show();
finish();
} else {
int stringRes = R.string.adding_contact_error;
Toast.makeText(getContext(), stringRes, LENGTH_LONG).show();
finish();
}
}
private void handleExistingContact(String name, Author existing) {
OnClickListener listener = (d, w) -> {
d.dismiss();
String str = getString(R.string.contact_already_exists, name);
Toast.makeText(getContext(), str, LENGTH_LONG).show();
finish();
};
showSameLinkDialog(existing.getName(), name,
R.string.duplicate_link_dialog_text_1_contact, listener);
}
private void handleExistingPendingContact(String name, PendingContact p) {
OnClickListener listener = (d, w) -> {
viewModel.updatePendingContact(name, p);
Toast.makeText(getContext(), R.string.pending_contact_updated_toast,
LENGTH_LONG).show();
d.dismiss();
showPendingContactListActivity();
};
showSameLinkDialog(p.getAlias(), name,
R.string.duplicate_link_dialog_text_1, listener);
}
private void showSameLinkDialog(String name1, String name2,
@StringRes int existsRes, OnClickListener samePersonListener) {
Context ctx = requireContext();
Builder b = new Builder(ctx, R.style.BriarDialogTheme_Neutral);
b.setTitle(getString(R.string.duplicate_link_dialog_title));
String msg = getString(existsRes, name1) + "\n\n" +
getString(R.string.duplicate_link_dialog_text_2, name2, name1);
b.setMessage(msg);
b.setPositiveButton(R.string.same_person_button, samePersonListener);
b.setNegativeButton(R.string.different_person_button, (d, w) -> {
d.dismiss();
showWarningDialog(name1, name2);
});
b.setCancelable(false);
b.show();
}
private void showWarningDialog(String name1, String name2) {
Context ctx = requireContext();
Builder b = new Builder(ctx, R.style.BriarDialogTheme);
Drawable icon = getDrawable(ctx, R.drawable.alerts_and_states_error);
setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary));
b.setIcon(icon);
b.setTitle(getString(R.string.duplicate_link_dialog_title));
b.setMessage(
getString(R.string.duplicate_link_dialog_text_3, name1, name2));
b.setPositiveButton(R.string.ok, (d, w) -> {
d.dismiss();
finish();
});
b.setCancelable(false);
b.show();
}
}

View File

@@ -91,7 +91,7 @@ public class PendingContactListViewModel extends AndroidViewModel
Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = items.isEmpty();
boolean online = false;
for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond();

View File

@@ -21,12 +21,9 @@ import org.briarproject.briar.android.activity.BaseActivity;
import javax.inject.Inject;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -79,14 +76,13 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
setButton.setOnClickListener(v1 -> onSetButtonClicked());
Button cancelButton = v.findViewById(R.id.cancelButton);
cancelButton.setOnClickListener(v1 -> onCancelButtonClicked());
cancelButton.setOnClickListener(v1 -> getDialog().cancel());
return v;
}
private void onSetButtonClicked() {
hideSoftKeyboard(aliasEditText);
String alias = aliasEditText.getText().toString().trim();
String alias = aliasEditText.getText().toString();
if (toUtf8(alias).length > MAX_AUTHOR_NAME_LENGTH) {
aliasEditLayout.setError(getString(R.string.name_too_long));
} else {
@@ -95,17 +91,4 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
}
}
private void onCancelButtonClicked() {
hideSoftKeyboard(aliasEditText);
getDialog().cancel();
}
@Override
public void onStart() {
super.onStart();
requireNonNull(getDialog().getWindow())
.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
showSoftKeyboard(aliasEditText);
}
}

View File

@@ -37,7 +37,6 @@ import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
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.event.EventListener;
@@ -82,7 +81,6 @@ import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import java.util.ArrayList;
@@ -109,12 +107,10 @@ import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort;
import static java.util.Objects.requireNonNull;
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;
@@ -142,7 +138,7 @@ public class ConversationActivity extends BriarActivity
public static final String CONTACT_ID = "briar.CONTACT_ID";
private static final Logger LOG =
getLogger(ConversationActivity.class.getName());
Logger.getLogger(ConversationActivity.class.getName());
private static final int TRANSITION_DURATION_MS = 500;
private static final int ONBOARDING_DELAY_MS = 250;
@@ -175,8 +171,6 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, PrivateMessageHeader> missingAttachments =
new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> {
requireNonNull(name);
loadMessages();
@@ -276,7 +270,7 @@ public class ConversationActivity extends BriarActivity
textInputView.setSendController(sendController);
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false);
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
textInputView.addOnKeyboardShownListener(this::scrollToBottom);
}
private void scrollToBottom() {
@@ -356,11 +350,6 @@ public class ConversationActivity extends BriarActivity
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.conversation_actions, menu);
// Hide private message deletion action if feature is not enabled
if (!featureFlags.shouldEnablePrivateMessageDeletion()) {
menu.removeItem(R.id.action_delete_all_messages);
}
// enable introduction action if available
observeOnce(viewModel.showIntroductionAction(), this, enable -> {
if (enable != null && enable) {
@@ -394,9 +383,6 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG);
return true;
case R.id.action_delete_all_messages:
askToDeleteAllMessages();
return true;
case R.id.action_social_remove_person:
askToRemoveContact();
return true;
@@ -448,40 +434,29 @@ public class ConversationActivity extends BriarActivity
});
}
private void eagerlyLoadMessageSize(PrivateMessageHeader h) {
try {
MessageId id = h.getId();
// If the message has text, load it
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, requireNonNull(text));
}
private void eagerlyLoadMessageSize(PrivateMessageHeader h)
throws DbException {
MessageId id = h.getId();
// If the message has text, load it
if (h.hasText()) {
String text = textCache.get(id);
if (text == null) {
LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id);
textCache.put(id, requireNonNull(text));
}
// If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed
List<AttachmentHeader> headers = h.getAttachmentHeaders();
if (headers.size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
AttachmentHeader header = headers.get(0);
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item =
attachmentRetriever.getAttachmentItem(a, true);
attachmentRetriever.cachePut(id, singletonList(item));
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
}
}
}
// If the message has a single image, load its size - for multiple
// images we use a grid so the size is fixed
if (h.getAttachmentHeaders().size() == 1) {
List<AttachmentItem> items = attachmentRetriever.cacheGet(id);
if (items == null) {
LOG.info("Eagerly loading image size for latest message");
items = attachmentRetriever.getAttachmentItems(
attachmentRetriever.getMessageAttachments(
h.getAttachmentHeaders()));
attachmentRetriever.cachePut(id, items);
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
@@ -559,30 +534,16 @@ public class ConversationActivity extends BriarActivity
&& adapter.isScrolledToBottom(layoutManager);
}
private void loadMessageAttachments(PrivateMessageHeader h) {
// TODO: Use placeholders for missing/invalid attachments
private void loadMessageAttachments(MessageId messageId,
List<AttachmentHeader> headers) {
runOnDbThread(() -> {
try {
List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentRetriever.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentHeader> headers = h.getAttachmentHeaders();
boolean needsSize = headers.size() == 1;
List<AttachmentItem> items = new ArrayList<>(headers.size());
for (AttachmentHeader header : headers) {
try {
Attachment a = attachmentRetriever
.getMessageAttachment(header);
AttachmentItem item = attachmentRetriever
.getAttachmentItem(a, needsSize);
items.add(item);
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
return;
}
}
// Don't cache items unless all are present and valid
attachmentRetriever.cachePut(h.getId(), items);
displayMessageAttachments(h.getId(), items);
List<AttachmentItem> items =
attachmentRetriever.getAttachmentItems(attachments);
displayMessageAttachments(messageId, items);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
@@ -592,6 +553,7 @@ public class ConversationActivity extends BriarActivity
private void displayMessageAttachments(MessageId m,
List<AttachmentItem> items) {
runOnUiThreadUnlessDestroyed(() -> {
attachmentRetriever.cachePut(m, items);
Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m);
if (pair != null) {
@@ -605,13 +567,6 @@ public class ConversationActivity extends BriarActivity
@Override
public void eventOccurred(Event e) {
if (e instanceof AttachmentReceivedEvent) {
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
onAttachmentReceived(a.getMessageId());
}
}
if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) {
@@ -662,15 +617,6 @@ public class ConversationActivity extends BriarActivity
scrollToBottom();
}
@UiThread
private void onAttachmentReceived(MessageId attachmentId) {
PrivateMessageHeader h = missingAttachments.remove(attachmentId);
if (h != null) {
LOG.info("Missing attachment received");
loadMessageAttachments(h);
}
}
@UiThread
private void onNewConversationMessage(ConversationMessageHeader h) {
if (h instanceof ConversationRequest ||
@@ -735,52 +681,6 @@ public class ConversationActivity extends BriarActivity
addConversationItem(h.accept(visitor));
}
private void askToDeleteAllMessages() {
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_delete_all_messages));
builder.setMessage(
getString(R.string.dialog_message_delete_all_messages));
builder.setNegativeButton(R.string.delete,
(dialog, which) -> deleteAllMessages());
builder.setPositiveButton(R.string.cancel, null);
builder.show();
}
private void deleteAllMessages() {
list.showProgressBar();
runOnDbThread(() -> {
try {
boolean allDeleted =
conversationManager.deleteAllMessages(contactId);
reloadConversationAfterDeletingAllMessages(allDeleted);
} catch (DbException e) {
logException(LOG, WARNING, e);
runOnUiThreadUnlessDestroyed(() -> list.showData());
}
});
}
private void reloadConversationAfterDeletingAllMessages(
boolean allDeleted) {
runOnUiThreadUnlessDestroyed(() -> {
adapter.clear();
loadMessages();
if (!allDeleted) showNotAllDeletedDialog();
});
}
private void showNotAllDeletedDialog() {
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(
getString(R.string.dialog_title_not_all_messages_deleted));
builder.setMessage(
getString(R.string.dialog_message_not_all_messages_deleted));
builder.setPositiveButton(R.string.ok, null);
builder.show();
}
private void askToRemoveContact() {
DialogInterface.OnClickListener okListener =
(dialog, which) -> removeContact();
@@ -1003,11 +903,11 @@ public class ConversationActivity extends BriarActivity
}
@Override
public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) {
List<AttachmentItem> attachments =
attachmentRetriever.cacheGet(h.getId());
public List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers) {
List<AttachmentItem> attachments = attachmentRetriever.cacheGet(m);
if (attachments == null) {
loadMessageAttachments(h);
loadMessageAttachments(m, headers);
return emptyList();
}
return attachments;

View File

@@ -18,6 +18,7 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
@@ -50,6 +51,7 @@ 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.attachment.AttachmentDimensions.getAttachmentDimensions;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@@ -99,13 +101,10 @@ public class ConversationViewModel extends AndroidViewModel
@Inject
ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
TransactionManager db,
MessagingManager messagingManager,
ContactManager contactManager,
@IoExecutor Executor ioExecutor, TransactionManager db,
MessagingManager messagingManager, ContactManager contactManager,
SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
PrivateMessageFactory privateMessageFactory) {
super(application);
this.dbExecutor = dbExecutor;
this.db = db;
@@ -113,8 +112,10 @@ public class ConversationViewModel extends AndroidViewModel
this.contactManager = contactManager;
this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = attachmentRetriever;
this.attachmentCreator = attachmentCreator;
this.attachmentRetriever = new AttachmentRetriever(messagingManager,
getAttachmentDimensions(application.getResources()));
this.attachmentCreator = new AttachmentCreator(getApplication(),
ioExecutor, messagingManager, attachmentRetriever);
messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false);

View File

@@ -15,6 +15,7 @@ import org.briarproject.briar.api.forum.ForumInvitationRequest;
import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.introduction.IntroductionRequest;
import org.briarproject.briar.api.introduction.IntroductionResponse;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
@@ -55,7 +56,8 @@ class ConversationVisitor implements
if (h.getAttachmentHeaders().isEmpty()) {
attachments = emptyList();
} else {
attachments = attachmentCache.getAttachmentItems(h);
attachments = attachmentCache
.getAttachmentItems(h.getId(), h.getAttachmentHeaders());
}
if (h.isLocal()) {
item = new ConversationMessageItem(
@@ -293,6 +295,7 @@ class ConversationVisitor implements
}
interface AttachmentCache {
List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h);
List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers);
}
}

View File

@@ -142,8 +142,11 @@ public class ImageActivity extends BriarActivity
viewPager.setAdapter(pagerAdapter);
viewPager.setCurrentItem(position);
viewModel.getOnImageClicked().observeEvent(this, this::onImageClicked);
window.getDecorView().setSystemUiVisibility(UI_FLAGS_DEFAULT);
if (SDK_INT >= 16) {
viewModel.getOnImageClicked()
.observeEvent(this, this::onImageClicked);
window.getDecorView().setSystemUiVisibility(UI_FLAGS_DEFAULT);
}
}
@Override
@@ -171,7 +174,11 @@ public class ImageActivity extends BriarActivity
viewModel.setToolbarPosition(
appBarLayout.getTop(), appBarLayout.getBottom()
);
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
if (SDK_INT >= 16) {
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
@Override
@@ -258,7 +265,7 @@ public class ImageActivity extends BriarActivity
* when the previous activity (with visible status bar) is shown.
*/
private void showStatusBarBeforeFinishing() {
if (appBarLayout.getVisibility() == GONE) {
if (SDK_INT >= 16 && appBarLayout.getVisibility() == GONE) {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(UI_FLAGS_DEFAULT);
}

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
@@ -134,10 +135,10 @@ public class ImageViewModel extends AndroidViewModel {
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
MessageId messageId = attachment.getMessageId();
dbExecutor.execute(() -> {
try {
Attachment a =
messagingManager.getAttachment(attachment.getHeader());
Attachment a = messagingManager.getAttachment(messageId);
copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) {
logException(LOG, WARNING, e);

View File

@@ -9,8 +9,8 @@ import com.bumptech.glide.load.data.DataFetcher;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.InputStream;
@@ -50,12 +50,11 @@ class BriarDataFetcher implements DataFetcher<InputStream> {
@Override
public void loadData(Priority priority,
DataCallback<? super InputStream> callback) {
MessageId id = attachment.getMessageId();
dbExecutor.execute(() -> {
if (cancel) return;
try {
Attachment a =
messagingManager.getAttachment(attachment.getHeader());
inputStream = a.getStream();
inputStream = messagingManager.getAttachment(id).getStream();
callback.onDataReady(inputStream);
} catch (DbException e) {
callback.onLoadFailed(e);

View File

@@ -34,8 +34,6 @@ 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.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
@MethodsNotNullByDefault
@@ -93,6 +91,12 @@ public class CreateForumActivity extends BriarActivity {
progress = findViewById(R.id.createForumProgressBar);
}
@Override
public void onStart() {
super.onStart();
showSoftKeyboard(nameEntry);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);

View File

@@ -182,7 +182,7 @@ public class ForumListFragment extends BaseEventFragment implements
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (forums.isEmpty()) list.showData();
else adapter.replaceAll(forums);
else adapter.addAll(forums);
} else {
LOG.info("Concurrent update, reloading");
loadForums();

View File

@@ -43,7 +43,6 @@ import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@@ -185,7 +184,7 @@ public class IntroductionMessageFragment extends BaseFragment
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
hideSoftKeyboard(ui.message);
introductionActivity.hideSoftKeyboard(ui.message);
introductionActivity.onBackPressed();
return true;
default:
@@ -202,7 +201,7 @@ public class IntroductionMessageFragment extends BaseFragment
makeIntroduction(contact1, contact2, text);
// don't wait for the introduction to be made before finishing activity
hideSoftKeyboard(ui.message);
introductionActivity.hideSoftKeyboard(ui.message);
introductionActivity.setResult(RESULT_OK);
introductionActivity.supportFinishAfterTransition();
}

View File

@@ -35,6 +35,7 @@ import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO;
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@@ -339,7 +340,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread
private void setVideoStabilisation(Parameters params) {
if (params.isVideoStabilizationSupported()) {
if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) {
params.setVideoStabilization(true);
}
}
@@ -414,8 +415,10 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} catch (RuntimeException e) {
throw new CameraException(e);
}
LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization());
if (SDK_INT >= 15) {
LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization());
}
LOG.info("Scene mode: " + params.getSceneMode());
LOG.info("Focus mode: " + params.getFocusMode());
LOG.info("Flash mode: " + params.getFlashMode());

View File

@@ -62,7 +62,8 @@ public class ContactExchangeActivity extends KeyAgreementActivity {
@UiThread
private void contactExchangeSucceeded(Author remoteAuthor) {
String contactName = remoteAuthor.getName();
String text = getString(R.string.contact_added_toast, contactName);
String format = getString(R.string.contact_added_toast);
String text = String.format(format, contactName);
Toast.makeText(this, text, LENGTH_LONG).show();
supportFinishAfterTransition();
}

View File

@@ -183,14 +183,10 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
if (bt == null) {
setBluetoothState(BluetoothState.NO_ADAPTER);
} else {
setBluetoothState(BluetoothState.WAITING);
wasAdapterEnabled = bt.isEnabled();
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
if (i.resolveActivity(getPackageManager()) != null) {
setBluetoothState(BluetoothState.WAITING);
wasAdapterEnabled = bt.isEnabled();
startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE);
} else {
setBluetoothState(BluetoothState.NO_ADAPTER);
}
startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE);
}
}

View File

@@ -26,8 +26,6 @@ import javax.inject.Inject;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
public class ChangePasswordActivity extends BriarActivity
implements OnClickListener, OnEditorActionListener {

View File

@@ -83,6 +83,12 @@ public class PasswordFragment extends BaseFragment implements TextWatcher {
return v;
}
@Override
public void onResume() {
super.onResume();
showSoftKeyboard(password);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {

View File

@@ -47,9 +47,7 @@ public class GroupActivity extends
@Inject
GroupController controller;
@Nullable
private Boolean isCreator = null;
private boolean isDissolved = false;
private boolean isCreator, isDissolved = false;
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
dissolveMenuItem;
@@ -139,14 +137,6 @@ public class GroupActivity extends
inviteMenuItem = menu.findItem(R.id.action_group_invite);
leaveMenuItem = menu.findItem(R.id.action_group_leave);
dissolveMenuItem = menu.findItem(R.id.action_group_dissolve);
// all role-dependent items are invisible until we know our role
revealMenuItem.setVisible(false);
inviteMenuItem.setVisible(false);
leaveMenuItem.setVisible(false);
dissolveMenuItem.setVisible(false);
// show items based on role
showMenuItems();
return super.onCreateOptionsMenu(menu);
@@ -161,27 +151,19 @@ public class GroupActivity extends
startActivity(i1);
return true;
case R.id.action_group_reveal:
if (isCreator == null || isCreator)
throw new IllegalStateException();
Intent i2 = new Intent(this, RevealContactsActivity.class);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i2);
return true;
case R.id.action_group_invite:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
Intent i3 = new Intent(this, GroupInviteActivity.class);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i3, REQUEST_GROUP_INVITE);
return true;
case R.id.action_group_leave:
if (isCreator == null || isCreator)
throw new IllegalStateException();
showLeaveGroupDialog();
return true;
case R.id.action_group_dissolve:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
showDissolveGroupDialog();
default:
return super.onOptionsItemSelected(item);
@@ -227,12 +209,18 @@ public class GroupActivity extends
}
private void showMenuItems() {
// we need to have the menu items and know if we are the creator
if (leaveMenuItem == null || isCreator == null) return;
revealMenuItem.setVisible(!isCreator);
inviteMenuItem.setVisible(isCreator);
leaveMenuItem.setVisible(!isCreator);
dissolveMenuItem.setVisible(isCreator);
if (leaveMenuItem == null || dissolveMenuItem == null) return;
if (isCreator) {
revealMenuItem.setVisible(false);
inviteMenuItem.setVisible(true);
leaveMenuItem.setVisible(false);
dissolveMenuItem.setVisible(true);
} else {
revealMenuItem.setVisible(true);
inviteMenuItem.setVisible(false);
leaveMenuItem.setVisible(true);
dissolveMenuItem.setVisible(false);
}
}
private void showLeaveGroupDialog() {

View File

@@ -24,7 +24,6 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static org.briarproject.briar.android.util.UiUtils.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
@MethodsNotNullByDefault
@@ -92,6 +91,12 @@ public class CreateGroupFragment extends BaseFragment {
return v;
}
@Override
public void onStart() {
super.onStart();
listener.showSoftKeyboard(nameEntry);
}
@Override
public String getUniqueTag() {
return TAG;
@@ -115,7 +120,7 @@ public class CreateGroupFragment extends BaseFragment {
private void createGroup() {
if (!validateName()) return;
hideSoftKeyboard(nameEntry);
listener.hideSoftKeyboard(nameEntry);
createGroupButton.setVisibility(GONE);
progress.setVisibility(VISIBLE);
listener.onGroupNameChosen(nameEntry.getText().toString());

View File

@@ -1,8 +1,15 @@
package org.briarproject.briar.android.privategroup.creation;
import android.view.View;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
interface CreateGroupListener extends BaseFragmentListener {
void onGroupNameChosen(String name);
void showSoftKeyboard(View view);
void hideSoftKeyboard(View view);
}

View File

@@ -194,7 +194,7 @@ public class GroupListFragment extends BaseFragment implements
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (groups.isEmpty()) list.showData();
else adapter.replaceAll(groups);
else adapter.addAll(groups);
} else {
LOG.info("Concurrent update, reloading");
loadGroups();

View File

@@ -90,9 +90,14 @@ public class BriarReportPrimer implements ReportPrimer {
ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mem);
String systemMemory;
systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, "
+ (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
if (Build.VERSION.SDK_INT >= 16) {
systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, "
+ (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
} else {
systemMemory = (mem.availMem / 1024 / 1204) + " MiB free, "
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
}
customData.put("System memory", systemMemory);
// Virtual machine memory

View File

@@ -218,17 +218,11 @@ public class SettingsFragment extends PreferenceFragmentCompat
});
if (SDK_INT < 27) {
// remove System Default Theme option from preference entries
// as it is not functional on this API anyway
// remove System Default Theme option
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]));
}
if (IS_DEBUG_BUILD) {
findPreference("pref_key_explode").setOnPreferenceClickListener(
@@ -495,13 +489,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
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();
}
startActivity(intent);
return true;
});
}
@@ -523,12 +511,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
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();
}
startActivityForResult(i, REQUEST_RINGTONE);
return true;
}
@@ -663,7 +646,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
} else {
// The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(getContext(), uri);
if (r == null || "file".equals(uri.getScheme())) {
if (r == null) {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
} else {

View File

@@ -64,6 +64,12 @@ public abstract class BaseMessageFragment extends BaseFragment
@StringRes
protected abstract int getHintText();
@Override
public void onStart() {
super.onStart();
message.showSoftKeyboard();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {

View File

@@ -27,6 +27,7 @@ import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDa
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.KeyboardAwareLinearLayout;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
@@ -283,10 +284,14 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
scrollToItemAtTop(item);
} else {
// wait with scrolling until keyboard opened
textInput.setOnKeyboardShownListener(() -> {
scrollToItemAtTop(item);
textInput.setOnKeyboardShownListener(null);
});
textInput.addOnKeyboardShownListener(
new KeyboardAwareLinearLayout.OnKeyboardShownListener() {
@Override
public void onKeyboardShown() {
scrollToItemAtTop(item);
textInput.removeOnKeyboardShownListener(this);
}
});
}
}
@@ -327,6 +332,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
private void updateTextInput() {
if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint);
textInput.requestFocus();
textInput.showSoftKeyboard();
} else {
textInput.setHint(R.string.forum_new_message_hint);

View File

@@ -79,10 +79,6 @@ public abstract class BriarAdapter<T, V extends ViewHolder>
this.items.addAll(items);
}
public void replaceAll(Collection<T> items) {
this.items.replaceAll(items);
}
public void setItems(Collection<T> items) {
this.items.beginBatchedUpdates();
this.items.clear();

View File

@@ -4,18 +4,13 @@ import android.support.annotation.ColorRes;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.design.widget.Snackbar.Callback;
import android.view.View;
import android.view.View.OnClickListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
import static android.support.v4.content.ContextCompat.getColor;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@NotNullByDefault
public class BriarSnackbarBuilder {
@@ -35,24 +30,6 @@ public class BriarSnackbarBuilder {
R.color.briar_button_text_positive));
s.setAction(actionResId, onClickListener);
}
// Workaround for https://issuetracker.google.com/issues/64285517
if (duration == LENGTH_INDEFINITE && SDK_INT < 21) {
// Hide snackbar while it's opening to make bouncing less noticeable
s.getView().setVisibility(INVISIBLE);
s.addCallback(new Callback() {
@Override
public void onShown(Snackbar snackbar) {
snackbar.getView().setVisibility(VISIBLE);
// Request layout again in case snackbar is in wrong place
snackbar.getView().requestLayout();
}
@Override
public void onDismissed(Snackbar snackbar, int event) {
snackbar.getView().setVisibility(INVISIBLE);
}
});
}
return s;
}

View File

@@ -92,11 +92,9 @@ public class UiUtils {
public static final float GREY_OUT = 0.5f;
public static void showSoftKeyboard(View view) {
if (view.requestFocus()) {
InputMethodManager imm = requireNonNull(getSystemService(
view.getContext(), InputMethodManager.class));
imm.showSoftInput(view, SHOW_IMPLICIT);
}
InputMethodManager imm = requireNonNull(
getSystemService(view.getContext(), InputMethodManager.class));
imm.showSoftInput(view, SHOW_IMPLICIT);
}
public static void hideSoftKeyboard(View view) {

View File

@@ -0,0 +1,44 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.util.AttributeSet;
import android.view.View;
public class BriarRecyclerViewBehavior
extends CoordinatorLayout.Behavior<BriarRecyclerView> {
public BriarRecyclerViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent,
BriarRecyclerView child, View dependency) {
// FIXME the below code works, but does not reset margin when snackbar is dismissed
/*
int margin = 0;
if (dependency.isShown()) margin = dependency.getHeight();
// set snackbar height as bottom margin if it is shown
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
params.setMargins(0, 0, 0, margin);
child.setLayoutParams(params);
child.scrollToPosition(0);
*/
return true;
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
BriarRecyclerView child, View dependency) {
// we only want to trigger the change
// only when the changes is from a snackbar
return dependency instanceof Snackbar.SnackbarLayout;
}
}

View File

@@ -13,6 +13,7 @@ import android.widget.ProgressBar;
import org.briarproject.briar.R;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Objects.requireNonNull;
public class CompositeSendButton extends FrameLayout {
@@ -74,24 +75,33 @@ public class CompositeSendButton extends FrameLayout {
if (showImageButton) {
imageButton.setVisibility(VISIBLE);
sendButton.setEnabled(false);
sendButton.clearAnimation();
sendButton.animate().alpha(0f).withEndAction(() -> {
if (SDK_INT <= 15) {
sendButton.setVisibility(INVISIBLE);
imageButton.setEnabled(true);
}).start();
imageButton.clearAnimation();
imageButton.animate().alpha(1f).start();
} else {
sendButton.clearAnimation();
sendButton.animate().alpha(0f).withEndAction(() -> {
sendButton.setVisibility(INVISIBLE);
imageButton.setEnabled(true);
}).start();
imageButton.clearAnimation();
imageButton.animate().alpha(1f).start();
}
} else {
sendButton.setVisibility(VISIBLE);
// enable/disable buttons right away to allow fast sending
sendButton.setEnabled(sendEnabled);
imageButton.setEnabled(false);
sendButton.clearAnimation();
sendButton.animate().alpha(1f).start();
imageButton.clearAnimation();
imageButton.animate().alpha(0f).withEndAction(() ->
imageButton.setVisibility(INVISIBLE)
).start();
if (SDK_INT <= 15) {
imageButton.setVisibility(INVISIBLE);
} else {
sendButton.clearAnimation();
sendButton.animate().alpha(1f).start();
imageButton.clearAnimation();
imageButton.animate().alpha(0f).withEndAction(() ->
imageButton.setVisibility(INVISIBLE)
).start();
}
}
}

View File

@@ -13,8 +13,8 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import com.vanniktech.emoji.EmojiEditText;
import com.vanniktech.emoji.EmojiPopup;
import com.vanniktech.emoji.RecentEmoji;
@@ -28,12 +28,10 @@ import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.inputmethod.EditorInfo.IME_ACTION_SEND;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.lang.Character.isWhitespace;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
public class EmojiTextInputView extends LinearLayout implements
public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
TextWatcher {
@Inject
@@ -42,16 +40,12 @@ public class EmojiTextInputView extends LinearLayout implements
private final AppCompatImageButton emojiToggle;
private final EmojiPopup emojiPopup;
private final EditText editText;
private final InputMethodManager imm;
@Nullable
private TextInputListener listener;
@Nullable
private OnKeyboardShownListener keyboardShownListener;
private int maxLength = Integer.MAX_VALUE;
private boolean emptyTextAllowed = false;
private boolean isEmpty = true;
private boolean keyboardOpen = false;
public EmojiTextInputView(Context context) {
this(context, null);
@@ -85,6 +79,7 @@ public class EmojiTextInputView extends LinearLayout implements
editText = findViewById(R.id.input_text);
editText.setPadding(0, 0, paddingEnd, paddingBottom);
if (maxLines > 0) editText.setMaxLines(maxLines);
editText.setOnClickListener(v -> showSoftKeyboard());
editText.addTextChangedListener(this);
editText.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == IME_ACTION_SEND) {
@@ -108,30 +103,18 @@ public class EmojiTextInputView extends LinearLayout implements
// stuff we can't do in edit mode goes below
if (isInEditMode()) {
emojiPopup = null;
imm = null;
return;
}
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
imm = (InputMethodManager) requireNonNull(o);
BriarApplication app =
(BriarApplication) context.getApplicationContext();
app.getApplicationComponent().inject(this);
emojiPopup = EmojiPopup.Builder
.fromRootView(getRootView())
.fromRootView(this)
.setRecentEmoji(recentEmoji)
.setOnEmojiPopupShownListener(this::showKeyboardIcon)
.setOnEmojiPopupDismissListener(this::showEmojiIcon)
.setKeyboardAnimationStyle(R.style.emoji_fade_animation_style)
.setOnSoftKeyboardOpenListener(this::onKeyboardOpened)
.setOnSoftKeyboardCloseListener(this::onKeyboardClosed)
.setIconColor(resolveColorAttribute(getContext(),
R.attr.colorControlNormal))
.build(editText);
.build((EmojiEditText) editText);
emojiToggle.setOnClickListener(v -> emojiPopup.toggle());
editText.setOnClickListener(v -> {
if (emojiPopup.isShowing()) emojiPopup.dismiss();
});
}
@Override
@@ -142,31 +125,19 @@ public class EmojiTextInputView extends LinearLayout implements
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
if (emptyTextAllowed || listener == null) return;
// Work out whether the trimmed text has become empty or non-empty
if (isEmpty) {
// We only need to check the characters that were added
if (countLeadingWhitespace(s, start, count) < count) {
isEmpty = false;
listener.onTextIsEmptyChanged(false);
}
} else if (before > 0) {
// Characters have been removed or replaced - check from the start
int length = s.length();
if (countLeadingWhitespace(s, 0, length) == length) {
// Need to start at position 0 to change empty
if (start != 0 || emptyTextAllowed || listener == null) return;
if (s.length() == 0) {
if (!isEmpty) {
isEmpty = true;
listener.onTextIsEmptyChanged(true);
}
} else if (isEmpty) {
isEmpty = false;
listener.onTextIsEmptyChanged(false);
}
}
private int countLeadingWhitespace(CharSequence s, int off, int len) {
for (int i = 0; i < len; i++) {
if (!isWhitespace(s.charAt(off + i))) return i;
}
return len;
}
@Override
public void afterTextChanged(Editable s) {
}
@@ -247,10 +218,6 @@ public class EmojiTextInputView extends LinearLayout implements
editText.setHint(hint);
}
boolean isKeyboardOpen() {
return keyboardOpen || imm.isFullscreenMode();
}
private void showEmojiIcon() {
emojiToggle.setImageResource(R.drawable.ic_emoji_toggle);
}
@@ -260,43 +227,22 @@ public class EmojiTextInputView extends LinearLayout implements
}
void showSoftKeyboard() {
if (editText.requestFocus()) imm.showSoftInput(editText, SHOW_IMPLICIT);
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.showSoftInput(editText, SHOW_IMPLICIT);
}
void hideSoftKeyboard() {
if (emojiPopup.isShowing()) emojiPopup.dismiss();
IBinder token = editText.getWindowToken();
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.hideSoftInputFromWindow(token, 0);
}
private void onKeyboardOpened(
@SuppressWarnings("unused") int keyboardHeight) {
keyboardOpen = true;
if (keyboardShownListener != null)
keyboardShownListener.onKeyboardShown();
}
private void onKeyboardClosed() {
if (imm.isFullscreenMode()) {
onKeyboardOpened(0);
return;
}
keyboardOpen = false;
}
void setOnKeyboardShownListener(
@Nullable OnKeyboardShownListener listener) {
keyboardShownListener = listener;
}
interface TextInputListener {
void onTextIsEmptyChanged(boolean isEmpty);
void onSendEvent();
}
public interface OnKeyboardShownListener {
void onKeyboardShown();
}
}

View File

@@ -0,0 +1,225 @@
/*
Taken from Signal, licences under GPLv3
*/
package org.briarproject.briar.android.view;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import org.briarproject.briar.R;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static android.content.Context.WINDOW_SERVICE;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
/**
* RelativeLayout that, when a view container, will report back when it thinks
* a soft keyboard has been opened and what its height would be.
*/
@UiThread
public class KeyboardAwareLinearLayout extends LinearLayout {
private static final Logger LOG =
Logger.getLogger(KeyboardAwareLinearLayout.class.getName());
private final Rect rect = new Rect();
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
private final int minKeyboardSize;
private final int minCustomKeyboardSize;
private final int defaultCustomKeyboardSize;
private final int minCustomKeyboardTopMargin;
private final int statusBarHeight;
private int viewInset;
private boolean keyboardOpen = false;
private int rotation = -1;
public KeyboardAwareLinearLayout(Context context) {
this(context, null);
}
public KeyboardAwareLinearLayout(Context context,
@Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public KeyboardAwareLinearLayout(Context context,
@Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
rotation = getDeviceRotation();
int statusBarRes = getResources()
.getIdentifier("status_bar_height", "dimen", "android");
minKeyboardSize =
getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
minCustomKeyboardSize = getResources()
.getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
defaultCustomKeyboardSize = getResources()
.getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
minCustomKeyboardTopMargin = getResources()
.getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin);
statusBarHeight = statusBarRes > 0 ?
getResources().getDimensionPixelSize(statusBarRes) : 0;
viewInset = getViewInset();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateRotation();
updateKeyboardState();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void updateRotation() {
int oldRotation = rotation;
rotation = getDeviceRotation();
if (oldRotation != rotation) {
LOG.info("Rotation changed");
onKeyboardClose();
}
}
private void updateKeyboardState() {
if (isLandscape()) {
if (keyboardOpen) onKeyboardClose();
return;
}
if (viewInset == 0 && Build.VERSION.SDK_INT >= 21)
viewInset = getViewInset();
int availableHeight =
getRootView().getHeight() - statusBarHeight - viewInset;
getWindowVisibleDisplayFrame(rect);
int keyboardHeight = availableHeight - (rect.bottom - rect.top);
if (keyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != keyboardHeight)
setKeyboardPortraitHeight(keyboardHeight);
if (!keyboardOpen) onKeyboardOpen(keyboardHeight);
} else if (keyboardOpen) {
onKeyboardClose();
}
}
@TargetApi(21)
private int getViewInset() {
try {
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
attachInfoField.setAccessible(true);
Object attachInfo = attachInfoField.get(this);
if (attachInfo != null) {
Field stableInsetsField =
attachInfo.getClass().getDeclaredField("mStableInsets");
stableInsetsField.setAccessible(true);
Rect insets = (Rect) stableInsetsField.get(attachInfo);
return insets.bottom;
}
} catch (NoSuchFieldException e) {
LOG.log(WARNING,
"field reflection error when measuring view inset", e);
} catch (IllegalAccessException e) {
LOG.log(WARNING,
"access reflection error when measuring view inset", e);
}
return 0;
}
protected void onKeyboardOpen(int keyboardHeight) {
if (LOG.isLoggable(INFO))
LOG.info("onKeyboardOpen(" + keyboardHeight + ")");
keyboardOpen = true;
notifyShownListeners();
}
protected void onKeyboardClose() {
LOG.info("onKeyboardClose()");
keyboardOpen = false;
}
public boolean isKeyboardOpen() {
return keyboardOpen;
}
public int getKeyboardHeight() {
return isLandscape() ? getKeyboardLandscapeHeight() :
getKeyboardPortraitHeight();
}
public boolean isLandscape() {
int rotation = getDeviceRotation();
return rotation == ROTATION_90 || rotation == ROTATION_270;
}
private int getDeviceRotation() {
WindowManager windowManager =
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
return requireNonNull(windowManager).getDefaultDisplay().getRotation();
}
private int getKeyboardLandscapeHeight() {
return Math.max(getHeight(), getRootView().getHeight()) / 2;
}
private int getKeyboardPortraitHeight() {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext());
int keyboardHeight = prefs.getInt("keyboard_height_portrait",
defaultCustomKeyboardSize);
return clamp(keyboardHeight, minCustomKeyboardSize,
getRootView().getHeight() - minCustomKeyboardTopMargin);
}
private int clamp(int value, int min, int max) {
return Math.min(Math.max(value, min), max);
}
private void setKeyboardPortraitHeight(int height) {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext());
prefs.edit().putInt("keyboard_height_portrait", height).apply();
}
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
shownListeners.add(listener);
}
public void removeOnKeyboardShownListener(
OnKeyboardShownListener listener) {
shownListeners.remove(listener);
}
private void notifyShownListeners() {
// Make a copy as listeners may remove themselves when called
Set<OnKeyboardShownListener> listeners = new HashSet<>(shownListeners);
for (OnKeyboardShownListener listener : listeners) {
listener.onKeyboardShown();
}
}
public interface OnKeyboardShownListener {
void onKeyboardShown();
}
}

View File

@@ -1,50 +0,0 @@
package org.briarproject.briar.android.view;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.CoordinatorLayout.Behavior;
import android.support.design.widget.CoordinatorLayout.LayoutParams;
import android.support.design.widget.Snackbar.SnackbarLayout;
import android.util.AttributeSet;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
/**
* This behavior makes room for a snackbar at the bottom of the screen. The
* proper solution is to use layout_dodgeInsetEdges="bottom", but when used on
* a scrollable view that results in the view being pushed under the app bar
* (see https://issuetracker.google.com/issues/116541304).
*/
@NotNullByDefault
public class SnackbarAwareBehavior<V extends View> extends Behavior<V> {
public SnackbarAwareBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent,
V child, View snackbar) {
setMargin(child, snackbar.getHeight());
return true;
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent,
V child, View snackbar) {
setMargin(child, 0);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent,
V child, View dependency) {
return dependency instanceof SnackbarLayout;
}
private void setMargin(V child, int margin) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
params.setMargins(0, 0, 0, margin);
child.setLayoutParams(params);
}
}

View File

@@ -16,7 +16,7 @@ import android.widget.LinearLayout;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.view.EmojiTextInputView.OnKeyboardShownListener;
import org.briarproject.briar.android.view.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static java.util.Objects.requireNonNull;
@@ -139,9 +139,13 @@ public class TextInputView extends LinearLayout {
textInput.hideSoftKeyboard();
}
public void setOnKeyboardShownListener(
@Nullable OnKeyboardShownListener listener) {
textInput.setOnKeyboardShownListener(listener);
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
textInput.addOnKeyboardShownListener(listener);
}
public void removeOnKeyboardShownListener(
OnKeyboardShownListener listener) {
textInput.removeOnKeyboardShownListener(listener);
}
}

View File

@@ -29,17 +29,13 @@
app:layout_constraintTop_toTopOf="parent"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
<EditText
android:id="@+id/current_password_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/current_password"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
@@ -53,12 +49,11 @@
app:layout_constraintTop_toBottomOf="@id/current_password_entry_wrapper"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
<EditText
android:id="@+id/new_password_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/choose_new_password"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
@@ -74,13 +69,12 @@
app:layout_constraintTop_toBottomOf="@id/new_password_entry_wrapper"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
<EditText
android:id="@+id/new_password_confirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/confirm_new_password"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>

View File

@@ -15,17 +15,14 @@
app:errorEnabled="true"
app:hintEnabled="false">
<android.support.design.widget.TextInputEditText
<EditText
android:id="@+id/createForumNameEntry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/choose_forum_hint"
android:importantForAutofill="no"
android:inputType="text|textCapSentences"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -18,7 +18,7 @@
app:cardCornerRadius="0dp"
app:cardUseCompatPadding="false">
<android.support.design.widget.TextInputEditText
<EditText
android:id="@+id/urlInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -26,14 +26,9 @@
android:gravity="top"
android:hint="@string/blogs_rss_feeds_import_hint"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="textUri"
android:padding="@dimen/margin_medium"
android:textColor="?android:attr/textColorPrimary">
<requestFocus/>
</android.support.design.widget.TextInputEditText>
android:textColor="?android:attr/textColorPrimary"/>
</android.support.v7.widget.CardView>

View File

@@ -14,11 +14,7 @@
android:gravity="bottom"
app:buttonText="@string/blogs_publish_blog_post"
app:fillHeight="true"
app:hint="@string/blogs_write_blog_post_body_hint">
<requestFocus/>
</org.briarproject.briar.android.view.LargeTextInputView>
app:hint="@string/blogs_write_blog_post_body_hint"/>
<ProgressBar
android:id="@+id/progressBar"

View File

@@ -6,7 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout"
tools:parentTag="org.briarproject.briar.android.view.KeyboardAwareLinearLayout"
tools:showIn="@layout/fragment_reblog">
<android.support.v7.widget.AppCompatImageButton

View File

@@ -31,7 +31,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/set_contact_alias_hint"
android:inputType="text|textCapWords"
android:inputType="textPersonName"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"/>

View File

@@ -9,7 +9,6 @@
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="org.briarproject.briar.android.view.SnackbarAwareBehavior"
app:scrollToEnd="false"/>
<io.github.kobakei.materialfabspeeddial.FabSpeedDial
@@ -20,8 +19,6 @@
app:fab_fabRippleColor="@android:color/transparent"
app:fab_menu="@menu/contact_list_actions"
app:fab_miniFabTextBackground="@color/briar_accent"
app:fab_miniFabTextColor="@android:color/white"
app:layout_anchorGravity="bottom|right|end"
app:layout_behavior="org.briarproject.briar.android.view.SnackbarAwareBehavior"/>
app:fab_miniFabTextColor="@android:color/white"/>
</android.support.design.widget.CoordinatorLayout>

View File

@@ -15,17 +15,14 @@
app:errorEnabled="true"
app:hintEnabled="false">
<android.support.design.widget.TextInputEditText
<EditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/groups_create_group_hint"
android:importantForAutofill="no"
android:inputType="text|textCapSentences"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -26,8 +26,7 @@
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed"/>
app:layout_constraintVertical_bias="0.0"/>
<TextView
android:id="@+id/stepOneText"
@@ -75,14 +74,12 @@
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:src="@drawable/ic_nickname"
app:layout_constraintBottom_toTopOf="@+id/nicknameIcon"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="256dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stepOneText"
tools:ignore="ContentDescription"/>
@@ -121,7 +118,6 @@
android:layout_marginTop="16dp"
app:errorEnabled="true"
app:hintEnabled="false"
app:layout_constraintBottom_toTopOf="@+id/space"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
@@ -137,16 +133,6 @@
</android.support.design.widget.TextInputLayout>
<Space
android:id="@+id/space"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/addButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/contactNameLayout"/>
<Button
android:id="@+id/addButton"
style="@style/BriarButton"

View File

@@ -31,9 +31,6 @@
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -38,7 +38,6 @@
android:maxLines="1"/>
<requestFocus/>
</android.support.design.widget.TextInputLayout>
<Button

View File

@@ -39,7 +39,6 @@
android:maxLines="1">
<requestFocus/>
</android.support.design.widget.TextInputEditText>
</android.support.design.widget.TextInputLayout>

View File

@@ -16,11 +16,6 @@
android:enabled="false"
app:showAsAction="never"/>
<item
android:id="@+id/action_delete_all_messages"
android:title="@string/delete_all_messages"
app:showAsAction="never"/>
<item
android:id="@+id/action_social_remove_person"
android:icon="@drawable/action_delete_white"

View File

@@ -175,13 +175,6 @@
<string name="step_1">‮واحدة</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<!--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-->
<!--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-->
<!--Introductions-->
<string name="introduction_onboarding_title">قم بتقديم جهات إتصالك</string>
<string name="introduction_onboarding_text">يمكنك أن تقدم جهات إتصالك لبعضها البعض، فلا يحتاجون للمقابلة الشخصية ليتواصلوا عبر Briar (براير).</string>

View File

@@ -118,9 +118,7 @@
<string name="message_hint">Mesaj yazın</string>
<string name="image_caption_hint">Mövzu əlavə edin (isteyə bağlı)</string>
<string name="image_attach">Şəkil əlavə edin</string>
<string name="image_attach_error">Şəkil (lər) əlavə etmək alınmadı</string>
<string name="image_attach_error_too_big">Şəkilin həcmi böyükdür. Limit %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Şəkil formatı dəstəklənmir: %s</string>
<string name="image_attach_error">Şəkil əlavə edilmədi</string>
<string name="set_contact_alias">Kontakt adı dəyişdirin</string>
<string name="set_contact_alias_hint">Əlaqə adı</string>
<string name="set_alias_button">Dəyiş</string>
@@ -140,7 +138,6 @@
<string name="dialog_title_image_support">İndi bu kontakta şəkilləri göndərə bilərsiniz</string>
<string name="dialog_message_image_support">Şəkilləri əlavə etmək üçün bu simvola toxunun.</string>
<!--Adding Contacts-->
<string name="add_contact_title">Yaxında kontakt əlavə etmək</string>
<string name="face_to_face">Kontakta əlavə etmək istədiyiniz şəxslə tanış olmalısınız. Bu, hər kəsin kimliyinizi və ya mesajlarınızı gələcəkdə oxumasını maneə törədir.</string>
<string name="continue_button">Davam et</string>
<string name="try_again_button">Yenidən cəhd elə</string>
@@ -158,56 +155,12 @@
<string name="connection_error_explanation">Həmin Wi-Fi şəbəkəsinə qoşulduğunuzu yoxlayın.</string>
<string name="connection_error_feedback">Bu problem davam edərsə, tətbiqin təkmilləşdirilməsinə kömək etmək üçün rəy göndərin.</string>
<!--Adding Contacts Remotely-->
<string name="add_contact_remotely_title_case">Məsafədə kontakt əlavə etmək </string>
<string name="add_contact_nearby_title">Yaxında kontakt əlavə etmək </string>
<string name="add_contact_remotely_title">Məsafədə kontakt əlavə etmək </string>
<string name="contact_name_hint">Niklə kontakt saxlayın</string>
<string name="contact_link_intro">Kontaktın linkini buraya daxil edin</string>
<string name="contact_link_hint">Kontaktın linki</string>
<string name="paste_button">Yapışdır</string>
<string name="add_contact_button">Kontankt əlavə et</string>
<string name="copy_button">Kopyala</string>
<string name="share_button">Paylaş</string>
<string name="send_link_title">Link mübadiləsi</string>
<string name="add_contact_choose_nickname">Ad seç</string>
<string name="add_contact_choose_a_nickname">Ad daxil et</string>
<string name="nickname_intro">Niklə kontakt saxlayın. Bunu ancaq siz görəcəksiniz. </string>
<string name="your_link">Əlavə etmək istədiyiniz kontakt üçün bu linki verin</string>
<string name="link_clip_label">Briar link</string>
<string name="link_copied_toast">Link kopyalandı</string>
<string name="adding_contact_error">Kontaktı əlavə edərkən səhv baş verdi.</string>
<string name="pending_contact_requests_snackbar">Gözləyən kontakt sorğuları var. </string>
<string name="pending_contact_requests">Kənara qoyulmuş kontak sorğuları</string>
<string name="no_pending_contacts">Gözləyən kontakt yoxdur</string>
<string name="add_contact_remote_connecting">Qoşulur...</string>
<string name="waiting_for_contact_to_come_online">Kontaktı xəttdə gözləyin ...</string>
<string name="connecting">Qoşulur...</string>
<string name="adding_contact">Kontaktın əlavə etməsi... </string>
<string name="adding_contact_failed">Kontakt əlavə etməsi alınmadı</string>
<string name="dialog_title_remove_pending_contact">Silməyi təstiqlə</string>
<string name="dialog_message_remove_pending_contact">Bu kontakt hələ də əlavə olunur. İndi onu çıxararsanız, əlavə olunmayacaq.</string>
<string name="own_link_error">Kontaktınızın linkini daxil edin, özünüzü deyil</string>
<string name="nickname_missing">Zəhmət olmasa adı daxil edin</string>
<string name="invalid_link">Səhv link</string>
<string name="unsupported_link">Bu link Briar-ın daha yeni versiyasından gəlir. Ən son versiyaya keçin və yenidən cəhd edin.</string>
<string name="intent_own_link">Öz linkini açdın. Əlavə etmək istədiyiniz kontaktlardan birini istifadə edin!</string>
<string name="missing_link">Zəhmət olmasa linki daxil edin</string>
<!--This is a numeral indicating the first step in a series of screens-->
<string name="step_1">1</string>
<!--This is a numeral indicating the second step in a series of screens-->
<string name="step_2">2</string>
<string name="offline_state">İnternet bağlantısı yoxdur</string>
<string name="duplicate_link_dialog_title">Dublikat Link</string>
<!--This is a question asking whether two nicknames refer to the same person-->
<!--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">Eyni şəxs</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">Fərqli şəxs</string>
<string name="pending_contact_updated_toast">Gözləyən kontakt yeniləndi</string>
<!--Introductions-->
<string name="introduction_onboarding_title">Kontaktınızı tanıtın</string>
<string name="introduction_onboarding_text">Kontaktlarınızı bir-birinizə təqdim edə bilərsiniz, lakin Briar-a qoşulmaq üçün şəxsən görüşmək lazım deyil.</string>

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