Compare commits

..

4 Commits

Author SHA1 Message Date
akwizgran
b6b15fe657 Only cache attachment items that include size. 2019-06-19 13:30:06 +01:00
akwizgran
f3bbc7179e Move deletion of unsent attachments into task. 2019-06-19 13:30:06 +01:00
akwizgran
9abe32ab4b Refactor attachment code to reduce mutable state. 2019-06-19 13:30:06 +01:00
akwizgran
d07b98eae1 Code cleanups, javadoc. 2019-06-19 13:30:05 +01:00
177 changed files with 1845 additions and 9087 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,8 +66,8 @@ dependencyVerification {
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8', '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:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349', '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:obfs4proxy-android:0.0.9:obfs4proxy-android-0.0.9.zip:9b7e9181535ea8d8bbe8ae6338e08cf4c5fc1e357a779393e0ce49586d459ae0',
'org.briarproject:tor-android:0.3.5.8-64:tor-android-0.3.5.8-64.zip:9f144088c0fe845d1cf3232cdc2b51c68e6f9a22660592009f43a5633fca8824', '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-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.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', '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 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.Pair;
import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.crypto.SecretKey; 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.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; 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.db.Transaction;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
@@ -119,14 +117,9 @@ public interface ContactManager {
* @throws FormatException If the link is invalid * @throws FormatException If the link is invalid
* @throws GeneralSecurityException If the pending contact's handshake * @throws GeneralSecurityException If the pending contact's handshake
* public key is invalid * 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) PendingContact addPendingContact(String link, String alias)
throws DbException, FormatException, GeneralSecurityException, throws DbException, FormatException, GeneralSecurityException;
ContactExistsException, PendingContactExistsException;
/** /**
* Returns the pending contact with the given ID. * Returns the pending contact with the given ID.

View File

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

View File

@@ -1,21 +1,9 @@
package org.briarproject.bramble.api.db; 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 * Thrown when a duplicate pending contact is added to the database. This
* exception may occur due to concurrent updates and does not indicate a * exception may occur due to concurrent updates and does not indicate a
* database error. * database error.
*/ */
public class PendingContactExistsException extends DbException { 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); pendingContactFactory.createPendingContact(link, alias);
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
AuthorId local = identityManager.getLocalAuthor(txn).getId(); db.addPendingContact(txn, p);
db.addPendingContact(txn, p, local);
KeyPair ourKeyPair = identityManager.getHandshakeKeys(txn); KeyPair ourKeyPair = identityManager.getHandshakeKeys(txn);
keyManager.addPendingContact(txn, p.getId(), p.getPublicKey(), keyManager.addPendingContact(txn, p.getId(), p.getPublicKey(),
ourKeyPair); ourKeyPair);

View File

@@ -267,16 +267,6 @@ interface Database<T> {
*/ */
Collection<ContactId> getContacts(T txn, AuthorId local) throws DbException; 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. * Returns the group with the given ID.
* <p/> * <p/>

View File

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

View File

@@ -195,6 +195,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!assetsAreUpToDate()) installAssets(); if (!assetsAreUpToDate()) installAssets();
if (cookieFile.exists() && !cookieFile.delete()) if (cookieFile.exists() && !cookieFile.delete())
LOG.warning("Old auth cookie not deleted"); LOG.warning("Old auth cookie not deleted");
// Migrate old settings before having a chance to stop
migrateSettings();
// Start a new Tor process // Start a new Tor process
LOG.info("Starting Tor"); LOG.info("Starting Tor");
String torPath = torFile.getAbsolutePath(); String torPath = torFile.getAbsolutePath();
@@ -814,6 +816,21 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0"); 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 { private static class ConnectionStatus {
// All of the following are locking: this // 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.SettableClock;
import org.briarproject.bramble.test.TestDatabaseConfig; import org.briarproject.bramble.test.TestDatabaseConfig;
import org.briarproject.bramble.test.TestMessageFactory; import org.briarproject.bramble.test.TestMessageFactory;
import org.briarproject.bramble.test.TestUtils;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -1150,43 +1149,6 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close(); 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 @Test
public void testOfferedMessages() throws Exception { public void testOfferedMessages() throws Exception {
Database<Connection> db = open(false); Database<Connection> db = open(false);

View File

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

View File

@@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com 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] [briar.stringsxml-5]
file_filter = src/main/res/values-<lang>/strings.xml file_filter = src/main/res/values-<lang>/strings.xml

View File

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

View File

@@ -5,10 +5,6 @@
# QR codes # QR codes
-keep class com.google.zxing.Result -keep class com.google.zxing.Result
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# RSS libraries # RSS libraries
-keep,includedescriptorclasses class com.rometools.rome.feed.synd.impl.** { *; } -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.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest; import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -14,7 +13,6 @@ import dagger.Component;
@Singleton @Singleton
@Component(modules = { @Component(modules = {
AppModule.class, AppModule.class,
AttachmentModule.class,
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,
BriarAccountModule.class, BriarAccountModule.class,

View File

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

View File

@@ -73,7 +73,7 @@
android:label="@string/crash_report_title" android:label="@string/crash_report_title"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:theme="@style/BriarTheme.NoActionBar" android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden"> android:windowSoftInputMode="stateHidden">
</activity> </activity>
<activity <activity
@@ -89,7 +89,7 @@
<activity <activity
android:name="org.briarproject.briar.android.account.SetupActivity" android:name="org.briarproject.briar.android.account.SetupActivity"
android:label="@string/setup_title" android:label="@string/setup_title"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> android:windowSoftInputMode="adjustResize">
</activity> </activity>
<activity <activity
@@ -126,7 +126,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity" android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar" android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateUnchanged"> android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/> android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -145,7 +145,7 @@
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity" android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
android:label="@string/groups_create_group_title" android:label="@string/groups_create_group_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity" android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> android:windowSoftInputMode="adjustResize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/> android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -174,7 +174,8 @@
<activity <activity
android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity" android:name="org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity"
android:label="@string/groups_member_list" 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 <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/> android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/>
@@ -183,7 +184,8 @@
<activity <activity
android:name="org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity" android:name="org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity"
android:label="@string/groups_reveal_contacts" 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 <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/> android:value="org.briarproject.briar.android.privategroup.conversation.GroupActivity"/>
@@ -221,7 +223,7 @@
android:name="org.briarproject.briar.android.forum.CreateForumActivity" android:name="org.briarproject.briar.android.forum.CreateForumActivity"
android:label="@string/create_forum_title" android:label="@string/create_forum_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity" android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> android:windowSoftInputMode="adjustResize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/> android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -290,7 +292,7 @@
android:name="org.briarproject.briar.android.blog.WriteBlogPostActivity" android:name="org.briarproject.briar.android.blog.WriteBlogPostActivity"
android:label="@string/blogs_write_blog_post" android:label="@string/blogs_write_blog_post"
android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity" android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> android:windowSoftInputMode="stateVisible|adjustResize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.blog.BlogActivity"/> android:value="org.briarproject.briar.android.blog.BlogActivity"/>
@@ -300,7 +302,7 @@
android:name="org.briarproject.briar.android.blog.ReblogActivity" android:name="org.briarproject.briar.android.blog.ReblogActivity"
android:label="@string/blogs_reblog_button" android:label="@string/blogs_reblog_button"
android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity" android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity"
android:windowSoftInputMode="adjustResize|stateHidden"> android:windowSoftInputMode="stateHidden">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.blog.BlogActivity"/> android:value="org.briarproject.briar.android.blog.BlogActivity"/>
@@ -310,7 +312,7 @@
android:name="org.briarproject.briar.android.blog.RssFeedImportActivity" android:name="org.briarproject.briar.android.blog.RssFeedImportActivity"
android:label="@string/blogs_rss_feeds_import" android:label="@string/blogs_rss_feeds_import"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity" android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> android:windowSoftInputMode="stateVisible|adjustResize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/> android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
@@ -339,7 +341,7 @@
android:name="org.briarproject.briar.android.introduction.IntroductionActivity" android:name="org.briarproject.briar.android.introduction.IntroductionActivity"
android:label="@string/introduction_activity_title" android:label="@string/introduction_activity_title"
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity" android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
android:windowSoftInputMode="adjustResize|stateHidden"> android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.conversation.ConversationActivity"/> android:value="org.briarproject.briar.android.conversation.ConversationActivity"/>
@@ -367,8 +369,7 @@
<activity <activity
android:name="org.briarproject.briar.android.login.ChangePasswordActivity" android:name="org.briarproject.briar.android.login.ChangePasswordActivity"
android:label="@string/change_password" android:label="@string/change_password"
android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity" android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity">
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.settings.SettingsActivity"/> android:value="org.briarproject.briar.android.settings.SettingsActivity"/>
@@ -423,7 +424,7 @@
android:name=".android.contact.add.remote.AddContactActivity" android:name=".android.contact.add.remote.AddContactActivity"
android:label="@string/add_contact_remotely_title_case" android:label="@string/add_contact_remotely_title_case"
android:theme="@style/BriarTheme" android:theme="@style/BriarTheme"
android:windowSoftInputMode="adjustResize|stateHidden"/> android:windowSoftInputMode="stateHidden|adjustResize"/>
<activity <activity
android:name=".android.contact.add.remote.PendingContactListActivity" 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.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BriarCoreModule; 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.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender; import org.briarproject.briar.android.reporting.BriarReportSender;
@@ -69,8 +68,7 @@ import dagger.Component;
BriarCoreModule.class, BriarCoreModule.class,
BrambleAndroidModule.class, BrambleAndroidModule.class,
BriarAccountModule.class, BriarAccountModule.class,
AppModule.class, AppModule.class
AttachmentModule.class
}) })
public interface AndroidComponent public interface AndroidComponent
extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons, extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,

View File

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

View File

@@ -243,7 +243,7 @@ public class AppModule {
} }
@Override @Override
public boolean shouldEnablePrivateMessageDeletion() { public boolean shouldEnableRemoteContacts() {
return IS_DEBUG_BUILD; 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 java.util.logging.Logger;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; 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.FINE;
import static java.util.logging.Level.INFO; 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.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE; import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME; import static org.acra.ReportField.APP_VERSION_NAME;
@@ -82,9 +82,10 @@ public class BriarApplicationImpl extends Application
implements BriarApplication { implements BriarApplication {
private static final Logger LOG = private static final Logger LOG =
getLogger(BriarApplicationImpl.class.getName()); Logger.getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler(); private final CachingLogHandler logHandler = new CachingLogHandler();
private final BackgroundMonitor backgroundMonitor = new BackgroundMonitor();
private AndroidComponent applicationComponent; private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs; private volatile SharedPreferences prefs;
@@ -107,16 +108,12 @@ public class BriarApplicationImpl extends Application
if (IS_DEBUG_BUILD) enableStrictMode(); if (IS_DEBUG_BUILD) enableStrictMode();
Logger rootLogger = getLogger(""); Logger rootLogger = Logger.getLogger("");
Handler[] handlers = rootLogger.getHandlers(); if (!IS_DEBUG_BUILD && !IS_BETA_BUILD) {
// Disable the Android logger for release builds // Remove default log handlers so system log is not used
for (Handler handler : handlers) rootLogger.removeHandler(handler); for (Handler handler : rootLogger.getHandlers()) {
if (IS_DEBUG_BUILD || IS_BETA_BUILD) { rootLogger.removeHandler(handler);
// 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);
} }
rootLogger.addHandler(logHandler); rootLogger.addHandler(logHandler);
rootLogger.setLevel(IS_DEBUG_BUILD || IS_BETA_BUILD ? FINE : INFO); rootLogger.setLevel(IS_DEBUG_BUILD || IS_BETA_BUILD ? FINE : INFO);
@@ -125,6 +122,9 @@ public class BriarApplicationImpl extends Application
applicationComponent = createApplicationComponent(); applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider()); EmojiManager.install(new GoogleEmojiProvider());
if (SDK_INT < 16)
registerActivityLifecycleCallbacks(backgroundMonitor);
} }
protected AndroidComponent createApplicationComponent() { protected AndroidComponent createApplicationComponent() {
@@ -186,8 +186,12 @@ public class BriarApplicationImpl extends Application
@Override @Override
public boolean isRunningInBackground() { public boolean isRunningInBackground() {
RunningAppProcessInfo info = new RunningAppProcessInfo(); if (SDK_INT >= 16) {
ActivityManager.getMyMemoryState(info); RunningAppProcessInfo info = new RunningAppProcessInfo();
return (info.importance != IMPORTANCE_FOREGROUND); 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) { } else if (level == TRIM_MEMORY_RUNNING_LOW) {
LOG.info("Trim memory: running low"); LOG.info("Trim memory: running low");
} else if (level == TRIM_MEMORY_RUNNING_CRITICAL) { } 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"); LOG.warning("Trim memory: running critically low");
// If we're not in the foreground, clear the UI to save memory // If we're not in the foreground, clear the UI to save memory
if (app.isRunningInBackground()) hideUi(); 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 // Get permissions
String[] requestedPermissions = packageInfo.requestedPermissions; String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions == null) return false; 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 // Check whether the permission has been requested and granted
int[] flags = packageInfo.requestedPermissionsFlags; int[] flags = packageInfo.requestedPermissionsFlags;
for (int i = 0; i < requestedPermissions.length; i++) { 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.os.Bundle;
import android.support.design.widget.TextInputEditText; import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout; import android.support.design.widget.TextInputLayout;
import android.text.Editable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import javax.annotation.Nullable; 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 java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; 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.setError;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -62,6 +64,12 @@ public class AuthorNameFragment extends SetupFragment {
return TAG; return TAG;
} }
@Override
public void onResume() {
super.onResume();
showSoftKeyboard(authorNameInput);
}
@Override @Override
protected String getHelpText() { protected String getHelpText() {
return getString(R.string.setup_name_explanation); return getString(R.string.setup_name_explanation);
@@ -69,21 +77,20 @@ public class AuthorNameFragment extends SetupFragment {
@Override @Override
public void onTextChanged(CharSequence authorName, int i, int i1, int i2) { 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; boolean error = authorNameLength > MAX_AUTHOR_NAME_LENGTH;
setError(authorNameWrapper, getString(R.string.name_too_long), error); setError(authorNameWrapper, getString(R.string.name_too_long), error);
boolean enabled = authorNameLength > 0 && !error; boolean enabled = authorNameLength > 0 && !error;
authorNameInput
.setImeOptions(enabled ? IME_ACTION_NEXT : IME_ACTION_NONE);
authorNameInput.setOnEditorActionListener(enabled ? this : null); authorNameInput.setOnEditorActionListener(enabled ? this : null);
nextButton.setEnabled(enabled); nextButton.setEnabled(enabled);
} }
@Override @Override
public void onClick(View view) { public void onClick(View view) {
Editable text = authorNameInput.getText(); setupController.setAuthorName(authorNameInput.getText().toString());
if (text != null) { setupController.showPasswordFragment();
setupController.setAuthorName(text.toString().trim());
setupController.showPasswordFragment();
}
} }
} }

View File

@@ -61,6 +61,7 @@ public class SetPasswordFragment extends SetupFragment {
strengthMeter = v.findViewById(R.id.strength_meter); strengthMeter = v.findViewById(R.id.strength_meter);
passwordEntryWrapper = v.findViewById(R.id.password_entry_wrapper); passwordEntryWrapper = v.findViewById(R.id.password_entry_wrapper);
passwordEntry = v.findViewById(R.id.password_entry); passwordEntry = v.findViewById(R.id.password_entry);
passwordEntry.requestFocus();
passwordConfirmationWrapper = passwordConfirmationWrapper =
v.findViewById(R.id.password_confirm_wrapper); v.findViewById(R.id.password_confirm_wrapper);
passwordConfirmation = v.findViewById(R.id.password_confirm); 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.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.LayoutRes; import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
@@ -11,6 +12,7 @@ import android.support.v7.widget.Toolbar;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; 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.arch.lifecycle.Lifecycle.State.STARTED;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE; 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.Level.INFO;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS; import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
/** /**
* Warning: Some activities don't extend {@link BaseActivity}. * 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 @UiThread
public void handleDbException(DbException e) { public void handleDbException(DbException e) {
supportFinishAfterTransition(); supportFinishAfterTransition();

View File

@@ -1,29 +1,32 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory.Options;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException; import org.jsoup.UnsupportedMimeTypeException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import static android.graphics.Bitmap.CompressFormat.JPEG; import javax.annotation.Nullable;
import static android.graphics.BitmapFactory.decodeStream; import javax.annotation.concurrent.ThreadSafe;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
@@ -32,71 +35,98 @@ import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; 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.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
@ThreadSafe
@NotNullByDefault @NotNullByDefault
class AttachmentCreationTask { class AttachmentCreationTask {
private static Logger LOG = private static Logger LOG =
getLogger(AttachmentCreationTask.class.getName()); getLogger(AttachmentCreationTask.class.getName());
private static final int MAX_ATTACHMENT_DIMENSION = 1000; private final Executor ioExecutor;
private final MessagingManager messagingManager; private final MessagingManager messagingManager;
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
private final ImageSizeCalculator imageSizeCalculator; private final AttachmentRetriever retriever;
private final GroupId groupId; private final GroupId groupId;
private final Collection<Uri> uris; private final Collection<Uri> uris;
private final boolean needsSize; private final boolean needsSize;
@Nullable private final MutableLiveData<AttachmentResult> result;
private volatile AttachmentCreator attachmentCreator;
private volatile boolean canceled = false; private volatile boolean canceled = false;
AttachmentCreationTask(MessagingManager messagingManager, AttachmentCreationTask(Executor ioExecutor,
ContentResolver contentResolver, MessagingManager messagingManager, ContentResolver contentResolver,
AttachmentCreator attachmentCreator, AttachmentRetriever retriever, GroupId groupId,
ImageSizeCalculator imageSizeCalculator, Collection<Uri> uris, boolean needsSize) {
GroupId groupId, Collection<Uri> uris, boolean needsSize) { this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager; this.messagingManager = messagingManager;
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
this.imageSizeCalculator = imageSizeCalculator; this.retriever = retriever;
this.groupId = groupId; this.groupId = groupId;
this.uris = uris; this.uris = uris;
this.needsSize = needsSize; this.needsSize = needsSize;
this.attachmentCreator = attachmentCreator; result = new MutableLiveData<>();
} }
LiveData<AttachmentResult> getResult() {
return result;
}
/**
* Cancels the task, asynchronously waits for it to finish, and deletes any
* created attachments.
*/
void cancel() { void cancel() {
canceled = true; canceled = true;
attachmentCreator = null; // Observe the task until it finishes (which may already have happened)
} result.observeForever(new Observer<AttachmentResult>() {
@Override
@IoExecutor public void onChanged(@Nullable AttachmentResult attachmentResult) {
void storeAttachments() { requireNonNull(attachmentResult);
for (Uri uri : uris) processUri(uri); if (attachmentResult.isFinished()) {
AttachmentCreator attachmentCreator = this.attachmentCreator; deleteUnsentAttachments(attachmentResult.getItemResults());
if (!canceled && attachmentCreator != null) result.removeObserver(this);
attachmentCreator.onAttachmentCreationFinished(); }
this.attachmentCreator = null;
}
@IoExecutor
private void processUri(Uri uri) {
if (canceled) return;
try {
AttachmentHeader h = storeAttachment(uri);
AttachmentCreator attachmentCreator = this.attachmentCreator;
if (attachmentCreator != null) {
attachmentCreator.onAttachmentHeaderReceived(uri, h, needsSize);
} }
});
}
/**
* Asynchronously creates and stores the attachments.
*/
void storeAttachments() {
ioExecutor.execute(() -> {
if (LOG.isLoggable(INFO))
LOG.info("Storing " + uris.size() + " attachments");
List<AttachmentItemResult> results = new ArrayList<>();
for (Uri uri : uris) {
if (canceled) break;
results.add(processUri(uri));
result.postValue(new AttachmentResult(new ArrayList<>(results),
false, false));
}
result.postValue(new AttachmentResult(new ArrayList<>(results),
true, !canceled));
});
}
@IoExecutor
private AttachmentItemResult processUri(Uri uri) {
AttachmentHeader header = null;
try {
header = storeAttachment(uri);
Attachment a = retriever.getMessageAttachment(header);
AttachmentItem item =
retriever.getAttachmentItem(header, a, needsSize);
if (item.hasError()) throw new IOException();
if (needsSize) retriever.cachePut(item);
return new AttachmentItemResult(uri, item);
} catch (DbException | IOException e) { } catch (DbException | IOException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
AttachmentCreator attachmentCreator = this.attachmentCreator; // If the attachment was already stored, delete it
if (attachmentCreator != null) { tryToRemove(header);
attachmentCreator.onAttachmentError(uri, e);
}
canceled = true; canceled = true;
return new AttachmentItemResult(uri, e);
} }
} }
@@ -112,8 +142,6 @@ class AttachmentCreationTask {
} }
InputStream is = contentResolver.openInputStream(uri); InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException(); if (is == null) throw new IOException();
is = compressImage(is, contentType);
contentType = "image/jpeg";
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is); .addLocalAttachment(groupId, timestamp, contentType, is);
@@ -129,48 +157,31 @@ class AttachmentCreationTask {
return false; return false;
} }
private InputStream compressImage(InputStream is, String contentType) private void tryToRemove(@Nullable AttachmentHeader h) {
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try { try {
Bitmap bitmap = createBitmap(is, contentType); if (h != null) messagingManager.removeAttachment(h);
for (int quality = 100; quality >= 0; quality -= 10) { } catch (DbException e1) {
if (!bitmap.compress(JPEG, quality, out)) logException(LOG, WARNING, e1);
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) private void deleteUnsentAttachments(
throws IOException { Collection<AttachmentItemResult> itemResults) {
is = new BufferedInputStream(is); List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
Size size = imageSizeCalculator.getSize(is, contentType); for (AttachmentItemResult itemResult : itemResults) {
if (size.error) throw new IOException(); AttachmentItem item = itemResult.getItem();
if (LOG.isLoggable(INFO)) if (item != null) headers.add(item.getHeader());
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)) if (LOG.isLoggable(INFO))
LOG.info("Scaling attachment by factor of " + inSampleSize); LOG.info("Deleting " + headers.size() + " unsent attachments");
Options options = new Options(); ioExecutor.execute(() -> {
options.inSampleSize = inSampleSize; for (AttachmentHeader header : headers) {
Bitmap bitmap = decodeStream(is, null, options); try {
if (bitmap == null) throw new IOException(); messagingManager.removeAttachment(header);
return bitmap; } catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
} }
} }

View File

@@ -1,24 +1,58 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.app.Application;
import android.arch.lifecycle.LiveData; import android.arch.lifecycle.LiveData;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor;
import static java.util.Collections.emptyList;
@NotNullByDefault @NotNullByDefault
public interface AttachmentCreator { public class AttachmentCreator {
private final Application app;
@IoExecutor
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
@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;
}
/**
* Starts a background task to create attachments from the given URIs and
* returns a LiveData to monitor the progress of the task.
*/
@UiThread @UiThread
LiveData<AttachmentResult> storeAttachments(LiveData<GroupId> groupId, public LiveData<AttachmentResult> storeAttachments(GroupId groupId,
Collection<Uri> newUris); Collection<Uri> uris) {
if (task != null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(ioExecutor, messagingManager,
app.getContentResolver(), retriever, groupId, uris, needsSize);
task.storeAttachments();
return task.getResult();
}
/** /**
* This should be only called after configuration changes. * This should be only called after configuration changes.
@@ -26,35 +60,50 @@ public interface AttachmentCreator {
* They are already being created and returned by the existing LiveData. * They are already being created and returned by the existing LiveData.
*/ */
@UiThread @UiThread
LiveData<AttachmentResult> getLiveAttachments(); public LiveData<AttachmentResult> getLiveAttachments() {
if (task == null) throw new IllegalStateException();
@UiThread // A task is already running. It will update the result LiveData.
List<AttachmentHeader> getAttachmentHeadersForSending(); // So nothing more to do here.
return task.getResult();
}
/** /**
* Marks the attachments as sent and adds the items to the cache for display * Returns the headers of any attachments created by
* * {@link #storeAttachments(GroupId, Collection)}, unless
* @param id The MessageId of the sent message. * {@link #onAttachmentsSent()} or {@link #cancel()} has been called.
*/ */
@UiThread @UiThread
void onAttachmentsSent(MessageId id); public List<AttachmentHeader> getAttachmentHeadersForSending() {
if (task == null) return emptyList();
AttachmentResult result = task.getResult().getValue();
if (result == null) return emptyList();
List<AttachmentHeader> headers = new ArrayList<>();
for (AttachmentItemResult itemResult : result.getItemResults()) {
AttachmentItem item = itemResult.getItem();
if (item != null) headers.add(item.getHeader());
}
return headers;
}
/** /**
* Needs to be called when created attachments will not be sent anymore. * Informs the AttachmentCreator that the attachments created by
* {@link #storeAttachments(GroupId, Collection)} will be sent.
*/ */
@UiThread @UiThread
void cancel(); public void onAttachmentsSent() {
task = null; // Prevent cancel() from cancelling the task
}
/**
* Cancels the task started by
* {@link #storeAttachments(GroupId, Collection)}, if any, unless
* {@link #onAttachmentsSent()} has been called.
*/
@UiThread @UiThread
void deleteUnsentAttachments(); public void cancel() {
if (task != null) {
@IoExecutor task.cancel();
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h, task = null;
boolean needsSize); }
}
@IoExecutor }
void onAttachmentError(Uri uri, Throwable t);
@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 @Immutable
@NotNullByDefault @NotNullByDefault
class AttachmentDimensions { public class AttachmentDimensions {
final int defaultSize; final int defaultSize;
final int minWidth, maxWidth; final int minWidth, maxWidth;
@@ -26,7 +26,7 @@ class AttachmentDimensions {
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
} }
static AttachmentDimensions getAttachmentDimensions(Resources res) { public static AttachmentDimensions getAttachmentDimensions(Resources res) {
int defaultSize = int defaultSize =
res.getDimensionPixelSize(R.dimen.message_bubble_image_default); res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
int minWidth = res.getDimensionPixelSize( int minWidth = res.getDimensionPixelSize(

View File

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

View File

@@ -15,18 +15,18 @@ public class AttachmentItemResult {
@Nullable @Nullable
private final AttachmentItem item; private final AttachmentItem item;
@Nullable @Nullable
private final String errorMsg; private final Exception exception;
AttachmentItemResult(Uri uri, AttachmentItem item) { AttachmentItemResult(Uri uri, AttachmentItem item) {
this.uri = uri; this.uri = uri;
this.item = item; this.item = item;
this.errorMsg = null; this.exception = null;
} }
AttachmentItemResult(Uri uri, @Nullable String errorMsg) { AttachmentItemResult(Uri uri, Exception exception) {
this.uri = uri; this.uri = uri;
this.item = null; this.item = null;
this.errorMsg = errorMsg; this.exception = exception;
} }
public Uri getUri() { public Uri getUri() {
@@ -43,8 +43,8 @@ public class AttachmentItemResult {
} }
@Nullable @Nullable
public String getErrorMsg() { public Exception getException() {
return errorMsg; return exception;
} }
} }

View File

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

@@ -12,11 +12,13 @@ public class AttachmentResult {
private final Collection<AttachmentItemResult> itemResults; private final Collection<AttachmentItemResult> itemResults;
private final boolean finished; private final boolean finished;
private final boolean success;
public AttachmentResult(Collection<AttachmentItemResult> itemResults, AttachmentResult(Collection<AttachmentItemResult> itemResults,
boolean finished) { boolean finished, boolean success) {
this.itemResults = itemResults; this.itemResults = itemResults;
this.finished = finished; this.finished = finished;
this.success = success;
} }
public Collection<AttachmentItemResult> getItemResults() { public Collection<AttachmentItemResult> getItemResults() {
@@ -27,4 +29,7 @@ public class AttachmentResult {
return finished; return finished;
} }
public boolean isSuccess() {
return success;
}
} }

View File

@@ -1,29 +1,276 @@
package org.briarproject.briar.android.attachment; package org.briarproject.briar.android.attachment;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.support.annotation.Nullable; 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.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; 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.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; 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.io.InputStream;
import java.util.ArrayList;
import java.util.List; 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 @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, 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(AttachmentItem item) {
attachmentCache.put(item.getMessageId(), item);
}
@Nullable @Nullable
List<AttachmentItem> cacheGet(MessageId messageId); public AttachmentItem cacheGet(MessageId attachmentId) {
return attachmentCache.get(attachmentId);
}
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;
}
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 * Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream} which will be closed when this method returns. * {@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; import java.io.InputStream;
@NotNullByDefault @NotNullByDefault
public interface ImageHelper { interface ImageHelper {
DecodeResult decodeStream(InputStream is); 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 android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
public class RssFeedImportActivity extends BriarActivity { public class RssFeedImportActivity extends BriarActivity {
@@ -78,6 +77,7 @@ public class RssFeedImportActivity extends BriarActivity {
if (actionId == IME_ACTION_DONE && importButton.isEnabled() && if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
importButton.getVisibility() == VISIBLE) { importButton.getVisibility() == VISIBLE) {
publish(); publish();
hideSoftKeyboard(urlInput);
return true; return true;
} }
return false; return false;
@@ -123,7 +123,6 @@ public class RssFeedImportActivity extends BriarActivity {
// hide import button, show progress bar // hide import button, show progress bar
importButton.setVisibility(GONE); importButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE); progressBar.setVisibility(VISIBLE);
hideSoftKeyboard(urlInput);
String url = validateAndNormaliseUrl(urlInput.getText().toString()); String url = validateAndNormaliseUrl(urlInput.getText().toString());
if (url == null) throw new AssertionError(); if (url == null) throw new AssertionError();

View File

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

View File

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

View File

@@ -9,10 +9,8 @@ import android.support.annotation.Nullable;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.ContactManager; 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.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchPendingContactException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult; import org.briarproject.briar.android.viewmodel.LiveResult;
@@ -120,19 +118,4 @@ public class AddContactViewModel extends AndroidViewModel {
return addContactResult; 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; package org.briarproject.briar.android.contact.add.remote;
import android.animation.ObjectAnimator;
import android.arch.lifecycle.ViewModelProvider; import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.content.ClipData; import android.content.ClipData;
@@ -12,9 +11,7 @@ import android.support.v4.app.ShareCompat.IntentBuilder;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.Button; import android.widget.Button;
import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@@ -37,8 +34,7 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class LinkExchangeFragment extends BaseFragment public class LinkExchangeFragment extends BaseFragment {
implements OnGlobalLayoutListener {
private static final String TAG = LinkExchangeFragment.class.getName(); private static final String TAG = LinkExchangeFragment.class.getName();
@@ -94,30 +90,9 @@ public class LinkExchangeFragment extends BaseFragment
observeOnce(viewModel.getHandshakeLink(), this, observeOnce(viewModel.getHandshakeLink(), this,
this::onHandshakeLinkLoaded); 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; 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) { private void onHandshakeLinkLoaded(String link) {
View v = requireNonNull(getView()); 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.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.StringRes;
import android.support.design.widget.TextInputEditText; import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout; import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AlertDialog.Builder;
import android.text.Editable; import android.text.Editable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -20,10 +15,6 @@ import android.widget.ProgressBar;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.bramble.api.UnsupportedVersionException; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
@@ -33,13 +24,9 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; 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.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_LONG; 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.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong; import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
@@ -95,20 +82,19 @@ public class NicknameFragment extends BaseFragment {
@Nullable @Nullable
private String getNicknameOrNull() { private String getNicknameOrNull() {
Editable text = contactNameInput.getText(); Editable name = contactNameInput.getText();
if (text == null || text.toString().trim().length() == 0) { if (name == null || name.toString().trim().length() == 0) {
contactNameLayout.setError(getString(R.string.nickname_missing)); contactNameLayout.setError(getString(R.string.nickname_missing));
contactNameInput.requestFocus(); contactNameInput.requestFocus();
return null; return null;
} }
String name = text.toString().trim(); if (utf8IsTooLong(name.toString(), MAX_AUTHOR_NAME_LENGTH)) {
if (utf8IsTooLong(name, MAX_AUTHOR_NAME_LENGTH)) {
contactNameLayout.setError(getString(R.string.name_too_long)); contactNameLayout.setError(getString(R.string.name_too_long));
contactNameInput.requestFocus(); contactNameInput.requestFocus();
return null; return null;
} }
contactNameLayout.setError(null); contactNameLayout.setError(null);
return name; return name.toString().trim();
} }
private void onAddButtonClicked() { private void onAddButtonClicked() {
@@ -120,95 +106,23 @@ public class NicknameFragment extends BaseFragment {
viewModel.getAddContactResult().observe(this, result -> { viewModel.getAddContactResult().observe(this, result -> {
if (result == null) return; if (result == null) return;
if (result.hasError()) if (result.hasError()) {
handleException(name, requireNonNull(result.getException())); int stringRes;
else if (result
showPendingContactListActivity(); .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); 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 = Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts(); contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size()); List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = items.isEmpty(); boolean online = false;
for (Pair<PendingContact, PendingContactState> pair : pairs) { for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst(); PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond(); PendingContactState state = pair.getSecond();

View File

@@ -21,12 +21,9 @@ import org.briarproject.briar.android.activity.BaseActivity;
import javax.inject.Inject; import javax.inject.Inject;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8; 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 @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
@@ -79,14 +76,13 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
setButton.setOnClickListener(v1 -> onSetButtonClicked()); setButton.setOnClickListener(v1 -> onSetButtonClicked());
Button cancelButton = v.findViewById(R.id.cancelButton); Button cancelButton = v.findViewById(R.id.cancelButton);
cancelButton.setOnClickListener(v1 -> onCancelButtonClicked()); cancelButton.setOnClickListener(v1 -> getDialog().cancel());
return v; return v;
} }
private void onSetButtonClicked() { private void onSetButtonClicked() {
hideSoftKeyboard(aliasEditText); String alias = aliasEditText.getText().toString();
String alias = aliasEditText.getText().toString().trim();
if (toUtf8(alias).length > MAX_AUTHOR_NAME_LENGTH) { if (toUtf8(alias).length > MAX_AUTHOR_NAME_LENGTH) {
aliasEditLayout.setError(getString(R.string.name_too_long)); aliasEditLayout.setError(getString(R.string.name_too_long));
} else { } 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.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; 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.Event;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener; 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.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
import java.util.ArrayList; 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.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.sort; import static java.util.Collections.sort;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; 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.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; 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"; public static final String CONTACT_ID = "briar.CONTACT_ID";
private static final Logger LOG = 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 TRANSITION_DURATION_MS = 500;
private static final int ONBOARDING_DELAY_MS = 250; private static final int ONBOARDING_DELAY_MS = 250;
@@ -175,8 +171,6 @@ public class ConversationActivity extends BriarActivity
volatile GroupInvitationManager groupInvitationManager; volatile GroupInvitationManager groupInvitationManager;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>(); private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private final Map<MessageId, PrivateMessageHeader> missingAttachments =
new ConcurrentHashMap<>();
private final Observer<String> contactNameObserver = name -> { private final Observer<String> contactNameObserver = name -> {
requireNonNull(name); requireNonNull(name);
loadMessages(); loadMessages();
@@ -276,7 +270,7 @@ public class ConversationActivity extends BriarActivity
textInputView.setSendController(sendController); textInputView.setSendController(sendController);
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH); textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false); textInputView.setReady(false);
textInputView.setOnKeyboardShownListener(this::scrollToBottom); textInputView.addOnKeyboardShownListener(this::scrollToBottom);
} }
private void scrollToBottom() { private void scrollToBottom() {
@@ -356,11 +350,6 @@ public class ConversationActivity extends BriarActivity
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.conversation_actions, menu); 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 // enable introduction action if available
observeOnce(viewModel.showIntroductionAction(), this, enable -> { observeOnce(viewModel.showIntroductionAction(), this, enable -> {
if (enable != null && enable) { if (enable != null && enable) {
@@ -394,9 +383,6 @@ public class ConversationActivity extends BriarActivity
AliasDialogFragment.newInstance().show( AliasDialogFragment.newInstance().show(
getSupportFragmentManager(), AliasDialogFragment.TAG); getSupportFragmentManager(), AliasDialogFragment.TAG);
return true; return true;
case R.id.action_delete_all_messages:
askToDeleteAllMessages();
return true;
case R.id.action_social_remove_person: case R.id.action_social_remove_person:
askToRemoveContact(); askToRemoveContact();
return true; return true;
@@ -448,40 +434,31 @@ public class ConversationActivity extends BriarActivity
}); });
} }
private void eagerlyLoadMessageSize(PrivateMessageHeader h) { private void eagerlyLoadMessageSize(PrivateMessageHeader h)
try { throws DbException {
MessageId id = h.getId(); MessageId id = h.getId();
// If the message has text, load it // If the message has text, load it
if (h.hasText()) { if (h.hasText()) {
String text = textCache.get(id); String text = textCache.get(id);
if (text == null) { if (text == null) {
LOG.info("Eagerly loading text for latest message"); LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id); text = messagingManager.getMessageText(id);
textCache.put(id, requireNonNull(text)); 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 // If the message has a single image, load its size - for multiple
List<AttachmentHeader> headers = h.getAttachmentHeaders(); // images we use a grid so the size is fixed
if (headers.size() == 1) { List<AttachmentHeader> headers = h.getAttachmentHeaders();
List<AttachmentItem> items = attachmentRetriever.cacheGet(id); if (headers.size() == 1) {
if (items == null) { MessageId attachmentId = headers.get(0).getMessageId();
LOG.info("Eagerly loading image size for latest message"); AttachmentItem item = attachmentRetriever.cacheGet(attachmentId);
AttachmentHeader header = headers.get(0); if (item == null) {
try { LOG.info("Eagerly loading image size for latest message");
Attachment a = attachmentRetriever item = attachmentRetriever.getAttachmentItems(
.getMessageAttachment(header); attachmentRetriever.getMessageAttachments(headers))
AttachmentItem item = .get(0);
attachmentRetriever.getAttachmentItem(a, true); attachmentRetriever.cachePut(item);
attachmentRetriever.cachePut(id, singletonList(item));
} catch (NoSuchMessageException e) {
LOG.info("Attachment not received yet");
missingAttachments.put(header.getMessageId(), h);
}
}
} }
} catch (DbException e) {
logException(LOG, WARNING, e);
} }
} }
@@ -559,30 +536,18 @@ public class ConversationActivity extends BriarActivity
&& adapter.isScrolledToBottom(layoutManager); && adapter.isScrolledToBottom(layoutManager);
} }
private void loadMessageAttachments(PrivateMessageHeader h) { private void loadMessageAttachments(MessageId messageId,
// TODO: Use placeholders for missing/invalid attachments List<AttachmentHeader> headers) {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
List<Pair<AttachmentHeader, Attachment>> attachments =
attachmentRetriever.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1 // TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentHeader> headers = h.getAttachmentHeaders(); List<AttachmentItem> items =
boolean needsSize = headers.size() == 1; attachmentRetriever.getAttachmentItems(attachments);
List<AttachmentItem> items = new ArrayList<>(headers.size()); if (items.size() == 1)
for (AttachmentHeader header : headers) { attachmentRetriever.cachePut(items.get(0));
try { displayMessageAttachments(messageId, items);
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);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
@@ -605,13 +570,6 @@ public class ConversationActivity extends BriarActivity
@Override @Override
public void eventOccurred(Event e) { 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) { if (e instanceof ContactRemovedEvent) {
ContactRemovedEvent c = (ContactRemovedEvent) e; ContactRemovedEvent c = (ContactRemovedEvent) e;
if (c.getContactId().equals(contactId)) { if (c.getContactId().equals(contactId)) {
@@ -662,15 +620,6 @@ public class ConversationActivity extends BriarActivity
scrollToBottom(); scrollToBottom();
} }
@UiThread
private void onAttachmentReceived(MessageId attachmentId) {
PrivateMessageHeader h = missingAttachments.remove(attachmentId);
if (h != null) {
LOG.info("Missing attachment received");
loadMessageAttachments(h);
}
}
@UiThread @UiThread
private void onNewConversationMessage(ConversationMessageHeader h) { private void onNewConversationMessage(ConversationMessageHeader h) {
if (h instanceof ConversationRequest || if (h instanceof ConversationRequest ||
@@ -735,52 +684,6 @@ public class ConversationActivity extends BriarActivity
addConversationItem(h.accept(visitor)); 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() { private void askToRemoveContact() {
DialogInterface.OnClickListener okListener = DialogInterface.OnClickListener okListener =
(dialog, which) -> removeContact(); (dialog, which) -> removeContact();
@@ -1003,14 +906,19 @@ public class ConversationActivity extends BriarActivity
} }
@Override @Override
public List<AttachmentItem> getAttachmentItems(PrivateMessageHeader h) { public List<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentItem> attachments = List<AttachmentHeader> headers) {
attachmentRetriever.cacheGet(h.getId()); List<AttachmentItem> items = new ArrayList<>(headers.size());
if (attachments == null) { for (AttachmentHeader header : headers) {
loadMessageAttachments(h); AttachmentItem item =
return emptyList(); attachmentRetriever.cacheGet(header.getMessageId());
if (item == null) {
loadMessageAttachments(m, headers);
return emptyList();
}
items.add(item);
} }
return attachments; return items;
} }
} }

View File

@@ -4,6 +4,7 @@ import android.app.Application;
import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData; import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Observer;
import android.arch.lifecycle.Transformations; import android.arch.lifecycle.Transformations;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@@ -18,6 +19,7 @@ import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.identity.AuthorId; 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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.settings.SettingsManager;
@@ -50,6 +52,7 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; 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.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce; import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@@ -99,13 +102,10 @@ public class ConversationViewModel extends AndroidViewModel
@Inject @Inject
ConversationViewModel(Application application, ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
TransactionManager db, @IoExecutor Executor ioExecutor, TransactionManager db,
MessagingManager messagingManager, MessagingManager messagingManager, ContactManager contactManager,
ContactManager contactManager,
SettingsManager settingsManager, SettingsManager settingsManager,
PrivateMessageFactory privateMessageFactory, PrivateMessageFactory privateMessageFactory) {
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
super(application); super(application);
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.db = db; this.db = db;
@@ -113,8 +113,10 @@ public class ConversationViewModel extends AndroidViewModel
this.contactManager = contactManager; this.contactManager = contactManager;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.privateMessageFactory = privateMessageFactory; this.privateMessageFactory = privateMessageFactory;
this.attachmentRetriever = attachmentRetriever; this.attachmentRetriever = new AttachmentRetriever(messagingManager,
this.attachmentCreator = attachmentCreator; getAttachmentDimensions(application.getResources()));
this.attachmentCreator = new AttachmentCreator(getApplication(),
ioExecutor, messagingManager, attachmentRetriever);
messagingGroupId = Transformations messagingGroupId = Transformations
.map(contact, c -> messagingManager.getContactGroup(c).getId()); .map(contact, c -> messagingManager.getContactGroup(c).getId());
contactDeleted.setValue(false); contactDeleted.setValue(false);
@@ -123,7 +125,7 @@ public class ConversationViewModel extends AndroidViewModel
@Override @Override
protected void onCleared() { protected void onCleared() {
super.onCleared(); super.onCleared();
attachmentCreator.deleteUnsentAttachments(); attachmentCreator.cancel();
} }
/** /**
@@ -199,12 +201,23 @@ public class ConversationViewModel extends AndroidViewModel
@UiThread @UiThread
public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris, public LiveData<AttachmentResult> storeAttachments(Collection<Uri> uris,
boolean restart) { boolean restart) {
if (restart) { MutableLiveData<AttachmentResult> delegate = new MutableLiveData<>();
return attachmentCreator.getLiveAttachments(); // messagingGroupId is loaded with the contact
} else { observeForeverOnce(messagingGroupId, groupId -> {
// messagingGroupId is loaded with the contact requireNonNull(groupId);
return attachmentCreator.storeAttachments(messagingGroupId, uris); LiveData<AttachmentResult> result;
} if (restart) result = attachmentCreator.getLiveAttachments();
else result = attachmentCreator.storeAttachments(groupId, uris);
result.observeForever(new Observer<AttachmentResult>() {
@Override
public void onChanged(@Nullable AttachmentResult value) {
requireNonNull(value);
if (value.isFinished()) result.removeObserver(this);
delegate.setValue(value);
}
});
});
return delegate;
} }
@Override @Override
@@ -293,7 +306,7 @@ public class ConversationViewModel extends AndroidViewModel
} }
private void storeMessage(PrivateMessage m) { private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId()); attachmentCreator.onAttachmentsSent();
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
try { try {
long start = now(); long start = now();

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.forum.ForumInvitationResponse;
import org.briarproject.briar.api.introduction.IntroductionRequest; import org.briarproject.briar.api.introduction.IntroductionRequest;
import org.briarproject.briar.api.introduction.IntroductionResponse; 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.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
@@ -55,7 +56,8 @@ class ConversationVisitor implements
if (h.getAttachmentHeaders().isEmpty()) { if (h.getAttachmentHeaders().isEmpty()) {
attachments = emptyList(); attachments = emptyList();
} else { } else {
attachments = attachmentCache.getAttachmentItems(h); attachments = attachmentCache
.getAttachmentItems(h.getId(), h.getAttachmentHeaders());
} }
if (h.isLocal()) { if (h.isLocal()) {
item = new ConversationMessageItem( item = new ConversationMessageItem(
@@ -293,6 +295,7 @@ class ConversationVisitor implements
} }
interface AttachmentCache { 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.setAdapter(pagerAdapter);
viewPager.setCurrentItem(position); viewPager.setCurrentItem(position);
viewModel.getOnImageClicked().observeEvent(this, this::onImageClicked); if (SDK_INT >= 16) {
window.getDecorView().setSystemUiVisibility(UI_FLAGS_DEFAULT); viewModel.getOnImageClicked()
.observeEvent(this, this::onImageClicked);
window.getDecorView().setSystemUiVisibility(UI_FLAGS_DEFAULT);
}
} }
@Override @Override
@@ -171,7 +174,11 @@ public class ImageActivity extends BriarActivity
viewModel.setToolbarPosition( viewModel.setToolbarPosition(
appBarLayout.getTop(), appBarLayout.getBottom() appBarLayout.getTop(), appBarLayout.getBottom()
); );
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this); if (SDK_INT >= 16) {
layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
} }
@Override @Override
@@ -258,7 +265,7 @@ public class ImageActivity extends BriarActivity
* when the previous activity (with visible status bar) is shown. * when the previous activity (with visible status bar) is shown.
*/ */
private void showStatusBarBeforeFinishing() { private void showStatusBarBeforeFinishing() {
if (appBarLayout.getVisibility() == GONE) { if (SDK_INT >= 16 && appBarLayout.getVisibility() == GONE) {
View decorView = getWindow().getDecorView(); View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(UI_FLAGS_DEFAULT); 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.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; 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.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
@@ -134,10 +135,10 @@ public class ImageViewModel extends AndroidViewModel {
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp, private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) { @Nullable Runnable afterCopy) {
MessageId messageId = attachment.getMessageId();
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
try { try {
Attachment a = Attachment a = messagingManager.getAttachment(messageId);
messagingManager.getAttachment(attachment.getHeader());
copyImageFromDb(a, osp, afterCopy); copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, 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.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; 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.attachment.AttachmentItem;
import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import java.io.InputStream; import java.io.InputStream;
@@ -50,12 +50,11 @@ class BriarDataFetcher implements DataFetcher<InputStream> {
@Override @Override
public void loadData(Priority priority, public void loadData(Priority priority,
DataCallback<? super InputStream> callback) { DataCallback<? super InputStream> callback) {
MessageId id = attachment.getMessageId();
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
if (cancel) return; if (cancel) return;
try { try {
Attachment a = inputStream = messagingManager.getAttachment(id).getStream();
messagingManager.getAttachment(attachment.getHeader());
inputStream = a.getStream();
callback.onDataReady(inputStream); callback.onDataReady(inputStream);
} catch (DbException e) { } catch (DbException e) {
callback.onLoadFailed(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.logException;
import static org.briarproject.bramble.util.LogUtils.now; 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.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; import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -93,6 +91,12 @@ public class CreateForumActivity extends BriarActivity {
progress = findViewById(R.id.createForumProgressBar); progress = findViewById(R.id.createForumProgressBar);
} }
@Override
public void onStart() {
super.onStart();
showSoftKeyboard(nameEntry);
}
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
component.inject(this); component.inject(this);

View File

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

View File

@@ -43,7 +43,6 @@ import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; 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.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH; import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -185,7 +184,7 @@ public class IntroductionMessageFragment extends BaseFragment
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: case android.R.id.home:
hideSoftKeyboard(ui.message); introductionActivity.hideSoftKeyboard(ui.message);
introductionActivity.onBackPressed(); introductionActivity.onBackPressed();
return true; return true;
default: default:
@@ -202,7 +201,7 @@ public class IntroductionMessageFragment extends BaseFragment
makeIntroduction(contact1, contact2, text); makeIntroduction(contact1, contact2, text);
// don't wait for the introduction to be made before finishing activity // don't wait for the introduction to be made before finishing activity
hideSoftKeyboard(ui.message); introductionActivity.hideSoftKeyboard(ui.message);
introductionActivity.setResult(RESULT_OK); introductionActivity.setResult(RESULT_OK);
introductionActivity.supportFinishAfterTransition(); 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.FOCUS_MODE_MACRO;
import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO; import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO;
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE; 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.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
@@ -339,7 +340,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread @UiThread
private void setVideoStabilisation(Parameters params) { private void setVideoStabilisation(Parameters params) {
if (params.isVideoStabilizationSupported()) { if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) {
params.setVideoStabilization(true); params.setVideoStabilization(true);
} }
} }
@@ -414,8 +415,10 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
LOG.info("Video stabilisation enabled: " if (SDK_INT >= 15) {
+ params.getVideoStabilization()); LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization());
}
LOG.info("Scene mode: " + params.getSceneMode()); LOG.info("Scene mode: " + params.getSceneMode());
LOG.info("Focus mode: " + params.getFocusMode()); LOG.info("Focus mode: " + params.getFocusMode());
LOG.info("Flash mode: " + params.getFlashMode()); LOG.info("Flash mode: " + params.getFlashMode());

View File

@@ -62,7 +62,8 @@ public class ContactExchangeActivity extends KeyAgreementActivity {
@UiThread @UiThread
private void contactExchangeSucceeded(Author remoteAuthor) { private void contactExchangeSucceeded(Author remoteAuthor) {
String contactName = remoteAuthor.getName(); 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(); Toast.makeText(this, text, LENGTH_LONG).show();
supportFinishAfterTransition(); supportFinishAfterTransition();
} }

View File

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

View File

@@ -26,8 +26,6 @@ import javax.inject.Inject;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; 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 public class ChangePasswordActivity extends BriarActivity
implements OnClickListener, OnEditorActionListener { implements OnClickListener, OnEditorActionListener {

View File

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

View File

@@ -47,9 +47,7 @@ public class GroupActivity extends
@Inject @Inject
GroupController controller; GroupController controller;
@Nullable private boolean isCreator, isDissolved = false;
private Boolean isCreator = null;
private boolean isDissolved = false;
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem, private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
dissolveMenuItem; dissolveMenuItem;
@@ -139,14 +137,6 @@ public class GroupActivity extends
inviteMenuItem = menu.findItem(R.id.action_group_invite); inviteMenuItem = menu.findItem(R.id.action_group_invite);
leaveMenuItem = menu.findItem(R.id.action_group_leave); leaveMenuItem = menu.findItem(R.id.action_group_leave);
dissolveMenuItem = menu.findItem(R.id.action_group_dissolve); 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(); showMenuItems();
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
@@ -161,27 +151,19 @@ public class GroupActivity extends
startActivity(i1); startActivity(i1);
return true; return true;
case R.id.action_group_reveal: case R.id.action_group_reveal:
if (isCreator == null || isCreator)
throw new IllegalStateException();
Intent i2 = new Intent(this, RevealContactsActivity.class); Intent i2 = new Intent(this, RevealContactsActivity.class);
i2.putExtra(GROUP_ID, groupId.getBytes()); i2.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i2); startActivity(i2);
return true; return true;
case R.id.action_group_invite: case R.id.action_group_invite:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
Intent i3 = new Intent(this, GroupInviteActivity.class); Intent i3 = new Intent(this, GroupInviteActivity.class);
i3.putExtra(GROUP_ID, groupId.getBytes()); i3.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i3, REQUEST_GROUP_INVITE); startActivityForResult(i3, REQUEST_GROUP_INVITE);
return true; return true;
case R.id.action_group_leave: case R.id.action_group_leave:
if (isCreator == null || isCreator)
throw new IllegalStateException();
showLeaveGroupDialog(); showLeaveGroupDialog();
return true; return true;
case R.id.action_group_dissolve: case R.id.action_group_dissolve:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
showDissolveGroupDialog(); showDissolveGroupDialog();
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@@ -227,12 +209,18 @@ public class GroupActivity extends
} }
private void showMenuItems() { private void showMenuItems() {
// we need to have the menu items and know if we are the creator if (leaveMenuItem == null || dissolveMenuItem == null) return;
if (leaveMenuItem == null || isCreator == null) return; if (isCreator) {
revealMenuItem.setVisible(!isCreator); revealMenuItem.setVisible(false);
inviteMenuItem.setVisible(isCreator); inviteMenuItem.setVisible(true);
leaveMenuItem.setVisible(!isCreator); leaveMenuItem.setVisible(false);
dissolveMenuItem.setVisible(isCreator); dissolveMenuItem.setVisible(true);
} else {
revealMenuItem.setVisible(true);
inviteMenuItem.setVisible(false);
leaveMenuItem.setVisible(true);
dissolveMenuItem.setVisible(false);
}
} }
private void showLeaveGroupDialog() { 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.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; 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.enterPressed;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH; import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@@ -92,6 +91,12 @@ public class CreateGroupFragment extends BaseFragment {
return v; return v;
} }
@Override
public void onStart() {
super.onStart();
listener.showSoftKeyboard(nameEntry);
}
@Override @Override
public String getUniqueTag() { public String getUniqueTag() {
return TAG; return TAG;
@@ -115,7 +120,7 @@ public class CreateGroupFragment extends BaseFragment {
private void createGroup() { private void createGroup() {
if (!validateName()) return; if (!validateName()) return;
hideSoftKeyboard(nameEntry); listener.hideSoftKeyboard(nameEntry);
createGroupButton.setVisibility(GONE); createGroupButton.setVisibility(GONE);
progress.setVisibility(VISIBLE); progress.setVisibility(VISIBLE);
listener.onGroupNameChosen(nameEntry.getText().toString()); listener.onGroupNameChosen(nameEntry.getText().toString());

View File

@@ -1,8 +1,15 @@
package org.briarproject.briar.android.privategroup.creation; package org.briarproject.briar.android.privategroup.creation;
import android.view.View;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
interface CreateGroupListener extends BaseFragmentListener { interface CreateGroupListener extends BaseFragmentListener {
void onGroupNameChosen(String name); 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()) { if (revision == adapter.getRevision()) {
adapter.incrementRevision(); adapter.incrementRevision();
if (groups.isEmpty()) list.showData(); if (groups.isEmpty()) list.showData();
else adapter.replaceAll(groups); else adapter.addAll(groups);
} else { } else {
LOG.info("Concurrent update, reloading"); LOG.info("Concurrent update, reloading");
loadGroups(); loadGroups();

View File

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

View File

@@ -218,17 +218,11 @@ public class SettingsFragment extends PreferenceFragmentCompat
}); });
if (SDK_INT < 27) { if (SDK_INT < 27) {
// remove System Default Theme option from preference entries // remove System Default Theme option
// as it is not functional on this API anyway
List<CharSequence> entries = List<CharSequence> entries =
new ArrayList<>(Arrays.asList(theme.getEntries())); new ArrayList<>(Arrays.asList(theme.getEntries()));
entries.remove(getString(R.string.pref_theme_system)); entries.remove(getString(R.string.pref_theme_system));
theme.setEntries(entries.toArray(new CharSequence[0])); 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) { if (IS_DEBUG_BUILD) {
findPreference("pref_key_explode").setOnPreferenceClickListener( findPreference("pref_key_explode").setOnPreferenceClickListener(
@@ -495,13 +489,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS) Intent intent = new Intent(ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, packageName) .putExtra(EXTRA_APP_PACKAGE, packageName)
.putExtra(EXTRA_CHANNEL_ID, channelId); .putExtra(EXTRA_CHANNEL_ID, channelId);
Context ctx = requireContext(); startActivity(intent);
if (intent.resolveActivity(ctx.getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(ctx, R.string.error_start_activity, LENGTH_SHORT)
.show();
}
return true; return true;
}); });
} }
@@ -523,12 +511,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
else uri = Uri.parse(ringtoneUri); else uri = Uri.parse(ringtoneUri);
i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri); i.putExtra(EXTRA_RINGTONE_EXISTING_URI, uri);
} }
if (i.resolveActivity(requireActivity().getPackageManager()) != null) { startActivityForResult(i, REQUEST_RINGTONE);
startActivityForResult(i, REQUEST_RINGTONE);
} else {
Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show();
}
return true; return true;
} }
@@ -663,7 +646,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
} else { } else {
// The user chose a ringtone other than the default // The user chose a ringtone other than the default
Ringtone r = RingtoneManager.getRingtone(getContext(), uri); Ringtone r = RingtoneManager.getRingtone(getContext(), uri);
if (r == null || "file".equals(uri.getScheme())) { if (r == null) {
Toast.makeText(getContext(), R.string.cannot_load_ringtone, Toast.makeText(getContext(), R.string.cannot_load_ringtone,
LENGTH_SHORT).show(); LENGTH_SHORT).show();
} else { } else {

View File

@@ -64,6 +64,12 @@ public abstract class BaseMessageFragment extends BaseFragment
@StringRes @StringRes
protected abstract int getHintText(); protected abstract int getHintText();
@Override
public void onStart() {
super.onStart();
message.showSoftKeyboard();
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { 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.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; 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.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.TextSendController.SendListener;
@@ -283,10 +284,14 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
scrollToItemAtTop(item); scrollToItemAtTop(item);
} else { } else {
// wait with scrolling until keyboard opened // wait with scrolling until keyboard opened
textInput.setOnKeyboardShownListener(() -> { textInput.addOnKeyboardShownListener(
scrollToItemAtTop(item); new KeyboardAwareLinearLayout.OnKeyboardShownListener() {
textInput.setOnKeyboardShownListener(null); @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() { private void updateTextInput() {
if (replyId != null) { if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint); textInput.setHint(R.string.forum_message_reply_hint);
textInput.requestFocus();
textInput.showSoftKeyboard(); textInput.showSoftKeyboard();
} else { } else {
textInput.setHint(R.string.forum_new_message_hint); 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); this.items.addAll(items);
} }
public void replaceAll(Collection<T> items) {
this.items.replaceAll(items);
}
public void setItems(Collection<T> items) { public void setItems(Collection<T> items) {
this.items.beginBatchedUpdates(); this.items.beginBatchedUpdates();
this.items.clear(); this.items.clear();

View File

@@ -4,18 +4,13 @@ import android.support.annotation.ColorRes;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.design.widget.Snackbar.Callback;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; 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.support.v4.content.ContextCompat.getColor;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@NotNullByDefault @NotNullByDefault
public class BriarSnackbarBuilder { public class BriarSnackbarBuilder {
@@ -35,24 +30,6 @@ public class BriarSnackbarBuilder {
R.color.briar_button_text_positive)); R.color.briar_button_text_positive));
s.setAction(actionResId, onClickListener); 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; return s;
} }

View File

@@ -92,11 +92,9 @@ public class UiUtils {
public static final float GREY_OUT = 0.5f; public static final float GREY_OUT = 0.5f;
public static void showSoftKeyboard(View view) { public static void showSoftKeyboard(View view) {
if (view.requestFocus()) { InputMethodManager imm = requireNonNull(
InputMethodManager imm = requireNonNull(getSystemService( getSystemService(view.getContext(), InputMethodManager.class));
view.getContext(), InputMethodManager.class)); imm.showSoftInput(view, SHOW_IMPLICIT);
imm.showSoftInput(view, SHOW_IMPLICIT);
}
} }
public static void hideSoftKeyboard(View view) { 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 org.briarproject.briar.R;
import static android.content.Context.LAYOUT_INFLATER_SERVICE; import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
public class CompositeSendButton extends FrameLayout { public class CompositeSendButton extends FrameLayout {
@@ -74,24 +75,33 @@ public class CompositeSendButton extends FrameLayout {
if (showImageButton) { if (showImageButton) {
imageButton.setVisibility(VISIBLE); imageButton.setVisibility(VISIBLE);
sendButton.setEnabled(false); sendButton.setEnabled(false);
sendButton.clearAnimation(); if (SDK_INT <= 15) {
sendButton.animate().alpha(0f).withEndAction(() -> {
sendButton.setVisibility(INVISIBLE); sendButton.setVisibility(INVISIBLE);
imageButton.setEnabled(true); imageButton.setEnabled(true);
}).start(); } else {
imageButton.clearAnimation(); sendButton.clearAnimation();
imageButton.animate().alpha(1f).start(); sendButton.animate().alpha(0f).withEndAction(() -> {
sendButton.setVisibility(INVISIBLE);
imageButton.setEnabled(true);
}).start();
imageButton.clearAnimation();
imageButton.animate().alpha(1f).start();
}
} else { } else {
sendButton.setVisibility(VISIBLE); sendButton.setVisibility(VISIBLE);
// enable/disable buttons right away to allow fast sending // enable/disable buttons right away to allow fast sending
sendButton.setEnabled(sendEnabled); sendButton.setEnabled(sendEnabled);
imageButton.setEnabled(false); imageButton.setEnabled(false);
sendButton.clearAnimation(); if (SDK_INT <= 15) {
sendButton.animate().alpha(1f).start(); imageButton.setVisibility(INVISIBLE);
imageButton.clearAnimation(); } else {
imageButton.animate().alpha(0f).withEndAction(() -> sendButton.clearAnimation();
imageButton.setVisibility(INVISIBLE) sendButton.animate().alpha(1f).start();
).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.LayoutInflater;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout;
import com.vanniktech.emoji.EmojiEditText;
import com.vanniktech.emoji.EmojiPopup; import com.vanniktech.emoji.EmojiPopup;
import com.vanniktech.emoji.RecentEmoji; 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.KeyEvent.KEYCODE_ENTER;
import static android.view.inputmethod.EditorInfo.IME_ACTION_SEND; import static android.view.inputmethod.EditorInfo.IME_ACTION_SEND;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT; import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.lang.Character.isWhitespace;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong; 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 { TextWatcher {
@Inject @Inject
@@ -42,16 +40,12 @@ public class EmojiTextInputView extends LinearLayout implements
private final AppCompatImageButton emojiToggle; private final AppCompatImageButton emojiToggle;
private final EmojiPopup emojiPopup; private final EmojiPopup emojiPopup;
private final EditText editText; private final EditText editText;
private final InputMethodManager imm;
@Nullable @Nullable
private TextInputListener listener; private TextInputListener listener;
@Nullable
private OnKeyboardShownListener keyboardShownListener;
private int maxLength = Integer.MAX_VALUE; private int maxLength = Integer.MAX_VALUE;
private boolean emptyTextAllowed = false; private boolean emptyTextAllowed = false;
private boolean isEmpty = true; private boolean isEmpty = true;
private boolean keyboardOpen = false;
public EmojiTextInputView(Context context) { public EmojiTextInputView(Context context) {
this(context, null); this(context, null);
@@ -85,6 +79,7 @@ public class EmojiTextInputView extends LinearLayout implements
editText = findViewById(R.id.input_text); editText = findViewById(R.id.input_text);
editText.setPadding(0, 0, paddingEnd, paddingBottom); editText.setPadding(0, 0, paddingEnd, paddingBottom);
if (maxLines > 0) editText.setMaxLines(maxLines); if (maxLines > 0) editText.setMaxLines(maxLines);
editText.setOnClickListener(v -> showSoftKeyboard());
editText.addTextChangedListener(this); editText.addTextChangedListener(this);
editText.setOnEditorActionListener((v, actionId, event) -> { editText.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == IME_ACTION_SEND) { 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 // stuff we can't do in edit mode goes below
if (isInEditMode()) { if (isInEditMode()) {
emojiPopup = null; emojiPopup = null;
imm = null;
return; return;
} }
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
imm = (InputMethodManager) requireNonNull(o);
BriarApplication app = BriarApplication app =
(BriarApplication) context.getApplicationContext(); (BriarApplication) context.getApplicationContext();
app.getApplicationComponent().inject(this); app.getApplicationComponent().inject(this);
emojiPopup = EmojiPopup.Builder emojiPopup = EmojiPopup.Builder
.fromRootView(getRootView()) .fromRootView(this)
.setRecentEmoji(recentEmoji) .setRecentEmoji(recentEmoji)
.setOnEmojiPopupShownListener(this::showKeyboardIcon) .setOnEmojiPopupShownListener(this::showKeyboardIcon)
.setOnEmojiPopupDismissListener(this::showEmojiIcon) .setOnEmojiPopupDismissListener(this::showEmojiIcon)
.setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) .build((EmojiEditText) editText);
.setOnSoftKeyboardOpenListener(this::onKeyboardOpened)
.setOnSoftKeyboardCloseListener(this::onKeyboardClosed)
.setIconColor(resolveColorAttribute(getContext(),
R.attr.colorControlNormal))
.build(editText);
emojiToggle.setOnClickListener(v -> emojiPopup.toggle()); emojiToggle.setOnClickListener(v -> emojiPopup.toggle());
editText.setOnClickListener(v -> {
if (emojiPopup.isShowing()) emojiPopup.dismiss();
});
} }
@Override @Override
@@ -142,31 +125,19 @@ public class EmojiTextInputView extends LinearLayout implements
@Override @Override
public void onTextChanged(CharSequence s, int start, int before, public void onTextChanged(CharSequence s, int start, int before,
int count) { int count) {
if (emptyTextAllowed || listener == null) return; // Need to start at position 0 to change empty
// Work out whether the trimmed text has become empty or non-empty if (start != 0 || emptyTextAllowed || listener == null) return;
if (isEmpty) { if (s.length() == 0) {
// We only need to check the characters that were added if (!isEmpty) {
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) {
isEmpty = true; isEmpty = true;
listener.onTextIsEmptyChanged(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 @Override
public void afterTextChanged(Editable s) { public void afterTextChanged(Editable s) {
} }
@@ -247,10 +218,6 @@ public class EmojiTextInputView extends LinearLayout implements
editText.setHint(hint); editText.setHint(hint);
} }
boolean isKeyboardOpen() {
return keyboardOpen || imm.isFullscreenMode();
}
private void showEmojiIcon() { private void showEmojiIcon() {
emojiToggle.setImageResource(R.drawable.ic_emoji_toggle); emojiToggle.setImageResource(R.drawable.ic_emoji_toggle);
} }
@@ -260,43 +227,22 @@ public class EmojiTextInputView extends LinearLayout implements
} }
void showSoftKeyboard() { 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() { void hideSoftKeyboard() {
if (emojiPopup.isShowing()) emojiPopup.dismiss(); if (emojiPopup.isShowing()) emojiPopup.dismiss();
IBinder token = editText.getWindowToken(); IBinder token = editText.getWindowToken();
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.hideSoftInputFromWindow(token, 0); 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 { interface TextInputListener {
void onTextIsEmptyChanged(boolean isEmpty); void onTextIsEmptyChanged(boolean isEmpty);
void onSendEvent(); 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

@@ -22,6 +22,8 @@ import org.briarproject.briar.android.attachment.AttachmentItemResult;
import org.briarproject.briar.android.attachment.AttachmentManager; import org.briarproject.briar.android.attachment.AttachmentManager;
import org.briarproject.briar.android.attachment.AttachmentResult; import org.briarproject.briar.android.attachment.AttachmentResult;
import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener;
import org.briarproject.briar.api.messaging.FileTooBigException;
import org.jsoup.UnsupportedMimeTypeException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -41,9 +43,11 @@ import static android.support.v4.content.ContextCompat.getColor;
import static android.support.v4.view.AbsSavedState.EMPTY_STATE; import static android.support.v4.view.AbsSavedState.EMPTY_STATE;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute; import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES; import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED;
@@ -186,18 +190,16 @@ public class TextAttachmentController extends TextSendController
result.observe(attachmentListener, new Observer<AttachmentResult>() { result.observe(attachmentListener, new Observer<AttachmentResult>() {
@Override @Override
public void onChanged(@Nullable AttachmentResult attachmentResult) { public void onChanged(@Nullable AttachmentResult attachmentResult) {
if (attachmentResult == null) { requireNonNull(attachmentResult);
// The fresh LiveData was deliberately set to null. boolean finished = attachmentResult.isFinished();
// This means that we can stop observing it. boolean success = attachmentResult.isSuccess();
if (finished) {
result.removeObserver(this); result.removeObserver(this);
} else { if (!success) return;
boolean noError = onNewAttachmentItemResults(
attachmentResult.getItemResults());
if (noError && attachmentResult.isFinished()) {
onAllAttachmentsCreated();
result.removeObserver(this);
}
} }
boolean noError = onNewAttachmentItemResults(
attachmentResult.getItemResults());
if (noError && success) onAllAttachmentsCreated();
} }
}); });
} }
@@ -207,7 +209,7 @@ public class TextAttachmentController extends TextSendController
if (!loadingUris) throw new AssertionError(); if (!loadingUris) throw new AssertionError();
for (AttachmentItemResult result : itemResults) { for (AttachmentItemResult result : itemResults) {
if (result.hasError()) { if (result.hasError()) {
onError(result.getErrorMsg()); onError(requireNonNull(result.getException()));
return false; return false;
} else { } else {
imagePreview.loadPreviewImage(result); imagePreview.loadPreviewImage(result);
@@ -253,12 +255,20 @@ public class TextAttachmentController extends TextSendController
} }
@UiThread @UiThread
private void onError(@Nullable String errorMsg) { private void onError(Exception e) {
if (errorMsg == null) { String errorMsg;
errorMsg = imagePreview.getContext() Context ctx = imagePreview.getContext();
.getString(R.string.image_attach_error); if (e instanceof UnsupportedMimeTypeException) {
String mimeType = ((UnsupportedMimeTypeException) e).getMimeType();
errorMsg = ctx.getString(
R.string.image_attach_error_invalid_mime_type, mimeType);
} else if (e instanceof FileTooBigException) {
int mb = MAX_IMAGE_SIZE / 1024 / 1024;
errorMsg = ctx.getString(R.string.image_attach_error_too_big, mb);
} else {
errorMsg = ctx.getString(R.string.image_attach_error);
} }
Toast.makeText(textInput.getContext(), errorMsg, LENGTH_LONG).show(); Toast.makeText(ctx, errorMsg, LENGTH_LONG).show();
onCancel(); onCancel();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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