Compare commits

..

5 Commits

Author SHA1 Message Date
ialokim
4a0f8b2669 apply patch 2021-09-09 13:24:51 +02:00
ialokim
c9642be4ca add instructions for patch 2021-09-09 13:24:03 +02:00
ialokim
42c148ea89 Merge tag 'release-1.3.6' into briar-as-subproject
release-1.3.6
2021-09-09 13:16:27 +02:00
ialokim
4de8dfa867 script applying patch to use briar as submodule 2021-09-09 13:11:50 +02:00
Nico Alt
3a40401970 Make Briar usable as subproject 2021-06-15 08:37:55 +02:00
123 changed files with 632 additions and 3884 deletions

View File

@@ -33,7 +33,7 @@ test:
stage: test
script:
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources check
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: always

View File

View File

@@ -1,35 +0,0 @@
# Summary
(Briefly describe the bug encontered)
# Steps to reproduce
(Please list the steps you performed when you encountered the bug)
-
-
-
# How often does it happen?
(Every time, Sometimes, It happened only once)
# What did you expect to happen
(Describe the app behaviour that you expected to happen)
# What actually happened?
(Describe what actually happened)
* What version of Briar app do you have installed? [Please fill in]
* Did you send the feedback from your device? [Yes/No]
* Your device manufacturer: [Please fill in]
* Your device model: [Please fill in]
* Android version: [Please fill in]

View File

@@ -1,15 +0,0 @@
# Are you requesting a new feature or an improvement to an existing feature?
# Brief description of the new or improved feature you would like to see in the Briar app
# Please describe in detail what you would like to do in Briar app, for which a new or improved feature is required?
* Does the absence of this feature prevent you from using the Briar app for certain activities? What are those activities?
* Would your use of Briar app increase with this new feature?

View File

@@ -1,28 +1 @@
# Briar
Briar is a messaging app designed for activists, journalists, and anyone else who needs a safe, easy and robust way to communicate.
Unlike traditional messaging tools such as email, Twitter or Telegram, Briar doesn't rely on a central server - messages are synchronized directly between the users' devices.
If the Internet's down, Briar can sync via Bluetooth or Wi-Fi, keeping information flowing in a crisis. If the Internet's up, Briar can sync via the Tor network, protecting users and their relationships from surveillance.
## Download Briar
[<img src="https://briarproject.org//img/fdroid_badge.png" width="240">](https://briarproject.org/fdroid)
[<img src="https://briarproject.org/img/google_play_badge_web_generic.png" width="240">](https://play.google.com/store/apps/details?id=org.briarproject.briar.android)
You can also [download the APK file](https://briarproject.org/apk) directly from
our site.
## Useful links
[briarproject.org](https://briarproject.org/)
[Source code](https://code.briarproject.org/briar/briar/tree/master)
[Manual](https://briarproject.org/manual/)
[Wiki](https://code.briarproject.org/briar/briar/-/wikis/home)
## Donate
[![Donate using Liberapay](https://briarproject.org/img/liberapay.svg)](https://liberapay.com/Briar/donate) [![Flattr this](https://briarproject.org/img/flattr-badge-large.png "Flattr this")](https://flattr.com/t/592836/)
Bitcoin and BCH: 1NZCKkUCtJV2U2Y9hDb9uq8S7ksFCFGR6K
Briar is a messaging app designed for activists, journalists, and anyone else who needs a safe, easy and robust way to communicate. Unlike traditional messaging tools such as email, Twitter or Telegram, Briar doesn't rely on a central server - messages are synchronized directly between the users' devices. If the Internet's down, Briar can sync via Bluetooth or Wi-Fi, keeping the information flowing in a crisis. If the Internet's up, Briar can sync via the Tor network, protecting users and their relationships from surveillance.

View File

@@ -41,7 +41,7 @@ configurations {
}
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
implementation project(path: ':briar:bramble-core', configuration: 'default')
tor 'org.briarproject:tor-android:0.3.5.15'
tor 'org.briarproject:obfs4proxy-android:0.0.12-dev-40245c4a@zip'
@@ -49,7 +49,7 @@ dependencies {
compileOnly 'javax.annotation:jsr250-api:1.0'
testImplementation project(path: ':bramble-api', configuration: 'testOutput')
testImplementation project(path: ':briar:bramble-api', configuration: 'testOutput')
testImplementation "junit:junit:$junit_version"
testImplementation "org.jmock:jmock:$jmock_version"
testImplementation "org.jmock:jmock-junit4:$jmock_version"

View File

@@ -29,7 +29,6 @@ import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.net.SocketFactory;
@@ -49,7 +48,6 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.NetworkUtils.getNetworkInterfaces;
@NotNullByDefault
class AndroidLanTcpPlugin extends LanTcpPlugin {
@@ -57,13 +55,6 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
private static final Logger LOG =
getLogger(AndroidLanTcpPlugin.class.getName());
/**
* The interface name is used as a heuristic for deciding whether the
* device is providing a wifi access point.
*/
private static final Pattern AP_INTERFACE_NAME =
Pattern.compile("^(wlan|ap|p2p)[-0-9]");
private final Executor connectionStatusExecutor;
private final ConnectivityManager connectivityManager;
@Nullable
@@ -139,14 +130,17 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
if (info != null && info.getIpAddress() != 0) {
return new Pair<>(intToInetAddress(info.getIpAddress()), false);
}
// If we're providing an access point, return its address
for (NetworkInterface iface : getNetworkInterfaces()) {
if (AP_INTERFACE_NAME.matcher(iface.getName()).find()) {
for (InterfaceAddress ifAddr : iface.getInterfaceAddresses()) {
if (isPossibleWifiApInterface(ifAddr)) {
return new Pair<>(ifAddr.getAddress(), true);
}
}
List<InterfaceAddress> ifAddrs = getLocalInterfaceAddresses();
// If we're providing a normal access point, return its address
for (InterfaceAddress ifAddr : ifAddrs) {
if (isAndroidWifiApAddress(ifAddr)) {
return new Pair<>(ifAddr.getAddress(), true);
}
}
// If we're providing a wifi direct access point, return its address
for (InterfaceAddress ifAddr : ifAddrs) {
if (isAndroidWifiDirectApAddress(ifAddr)) {
return new Pair<>(ifAddr.getAddress(), true);
}
}
// Not connected to wifi
@@ -154,18 +148,33 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
}
/**
* Returns true if the given address may belong to an interface providing
* a wifi access point (including wifi direct legacy mode access points).
* Returns true if the given address belongs to a network provided by an
* Android access point (including the access point's own address).
* <p>
* This method may return true for wifi client interfaces as well, but
* we've already checked for a wifi client connection above.
* The access point's address is usually 192.168.43.1, but at least one
* device (Honor 8A) may use other addresses in the range 192.168.43.0/24.
*/
private boolean isPossibleWifiApInterface(InterfaceAddress ifAddr) {
private boolean isAndroidWifiApAddress(InterfaceAddress ifAddr) {
if (ifAddr.getNetworkPrefixLength() != 24) return false;
byte[] ip = ifAddr.getAddress().getAddress();
return ip.length == 4
&& ip[0] == (byte) 192
&& ip[1] == (byte) 168;
&& ip[1] == (byte) 168
&& ip[2] == (byte) 43;
}
/**
* Returns true if the given address belongs to a network provided by an
* Android wifi direct legacy mode access point (including the access
* point's own address).
*/
private boolean isAndroidWifiDirectApAddress(InterfaceAddress ifAddr) {
if (ifAddr.getNetworkPrefixLength() != 24) return false;
byte[] ip = ifAddr.getAddress().getAddress();
return ip.length == 4
&& ip[0] == (byte) 192
&& ip[1] == (byte) 168
&& ip[2] == (byte) 49;
}
/**

View File

@@ -14,6 +14,4 @@ public interface FeatureFlags {
boolean shouldEnableConnectViaBluetooth();
boolean shouldEnableTransferData();
boolean shouldEnableShareAppViaOfflineHotspot();
}

View File

@@ -1,34 +0,0 @@
package org.briarproject.bramble.util;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Logger;
import static java.util.Collections.emptyList;
import static java.util.Collections.list;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class NetworkUtils {
private static final Logger LOG = getLogger(NetworkUtils.class.getName());
public static List<NetworkInterface> getNetworkInterfaces() {
try {
Enumeration<NetworkInterface> ifaces =
NetworkInterface.getNetworkInterfaces();
// Despite what the docs say, the return value can be null
//noinspection ConstantConditions
return ifaces == null ? emptyList() : list(ifaces);
} catch (SocketException e) {
logException(LOG, WARNING, e);
return emptyList();
}
}
}

View File

@@ -10,8 +10,8 @@ dependencyVerification {
'net.jcip:jcip-annotations:1.0:jcip-annotations-1.0.jar:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0',
'org.apache-extras.beanshell:bsh:2.0b6:bsh-2.0b6.jar:a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047',
'org.codehaus.mojo.signature:java16:1.1:java16-1.1.signature:53799223a2c98dba2d0add810bed76315460df285c69e4f397ae6098f87dd619',
'org.codehaus.mojo:animal-sniffer-ant-tasks:1.20:animal-sniffer-ant-tasks-1.20.jar:bb7d2498144118311d968bb08ff6fae3fc535fb1cb9cca8b8e9ea65b189422ac',
'org.codehaus.mojo:animal-sniffer:1.20:animal-sniffer-1.20.jar:80c422523c38db91260c6d78e5ee4b012862ab61cc55020c9e243dd7b5c62249',
'org.codehaus.mojo:animal-sniffer-ant-tasks:1.16:animal-sniffer-ant-tasks-1.16.jar:890040976fbe2d584619a6a61b1fd2e925b3b5eb342a85eb2762c467c0d64e90',
'org.codehaus.mojo:animal-sniffer:1.16:animal-sniffer-1.16.jar:72be8bcc226ba43b937c722a08a07852bfa1b11400089265d5df0ee7b38b1d52',
'org.hamcrest:hamcrest-core:2.1:hamcrest-core-2.1.jar:e09109e54a289d88506b9bfec987ddd199f4217c9464132668351b9a4f00bee9',
'org.hamcrest:hamcrest-library:2.1:hamcrest-library-2.1.jar:b7e2b6895b3b679f0e47b6380fda391b225e9b78505db9d8bdde8d3cc8d52a21',
'org.hamcrest:hamcrest:2.1:hamcrest-2.1.jar:ba93b2e3a562322ba432f0a1b53addcc55cb188253319a020ed77f824e692050',
@@ -21,7 +21,7 @@ dependencyVerification {
'org.jmock:jmock-testjar:2.12.0:jmock-testjar-2.12.0.jar:efefbcf6cd294d0e29f0c46eb2a3380d4ca4e1763ff719c69e2f2ac62f564a04',
'org.jmock:jmock:2.12.0:jmock-2.12.0.jar:266d07314c0cd343c46ff8a55601272de8cf406807caf55e6f313295f83d10be',
'org.objenesis:objenesis:3.0.1:objenesis-3.0.1.jar:7a8ff780b9ff48415d7c705f60030b0acaa616e7f823c98eede3b63508d4e984',
'org.ow2.asm:asm-all:5.2:asm-all-5.2.jar:7fbffbc1db3422e2101689fd88df8384b15817b52b9b2b267b9f6d2511dc198d',
'org.ow2.asm:asm:7.1:asm-7.1.jar:4ab2fa2b6d2cc9ccb1eaa05ea329c407b47b13ed2915f62f8c4b8cc96258d4de',
'org.ow2.asm:asm:9.1:asm-9.1.jar:cda4de455fab48ff0bcb7c48b4639447d4de859a7afc30a094a986f0936beba2',
]
}

View File

@@ -9,8 +9,8 @@ apply from: 'witness.gradle'
apply from: '../dagger.gradle'
dependencies {
implementation project(path: ':bramble-api', configuration: 'default')
implementation 'org.bouncycastle:bcprov-jdk15on:1.69'
implementation project(path: ':briar:bramble-api', configuration: 'default')
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6
implementation 'org.bitlet:weupnp:0.1.4'
implementation 'net.i2p.crypto:eddsa:0.2.0'
@@ -19,7 +19,7 @@ dependencies {
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
testImplementation project(path: ':bramble-api', configuration: 'testOutput')
testImplementation project(path: ':briar:bramble-api', configuration: 'testOutput')
testImplementation 'org.hsqldb:hsqldb:2.3.5' // The last version that supports Java 1.6
testImplementation 'net.jodah:concurrentunit:0.4.2'
testImplementation "junit:junit:$junit_version"
@@ -32,14 +32,6 @@ dependencies {
signature 'org.codehaus.mojo.signature:java16:1.1@signature'
}
animalsniffer {
// Allow requireNonNull: Android desugaring rewrites it (so it's safe for us to use),
// and it gets used when passing method references instead of lambdas with Java 11.
// Note that this line allows *all* methods from java.util.Objects.
// That's the best that we can do with the configuration options that Animal Sniffer offers.
ignore 'java.util.Objects'
}
// needed to make test output available to bramble-java
configurations {
testOutput.extendsFrom(testCompile)

View File

@@ -4,9 +4,6 @@ import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.KeyPairGenerator;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
import org.briarproject.bramble.api.crypto.AgreementPublicKey;
import org.briarproject.bramble.api.crypto.CryptoComponent;
@@ -23,6 +20,9 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.SecureRandomProvider;
import org.briarproject.bramble.util.ByteUtils;
import org.briarproject.bramble.util.StringUtils;
import org.spongycastle.crypto.CryptoException;
import org.spongycastle.crypto.Digest;
import org.spongycastle.crypto.digests.Blake2bDigest;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;

View File

@@ -1,38 +1,38 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.BasicAgreement;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.DerivationFunction;
import org.bouncycastle.crypto.KeyEncoder;
import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.engines.AESLightEngine;
import org.bouncycastle.crypto.engines.IESEngine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.generators.EphemeralKeyPairGenerator;
import org.bouncycastle.crypto.generators.KDF2BytesGenerator;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.IESWithCipherParameters;
import org.bouncycastle.crypto.parsers.ECIESPublicKeyParser;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.util.StringUtils;
import org.spongycastle.asn1.teletrust.TeleTrusTNamedCurves;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
import org.spongycastle.crypto.BasicAgreement;
import org.spongycastle.crypto.BlockCipher;
import org.spongycastle.crypto.CipherParameters;
import org.spongycastle.crypto.CryptoException;
import org.spongycastle.crypto.DerivationFunction;
import org.spongycastle.crypto.KeyEncoder;
import org.spongycastle.crypto.Mac;
import org.spongycastle.crypto.agreement.ECDHCBasicAgreement;
import org.spongycastle.crypto.digests.SHA256Digest;
import org.spongycastle.crypto.engines.AESLightEngine;
import org.spongycastle.crypto.engines.IESEngine;
import org.spongycastle.crypto.generators.ECKeyPairGenerator;
import org.spongycastle.crypto.generators.EphemeralKeyPairGenerator;
import org.spongycastle.crypto.generators.KDF2BytesGenerator;
import org.spongycastle.crypto.macs.HMac;
import org.spongycastle.crypto.modes.CBCBlockCipher;
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.spongycastle.crypto.params.AsymmetricKeyParameter;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECKeyGenerationParameters;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import org.spongycastle.crypto.params.IESWithCipherParameters;
import org.spongycastle.crypto.parsers.ECIESPublicKeyParser;
import java.io.FileInputStream;
import java.io.FileOutputStream;

View File

@@ -1,9 +1,9 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.generators.SCrypt;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.StringUtils;
import org.spongycastle.crypto.generators.SCrypt;
import java.util.logging.Logger;

View File

@@ -1,14 +1,14 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.briarproject.bramble.api.crypto.KeyParser;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECPoint;
import java.math.BigInteger;
import java.security.GeneralSecurityException;

View File

@@ -1,8 +1,8 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import javax.annotation.concurrent.Immutable;

View File

@@ -1,8 +1,8 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import javax.annotation.concurrent.Immutable;

View File

@@ -1,7 +1,5 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.PublicKey;
@@ -11,6 +9,8 @@ import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.transport.IncomingKeys;
import org.briarproject.bramble.api.transport.OutgoingKeys;
import org.briarproject.bramble.api.transport.TransportKeys;
import org.spongycastle.crypto.Digest;
import org.spongycastle.crypto.digests.Blake2bDigest;
import java.security.GeneralSecurityException;

View File

@@ -1,13 +1,13 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.engines.XSalsa20Engine;
import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
import org.bouncycastle.crypto.macs.Poly1305;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.spongycastle.crypto.DataLengthException;
import org.spongycastle.crypto.engines.XSalsa20Engine;
import org.spongycastle.crypto.generators.Poly1305KeyGenerator;
import org.spongycastle.crypto.macs.Poly1305;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
import java.security.GeneralSecurityException;

View File

@@ -28,9 +28,11 @@ import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -51,7 +53,7 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.NetworkUtils.getNetworkInterfaces;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@@ -96,6 +98,7 @@ abstract class TcpPlugin implements DuplexPlugin, EventListener {
/**
* Returns true if connections to the given address can be attempted.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
protected abstract boolean isConnectable(InterfaceAddress local,
InetSocketAddress remote);
@@ -395,6 +398,17 @@ abstract class TcpPlugin implements DuplexPlugin, EventListener {
return addrs;
}
private List<NetworkInterface> getNetworkInterfaces() {
try {
Enumeration<NetworkInterface> ifaces =
NetworkInterface.getNetworkInterfaces();
return ifaces == null ? emptyList() : list(ifaces);
} catch (SocketException e) {
logException(LOG, WARNING, e);
return emptyList();
}
}
@Override
public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) {

View File

@@ -4,10 +4,10 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA3Digest;
import org.bouncycastle.util.encoders.Base64;
import org.briarproject.bramble.util.Base32;
import org.spongycastle.crypto.Digest;
import org.spongycastle.crypto.digests.SHA3Digest;
import org.spongycastle.util.encoders.Base64;
import java.nio.charset.Charset;

View File

@@ -1,11 +1,11 @@
package org.briarproject.bramble.rendezvous;
import org.bouncycastle.crypto.engines.Salsa20Engine;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.spongycastle.crypto.engines.Salsa20Engine;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

View File

@@ -1,9 +1,9 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.briarproject.bramble.test.BrambleTestCase;
import org.briarproject.bramble.util.StringUtils;
import org.junit.Test;
import org.spongycastle.crypto.digests.Blake2bDigest;
import java.util.Random;

View File

@@ -0,0 +1,115 @@
package org.briarproject.bramble.crypto;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test;
import org.spongycastle.asn1.teletrust.TeleTrusTNamedCurves;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
import org.spongycastle.crypto.agreement.ECDHCBasicAgreement;
import org.spongycastle.crypto.generators.ECKeyPairGenerator;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECKeyGenerationParameters;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.math.ec.MontgomeryLadderMultiplier;
import java.math.BigInteger;
import java.security.SecureRandom;
import static org.junit.Assert.assertEquals;
public class EllipticCurveMultiplicationTest extends BrambleTestCase {
@Test
public void testMultiplierProducesSameResultsAsDefault() throws Exception {
// Instantiate the default implementation of the curve
X9ECParameters defaultX9Parameters =
TeleTrusTNamedCurves.getByName("brainpoolp256r1");
ECCurve defaultCurve = defaultX9Parameters.getCurve();
ECPoint defaultG = defaultX9Parameters.getG();
BigInteger defaultN = defaultX9Parameters.getN();
BigInteger defaultH = defaultX9Parameters.getH();
ECDomainParameters defaultParameters = new ECDomainParameters(
defaultCurve, defaultG, defaultN, defaultH);
// Instantiate an implementation using the Montgomery ladder multiplier
ECDomainParameters montgomeryParameters =
constantTime(defaultParameters);
// Generate two key pairs with each set of parameters, using the same
// deterministic PRNG for both sets of parameters
byte[] seed = new byte[32];
new SecureRandom().nextBytes(seed);
// Montgomery ladder multiplier
SecureRandom random = new PseudoSecureRandom(seed);
ECKeyGenerationParameters montgomeryGeneratorParams =
new ECKeyGenerationParameters(montgomeryParameters, random);
ECKeyPairGenerator montgomeryGenerator = new ECKeyPairGenerator();
montgomeryGenerator.init(montgomeryGeneratorParams);
AsymmetricCipherKeyPair montgomeryKeyPair1 =
montgomeryGenerator.generateKeyPair();
ECPrivateKeyParameters montgomeryPrivate1 =
(ECPrivateKeyParameters) montgomeryKeyPair1.getPrivate();
ECPublicKeyParameters montgomeryPublic1 =
(ECPublicKeyParameters) montgomeryKeyPair1.getPublic();
AsymmetricCipherKeyPair montgomeryKeyPair2 =
montgomeryGenerator.generateKeyPair();
ECPrivateKeyParameters montgomeryPrivate2 =
(ECPrivateKeyParameters) montgomeryKeyPair2.getPrivate();
ECPublicKeyParameters montgomeryPublic2 =
(ECPublicKeyParameters) montgomeryKeyPair2.getPublic();
// Default multiplier
random = new PseudoSecureRandom(seed);
ECKeyGenerationParameters defaultGeneratorParams =
new ECKeyGenerationParameters(defaultParameters, random);
ECKeyPairGenerator defaultGenerator = new ECKeyPairGenerator();
defaultGenerator.init(defaultGeneratorParams);
AsymmetricCipherKeyPair defaultKeyPair1 =
defaultGenerator.generateKeyPair();
ECPrivateKeyParameters defaultPrivate1 =
(ECPrivateKeyParameters) defaultKeyPair1.getPrivate();
ECPublicKeyParameters defaultPublic1 =
(ECPublicKeyParameters) defaultKeyPair1.getPublic();
AsymmetricCipherKeyPair defaultKeyPair2 =
defaultGenerator.generateKeyPair();
ECPrivateKeyParameters defaultPrivate2 =
(ECPrivateKeyParameters) defaultKeyPair2.getPrivate();
ECPublicKeyParameters defaultPublic2 =
(ECPublicKeyParameters) defaultKeyPair2.getPublic();
// The key pairs generated with both sets of parameters should be equal
assertEquals(montgomeryPrivate1.getD(), defaultPrivate1.getD());
assertEquals(montgomeryPublic1.getQ(), defaultPublic1.getQ());
assertEquals(montgomeryPrivate2.getD(), defaultPrivate2.getD());
assertEquals(montgomeryPublic2.getQ(), defaultPublic2.getQ());
// OK, all of the above was just sanity checks - now for the test!
ECDHCBasicAgreement agreement = new ECDHCBasicAgreement();
agreement.init(montgomeryPrivate1);
BigInteger sharedSecretMontgomeryMontgomery =
agreement.calculateAgreement(montgomeryPublic2);
agreement.init(montgomeryPrivate1);
BigInteger sharedSecretMontgomeryDefault =
agreement.calculateAgreement(defaultPublic2);
agreement.init(defaultPrivate1);
BigInteger sharedSecretDefaultMontgomery =
agreement.calculateAgreement(montgomeryPublic2);
agreement.init(defaultPrivate1);
BigInteger sharedSecretDefaultDefault =
agreement.calculateAgreement(defaultPublic2);
// Shared secrets calculated with different multipliers should be equal
assertEquals(sharedSecretMontgomeryMontgomery,
sharedSecretMontgomeryDefault);
assertEquals(sharedSecretMontgomeryMontgomery,
sharedSecretDefaultMontgomery);
assertEquals(sharedSecretMontgomeryMontgomery,
sharedSecretDefaultDefault);
}
private static ECDomainParameters constantTime(ECDomainParameters in) {
ECCurve curve = in.getCurve().configure().setMultiplier(
new MontgomeryLadderMultiplier()).create();
BigInteger x = in.getG().getAffineXCoord().toBigInteger();
BigInteger y = in.getG().getAffineYCoord().toBigInteger();
ECPoint g = curve.createPoint(x, y);
return new ECDomainParameters(curve, g, in.getN(), in.getH());
}
}

View File

@@ -3,26 +3,30 @@ package org.briarproject.bramble.crypto;
import net.i2p.crypto.eddsa.EdDSASecurityProvider;
import net.i2p.crypto.eddsa.KeyPairGenerator;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.BasicAgreement;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.DSADigestSigner;
import org.bouncycastle.crypto.signers.DSAKCalculator;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
import org.spongycastle.asn1.sec.SECNamedCurves;
import org.spongycastle.asn1.teletrust.TeleTrusTNamedCurves;
import org.spongycastle.asn1.x9.X9ECParameters;
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
import org.spongycastle.crypto.BasicAgreement;
import org.spongycastle.crypto.Digest;
import org.spongycastle.crypto.agreement.ECDHBasicAgreement;
import org.spongycastle.crypto.agreement.ECDHCBasicAgreement;
import org.spongycastle.crypto.digests.Blake2bDigest;
import org.spongycastle.crypto.generators.ECKeyPairGenerator;
import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECKeyGenerationParameters;
import org.spongycastle.crypto.params.ParametersWithRandom;
import org.spongycastle.crypto.signers.DSADigestSigner;
import org.spongycastle.crypto.signers.DSAKCalculator;
import org.spongycastle.crypto.signers.ECDSASigner;
import org.spongycastle.crypto.signers.HMacDSAKCalculator;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.math.ec.MontgomeryLadderMultiplier;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.Provider;
@@ -51,12 +55,14 @@ public class EllipticCurvePerformanceTest {
for (String name : SEC_NAMES) {
ECDomainParameters params =
convertParams(SECNamedCurves.getByName(name));
runTest(name, params);
runTest(name + " default", params);
runTest(name + " constant", constantTime(params));
}
for (String name : BRAINPOOL_NAMES) {
ECDomainParameters params =
convertParams(TeleTrusTNamedCurves.getByName(name));
runTest(name, params);
runTest(name + " default", params);
runTest(name + " constant", constantTime(params));
}
runCurve25519Test();
runEd25519Test();
@@ -187,4 +193,13 @@ public class EllipticCurvePerformanceTest {
return new ECDomainParameters(in.getCurve(), in.getG(), in.getN(),
in.getH());
}
private static ECDomainParameters constantTime(ECDomainParameters in) {
ECCurve curve = in.getCurve().configure().setMultiplier(
new MontgomeryLadderMultiplier()).create();
BigInteger x = in.getG().getAffineXCoord().toBigInteger();
BigInteger y = in.getG().getAffineYCoord().toBigInteger();
ECPoint g = curve.createPoint(x, y);
return new ECDomainParameters(curve, g, in.getN(), in.getH());
}
}

View File

@@ -1,11 +1,11 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.CryptoException;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test;
import org.spongycastle.crypto.CryptoException;
import java.security.SecureRandom;

View File

@@ -1,11 +1,11 @@
package org.briarproject.bramble.crypto;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.Blake2bDigest;
import org.bouncycastle.crypto.engines.Salsa20Engine;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.spongycastle.crypto.Digest;
import org.spongycastle.crypto.digests.Blake2bDigest;
import org.spongycastle.crypto.engines.Salsa20Engine;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
import javax.annotation.concurrent.NotThreadSafe;

View File

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

View File

@@ -15,6 +15,7 @@ dependencyVerification {
'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar:b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99',
'com.google.j2objc:j2objc-annotations:1.1:j2objc-annotations-1.1.jar:2994a7eb78f2710bd3d3bfb639b2c94e219cedac0d4d084d516e78c16dddecf6',
'com.h2database:h2:1.4.192:h2-1.4.192.jar:225b22e9857235c46c93861410b60b8c81c10dc8985f4faf188985ba5445126c',
'com.madgag.spongycastle:core:1.58.0.0:core-1.58.0.0.jar:199617dd5698c5a9312b898c0a4cec7ce9dd8649d07f65d91629f58229d72728',
'com.squareup:javapoet:1.13.0:javapoet-1.13.0.jar:4c7517e848a71b36d069d12bb3bf46a70fd4cda3105d822b0ed2e19c00b69291',
'javax.annotation:jsr250-api:1.0:jsr250-api-1.0.jar:a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f',
'javax.inject:javax.inject:1:javax.inject-1.jar:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
@@ -26,14 +27,13 @@ dependencyVerification {
'net.ltgt.gradle.incap:incap:0.2:incap-0.2.jar:b625b9806b0f1e4bc7a2e3457119488de3cd57ea20feedd513db070a573a4ffd',
'org.apache-extras.beanshell:bsh:2.0b6:bsh-2.0b6.jar:a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047',
'org.bitlet:weupnp:0.1.4:weupnp-0.1.4.jar:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb',
'org.bouncycastle:bcprov-jdk15on:1.69:bcprov-jdk15on-1.69.jar:e469bd39f936999f256002631003ff022a22951da9d5bd9789c7abfa9763a292',
'org.briarproject:jtorctl:0.3:jtorctl-0.3.jar:f2939238a097898998432effe93b0334d97a787972ab3a91a8973a1d309fc864',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.mojo.signature:java16:1.1:java16-1.1.signature:53799223a2c98dba2d0add810bed76315460df285c69e4f397ae6098f87dd619',
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',
'org.codehaus.mojo:animal-sniffer-ant-tasks:1.20:animal-sniffer-ant-tasks-1.20.jar:bb7d2498144118311d968bb08ff6fae3fc535fb1cb9cca8b8e9ea65b189422ac',
'org.codehaus.mojo:animal-sniffer:1.20:animal-sniffer-1.20.jar:80c422523c38db91260c6d78e5ee4b012862ab61cc55020c9e243dd7b5c62249',
'org.codehaus.mojo:animal-sniffer-ant-tasks:1.16:animal-sniffer-ant-tasks-1.16.jar:890040976fbe2d584619a6a61b1fd2e925b3b5eb342a85eb2762c467c0d64e90',
'org.codehaus.mojo:animal-sniffer:1.16:animal-sniffer-1.16.jar:72be8bcc226ba43b937c722a08a07852bfa1b11400089265d5df0ee7b38b1d52',
'org.hamcrest:hamcrest-core:2.1:hamcrest-core-2.1.jar:e09109e54a289d88506b9bfec987ddd199f4217c9464132668351b9a4f00bee9',
'org.hamcrest:hamcrest-library:2.1:hamcrest-library-2.1.jar:b7e2b6895b3b679f0e47b6380fda391b225e9b78505db9d8bdde8d3cc8d52a21',
'org.hamcrest:hamcrest:2.1:hamcrest-2.1.jar:ba93b2e3a562322ba432f0a1b53addcc55cb188253319a020ed77f824e692050',
@@ -48,8 +48,8 @@ dependencyVerification {
'org.jmock:jmock-testjar:2.12.0:jmock-testjar-2.12.0.jar:efefbcf6cd294d0e29f0c46eb2a3380d4ca4e1763ff719c69e2f2ac62f564a04',
'org.jmock:jmock:2.12.0:jmock-2.12.0.jar:266d07314c0cd343c46ff8a55601272de8cf406807caf55e6f313295f83d10be',
'org.objenesis:objenesis:3.0.1:objenesis-3.0.1.jar:7a8ff780b9ff48415d7c705f60030b0acaa616e7f823c98eede3b63508d4e984',
'org.ow2.asm:asm-all:5.2:asm-all-5.2.jar:7fbffbc1db3422e2101689fd88df8384b15817b52b9b2b267b9f6d2511dc198d',
'org.ow2.asm:asm:7.1:asm-7.1.jar:4ab2fa2b6d2cc9ccb1eaa05ea329c407b47b13ed2915f62f8c4b8cc96258d4de',
'org.ow2.asm:asm:9.1:asm-9.1.jar:cda4de455fab48ff0bcb7c48b4639447d4de859a7afc30a094a986f0936beba2',
'org.whispersystems:curve25519-java:0.5.0:curve25519-java-0.5.0.jar:0aadd43cf01d11e9b58f867b3c4f25c3194e8b0623d1953d32dfbfbee009e38d',
]
}

View File

@@ -12,7 +12,7 @@ configurations {
}
dependencies {
implementation project(path: ':bramble-core', configuration: 'default')
implementation project(path: ':briar:bramble-core', configuration: 'default')
implementation fileTree(dir: 'libs', include: '*.jar')
def jna_version = '4.5.2'
implementation "net.java.dev.jna:jna:$jna_version"
@@ -22,8 +22,8 @@ dependencies {
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
testImplementation project(path: ':bramble-api', configuration: 'testOutput')
testImplementation project(path: ':bramble-core', configuration: 'testOutput')
testImplementation project(path: ':briar:bramble-api', configuration: 'testOutput')
testImplementation project(path: ':briar:bramble-core', configuration: 'testOutput')
testImplementation "junit:junit:$junit_version"
testImplementation "org.jmock:jmock:$jmock_version"
testImplementation "org.jmock:jmock-junit4:$jmock_version"

View File

@@ -9,15 +9,16 @@ import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.list;
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.bramble.util.NetworkUtils.getNetworkInterfaces;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -34,14 +35,19 @@ class JavaNetworkManager implements NetworkManager {
public NetworkStatus getNetworkStatus() {
boolean connected = false, hasIpv4 = false, hasIpv6Unicast = false;
try {
for (NetworkInterface i : getNetworkInterfaces()) {
if (i.isLoopback() || !i.isUp()) continue;
for (InetAddress addr : list(i.getInetAddresses())) {
connected = true;
if (addr instanceof Inet4Address) {
hasIpv4 = true;
} else if (!addr.isMulticastAddress()) {
hasIpv6Unicast = true;
Enumeration<NetworkInterface> interfaces = getNetworkInterfaces();
if (interfaces == null) {
LOG.info("No network interfaces");
} else {
for (NetworkInterface i : list(interfaces)) {
if (i.isLoopback() || !i.isUp()) continue;
for (InetAddress addr : list(i.getInetAddresses())) {
connected = true;
if (addr instanceof Inet4Address) {
hasIpv4 = true;
} else if (!addr.isMulticastAddress()) {
hasIpv6Unicast = true;
}
}
}
}

View File

@@ -96,9 +96,9 @@ android {
}
dependencies {
implementation project(path: ':briar-core', configuration: 'default')
implementation project(path: ':bramble-core', configuration: 'default')
implementation project(':bramble-android')
implementation project(path: ':briar:briar-core', configuration: 'default')
implementation project(path: ':briar:bramble-core', configuration: 'default')
implementation project(':briar:bramble-android')
implementation 'androidx.fragment:fragment:1.3.4'
implementation 'androidx.preference:preference:1.1.1'
@@ -110,7 +110,7 @@ dependencies {
implementation 'info.guardianproject.panic:panic:1.0'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.hdodenhof:circleimageview:3.0.1'
implementation 'com.google.zxing:core:3.3.3' // newer version need minSdk 24
implementation 'uk.co.samuelwall:material-tap-target-prompt:3.3.0'
implementation 'com.vanniktech:emoji-google:0.6.0' // newer versions need minSdk 21
@@ -121,7 +121,6 @@ dependencies {
exclude group: 'com.android.support'
exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it
}
implementation 'org.nanohttpd:nanohttpd:2.3.1'
annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
@@ -129,8 +128,8 @@ dependencies {
compileOnly 'javax.annotation:jsr250-api:1.0'
def espressoVersion = '3.3.0'
testImplementation project(path: ':bramble-api', configuration: 'testOutput')
testImplementation project(path: ':bramble-core', configuration: 'testOutput')
testImplementation project(path: ':briar:bramble-api', configuration: 'testOutput')
testImplementation project(path: ':briar:bramble-core', configuration: 'testOutput')
testImplementation 'androidx.test:runner:1.3.0'
testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation 'androidx.fragment:fragment-testing:1.3.4'
@@ -144,7 +143,7 @@ dependencies {
testImplementation "org.jmock:jmock-legacy:$jmock_version"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"
androidTestImplementation project(path: ':bramble-api', configuration: 'testOutput')
androidTestImplementation project(path: ':briar:bramble-api', configuration: 'testOutput')
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"

View File

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

View File

@@ -18,7 +18,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.CAMERA" />
@@ -457,11 +456,6 @@
android:label="@string/pending_contact_requests"
android:theme="@style/BriarTheme" />
<activity
android:name=".android.hotspot.HotspotActivity"
android:label="@string/hotspot_title"
android:theme="@style/BriarTheme" />
</application>
<queries>

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,6 @@ import org.briarproject.briar.android.blog.BlogModule;
import org.briarproject.briar.android.contact.ContactListModule;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactModule;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.hotspot.HotspotModule;
import org.briarproject.briar.android.introduction.IntroductionModule;
import org.briarproject.briar.android.logging.LoggingModule;
import org.briarproject.briar.android.login.LoginModule;
@@ -95,7 +94,6 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
GroupListModule.class,
GroupConversationModule.class,
SharingModule.class,
HotspotModule.class,
TransferDataModule.class,
})
public class AppModule {
@@ -320,11 +318,6 @@ public class AppModule {
public boolean shouldEnableTransferData() {
return IS_DEBUG_BUILD;
}
@Override
public boolean shouldEnableShareAppViaOfflineHotspot() {
return IS_DEBUG_BUILD;
}
};
}
}

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
package org.briarproject.briar.android.blog;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.blog.BlogPostHeader;
import org.briarproject.briar.api.identity.AuthorInfo;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@@ -50,7 +50,7 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
return text;
}
boolean isRssFeed() {
public boolean isRssFeed() {
return header.isRssFeed();
}

View File

@@ -20,7 +20,6 @@ import androidx.annotation.UiThread;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@@ -45,7 +44,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
private final TextView text;
private final ViewGroup commentContainer;
private final boolean fullText, authorClickable;
private final int padding;
private final OnBlogPostClickListener listener;
@@ -63,8 +61,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
reblogButton = v.findViewById(R.id.commentView);
text = v.findViewById(R.id.textView);
commentContainer = v.findViewById(R.id.commentContainer);
padding = ctx.getResources()
.getDimensionPixelSize(R.dimen.listitem_vertical_margin);
}
void hideReblogButton() {
@@ -133,12 +129,6 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
} else {
reblogger.setVisibility(GONE);
}
// Apply Android 4 padding fix after setting up author/reblogger views
if (SDK_INT < 21) {
reblogger.setPadding(padding, padding, padding, padding);
author.setPadding(padding, padding, padding, padding);
}
}
private void onBindComment(BlogCommentItem item, boolean authorClickable) {

View File

@@ -57,12 +57,8 @@ public class RssFeedActivity extends BriarActivity
onBackPressed();
}
} else if (result == FAILED) {
String url = viewModel.getUrlFailedImport();
if (url == null) {
throw new AssertionError();
}
RssFeedImportFailedDialogFragment dialog =
RssFeedImportFailedDialogFragment.newInstance(url);
RssFeedImportFailedDialogFragment.newInstance();
dialog.show(getSupportFragmentManager(),
RssFeedImportFailedDialogFragment.TAG);
} else if (result == EXISTS) {

View File

@@ -25,15 +25,8 @@ public class RssFeedImportFailedDialogFragment extends DialogFragment {
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
private static final String ARG_URL = "url";
static RssFeedImportFailedDialogFragment newInstance(String retryUrl) {
Bundle args = new Bundle();
args.putString(ARG_URL, retryUrl);
RssFeedImportFailedDialogFragment f =
new RssFeedImportFailedDialogFragment();
f.setArguments(args);
return f;
static RssFeedImportFailedDialogFragment newInstance() {
return new RssFeedImportFailedDialogFragment();
}
@Override
@@ -52,8 +45,8 @@ public class RssFeedImportFailedDialogFragment extends DialogFragment {
R.style.BriarDialogTheme);
builder.setMessage(R.string.blogs_rss_feeds_import_error);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.try_again_button, (dialog, which) ->
viewModel.importFeed(requireArguments().getString(ARG_URL)));
builder.setPositiveButton(R.string.try_again_button,
(dialog, which) -> viewModel.retryImportFeed());
return builder.create();
}

View File

@@ -20,6 +20,7 @@ import org.briarproject.briar.api.feed.Feed;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;

View File

@@ -159,9 +159,11 @@ class RssFeedViewModel extends DbViewModel {
});
}
@Nullable
String getUrlFailedImport() {
return urlFailedImport;
void retryImportFeed() {
if (urlFailedImport == null) {
throw new AssertionError();
}
importFeed(urlFailedImport);
}
private boolean exists(String url) {

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ public class LinkExchangeFragment extends BaseFragment
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
viewModel = new ViewModelProvider(getActivity(), viewModelFactory)
.get(AddContactViewModel.class);
}

View File

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

View File

@@ -7,6 +7,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -21,7 +22,6 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.widget.ImageViewCompat;
import androidx.core.widget.NestedScrollView;
import androidx.fragment.app.Fragment;
import static android.view.View.FOCUS_DOWN;
@@ -58,7 +58,7 @@ public class FinalFragment extends Fragment {
return f;
}
private NestedScrollView scrollView;
private ScrollView scrollView;
protected Button buttonView;
@Nullable
@@ -69,7 +69,7 @@ public class FinalFragment extends Fragment {
View v = inflater
.inflate(R.layout.fragment_final, container, false);
scrollView = (NestedScrollView) v;
scrollView = (ScrollView) v;
ImageView iconView = v.findViewById(R.id.iconView);
TextView titleView = v.findViewById(R.id.titleView);
TextView textView = v.findViewById(R.id.textView);

View File

@@ -1,84 +0,0 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.content.DialogInterface;
import android.net.wifi.WifiManager;
import org.briarproject.briar.R;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentActivity;
import static android.content.Context.WIFI_SERVICE;
/**
* Abstract base class for the ConditionManagers that ensure that the conditions
* to open a hotspot are fulfilled. There are different extensions of this for
* API levels lower than 29 and 29+.
*/
abstract class AbstractConditionManager {
enum Permission {
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
}
protected final Consumer<Boolean> permissionUpdateCallback;
protected FragmentActivity ctx;
WifiManager wifiManager;
AbstractConditionManager(Consumer<Boolean> permissionUpdateCallback) {
this.permissionUpdateCallback = permissionUpdateCallback;
}
/**
* Pass a FragmentActivity context here during `onCreateView()`.
*/
void init(FragmentActivity ctx) {
this.ctx = ctx;
this.wifiManager = (WifiManager) ctx.getApplicationContext()
.getSystemService(WIFI_SERVICE);
}
/**
* Call this during onStart() in the fragment where the ConditionManager
* is used.
*/
abstract void onStart();
/**
* Check if all required conditions are met such that the hotspot can be
* started. If any precondition is not met yet, bring up relevant dialogs
* asking the user to grant relevant permissions or take relevant actions.
*
* @return true if conditions are fulfilled and flow can continue.
*/
abstract boolean checkAndRequestConditions();
void showDenialDialog(FragmentActivity ctx,
@StringRes int title, @StringRes int body,
DialogInterface.OnClickListener onOkClicked, Runnable onDismiss) {
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(title);
builder.setMessage(body);
builder.setPositiveButton(R.string.ok, onOkClicked);
builder.setNegativeButton(R.string.cancel,
(dialog, which) -> ctx.supportFinishAfterTransition());
builder.setOnDismissListener(dialog -> onDismiss.run());
builder.show();
}
void showRationale(Context ctx, @StringRes int title,
@StringRes int body, Runnable onContinueClicked,
Runnable onDismiss) {
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle(title);
builder.setMessage(body);
builder.setNeutralButton(R.string.continue_button,
(dialog, which) -> onContinueClicked.run());
builder.setOnDismissListener(dialog -> onDismiss.run());
builder.show();
}
}

View File

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

View File

@@ -1,83 +0,0 @@
package org.briarproject.briar.android.hotspot;
import android.content.Intent;
import android.provider.Settings;
import org.briarproject.briar.R;
import java.util.logging.Logger;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.core.util.Consumer;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
/**
* This class ensures that the conditions to open a hotspot are fulfilled on
* API levels < 29.
* <p>
* As soon as {@link #checkAndRequestConditions()} returns true,
* all conditions are fulfilled.
*/
class ConditionManager extends AbstractConditionManager {
private static final Logger LOG =
getLogger(ConditionManager.class.getName());
private final ActivityResultLauncher<Intent> wifiRequest;
ConditionManager(ActivityResultCaller arc,
Consumer<Boolean> permissionUpdateCallback) {
super(permissionUpdateCallback);
wifiRequest = arc.registerForActivityResult(
new StartActivityForResult(),
result -> permissionUpdateCallback
.accept(wifiManager.isWifiEnabled()));
}
@Override
void onStart() {
// nothing to do here
}
private boolean areEssentialPermissionsGranted() {
if (LOG.isLoggable(INFO)) {
LOG.info(String.format("areEssentialPermissionsGranted(): " +
"wifiManager.isWifiEnabled()? %b",
wifiManager.isWifiEnabled()));
}
return wifiManager.isWifiEnabled();
}
@Override
boolean checkAndRequestConditions() {
if (areEssentialPermissionsGranted()) return true;
if (!wifiManager.isWifiEnabled()) {
// Try enabling the Wifi and return true if that seems to have been
// successful, i.e. "Wifi is either already in the requested state, or
// in progress toward the requested state".
if (wifiManager.setWifiEnabled(true)) {
LOG.info("Enabled wifi");
return true;
}
// Wifi is not enabled and we can't seem to enable it, so ask the user
// to enable it for us.
showRationale(ctx, R.string.wifi_settings_title,
R.string.wifi_settings_request_enable_body,
this::requestEnableWiFi,
() -> permissionUpdateCallback.accept(false));
}
return false;
}
private void requestEnableWiFi() {
wifiRequest.launch(new Intent(Settings.ACTION_WIFI_SETTINGS));
}
}

View File

@@ -1,136 +0,0 @@
package org.briarproject.briar.android.hotspot;
import android.content.Intent;
import android.provider.Settings;
import org.briarproject.briar.R;
import java.util.logging.Logger;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Consumer;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
import static java.lang.Boolean.TRUE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
/**
* This class ensures that the conditions to open a hotspot are fulfilled on
* API levels >= 29.
* <p>
* As soon as {@link #checkAndRequestConditions()} returns true,
* all conditions are fulfilled.
*/
@RequiresApi(29)
class ConditionManager29 extends AbstractConditionManager {
private static final Logger LOG =
getLogger(ConditionManager29.class.getName());
private Permission locationPermission = Permission.UNKNOWN;
private final ActivityResultLauncher<String> locationRequest;
private final ActivityResultLauncher<Intent> wifiRequest;
ConditionManager29(ActivityResultCaller arc,
Consumer<Boolean> permissionUpdateCallback) {
super(permissionUpdateCallback);
locationRequest = arc.registerForActivityResult(
new RequestPermission(), granted -> {
onRequestPermissionResult(granted);
permissionUpdateCallback.accept(TRUE.equals(granted));
});
wifiRequest = arc.registerForActivityResult(
new StartActivityForResult(),
result -> permissionUpdateCallback
.accept(wifiManager.isWifiEnabled())
);
}
@Override
void onStart() {
locationPermission = Permission.UNKNOWN;
}
private boolean areEssentialPermissionsGranted() {
boolean isWifiEnabled = wifiManager.isWifiEnabled();
if (LOG.isLoggable(INFO)) {
LOG.info(String.format("areEssentialPermissionsGranted(): " +
"locationPermission? %s, " +
"wifiManager.isWifiEnabled()? %b",
locationPermission, isWifiEnabled));
}
return locationPermission == Permission.GRANTED && isWifiEnabled;
}
@Override
boolean checkAndRequestConditions() {
if (areEssentialPermissionsGranted()) return true;
if (locationPermission == Permission.UNKNOWN) {
locationRequest.launch(ACCESS_FINE_LOCATION);
return false;
}
// If the location permission has been permanently denied, ask the
// user to change the setting
if (locationPermission == Permission.PERMANENTLY_DENIED) {
showDenialDialog(ctx, R.string.permission_location_title,
R.string.permission_hotspot_location_denied_body,
getGoToSettingsListener(ctx),
() -> permissionUpdateCallback.accept(false));
return false;
}
// Should we show the rationale for location permission?
if (locationPermission == Permission.SHOW_RATIONALE) {
showRationale(ctx, R.string.permission_location_title,
R.string.permission_hotspot_location_request_body,
this::requestPermissions,
() -> permissionUpdateCallback.accept(false));
return false;
}
// If Wifi is not enabled, we show the rationale for enabling Wifi?
if (!wifiManager.isWifiEnabled()) {
showRationale(ctx, R.string.wifi_settings_title,
R.string.wifi_settings_request_enable_body,
this::requestEnableWiFi,
() -> permissionUpdateCallback.accept(false));
return false;
}
// we shouldn't usually reach this point, but if we do, return false
// anyway to force a recheck. Maybe some condition changed in the
// meantime.
return false;
}
private void onRequestPermissionResult(@Nullable Boolean granted) {
if (granted != null && granted) {
locationPermission = Permission.GRANTED;
} else if (shouldShowRequestPermissionRationale(ctx,
ACCESS_FINE_LOCATION)) {
locationPermission = Permission.SHOW_RATIONALE;
} else {
locationPermission = Permission.PERMANENTLY_DENIED;
}
}
private void requestPermissions() {
locationRequest.launch(ACCESS_FINE_LOCATION);
}
private void requestEnableWiFi() {
wifiRequest.launch(new Intent(Settings.Panel.ACTION_WIFI));
}
}

View File

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

View File

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

View File

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

View File

@@ -1,42 +0,0 @@
package org.briarproject.briar.android.hotspot;
import android.os.Bundle;
import android.view.View;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import static org.briarproject.briar.android.util.UiUtils.showFragment;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotFragment extends AbstractTabsFragment {
public final static String TAG = HotspotFragment.class.getName();
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
connectedButton.setOnClickListener(v -> showNextFragment());
}
@Override
protected Fragment getFirstFragment() {
return ManualHotspotFragment.newInstance(true);
}
@Override
protected Fragment getSecondFragment() {
return QrHotspotFragment.newInstance(true);
}
private void showNextFragment() {
Fragment f = new WebsiteFragment();
String tag = WebsiteFragment.TAG;
showFragment(getParentFragmentManager(), f, tag);
}
}

View File

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

View File

@@ -1,133 +0,0 @@
package org.briarproject.briar.android.hotspot;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.View.FOCUS_DOWN;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static androidx.transition.TransitionManager.beginDelayedTransition;
import static com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG;
import static org.briarproject.briar.android.AppModule.getAndroidComponent;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class HotspotIntroFragment extends Fragment {
public final static String TAG = HotspotIntroFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private HotspotViewModel viewModel;
private Button startButton;
private ProgressBar progressBar;
private TextView progressTextView;
private ScrollView scrollView;
private final AbstractConditionManager conditionManager = SDK_INT < 29 ?
new ConditionManager(this, this::onPermissionUpdate) :
new ConditionManager29(this, this::onPermissionUpdate);
@Override
public void onAttach(Context context) {
super.onAttach(context);
FragmentActivity activity = requireActivity();
getAndroidComponent(activity).inject(this);
viewModel = new ViewModelProvider(activity, viewModelFactory)
.get(HotspotViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater
.inflate(R.layout.fragment_hotspot_intro, container, false);
startButton = v.findViewById(R.id.startButton);
progressBar = v.findViewById(R.id.progressBar);
progressTextView = v.findViewById(R.id.progressTextView);
scrollView = v.findViewById(R.id.scrollView);
startButton.setOnClickListener(this::onButtonClick);
conditionManager.init(requireActivity());
return v;
}
@Override
public void onStart() {
super.onStart();
conditionManager.onStart();
// Scroll down in case the screen is small, so the button is visible
scrollView.post(() -> scrollView.fullScroll(FOCUS_DOWN));
}
private void onButtonClick(View view) {
startButton.setEnabled(false);
startHotspotIfConditionsFulfilled();
}
private void startHotspotIfConditionsFulfilled() {
if (conditionManager.checkAndRequestConditions()) {
showInstallWarningIfNeeded();
beginDelayedTransition((ViewGroup) requireView());
startButton.setVisibility(INVISIBLE);
progressBar.setVisibility(VISIBLE);
progressTextView.setVisibility(VISIBLE);
viewModel.startHotspot();
}
}
private void onPermissionUpdate(boolean recheckPermissions) {
startButton.setEnabled(true);
if (recheckPermissions) {
startHotspotIfConditionsFulfilled();
}
}
private void showInstallWarningIfNeeded() {
Context ctx = requireContext();
ApplicationInfo applicationInfo;
try {
applicationInfo = ctx.getPackageManager()
.getApplicationInfo(ctx.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError(e);
}
// test only apps can not be installed
if ((applicationInfo.flags & FLAG_TEST_ONLY) == FLAG_TEST_ONLY) {
int color = getResources().getColor(R.color.briar_red_500);
Snackbar.make(requireView(), R.string.hotspot_flag_test,
LENGTH_LONG).setBackgroundTint(color).show();
}
}
}

View File

@@ -1,452 +0,0 @@
package org.briarproject.briar.android.hotspot;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.wifi.WifiManager;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pGroup;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
import android.os.Handler;
import android.os.PowerManager;
import android.util.DisplayMetrics;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.hotspot.HotspotState.NetworkConfig;
import org.briarproject.briar.android.util.QrCodeUtils;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import static android.content.Context.POWER_SERVICE;
import static android.content.Context.WIFI_P2P_SERVICE;
import static android.content.Context.WIFI_SERVICE;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
import static android.net.wifi.p2p.WifiP2pConfig.GROUP_OWNER_BAND_2GHZ;
import static android.net.wifi.p2p.WifiP2pManager.BUSY;
import static android.net.wifi.p2p.WifiP2pManager.ERROR;
import static android.net.wifi.p2p.WifiP2pManager.NO_SERVICE_REQUESTS;
import static android.net.wifi.p2p.WifiP2pManager.P2P_UNSUPPORTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.FULL_WAKE_LOCK;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.util.UiUtils.handleException;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class HotspotManager {
interface HotspotListener {
@UiThread
void onStartingHotspot();
@IoExecutor
void onHotspotStarted(NetworkConfig networkConfig);
@UiThread
void onPeersUpdated(int peers);
@UiThread
void onHotspotError(String error);
}
private static final Logger LOG = getLogger(HotspotManager.class.getName());
private static final int MAX_FRAMEWORK_ATTEMPTS = 5;
private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
private static final int RETRY_DELAY_MILLIS = 1000;
private static final String HOTSPOT_NAMESPACE = "hotspot";
private static final String HOTSPOT_KEY_SSID = "ssid";
private static final String HOTSPOT_KEY_PASS = "pass";
private final Context ctx;
@DatabaseExecutor
private final Executor dbExecutor;
@IoExecutor
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
private final SettingsManager settingsManager;
private final SecureRandom random;
private final WifiManager wifiManager;
private final WifiP2pManager wifiP2pManager;
private final PowerManager powerManager;
private final Handler handler;
private final String lockTag;
private HotspotListener listener;
private WifiManager.WifiLock wifiLock;
private PowerManager.WakeLock wakeLock;
private WifiP2pManager.Channel channel;
@Nullable
@RequiresApi(29)
private volatile NetworkConfig savedNetworkConfig = null;
@Inject
HotspotManager(Application ctx,
@DatabaseExecutor Executor dbExecutor,
@IoExecutor Executor ioExecutor,
AndroidExecutor androidExecutor,
SettingsManager settingsManager,
SecureRandom random) {
this.ctx = ctx.getApplicationContext();
this.dbExecutor = dbExecutor;
this.ioExecutor = ioExecutor;
this.androidExecutor = androidExecutor;
this.settingsManager = settingsManager;
this.random = random;
wifiManager = (WifiManager) ctx.getApplicationContext()
.getSystemService(WIFI_SERVICE);
wifiP2pManager =
(WifiP2pManager) ctx.getSystemService(WIFI_P2P_SERVICE);
powerManager = (PowerManager) ctx.getSystemService(POWER_SERVICE);
handler = new Handler(ctx.getMainLooper());
lockTag = ctx.getPackageName() + ":app-sharing-hotspot";
}
void setHotspotListener(HotspotListener listener) {
this.listener = listener;
}
@UiThread
void startWifiP2pHotspot() {
if (wifiP2pManager == null) {
listener.onHotspotError(
ctx.getString(R.string.hotspot_error_no_wifi_direct));
return;
}
listener.onStartingHotspot();
acquireLocks();
startWifiP2pFramework(1);
}
/**
* As soon as Wifi is enabled, we try starting the WifiP2p framework.
* If Wifi has just been enabled, it is possible that will fail. If that
* happens we try again for MAX_FRAMEWORK_ATTEMPTS times after a delay of
* RETRY_DELAY_MILLIS after each attempt.
* <p>
* Rationale: it can take a few milliseconds for WifiP2p to become available
* after enabling Wifi. Depending on the API level it is possible to check this
* using {@link WifiP2pManager#requestP2pState} or register a BroadcastReceiver
* on the WIFI_P2P_STATE_CHANGED_ACTION to get notified when WifiP2p is really
* available. Trying to implement a solution that works reliably using these
* checks turned out to be a long rabbit-hole with lots of corner cases and
* workarounds for specific situations.
* Instead we now rely on this trial-and-error approach of just starting
* the framework and retrying if it fails.
* <p>
* We'll realize that the framework is busy when the ActionListener passed
* to {@link WifiP2pManager#createGroup} is called with onFailure(BUSY)
*/
@UiThread
private void startWifiP2pFramework(int attempt) {
if (LOG.isLoggable(INFO)) {
LOG.info("startWifiP2pFramework attempt: " + attempt);
}
/*
* It is important that we call WifiP2pManager#initialize again
* for every attempt to starting the framework because otherwise,
* createGroup() will continue to fail with a BUSY state.
*/
channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
if (channel == null) {
releaseHotspotWithError(
ctx.getString(R.string.hotspot_error_no_wifi_direct));
return;
}
ActionListener listener = new ActionListener() {
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onSuccess() {
requestGroupInfo(1);
}
@Override
// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
public void onFailure(int reason) {
if (LOG.isLoggable(INFO)) {
LOG.info("onFailure: " + reason);
}
if (reason == BUSY) {
// WifiP2p not ready yet or hotspot already running
restartWifiP2pFramework(attempt);
} else if (reason == P2P_UNSUPPORTED) {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed,
"p2p unsupported"));
} else if (reason == ERROR) {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed,
"p2p error"));
} else if (reason == NO_SERVICE_REQUESTS) {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed,
"no service requests"));
} else {
// all cases covered, in doubt set to error
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_failed_unknown,
reason));
}
}
};
try {
if (SDK_INT >= 29) {
Runnable createGroup = () -> {
NetworkConfig c = requireNonNull(savedNetworkConfig);
WifiP2pConfig config = new WifiP2pConfig.Builder()
.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
.setNetworkName(c.ssid)
.setPassphrase(c.password)
.build();
wifiP2pManager.createGroup(channel, config, listener);
};
if (savedNetworkConfig == null) {
// load savedNetworkConfig before starting hotspot
dbExecutor.execute(() -> {
loadSavedNetworkConfig();
androidExecutor.runOnUiThread(createGroup);
});
} else {
// savedNetworkConfig was already loaded, create group now
createGroup.run();
}
} else {
wifiP2pManager.createGroup(channel, listener);
}
} catch (SecurityException e) {
// this should never happen, because we request permissions before
throw new AssertionError(e);
}
}
@UiThread
private void restartWifiP2pFramework(int attempt) {
LOG.info("retrying to start WifiP2p framework");
if (attempt < MAX_FRAMEWORK_ATTEMPTS) {
if (SDK_INT >= 27 && channel != null) channel.close();
channel = null;
handler.postDelayed(() -> startWifiP2pFramework(attempt + 1),
RETRY_DELAY_MILLIS);
} else {
releaseHotspotWithError(
ctx.getString(R.string.hotspot_error_framework_busy));
}
}
@UiThread
void stopWifiP2pHotspot() {
if (channel == null) return;
wifiP2pManager.removeGroup(channel, new ActionListener() {
@Override
public void onSuccess() {
closeChannelAndReleaseLocks();
}
@Override
public void onFailure(int reason) {
// not propagating back error
if (LOG.isLoggable(WARNING)) {
LOG.warning("Error removing Wifi P2P group: " + reason);
}
closeChannelAndReleaseLocks();
}
});
}
@SuppressLint("WakelockTimeout")
private void acquireLocks() {
// FLAG_KEEP_SCREEN_ON is not respected on some Huawei devices.
wakeLock = powerManager.newWakeLock(FULL_WAKE_LOCK, lockTag);
wakeLock.acquire();
// WIFI_MODE_FULL has no effect on API >= 29
int lockType =
SDK_INT >= 29 ? WIFI_MODE_FULL_HIGH_PERF : WIFI_MODE_FULL;
wifiLock = wifiManager.createWifiLock(lockType, lockTag);
wifiLock.acquire();
}
@UiThread
private void releaseHotspotWithError(String error) {
listener.onHotspotError(error);
closeChannelAndReleaseLocks();
}
@UiThread
private void closeChannelAndReleaseLocks() {
if (SDK_INT >= 27 && channel != null) channel.close();
channel = null;
if (wakeLock.isHeld()) wakeLock.release();
if (wifiLock.isHeld()) wifiLock.release();
}
@UiThread
private void requestGroupInfo(int attempt) {
if (LOG.isLoggable(INFO)) {
LOG.info("requestGroupInfo attempt: " + attempt);
}
GroupInfoListener groupListener = group -> {
boolean valid = isGroupValid(group);
// If the group is valid, set the hotspot to started. If we don't
// have any attempts left, we try what we got
if (valid || attempt >= MAX_GROUP_INFO_ATTEMPTS) {
onHotspotStarted(group);
} else {
retryRequestingGroupInfo(attempt);
}
};
try {
if (channel == null) return;
wifiP2pManager.requestGroupInfo(channel, groupListener);
} catch (SecurityException e) {
// this should never happen, because we request permissions before
throw new AssertionError(e);
}
}
@UiThread
private void onHotspotStarted(WifiP2pGroup group) {
DisplayMetrics dm = ctx.getResources().getDisplayMetrics();
ioExecutor.execute(() -> {
String content = createWifiLoginString(group.getNetworkName(),
group.getPassphrase());
Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
NetworkConfig config = new NetworkConfig(group.getNetworkName(),
group.getPassphrase(), qrCode);
listener.onHotspotStarted(config);
});
requestGroupInfoForConnection();
}
private boolean isGroupValid(@Nullable WifiP2pGroup group) {
if (group == null) {
LOG.info("group is null");
return false;
} else if (!group.getNetworkName().startsWith("DIRECT-")) {
LOG.info("received networkName without prefix 'DIRECT-'");
return false;
} else if (SDK_INT >= 29) {
// if we get here, the savedNetworkConfig must have a value
String networkName = requireNonNull(savedNetworkConfig).ssid;
if (!networkName.equals(group.getNetworkName())) {
LOG.info("expected networkName does not match received one");
return false;
}
}
return true;
}
@UiThread
private void retryRequestingGroupInfo(int attempt) {
LOG.info("retrying to request group info");
// On some devices we need to wait for the group info to become available
if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
handler.postDelayed(() -> requestGroupInfo(attempt + 1),
RETRY_DELAY_MILLIS);
} else {
releaseHotspotWithError(ctx.getString(
R.string.hotspot_error_start_callback_no_group_info));
}
}
@UiThread
private void requestGroupInfoForConnection() {
LOG.info("requestGroupInfo for connection");
GroupInfoListener groupListener = group -> {
if (group != null) {
listener.onPeersUpdated(group.getClientList().size());
}
handler.postDelayed(this::requestGroupInfoForConnection,
RETRY_DELAY_MILLIS);
};
try {
if (channel == null) return;
wifiP2pManager.requestGroupInfo(channel, groupListener);
} catch (SecurityException e) {
throw new AssertionError(e);
}
}
/**
* Store persistent Wi-Fi SSID and passphrase in Settings to improve UX
* so that users don't have to change them when attempting to connect.
* Works only on API 29 and above.
*/
@RequiresApi(29)
@DatabaseExecutor
private void loadSavedNetworkConfig() {
try {
Settings settings = settingsManager.getSettings(HOTSPOT_NAMESPACE);
String ssid = settings.get(HOTSPOT_KEY_SSID);
String pass = settings.get(HOTSPOT_KEY_PASS);
if (ssid == null || pass == null) {
ssid = getSsid();
pass = getPassword();
settings.put(HOTSPOT_KEY_SSID, ssid);
settings.put(HOTSPOT_KEY_PASS, pass);
settingsManager.mergeSettings(settings, HOTSPOT_NAMESPACE);
}
savedNetworkConfig = new NetworkConfig(ssid, pass, null);
} catch (DbException e) {
handleException(ctx, androidExecutor, LOG, e);
// probably never happens, but if lets use non-persistent data
String ssid = getSsid();
String pass = getPassword();
savedNetworkConfig = new NetworkConfig(ssid, pass, null);
}
}
@RequiresApi(29)
private String getSsid() {
return "DIRECT-" + getRandomString(2) + "-" +
getRandomString(10);
}
@RequiresApi(29)
private String getPassword() {
return getRandomString(8);
}
private static String createWifiLoginString(String ssid, String password) {
// https://en.wikipedia.org/wiki/QR_code#WiFi_network_login
// do not remove the dangling ';', it can cause problems to omit it
return "WIFI:S:" + ssid + ";T:WPA;P:" + password + ";;";
}
// exclude chars that are easy to confuse: 0 O, 5 S, 1 l I
private static final String chars =
"2346789ABCDEFGHJKLMNPQRTUVWXYZabcdefghijkmnopqrstuvwxyz";
private String getRandomString(int length) {
char[] c = new char[length];
for (int i = 0; i < length; i++) {
c[i] = chars.charAt(random.nextInt(chars.length()));
}
return new String(c);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.reporting;
import android.app.Application;
import android.os.Process;
import android.util.Log;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.logging.LogEncrypter;
@@ -11,7 +10,6 @@ import java.lang.Thread.UncaughtExceptionHandler;
import javax.inject.Inject;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.util.UiUtils.startDevReportActivity;
@NotNullByDefault
@@ -30,15 +28,13 @@ class BriarExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
if (IS_DEBUG_BUILD) Log.w("Uncaught exception", e);
// encrypt logs to disk before handing over to new process
// the intent has limited space, so we can't reliably store them there.
byte[] logKey = logEncrypter.encryptLogs();
// activity runs in its own process, so we can kill the old one
startDevReportActivity(app.getApplicationContext(),
CrashReportActivity.class, e, appStartTime, logKey, null);
CrashReportActivity.class, e, appStartTime, logKey);
Process.killProcess(Process.myPid());
System.exit(10);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,6 @@ import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -112,7 +111,6 @@ import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_LOGCAT;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_START_TIME;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_INITIAL_COMMENT;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
@MethodsNotNullByDefault
@@ -139,18 +137,13 @@ public class UiUtils {
public static void showFragment(FragmentManager fm, Fragment f,
@Nullable String tag) {
showFragment(fm, f, tag, true);
}
public static void showFragment(FragmentManager fm, Fragment f,
@Nullable String tag, boolean addToBackStack) {
FragmentTransaction ta = fm.beginTransaction()
fm.beginTransaction()
.setCustomAnimations(R.anim.step_next_in,
R.anim.step_previous_out, R.anim.step_previous_in,
R.anim.step_next_out)
.replace(R.id.fragmentContainer, f, tag);
if (addToBackStack) ta.addToBackStack(tag);
ta.commit();
.replace(R.id.fragmentContainer, f, tag)
.addToBackStack(tag)
.commit();
}
public static String getContactDisplayName(Author author,
@@ -417,25 +410,17 @@ public class UiUtils {
}
public static void triggerFeedback(Context ctx) {
triggerFeedback(ctx, null);
}
public static void triggerFeedback(Context ctx,
@Nullable String initialComment) {
startDevReportActivity(ctx, FeedbackActivity.class, null, null, null,
initialComment);
startDevReportActivity(ctx, FeedbackActivity.class, null, null, null);
}
public static void startDevReportActivity(Context ctx,
Class<? extends FragmentActivity> activity, @Nullable Throwable t,
@Nullable Long appStartTime, @Nullable byte[] logKey, @Nullable
String initialComment) {
@Nullable Long appStartTime, @Nullable byte[] logKey) {
final Intent dialogIntent = new Intent(ctx, activity);
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
dialogIntent.putExtra(EXTRA_THROWABLE, t);
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
dialogIntent.putExtra(EXTRA_APP_LOGCAT, logKey);
dialogIntent.putExtra(EXTRA_INITIAL_COMMENT, initialComment);
ctx.startActivity(dialogIntent);
}
@@ -561,5 +546,4 @@ public class UiUtils {
activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE |
SOFT_INPUT_STATE_HIDDEN);
}
}

View File

@@ -3,7 +3,6 @@ package org.briarproject.briar.android.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ImageView;
@@ -30,7 +29,6 @@ import im.delight.android.identicons.IdenticonDrawable;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.graphics.Typeface.BOLD;
import static android.util.TypedValue.COMPLEX_UNIT_PX;
import static androidx.appcompat.content.res.AppCompatResources.getDrawable;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.android.util.UiUtils.resolveAttribute;
import static org.briarproject.briar.api.identity.AuthorInfo.Status.NONE;
@@ -179,14 +177,14 @@ public class AuthorView extends ConstraintLayout {
case RSS_FEED:
avatarIcon.setVisibility(INVISIBLE);
date.setVisibility(VISIBLE);
setRssVectorAvatar();
avatar.setImageResource(R.drawable.ic_rss_feed);
setAvatarSize(R.dimen.blogs_avatar_normal_size);
setTextSize(authorName, R.dimen.text_size_small);
break;
case RSS_FEED_REBLOGGED:
avatarIcon.setVisibility(INVISIBLE);
date.setVisibility(VISIBLE);
setRssVectorAvatar();
avatar.setImageResource(R.drawable.ic_rss_feed);
setAvatarSize(R.dimen.blogs_avatar_comment_size);
setTextSize(authorName, R.dimen.text_size_tiny);
break;
@@ -206,16 +204,4 @@ public class AuthorView extends ConstraintLayout {
v.setTextSize(COMPLEX_UNIT_PX, textSize);
}
/**
* Applies special hack to use AppCompat vector drawable support
* when setting the RSS vector drawable to the avatar view.
* {@link ImageView#setImageResource(int)} is not working as
* {@link CircleImageView} is not using
* {@link androidx.appcompat.widget.AppCompatImageView}.
*/
private void setRssVectorAvatar() {
Drawable d = getDrawable(getContext(), R.drawable.ic_rss_feed);
avatar.setImageDrawable(d);
}
}

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,30 +5,31 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="@+id/acceptButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_xlarge">
android:padding="@dimen/margin_large">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="@dimen/hero_square"
android:layout_height="@dimen/hero_square"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="8dp"
app:layout_constraintBottom_toTopOf="@+id/crashed"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="128dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_crash"
app:tint="?attr/colorControlNormal"
@@ -38,23 +39,25 @@
android:id="@+id/crashed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:gravity="center"
android:text="@string/briar_crashed"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/fault"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorIcon" />
app:layout_constraintTop_toBottomOf="@+id/errorIcon"
tools:layout_editor_absoluteY="8dp" />
<TextView
android:id="@+id/fault"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/not_your_fault"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/pleaseSend"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -67,7 +70,8 @@
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/please_send_report"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/encrypted"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -80,7 +84,8 @@
android:layout_marginTop="@dimen/margin_large"
android:gravity="center"
android:text="@string/report_is_encrypted"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -88,7 +93,7 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</ScrollView>
<Button
android:id="@+id/declineButton"
@@ -96,7 +101,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/acceptButton"
app:layout_constraintEnd_toStartOf="@+id/acceptButton"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -1,55 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/alerts_and_states_error"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/errorTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_xlarge">
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/sorry"
android:textSize="@dimen/text_size_xlarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorIcon" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="@dimen/hero_square"
android:layout_height="@dimen/hero_square"
app:layout_constraintBottom_toTopOf="@+id/errorTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/alerts_and_states_error"
app:tint="@color/briar_red_500"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/errorMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:textSize="@dimen/text_size_medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorTitle"
tools:text="@string/qr_code_unsupported" />
<TextView
android:id="@+id/errorTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:gravity="center"
android:text="@string/sorry"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toTopOf="@+id/errorMessage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorIcon" />
<TextView
android:id="@+id/errorMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorTitle"
tools:text="@string/startup_failed_service_error" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,102 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="@+id/tryAgainButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
<TextView
android:id="@+id/errorTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/connection_error_title"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_xlarge">
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorTitle"
app:srcCompat="@drawable/qr_code_error"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/errorIcon"
android:layout_width="@dimen/hero_rect_width"
android:layout_height="@dimen/hero_rect_height"
app:layout_constraintBottom_toTopOf="@+id/errorTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/qr_code_error"
app:tint="?attr/colorControlNormal"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/errorMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toTopOf="@+id/sendFeedback"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorIcon"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed"
tools:text="error explanation" />
<TextView
android:id="@+id/errorTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:gravity="center"
android:text="@string/connection_error_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toTopOf="@+id/errorMessage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorIcon" />
<TextView
android:id="@+id/sendFeedback"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/connection_error_feedback"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toTopOf="@+id/tryAgainButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorMessage" />
<TextView
android:id="@+id/errorMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_xlarge"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toTopOf="@+id/sendFeedback"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorTitle"
tools:text="error explanation" />
<Button
android:id="@+id/tryAgainButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:text="@string/try_again_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@+id/cancelButton" />
<TextView
android:id="@+id/sendFeedback"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:text="@string/connection_error_feedback"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/errorMessage" />
<Button
android:id="@+id/cancelButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tryAgainButton"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/cancelButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tryAgainButton"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tryAgainButton" />
<Button
android:id="@+id/tryAgainButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/try_again_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@+id/cancelButton"
app:layout_constraintTop_toBottomOf="@+id/scrollView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@@ -12,16 +12,18 @@
<ImageView
android:id="@+id/iconView"
android:layout_width="@dimen/hero_square"
android:layout_height="@dimen/hero_square"
android:layout_marginHorizontal="@dimen/margin_xlarge"
android:layout_marginTop="@dimen/margin_xlarge"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toTopOf="@+id/titleView"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintWidth_max="200dp"
app:layout_constraintWidth_percent="0.4"
tools:ignore="ContentDescription"
tools:srcCompat="@drawable/alerts_and_states_error"
tools:tint="@color/briar_red_500" />
@@ -30,8 +32,11 @@
android:id="@+id/titleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_xlarge"
android:layout_marginTop="@dimen/margin_xlarge"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
app:layout_constraintBottom_toTopOf="@+id/textView"
@@ -44,9 +49,10 @@
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_xlarge"
android:layout_marginTop="@dimen/margin_xlarge"
android:layout_marginBottom="@dimen/margin_large"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
@@ -59,7 +65,7 @@
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_large"
android:layout_margin="16dp"
android:text="@string/finish"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -67,4 +73,4 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</ScrollView>

View File

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

View File

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

View File

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

View File

@@ -1,89 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".android.hotspot.HotspotIntroFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="@dimen/hero_square"
android:layout_height="@dimen/hero_square"
android:layout_marginHorizontal="@dimen/margin_xlarge"
android:layout_marginTop="@dimen/margin_xlarge"
app:layout_constraintBottom_toTopOf="@+id/introView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_wifi_tethering"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/introView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_xlarge"
android:layout_marginTop="@dimen/margin_xlarge"
android:layout_marginBottom="@dimen/margin_large"
android:text="@string/hotspot_intro"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toTopOf="@+id/startButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<Button
android:id="@+id/startButton"
style="@style/BriarButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_large"
android:drawablePadding="8dp"
android:text="@string/hotspot_button_start_sharing"
app:drawableLeftCompat="@drawable/ic_wifi_tethering"
app:drawableStartCompat="@drawable/ic_wifi_tethering"
app:drawableTint="@color/button_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/startButton"
app:layout_constraintEnd_toStartOf="@+id/progressTextView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@+id/startButton"
app:layout_constraintTop_toTopOf="@+id/startButton"
tools:visibility="visible" />
<TextView
android:id="@+id/progressTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_large"
android:layout_marginLeft="@dimen/margin_large"
android:text="@string/hotspot_progress_text_start"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/startButton"
app:layout_constraintEnd_toEndOf="@+id/startButton"
app:layout_constraintStart_toEndOf="@+id/progressBar"
app:layout_constraintTop_toTopOf="@+id/startButton"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

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

View File

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

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