mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
Compare commits
39 Commits
deliver-te
...
beta-1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fdc7e7cc4 | ||
|
|
7569d5ffb3 | ||
|
|
deca5d56cc | ||
|
|
3d6b48bb34 | ||
|
|
0dc631b7a8 | ||
|
|
921e952b05 | ||
|
|
3b02797639 | ||
|
|
e2e67edbbe | ||
|
|
a9cd40faeb | ||
|
|
04517e942e | ||
|
|
9a25ad892d | ||
|
|
3457d8f9ab | ||
|
|
5fb2624ffa | ||
|
|
ed9a7bec2c | ||
|
|
ff70315d5c | ||
|
|
f197243273 | ||
|
|
6409a3b179 | ||
|
|
f882e46b33 | ||
|
|
efa63c306a | ||
|
|
205b4f77b2 | ||
|
|
015ecb1d99 | ||
|
|
fd86b73626 | ||
|
|
9048392d4e | ||
|
|
480aaaa35e | ||
|
|
002feb8e29 | ||
|
|
c6ba2b037a | ||
|
|
98788c7c80 | ||
|
|
e6f66ebc95 | ||
|
|
04485e58da | ||
|
|
97118fd92b | ||
|
|
ac4fbf202f | ||
|
|
b81495eac1 | ||
|
|
db90f75d2e | ||
|
|
bed3abfd40 | ||
|
|
0967f6c48e | ||
|
|
f9a8fcb207 | ||
|
|
eb3c2a3566 | ||
|
|
02ee678bab | ||
|
|
f6bdbb1b80 |
@@ -1,11 +1,15 @@
|
||||
package org.briarproject.bramble.network;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.LinkAddress;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkInfo;
|
||||
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
@@ -19,6 +23,11 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler.Cancellable;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -36,16 +45,22 @@ import static android.net.ConnectivityManager.TYPE_WIFI;
|
||||
import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
|
||||
import static java.net.NetworkInterface.getNetworkInterfaces;
|
||||
import static java.util.Collections.list;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
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.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class AndroidNetworkManager implements NetworkManager, Service {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AndroidNetworkManager.class.getName());
|
||||
getLogger(AndroidNetworkManager.class.getName());
|
||||
|
||||
// See android.net.wifi.WifiManager
|
||||
private static final String WIFI_AP_STATE_CHANGED_ACTION =
|
||||
@@ -54,7 +69,8 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
||||
private final TaskScheduler scheduler;
|
||||
private final EventBus eventBus;
|
||||
private final Executor eventExecutor;
|
||||
private final Context appContext;
|
||||
private final Application app;
|
||||
private final ConnectivityManager connectivityManager;
|
||||
private final AtomicReference<Cancellable> connectivityCheck =
|
||||
new AtomicReference<>();
|
||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||
@@ -67,7 +83,9 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
||||
this.scheduler = scheduler;
|
||||
this.eventBus = eventBus;
|
||||
this.eventExecutor = eventExecutor;
|
||||
this.appContext = app.getApplicationContext();
|
||||
this.app = app;
|
||||
connectivityManager = (ConnectivityManager)
|
||||
requireNonNull(app.getSystemService(CONNECTIVITY_SERVICE));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,24 +100,82 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
||||
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
|
||||
filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
|
||||
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
|
||||
appContext.registerReceiver(networkStateReceiver, filter);
|
||||
app.registerReceiver(networkStateReceiver, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopService() {
|
||||
if (networkStateReceiver != null)
|
||||
appContext.unregisterReceiver(networkStateReceiver);
|
||||
app.unregisterReceiver(networkStateReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkStatus getNetworkStatus() {
|
||||
ConnectivityManager cm = (ConnectivityManager)
|
||||
appContext.getSystemService(CONNECTIVITY_SERVICE);
|
||||
if (cm == null) throw new AssertionError();
|
||||
NetworkInfo net = cm.getActiveNetworkInfo();
|
||||
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
|
||||
boolean connected = net != null && net.isConnected();
|
||||
boolean wifi = connected && net.getType() == TYPE_WIFI;
|
||||
return new NetworkStatus(connected, wifi);
|
||||
boolean wifi = false, ipv6Only = false;
|
||||
if (connected) {
|
||||
wifi = net.getType() == TYPE_WIFI;
|
||||
if (SDK_INT >= 23) ipv6Only = isActiveNetworkIpv6Only();
|
||||
else ipv6Only = areAllAvailableNetworksIpv6Only();
|
||||
}
|
||||
return new NetworkStatus(connected, wifi, ipv6Only);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the
|
||||
* {@link ConnectivityManager#getActiveNetwork() active network} has an
|
||||
* IPv6 unicast address and no IPv4 addresses. The active network is
|
||||
* assumed not to be a loopback interface.
|
||||
*/
|
||||
@TargetApi(23)
|
||||
private boolean isActiveNetworkIpv6Only() {
|
||||
Network net = connectivityManager.getActiveNetwork();
|
||||
if (net == null) {
|
||||
LOG.info("No active network");
|
||||
return false;
|
||||
}
|
||||
LinkProperties props = connectivityManager.getLinkProperties(net);
|
||||
if (props == null) {
|
||||
LOG.info("No link properties for active network");
|
||||
return false;
|
||||
}
|
||||
boolean hasIpv6Unicast = false;
|
||||
for (LinkAddress linkAddress : props.getLinkAddresses()) {
|
||||
InetAddress addr = linkAddress.getAddress();
|
||||
if (addr instanceof Inet4Address) return false;
|
||||
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
|
||||
}
|
||||
return hasIpv6Unicast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device has at least one network interface with an
|
||||
* IPv6 unicast address and no interfaces with IPv4 addresses, excluding
|
||||
* loopback interfaces and interfaces that are
|
||||
* {@link NetworkInterface#isUp() down}. If this method returns true and
|
||||
* the device has internet access then it's via IPv6 only.
|
||||
*/
|
||||
private boolean areAllAvailableNetworksIpv6Only() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> interfaces = getNetworkInterfaces();
|
||||
if (interfaces == null) {
|
||||
LOG.info("No network interfaces");
|
||||
return false;
|
||||
}
|
||||
boolean hasIpv6Unicast = false;
|
||||
for (NetworkInterface i : list(interfaces)) {
|
||||
if (i.isLoopback() || !i.isUp()) continue;
|
||||
for (InetAddress addr : list(i.getInetAddresses())) {
|
||||
if (addr instanceof Inet4Address) return false;
|
||||
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
|
||||
}
|
||||
}
|
||||
return hasIpv6Unicast;
|
||||
} catch (SocketException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateConnectionStatus() {
|
||||
|
||||
@@ -6,8 +6,6 @@ import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.wifi.WifiConfiguration;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.os.StrictMode;
|
||||
@@ -17,12 +15,10 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.provider.Settings.Secure.ANDROID_ID;
|
||||
|
||||
@@ -52,15 +48,6 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
|
||||
String id = Settings.Secure.getString(contentResolver, ANDROID_ID);
|
||||
if (id != null) out.writeUTF(id);
|
||||
Parcel parcel = Parcel.obtain();
|
||||
WifiManager wm = (WifiManager) appContext.getApplicationContext()
|
||||
.getSystemService(WIFI_SERVICE);
|
||||
if (wm != null) {
|
||||
List<WifiConfiguration> configs = wm.getConfiguredNetworks();
|
||||
if (configs != null) {
|
||||
for (WifiConfiguration config : configs)
|
||||
parcel.writeParcelable(config, 0);
|
||||
}
|
||||
}
|
||||
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
|
||||
if (bt != null) {
|
||||
for (BluetoothDevice device : bt.getBondedDevices())
|
||||
|
||||
@@ -8,11 +8,12 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class NetworkStatus {
|
||||
|
||||
private final boolean connected, wifi;
|
||||
private final boolean connected, wifi, ipv6Only;
|
||||
|
||||
public NetworkStatus(boolean connected, boolean wifi) {
|
||||
public NetworkStatus(boolean connected, boolean wifi, boolean ipv6Only) {
|
||||
this.connected = connected;
|
||||
this.wifi = wifi;
|
||||
this.ipv6Only = ipv6Only;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
@@ -22,4 +23,8 @@ public class NetworkStatus {
|
||||
public boolean isWifi() {
|
||||
return wifi;
|
||||
}
|
||||
|
||||
public boolean isIpv6Only() {
|
||||
return ipv6Only;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ public interface DevReporter {
|
||||
|
||||
/**
|
||||
* Sends any reports previously stored on disk.
|
||||
*
|
||||
* @return The number of reports that were sent.
|
||||
*/
|
||||
void sendReports();
|
||||
int sendReports();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.util.Base32;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -52,7 +53,7 @@ class PendingContactFactoryImpl implements PendingContactFactory {
|
||||
byte[] raw = new byte[RAW_LINK_BYTES];
|
||||
raw[0] = FORMAT_VERSION;
|
||||
arraycopy(encoded, 0, raw, 1, encoded.length);
|
||||
return "briar://" + Base32.encode(raw).toLowerCase();
|
||||
return "briar://" + Base32.encode(raw).toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
private PublicKey parseHandshakeLink(String link) throws FormatException {
|
||||
|
||||
@@ -863,6 +863,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (!state.isTorRunning()) return;
|
||||
boolean online = status.isConnected();
|
||||
boolean wifi = status.isWifi();
|
||||
boolean ipv6Only = status.isIpv6Only();
|
||||
String country = locationUtils.getCurrentCountry();
|
||||
boolean blocked =
|
||||
circumventionProvider.isTorProbablyBlocked(country);
|
||||
@@ -879,7 +880,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
|
||||
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Online: " + online + ", wifi: " + wifi);
|
||||
LOG.info("Online: " + online + ", wifi: " + wifi
|
||||
+ ", IPv6 only: " + ipv6Only);
|
||||
if (country.isEmpty()) LOG.info("Country code unknown");
|
||||
else LOG.info("Country code: " + country);
|
||||
LOG.info("Charging: " + charging);
|
||||
@@ -916,7 +918,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
enableNetwork = true;
|
||||
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
|
||||
(automatic && bridgesWork)) {
|
||||
if (circumventionProvider.needsMeek(country)) {
|
||||
if (ipv6Only ||
|
||||
circumventionProvider.needsMeek(country)) {
|
||||
LOG.info("Using meek bridges");
|
||||
enableBridges = true;
|
||||
useMeek = true;
|
||||
@@ -942,6 +945,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
if (enableNetwork) {
|
||||
enableBridges(enableBridges, useMeek);
|
||||
enableConnectionPadding(enableConnectionPadding);
|
||||
useIpv6(ipv6Only);
|
||||
}
|
||||
enableNetwork(enableNetwork);
|
||||
} catch (IOException e) {
|
||||
@@ -954,6 +958,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
||||
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
|
||||
}
|
||||
|
||||
private void useIpv6(boolean ipv6Only) throws IOException {
|
||||
controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1");
|
||||
controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0");
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
@NotNullByDefault
|
||||
protected class PluginState {
|
||||
|
||||
@@ -29,6 +29,7 @@ import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||
|
||||
@@ -100,11 +101,12 @@ class DevReporterImpl implements DevReporter, EventListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendReports() {
|
||||
public int sendReports() {
|
||||
File reportDir = devConfig.getReportDir();
|
||||
File[] reports = reportDir.listFiles();
|
||||
int reportsSent = 0;
|
||||
if (reports == null || reports.length == 0)
|
||||
return; // No reports to send
|
||||
return reportsSent; // No reports to send
|
||||
|
||||
LOG.info("Sending reports to developers");
|
||||
for (File f : reports) {
|
||||
@@ -116,13 +118,15 @@ class DevReporterImpl implements DevReporter, EventListener {
|
||||
in = new FileInputStream(f);
|
||||
IoUtils.copyAndClose(in, out);
|
||||
f.delete();
|
||||
reportsSent++;
|
||||
} catch (IOException e) {
|
||||
LOG.log(WARNING, "Failed to send reports", e);
|
||||
tryToClose(out, LOG, WARNING);
|
||||
tryToClose(in, LOG, WARNING);
|
||||
return;
|
||||
return reportsSent;
|
||||
}
|
||||
}
|
||||
LOG.info("Reports sent");
|
||||
if (LOG.isLoggable(INFO)) LOG.info(reportsSent + " report(s) sent");
|
||||
return reportsSent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import org.briarproject.bramble.api.network.NetworkStatus;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Enumeration;
|
||||
@@ -14,8 +16,8 @@ import javax.inject.Inject;
|
||||
|
||||
import static java.net.NetworkInterface.getNetworkInterfaces;
|
||||
import static java.util.Collections.list;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@@ -23,7 +25,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
class JavaNetworkManager implements NetworkManager {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(JavaNetworkManager.class.getName());
|
||||
getLogger(JavaNetworkManager.class.getName());
|
||||
|
||||
@Inject
|
||||
JavaNetworkManager() {
|
||||
@@ -31,26 +33,28 @@ class JavaNetworkManager implements NetworkManager {
|
||||
|
||||
@Override
|
||||
public NetworkStatus getNetworkStatus() {
|
||||
boolean connected = false;
|
||||
boolean connected = false, hasIpv4 = false, hasIpv6Unicast = false;
|
||||
try {
|
||||
Enumeration<NetworkInterface> interfaces = getNetworkInterfaces();
|
||||
if (interfaces != null) {
|
||||
if (interfaces == null) {
|
||||
LOG.info("No network interfaces");
|
||||
} else {
|
||||
for (NetworkInterface i : list(interfaces)) {
|
||||
if (i.isLoopback()) continue;
|
||||
if (i.isUp() && i.getInetAddresses().hasMoreElements()) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Interface " + i.getDisplayName() +
|
||||
" is up with at least one address.");
|
||||
}
|
||||
if (i.isLoopback() || !i.isUp()) continue;
|
||||
for (InetAddress addr : list(i.getInetAddresses())) {
|
||||
connected = true;
|
||||
break;
|
||||
if (addr instanceof Inet4Address) {
|
||||
hasIpv4 = true;
|
||||
} else if (!addr.isMulticastAddress()) {
|
||||
hasIpv6Unicast = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
return new NetworkStatus(connected, false);
|
||||
return new NetworkStatus(connected, false, !hasIpv4 && hasIpv6Unicast);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc03'
|
||||
|
||||
implementation 'ch.acra:acra:4.11'
|
||||
implementation 'info.guardianproject.panic:panic:1.0'
|
||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||
implementation 'de.hdodenhof:circleimageview:3.0.1'
|
||||
@@ -127,10 +126,11 @@ dependencies {
|
||||
testImplementation 'androidx.test:runner:1.3.0'
|
||||
testImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
testImplementation 'androidx.fragment:fragment-testing:1.2.5'
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
testImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||
testImplementation 'org.robolectric:robolectric:4.3.1'
|
||||
testImplementation 'org.mockito:mockito-core:3.1.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation "org.jmock:jmock:$jmockVersion"
|
||||
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
|
||||
testImplementation "org.jmock:jmock-legacy:$jmockVersion"
|
||||
|
||||
@@ -65,30 +65,43 @@
|
||||
|
||||
<service
|
||||
android:name="org.briarproject.briar.android.NotificationCleanupService"
|
||||
android:exported="false"></service>
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.reporting.DevReportActivity"
|
||||
android:name="org.briarproject.briar.android.reporting.CrashReportActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:finishOnTaskLaunch="true"
|
||||
android:label="@string/crash_report_title"
|
||||
android:launchMode="singleInstance"
|
||||
android:process=":briar_error_handler"
|
||||
android:theme="@style/BriarTheme.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden"></activity>
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.reporting.FeedbackActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/feedback_title"
|
||||
android:parentActivityName="org.briarproject.briar.android.settings.SettingsActivity"
|
||||
android:theme="@style/BriarTheme.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.briarproject.briar.android.settings.SettingsActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.splash.ExpiredActivity"
|
||||
android:label="@string/app_name"></activity>
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.login.StartupActivity"
|
||||
android:label="@string/app_name"></activity>
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.account.SetupActivity"
|
||||
android:label="@string/setup_title"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"></activity>
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.splash.SplashScreenActivity"
|
||||
@@ -346,7 +359,7 @@
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.StartupFailureActivity"
|
||||
android:label="@string/startup_failed_activity_title"></activity>
|
||||
android:label="@string/startup_failed_activity_title" />
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.settings.SettingsActivity"
|
||||
@@ -412,11 +425,11 @@
|
||||
|
||||
<activity
|
||||
android:name="org.briarproject.briar.android.logout.ExitActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"></activity>
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".android.logout.HideUiActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"></activity>
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".android.account.UnlockActivity"
|
||||
@@ -436,4 +449,27 @@
|
||||
android:theme="@style/BriarTheme" />
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<package android:name="info.guardianproject.ripple" />
|
||||
<package android:name="com.huawei.systemmanager" />
|
||||
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="http" />
|
||||
</intent>
|
||||
<!-- white-listing the intents below does not seem necessary,
|
||||
but they are still included in case modified Android versions require it -->
|
||||
<intent>
|
||||
<action android:name="android.bluetooth.adapter.action.REQUEST_DISCOVERABLE" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.settings.CHANNEL_NOTIFICATION_SETTINGS" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
|
||||
@@ -33,7 +34,6 @@ import org.briarproject.briar.BriarCoreModule;
|
||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||
import org.briarproject.briar.android.reporting.BriarReportSender;
|
||||
import org.briarproject.briar.android.view.EmojiTextInputView;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||
@@ -86,6 +86,8 @@ public interface AndroidComponent
|
||||
@DatabaseExecutor
|
||||
Executor databaseExecutor();
|
||||
|
||||
TransactionManager transactionManager();
|
||||
|
||||
MessageTracker messageTracker();
|
||||
|
||||
LifecycleManager lifecycleManager();
|
||||
@@ -173,8 +175,6 @@ public interface AndroidComponent
|
||||
|
||||
void inject(BriarService briarService);
|
||||
|
||||
void inject(BriarReportSender briarReportSender);
|
||||
|
||||
void inject(NotificationCleanupService notificationCleanupService);
|
||||
|
||||
void inject(EmojiTextInputView textInputView);
|
||||
|
||||
@@ -109,7 +109,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
@Nullable
|
||||
private GroupId blockedGroup = null;
|
||||
private boolean blockSignInReminder = false;
|
||||
private boolean blockBlogs = false;
|
||||
private boolean blockForums = false, blockGroups = false,
|
||||
blockBlogs = false;
|
||||
private long lastSound = 0;
|
||||
|
||||
private volatile Settings settings = new Settings();
|
||||
@@ -223,8 +224,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
if (s.getNamespace().equals(SETTINGS_NAMESPACE))
|
||||
settings = s.getSettings();
|
||||
} else if (e instanceof ConversationMessageReceivedEvent) {
|
||||
ConversationMessageReceivedEvent p =
|
||||
(ConversationMessageReceivedEvent) e;
|
||||
ConversationMessageReceivedEvent<?> p =
|
||||
(ConversationMessageReceivedEvent<?>) e;
|
||||
showContactNotification(p.getContactId());
|
||||
} else if (e instanceof GroupMessageAddedEvent) {
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
@@ -385,6 +386,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
|
||||
@UiThread
|
||||
private void showGroupMessageNotification(GroupId g) {
|
||||
if (blockGroups) return;
|
||||
if (g.equals(blockedGroup)) return;
|
||||
groupCounts.add(g);
|
||||
updateGroupMessageNotification(true);
|
||||
@@ -452,6 +454,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
|
||||
@UiThread
|
||||
private void showForumPostNotification(GroupId g) {
|
||||
if (blockForums) return;
|
||||
if (g.equals(blockedGroup)) return;
|
||||
forumCounts.add(g);
|
||||
updateForumPostNotification(true);
|
||||
@@ -681,6 +684,26 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockAllForumPostNotifications() {
|
||||
androidExecutor.runOnUiThread((Runnable) () -> blockForums = true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unblockAllForumPostNotifications() {
|
||||
androidExecutor.runOnUiThread((Runnable) () -> blockForums = false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockAllGroupMessageNotifications() {
|
||||
androidExecutor.runOnUiThread((Runnable) () -> blockGroups = true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unblockAllGroupMessageNotifications() {
|
||||
androidExecutor.runOnUiThread((Runnable) () -> blockGroups = false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockAllBlogPostNotifications() {
|
||||
androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = true);
|
||||
|
||||
@@ -28,9 +28,12 @@ import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
import org.briarproject.briar.android.account.LockManagerImpl;
|
||||
import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
|
||||
import org.briarproject.briar.android.login.LoginModule;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupListModule;
|
||||
import org.briarproject.briar.android.reporting.DevReportModule;
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelModule;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||
@@ -63,7 +66,11 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
ContactExchangeModule.class,
|
||||
LoginModule.class,
|
||||
NavDrawerModule.class,
|
||||
ViewModelModule.class
|
||||
ViewModelModule.class,
|
||||
DevReportModule.class,
|
||||
// below need to be within same scope as ViewModelProvider.Factory
|
||||
ForumModule.BindsModule.class,
|
||||
GroupListModule.class,
|
||||
})
|
||||
public class AppModule {
|
||||
|
||||
@@ -196,7 +203,10 @@ public class AppModule {
|
||||
ScreenFilterMonitor provideScreenFilterMonitor(
|
||||
LifecycleManager lifecycleManager,
|
||||
ScreenFilterMonitorImpl screenFilterMonitor) {
|
||||
lifecycleManager.registerService(screenFilterMonitor);
|
||||
if (SDK_INT <= 29) {
|
||||
// this keeps track of installed apps and does not work on API 30+
|
||||
lifecycleManager.registerService(screenFilterMonitor);
|
||||
}
|
||||
return screenFilterMonitor;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,19 +14,13 @@ import android.preference.PreferenceManager;
|
||||
import com.vanniktech.emoji.EmojiManager;
|
||||
import com.vanniktech.emoji.google.GoogleEmojiProvider;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.ReportingInteractionMode;
|
||||
import org.acra.annotation.ReportsCrashes;
|
||||
import org.briarproject.bramble.BrambleAndroidEagerSingletons;
|
||||
import org.briarproject.bramble.BrambleAppComponent;
|
||||
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
||||
import org.briarproject.briar.BriarCoreEagerSingletons;
|
||||
import org.briarproject.briar.BuildConfig;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.logging.CachingLogHandler;
|
||||
import org.briarproject.briar.android.reporting.BriarReportPrimer;
|
||||
import org.briarproject.briar.android.reporting.BriarReportSenderFactory;
|
||||
import org.briarproject.briar.android.reporting.DevReportActivity;
|
||||
import org.briarproject.briar.android.reporting.BriarExceptionHandler;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -34,50 +28,14 @@ import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
|
||||
import static java.util.logging.Level.FINE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.acra.ReportField.ANDROID_VERSION;
|
||||
import static org.acra.ReportField.APP_VERSION_CODE;
|
||||
import static org.acra.ReportField.APP_VERSION_NAME;
|
||||
import static org.acra.ReportField.BRAND;
|
||||
import static org.acra.ReportField.BUILD_CONFIG;
|
||||
import static org.acra.ReportField.CRASH_CONFIGURATION;
|
||||
import static org.acra.ReportField.CUSTOM_DATA;
|
||||
import static org.acra.ReportField.DEVICE_FEATURES;
|
||||
import static org.acra.ReportField.DISPLAY;
|
||||
import static org.acra.ReportField.INITIAL_CONFIGURATION;
|
||||
import static org.acra.ReportField.PACKAGE_NAME;
|
||||
import static org.acra.ReportField.PHONE_MODEL;
|
||||
import static org.acra.ReportField.PRODUCT;
|
||||
import static org.acra.ReportField.REPORT_ID;
|
||||
import static org.acra.ReportField.STACK_TRACE;
|
||||
import static org.acra.ReportField.USER_APP_START_DATE;
|
||||
import static org.acra.ReportField.USER_CRASH_DATE;
|
||||
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
|
||||
@ReportsCrashes(
|
||||
reportPrimerClass = BriarReportPrimer.class,
|
||||
logcatArguments = {"-d", "-v", "time", "*:I"},
|
||||
reportSenderFactoryClasses = {BriarReportSenderFactory.class},
|
||||
mode = ReportingInteractionMode.DIALOG,
|
||||
reportDialogClass = DevReportActivity.class,
|
||||
resDialogOkToast = R.string.dev_report_saved,
|
||||
deleteOldUnsentReportsOnApplicationStart = false,
|
||||
buildConfigClass = BuildConfig.class,
|
||||
customReportContent = {
|
||||
REPORT_ID,
|
||||
APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME,
|
||||
PHONE_MODEL, ANDROID_VERSION, BRAND, PRODUCT,
|
||||
BUILD_CONFIG,
|
||||
CUSTOM_DATA,
|
||||
STACK_TRACE,
|
||||
INITIAL_CONFIGURATION, CRASH_CONFIGURATION,
|
||||
DISPLAY, DEVICE_FEATURES,
|
||||
USER_APP_START_DATE, USER_CRASH_DATE
|
||||
}
|
||||
)
|
||||
public class BriarApplicationImpl extends Application
|
||||
implements BriarApplication {
|
||||
|
||||
@@ -85,12 +43,15 @@ public class BriarApplicationImpl extends Application
|
||||
getLogger(BriarApplicationImpl.class.getName());
|
||||
|
||||
private final CachingLogHandler logHandler = new CachingLogHandler();
|
||||
private final BriarExceptionHandler exceptionHandler =
|
||||
new BriarExceptionHandler(this);
|
||||
|
||||
private AndroidComponent applicationComponent;
|
||||
private volatile SharedPreferences prefs;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
|
||||
if (prefs == null)
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(base);
|
||||
// Loading the language needs to be done here.
|
||||
@@ -98,7 +59,6 @@ public class BriarApplicationImpl extends Application
|
||||
super.attachBaseContext(
|
||||
Localizer.getInstance().setLocale(base));
|
||||
setTheme(base, prefs);
|
||||
ACRA.init(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,7 +104,7 @@ public class BriarApplicationImpl extends Application
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
Localizer.getInstance().setLocale(this);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
|
||||
Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
|
||||
|
||||
/*
|
||||
* Ignore Play Services if it uses this package name and public key - it's
|
||||
* Ignore Play Services if it uses this package name and public key - it's
|
||||
* effectively a system app, but not flagged as such on older systems
|
||||
*/
|
||||
private static final String PLAY_SERVICES_PACKAGE =
|
||||
@@ -108,7 +108,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
|
||||
Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
|
||||
Collections.emptySet());
|
||||
List<AppDetails> apps = new ArrayList<>();
|
||||
List<PackageInfo> packageInfos =
|
||||
@SuppressLint("QueryPermissionsNeeded") List<PackageInfo> packageInfos =
|
||||
pm.getInstalledPackages(GET_PERMISSIONS);
|
||||
for (PackageInfo packageInfo : packageInfos) {
|
||||
if (!allowed.contains(packageInfo.packageName)
|
||||
|
||||
@@ -60,12 +60,14 @@ import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment;
|
||||
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity;
|
||||
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationModule;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupListFragment;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupListModule;
|
||||
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity;
|
||||
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule;
|
||||
import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule;
|
||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsFragment;
|
||||
import org.briarproject.briar.android.reporting.CrashFragment;
|
||||
import org.briarproject.briar.android.reporting.CrashReportActivity;
|
||||
import org.briarproject.briar.android.reporting.ReportFormFragment;
|
||||
import org.briarproject.briar.android.settings.SettingsActivity;
|
||||
import org.briarproject.briar.android.settings.SettingsFragment;
|
||||
import org.briarproject.briar.android.sharing.BlogInvitationActivity;
|
||||
@@ -91,7 +93,6 @@ import dagger.Component;
|
||||
ForumModule.class,
|
||||
GroupInvitationModule.class,
|
||||
GroupConversationModule.class,
|
||||
GroupListModule.class,
|
||||
GroupMemberModule.class,
|
||||
GroupRevealModule.class,
|
||||
SharingModule.class
|
||||
@@ -184,6 +185,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(PendingContactListActivity activity);
|
||||
|
||||
void inject(CrashReportActivity crashReportActivity);
|
||||
|
||||
// Fragments
|
||||
|
||||
void inject(AuthorNameFragment fragment);
|
||||
@@ -234,4 +237,8 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(ImageFragment imageFragment);
|
||||
|
||||
void inject(ReportFormFragment reportFormFragment);
|
||||
|
||||
void inject(CrashFragment crashFragment);
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
@@ -18,7 +17,6 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
|
||||
import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
||||
import org.briarproject.briar.android.reporting.DevReportActivity;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout;
|
||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener;
|
||||
@@ -40,9 +38,11 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
||||
import static androidx.lifecycle.Lifecycle.State.STARTED;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
|
||||
@@ -50,7 +50,6 @@ import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
||||
|
||||
/**
|
||||
* Warning: Some activities don't extend {@link BaseActivity}.
|
||||
* E.g. {@link DevReportActivity}
|
||||
*/
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -123,6 +122,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
return new ActivityModule(this);
|
||||
}
|
||||
|
||||
// TODO use a test module where this is used in tests
|
||||
protected ForumModule getForumModule() {
|
||||
return new ForumModule();
|
||||
}
|
||||
@@ -202,9 +202,15 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
// If the dialog is already visible, filter the tap
|
||||
ScreenFilterDialogFragment f = findDialogFragment();
|
||||
if (f != null && f.isVisible()) return false;
|
||||
Collection<AppDetails> apps = screenFilterMonitor.getApps();
|
||||
// If all overlay apps have been allowed, allow the tap
|
||||
if (apps.isEmpty()) return true;
|
||||
Collection<AppDetails> apps;
|
||||
// querying all apps is only possible at API 29 and below
|
||||
if (SDK_INT <= 29) {
|
||||
apps = screenFilterMonitor.getApps();
|
||||
// If all overlay apps have been allowed, allow the tap
|
||||
if (apps.isEmpty()) return true;
|
||||
} else {
|
||||
apps = emptyList();
|
||||
}
|
||||
// Show dialog unless onSaveInstanceState() has been called, see #1112
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
if (!fm.isStateSaved()) {
|
||||
@@ -241,7 +247,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void handleDbException(DbException e) {
|
||||
public void handleException(Exception e) {
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
@@ -266,7 +272,12 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
private void protectToolbar() {
|
||||
findToolbar();
|
||||
if (toolbar != null) {
|
||||
boolean filter = !screenFilterMonitor.getApps().isEmpty();
|
||||
boolean filter;
|
||||
if (SDK_INT <= 29) {
|
||||
filter = !screenFilterMonitor.getApps().isEmpty();
|
||||
} else {
|
||||
filter = true;
|
||||
}
|
||||
UiUtils.setFilterTouchesWhenObscured(toolbar, filter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +129,6 @@ public abstract class BriarActivity extends BaseActivity {
|
||||
lockManager.onActivityStop();
|
||||
}
|
||||
|
||||
protected boolean signedIn() {
|
||||
return briarController.accountSignedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transition animations.
|
||||
*
|
||||
|
||||
@@ -232,7 +232,7 @@ public class BlogFragment extends BaseFragment
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -277,7 +277,7 @@ public class BlogFragment extends BaseFragment
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -296,7 +296,7 @@ public class BlogFragment extends BaseFragment
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -318,7 +318,7 @@ public class BlogFragment extends BaseFragment
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -398,7 +398,7 @@ public class BlogFragment extends BaseFragment
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class BlogPostFragment extends BasePostFragment implements BlogListener {
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ public class FeedFragment extends BaseFragment implements
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -187,7 +187,7 @@ public class FeedFragment extends BaseFragment implements
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -242,7 +242,7 @@ public class FeedFragment extends BaseFragment implements
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -79,7 +79,7 @@ public class FeedPostFragment extends BasePostFragment {
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
||||
new UiExceptionHandler<DbException>(this) {
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
finish();
|
||||
|
||||
@@ -9,7 +9,11 @@ import org.briarproject.bramble.api.contact.PendingContact;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchPendingContactException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
||||
import org.briarproject.briar.android.viewmodel.LiveResult;
|
||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||
@@ -21,7 +25,6 @@ import java.util.logging.Logger;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
@@ -31,14 +34,12 @@ import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_R
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
public class AddContactViewModel extends AndroidViewModel {
|
||||
public class AddContactViewModel extends DbViewModel {
|
||||
|
||||
private final static Logger LOG =
|
||||
getLogger(AddContactViewModel.class.getName());
|
||||
|
||||
private final ContactManager contactManager;
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
|
||||
private final MutableLiveData<String> handshakeLink =
|
||||
new MutableLiveData<>();
|
||||
@@ -52,10 +53,12 @@ public class AddContactViewModel extends AndroidViewModel {
|
||||
@Inject
|
||||
AddContactViewModel(Application application,
|
||||
ContactManager contactManager,
|
||||
@DatabaseExecutor Executor dbExecutor) {
|
||||
super(application);
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor) {
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.contactManager = contactManager;
|
||||
this.dbExecutor = dbExecutor;
|
||||
}
|
||||
|
||||
void onCreate() {
|
||||
@@ -63,7 +66,7 @@ public class AddContactViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
private void loadHandshakeLink() {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
handshakeLink.postValue(contactManager.getHandshakeLink());
|
||||
} catch (DbException e) {
|
||||
@@ -102,7 +105,7 @@ public class AddContactViewModel extends AndroidViewModel {
|
||||
|
||||
void addContact(String nickname) {
|
||||
if (remoteHandshakeLink == null) throw new IllegalStateException();
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
contactManager.addPendingContact(remoteHandshakeLink, nickname);
|
||||
addContactResult.postValue(new LiveResult<>(true));
|
||||
@@ -122,11 +125,11 @@ public class AddContactViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public void updatePendingContact(String name, PendingContact p) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
contactManager.removePendingContact(p.getId());
|
||||
addContact(name);
|
||||
} catch(NoSuchPendingContactException e) {
|
||||
} catch (NoSuchPendingContactException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
// no error in UI as pending contact was converted into contact
|
||||
} catch (DbException e) {
|
||||
|
||||
@@ -11,12 +11,16 @@ import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent;
|
||||
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.rendezvous.RendezvousPoller;
|
||||
import org.briarproject.bramble.api.rendezvous.event.RendezvousPollEvent;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -26,7 +30,6 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
@@ -36,14 +39,12 @@ import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
public class PendingContactListViewModel extends AndroidViewModel
|
||||
public class PendingContactListViewModel extends DbViewModel
|
||||
implements EventListener {
|
||||
|
||||
private final Logger LOG =
|
||||
getLogger(PendingContactListViewModel.class.getName());
|
||||
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
private final ContactManager contactManager;
|
||||
private final RendezvousPoller rendezvousPoller;
|
||||
private final EventBus eventBus;
|
||||
@@ -56,11 +57,13 @@ public class PendingContactListViewModel extends AndroidViewModel
|
||||
@Inject
|
||||
PendingContactListViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
ContactManager contactManager,
|
||||
RendezvousPoller rendezvousPoller,
|
||||
EventBus eventBus) {
|
||||
super(application);
|
||||
this.dbExecutor = dbExecutor;
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.contactManager = contactManager;
|
||||
this.rendezvousPoller = rendezvousPoller;
|
||||
this.eventBus = eventBus;
|
||||
@@ -87,7 +90,7 @@ public class PendingContactListViewModel extends AndroidViewModel
|
||||
}
|
||||
|
||||
private void loadPendingContacts() {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Collection<Pair<PendingContact, PendingContactState>> pairs =
|
||||
contactManager.getPendingContacts();
|
||||
@@ -113,7 +116,7 @@ public class PendingContactListViewModel extends AndroidViewModel
|
||||
}
|
||||
|
||||
void removePendingContact(PendingContactId id) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
contactManager.removePendingContact(id);
|
||||
} catch (DbException e) {
|
||||
|
||||
@@ -133,7 +133,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.android.controller;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@Deprecated
|
||||
@NotNullByDefault
|
||||
public interface DbController {
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@Immutable
|
||||
@Deprecated
|
||||
@NotNullByDefault
|
||||
public class DbControllerImpl implements DbController {
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.selection.Selection;
|
||||
import androidx.recyclerview.selection.SelectionPredicates;
|
||||
import androidx.recyclerview.selection.SelectionTracker;
|
||||
@@ -224,8 +223,9 @@ public class ConversationActivity extends BriarActivity
|
||||
if (id == -1) throw new IllegalStateException();
|
||||
contactId = new ContactId(id);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory)
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||
.get(ConversationViewModel.class);
|
||||
viewModel.setContactId(contactId);
|
||||
attachmentRetriever = viewModel.getAttachmentRetriever();
|
||||
|
||||
setContentView(R.layout.activity_conversation);
|
||||
@@ -330,16 +330,6 @@ public class ConversationActivity extends BriarActivity
|
||||
list.startPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Trigger loading of contact data, noop if data was loaded already.
|
||||
//
|
||||
// We can only start loading data *after* we are sure
|
||||
// the user has signed in. After sign-in, onCreate() isn't run again.
|
||||
if (signedIn()) viewModel.setContactId(contactId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
|
||||
@@ -15,17 +15,20 @@ import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.attachment.AttachmentCreator;
|
||||
import org.briarproject.briar.android.attachment.AttachmentManager;
|
||||
import org.briarproject.briar.android.attachment.AttachmentResult;
|
||||
import org.briarproject.briar.android.attachment.AttachmentRetriever;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
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.messaging.AttachmentHeader;
|
||||
@@ -44,7 +47,6 @@ import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
@@ -59,10 +61,10 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_
|
||||
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ConversationViewModel extends AndroidViewModel
|
||||
public class ConversationViewModel extends DbViewModel
|
||||
implements EventListener, AttachmentManager {
|
||||
|
||||
private static Logger LOG =
|
||||
private static final Logger LOG =
|
||||
getLogger(ConversationViewModel.class.getName());
|
||||
|
||||
private static final String SHOW_ONBOARDING_IMAGE =
|
||||
@@ -70,8 +72,6 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
private static final String SHOW_ONBOARDING_INTRODUCTION =
|
||||
"showOnboardingIntroduction";
|
||||
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
private final TransactionManager db;
|
||||
private final EventBus eventBus;
|
||||
private final MessagingManager messagingManager;
|
||||
@@ -105,7 +105,9 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
@Inject
|
||||
ConversationViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
EventBus eventBus,
|
||||
MessagingManager messagingManager,
|
||||
ContactManager contactManager,
|
||||
@@ -113,8 +115,7 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
PrivateMessageFactory privateMessageFactory,
|
||||
AttachmentRetriever attachmentRetriever,
|
||||
AttachmentCreator attachmentCreator) {
|
||||
super(application);
|
||||
this.dbExecutor = dbExecutor;
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.db = db;
|
||||
this.eventBus = eventBus;
|
||||
this.messagingManager = messagingManager;
|
||||
@@ -143,7 +144,7 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
|
||||
if (a.getContactId().equals(contactId)) {
|
||||
LOG.info("Attachment received");
|
||||
dbExecutor.execute(() -> attachmentRetriever
|
||||
runOnDbThread(() -> attachmentRetriever
|
||||
.loadAttachmentItem(a.getMessageId()));
|
||||
}
|
||||
}
|
||||
@@ -163,7 +164,7 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
}
|
||||
|
||||
private void loadContact(ContactId contactId) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
Contact c = contactManager.getContact(contactId);
|
||||
@@ -181,7 +182,7 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
}
|
||||
|
||||
void markMessageRead(GroupId g, MessageId m) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.setReadFlag(g, m, true);
|
||||
@@ -193,7 +194,7 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
}
|
||||
|
||||
void setContactAlias(String alias) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
contactManager.setContactAlias(requireNonNull(contactId),
|
||||
alias.isEmpty() ? null : alias);
|
||||
@@ -296,7 +297,7 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
@UiThread
|
||||
private void storeMessage(PrivateMessage m) {
|
||||
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.addLocalMessage(m);
|
||||
@@ -356,7 +357,7 @@ public class ConversationViewModel extends AndroidViewModel
|
||||
|
||||
@UiThread
|
||||
void recheckFeaturesAndOnboarding(ContactId contactId) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
checkFeaturesAndOnboarding(contactId);
|
||||
} catch (DbException e) {
|
||||
|
||||
@@ -7,13 +7,17 @@ import android.view.View;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.attachment.AttachmentItem;
|
||||
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.messaging.Attachment;
|
||||
@@ -37,7 +41,6 @@ import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
|
||||
import static android.media.MediaScannerConnection.scanFile;
|
||||
import static android.os.Environment.DIRECTORY_PICTURES;
|
||||
@@ -49,20 +52,18 @@ import static org.briarproject.bramble.util.IoUtils.copyAndClose;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ImageViewModel extends AndroidViewModel implements EventListener {
|
||||
public class ImageViewModel extends DbViewModel implements EventListener {
|
||||
|
||||
private static Logger LOG = getLogger(ImageViewModel.class.getName());
|
||||
private static final Logger LOG = getLogger(ImageViewModel.class.getName());
|
||||
|
||||
private final MessagingManager messagingManager;
|
||||
private final EventBus eventBus;
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
@IoExecutor
|
||||
private final Executor ioExecutor;
|
||||
|
||||
private boolean receivedAttachmentsInitialized = false;
|
||||
private HashMap<MessageId, MutableLiveEvent<Boolean>> receivedAttachments =
|
||||
new HashMap<>();
|
||||
private final HashMap<MessageId, MutableLiveEvent<Boolean>>
|
||||
receivedAttachments = new HashMap<>();
|
||||
|
||||
/**
|
||||
* true means there was an error saving the image, false if image was saved.
|
||||
@@ -75,13 +76,16 @@ public class ImageViewModel extends AndroidViewModel implements EventListener {
|
||||
|
||||
@Inject
|
||||
ImageViewModel(Application application,
|
||||
MessagingManager messagingManager, EventBus eventBus,
|
||||
MessagingManager messagingManager,
|
||||
EventBus eventBus,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
@IoExecutor Executor ioExecutor) {
|
||||
super(application);
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.messagingManager = messagingManager;
|
||||
this.eventBus = eventBus;
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.ioExecutor = ioExecutor;
|
||||
|
||||
eventBus.addListener(this);
|
||||
@@ -195,7 +199,7 @@ public class ImageViewModel extends AndroidViewModel implements EventListener {
|
||||
|
||||
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
|
||||
@Nullable Runnable afterCopy) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Attachment a =
|
||||
messagingManager.getAttachment(attachment.getHeader());
|
||||
|
||||
@@ -38,7 +38,7 @@ import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEX
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ForumActivity extends
|
||||
ThreadListActivity<Forum, ForumItem, ThreadItemAdapter<ForumItem>>
|
||||
ThreadListActivity<Forum, ForumPostItem, ThreadItemAdapter<ForumPostItem>>
|
||||
implements ForumListener {
|
||||
|
||||
@Inject
|
||||
@@ -50,7 +50,7 @@ public class ForumActivity extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadListController<Forum, ForumItem> getController() {
|
||||
protected ThreadListController<Forum, ForumPostItem> getController() {
|
||||
return forumController;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public class ForumActivity extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadItemAdapter<ForumItem> createAdapter(
|
||||
protected ThreadItemAdapter<ForumPostItem> createAdapter(
|
||||
LinearLayoutManager layoutManager) {
|
||||
return new ThreadItemAdapter<>(this, layoutManager);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ public class ForumActivity extends
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import org.briarproject.briar.api.forum.Forum;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@NotNullByDefault
|
||||
interface ForumController extends ThreadListController<Forum, ForumItem> {
|
||||
interface ForumController extends ThreadListController<Forum, ForumPostItem> {
|
||||
|
||||
interface ForumListener extends ThreadListListener<ForumItem> {
|
||||
interface ForumListener extends ThreadListListener<ForumPostItem> {
|
||||
@UiThread
|
||||
void onForumLeft(ContactId c);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
class ForumControllerImpl extends
|
||||
ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost, ForumListener>
|
||||
ThreadListControllerImpl<Forum, ForumPostItem, ForumPostHeader, ForumPost, ForumListener>
|
||||
implements ForumController {
|
||||
|
||||
private static final Logger LOG =
|
||||
@@ -138,8 +138,8 @@ class ForumControllerImpl extends
|
||||
|
||||
@Override
|
||||
public void createAndStoreMessage(String text,
|
||||
@Nullable ForumItem parentItem,
|
||||
ResultExceptionHandler<ForumItem, DbException> handler) {
|
||||
@Nullable ForumPostItem parentItem,
|
||||
ResultExceptionHandler<ForumPostItem, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
@@ -158,7 +158,7 @@ class ForumControllerImpl extends
|
||||
|
||||
private void createMessage(String text, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author,
|
||||
ResultExceptionHandler<ForumItem, DbException> handler) {
|
||||
ResultExceptionHandler<ForumPostItem, DbException> handler) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
LOG.info("Creating forum post...");
|
||||
ForumPost msg = forumManager.createLocalPost(getGroupId(), text,
|
||||
@@ -178,8 +178,8 @@ class ForumControllerImpl extends
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ForumItem buildItem(ForumPostHeader header, String text) {
|
||||
return new ForumItem(header, text);
|
||||
protected ForumPostItem buildItem(ForumPostHeader header, String text) {
|
||||
return new ForumPostItem(header, text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,134 +1,47 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.BriarAdapter;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
import org.briarproject.briar.android.view.TextAvatarView;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static androidx.recyclerview.widget.SortedList.INVALID_POSITION;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME;
|
||||
@NotNullByDefault
|
||||
class ForumListAdapter extends ListAdapter<ForumListItem, ForumViewHolder> {
|
||||
|
||||
class ForumListAdapter
|
||||
extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> {
|
||||
|
||||
ForumListAdapter(Context ctx) {
|
||||
super(ctx, ForumListItem.class);
|
||||
ForumListAdapter() {
|
||||
super(new ForumListCallback());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(ctx).inflate(
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(
|
||||
R.layout.list_item_forum, parent, false);
|
||||
return new ForumViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ForumViewHolder ui, int position) {
|
||||
ForumListItem item = getItemAt(position);
|
||||
if (item == null) return;
|
||||
public void onBindViewHolder(ForumViewHolder viewHolder, int position) {
|
||||
viewHolder.bind(getItem(position));
|
||||
}
|
||||
|
||||
// Avatar
|
||||
ui.avatar.setText(item.getForum().getName().substring(0, 1));
|
||||
ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes());
|
||||
ui.avatar.setUnreadCount(item.getUnreadCount());
|
||||
|
||||
// Forum Name
|
||||
ui.name.setText(item.getForum().getName());
|
||||
|
||||
// Post Count
|
||||
int postCount = item.getPostCount();
|
||||
if (postCount > 0) {
|
||||
ui.postCount.setText(ctx.getResources()
|
||||
.getQuantityString(R.plurals.posts, postCount,
|
||||
postCount));
|
||||
} else {
|
||||
ui.postCount.setText(ctx.getString(R.string.no_posts));
|
||||
@NotNullByDefault
|
||||
private static class ForumListCallback extends ItemCallback<ForumListItem> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
// Date
|
||||
if (item.isEmpty()) {
|
||||
ui.date.setVisibility(GONE);
|
||||
} else {
|
||||
long timestamp = item.getTimestamp();
|
||||
ui.date.setText(UiUtils.formatDate(ctx, timestamp));
|
||||
ui.date.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
// Open Forum on Click
|
||||
ui.layout.setOnClickListener(v -> {
|
||||
Intent i = new Intent(ctx, ForumActivity.class);
|
||||
Forum f = item.getForum();
|
||||
i.putExtra(GROUP_ID, f.getId().getBytes());
|
||||
i.putExtra(GROUP_NAME, f.getName());
|
||||
ctx.startActivity(i);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(ForumListItem a, ForumListItem b) {
|
||||
if (a == b) return 0;
|
||||
// The forum with the newest message comes first
|
||||
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
|
||||
if (aTime > bTime) return -1;
|
||||
if (aTime < bTime) return 1;
|
||||
// Break ties by forum name
|
||||
String aName = a.getForum().getName();
|
||||
String bName = b.getForum().getName();
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
|
||||
return a.isEmpty() == b.isEmpty() &&
|
||||
a.getTimestamp() == b.getTimestamp() &&
|
||||
a.getUnreadCount() == b.getUnreadCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
|
||||
return a.getForum().equals(b.getForum());
|
||||
}
|
||||
|
||||
int findItemPosition(GroupId g) {
|
||||
int count = getItemCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
ForumListItem item = getItemAt(i);
|
||||
if (item != null && item.getForum().getGroup().getId().equals(g))
|
||||
return i;
|
||||
}
|
||||
return INVALID_POSITION; // Not found
|
||||
}
|
||||
|
||||
static class ForumViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ViewGroup layout;
|
||||
private final TextAvatarView avatar;
|
||||
private final TextView name;
|
||||
private final TextView postCount;
|
||||
private final TextView date;
|
||||
|
||||
private ForumViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
layout = (ViewGroup) v;
|
||||
avatar = v.findViewById(R.id.avatarView);
|
||||
name = v.findViewById(R.id.forumNameView);
|
||||
postCount = v.findViewById(R.id.postCountView);
|
||||
date = v.findViewById(R.id.dateView);
|
||||
@Override
|
||||
public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
|
||||
return a.isEmpty() == b.isEmpty() &&
|
||||
a.getTimestamp() == b.getTimestamp() &&
|
||||
a.getUnreadCount() == b.getUnreadCount();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,80 +12,48 @@ import android.view.ViewGroup;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchGroupException;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseEventFragment;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.sharing.ForumInvitationActivity;
|
||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.api.forum.ForumManager;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
import org.briarproject.briar.api.forum.ForumSharingManager;
|
||||
import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent;
|
||||
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ForumListFragment extends BaseEventFragment implements
|
||||
public class ForumListFragment extends BaseFragment implements
|
||||
OnClickListener {
|
||||
|
||||
public final static String TAG = ForumListFragment.class.getName();
|
||||
private final static Logger LOG = Logger.getLogger(TAG);
|
||||
|
||||
private ForumListViewModel viewModel;
|
||||
private BriarRecyclerView list;
|
||||
private ForumListAdapter adapter;
|
||||
private Snackbar snackbar;
|
||||
private final ForumListAdapter adapter = new ForumListAdapter();
|
||||
|
||||
@Inject
|
||||
AndroidNotificationManager notificationManager;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
volatile ForumManager forumManager;
|
||||
@Inject
|
||||
volatile ForumSharingManager forumSharingManager;
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
public static ForumListFragment newInstance() {
|
||||
|
||||
Bundle args = new Bundle();
|
||||
|
||||
ForumListFragment fragment = new ForumListFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
return new ForumListFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||
.get(ForumListViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -93,24 +61,35 @@ public class ForumListFragment extends BaseEventFragment implements
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
||||
requireActivity().setTitle(R.string.forums_button);
|
||||
|
||||
View contentView =
|
||||
inflater.inflate(R.layout.fragment_forum_list, container,
|
||||
false);
|
||||
View v = inflater.inflate(R.layout.fragment_forum_list, container,
|
||||
false);
|
||||
|
||||
adapter = new ForumListAdapter(getActivity());
|
||||
|
||||
list = contentView.findViewById(R.id.forumList);
|
||||
list = v.findViewById(R.id.forumList);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setAdapter(adapter);
|
||||
viewModel.getForumListItems().observe(getViewLifecycleOwner(), result ->
|
||||
result.onError(this::handleException).onSuccess(items -> {
|
||||
adapter.submitList(items);
|
||||
if (requireNonNull(items).size() == 0) list.showData();
|
||||
})
|
||||
);
|
||||
|
||||
snackbar = new BriarSnackbarBuilder()
|
||||
.setAction(R.string.show, this)
|
||||
.make(list, "", LENGTH_INDEFINITE);
|
||||
viewModel.getNumInvitations().observe(getViewLifecycleOwner(), num -> {
|
||||
if (num == 0) {
|
||||
snackbar.dismiss();
|
||||
} else {
|
||||
snackbar.setText(getResources().getQuantityString(
|
||||
R.plurals.forums_shared, num, num));
|
||||
if (!snackbar.isShownOrQueued()) snackbar.show();
|
||||
}
|
||||
});
|
||||
|
||||
return contentView;
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,18 +100,23 @@ public class ForumListFragment extends BaseEventFragment implements
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
notificationManager.clearAllForumPostNotifications();
|
||||
loadForums();
|
||||
loadAvailableForums();
|
||||
viewModel.blockAllForumPostNotifications();
|
||||
viewModel.clearAllForumPostNotifications();
|
||||
// The attributes and sorting of the forums may have changed while we
|
||||
// were stopped and we have no way finding out about them, so re-load
|
||||
// e.g. less unread posts in a forum after viewing it.
|
||||
viewModel.loadForums();
|
||||
// The number of invitations might have changed while we were stopped
|
||||
// e.g. because of accepting an invitation which does not trigger event
|
||||
viewModel.loadForumInvitations();
|
||||
list.startPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
adapter.clear();
|
||||
list.showProgressBar();
|
||||
list.stopPeriodicUpdate();
|
||||
viewModel.unblockAllForumPostNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,123 +128,12 @@ public class ForumListFragment extends BaseEventFragment implements
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle presses on the action bar items
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_create_forum:
|
||||
Intent intent =
|
||||
new Intent(getContext(), CreateForumActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.action_create_forum) {
|
||||
Intent intent = new Intent(getContext(), CreateForumActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadForums() {
|
||||
int revision = adapter.getRevision();
|
||||
listener.runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
Collection<ForumListItem> forums = new ArrayList<>();
|
||||
for (Forum f : forumManager.getForums()) {
|
||||
try {
|
||||
GroupCount count =
|
||||
forumManager.getGroupCount(f.getId());
|
||||
forums.add(new ForumListItem(f, count));
|
||||
} catch (NoSuchGroupException e) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
logDuration(LOG, "Full load", start);
|
||||
displayForums(revision, forums);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayForums(int revision, Collection<ForumListItem> forums) {
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
if (revision == adapter.getRevision()) {
|
||||
adapter.incrementRevision();
|
||||
if (forums.isEmpty()) list.showData();
|
||||
else adapter.replaceAll(forums);
|
||||
} else {
|
||||
LOG.info("Concurrent update, reloading");
|
||||
loadForums();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAvailableForums() {
|
||||
listener.runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
int available = forumSharingManager.getInvitations().size();
|
||||
logDuration(LOG, "Loading available", start);
|
||||
displayAvailableForums(available);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayAvailableForums(int availableCount) {
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
if (availableCount == 0) {
|
||||
snackbar.dismiss();
|
||||
} else {
|
||||
snackbar.setText(getResources().getQuantityString(
|
||||
R.plurals.forums_shared, availableCount,
|
||||
availableCount));
|
||||
if (!snackbar.isShownOrQueued()) snackbar.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof ContactRemovedEvent) {
|
||||
LOG.info("Contact removed, reloading available forums");
|
||||
loadAvailableForums();
|
||||
} else if (e instanceof GroupAddedEvent) {
|
||||
GroupAddedEvent g = (GroupAddedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
|
||||
LOG.info("Forum added, reloading forums");
|
||||
loadForums();
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
|
||||
LOG.info("Forum removed, removing from list");
|
||||
removeForum(g.getGroup().getId());
|
||||
}
|
||||
} else if (e instanceof ForumPostReceivedEvent) {
|
||||
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
||||
LOG.info("Forum post added, updating item");
|
||||
updateItem(f.getGroupId(), f.getHeader());
|
||||
} else if (e instanceof ForumInvitationRequestReceivedEvent) {
|
||||
LOG.info("Forum invitation received, reloading available forums");
|
||||
loadAvailableForums();
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void updateItem(GroupId g, ForumPostHeader m) {
|
||||
adapter.incrementRevision();
|
||||
int position = adapter.findItemPosition(g);
|
||||
ForumListItem item = adapter.getItemAt(position);
|
||||
if (item != null) {
|
||||
item.addHeader(m);
|
||||
adapter.updateItemAt(position, item);
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void removeForum(GroupId g) {
|
||||
adapter.incrementRevision();
|
||||
int position = adapter.findItemPosition(g);
|
||||
ForumListItem item = adapter.getItemAt(position);
|
||||
if (item != null) adapter.remove(item);
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,12 +4,16 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
|
||||
// This class is NOT thread-safe
|
||||
class ForumListItem {
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@Immutable
|
||||
class ForumListItem implements Comparable<ForumListItem> {
|
||||
|
||||
private final Forum forum;
|
||||
private int postCount, unread;
|
||||
private long timestamp;
|
||||
private final int postCount, unread;
|
||||
private final long timestamp;
|
||||
|
||||
ForumListItem(Forum forum, GroupCount count) {
|
||||
this.forum = forum;
|
||||
@@ -18,10 +22,11 @@ class ForumListItem {
|
||||
this.timestamp = count.getLatestMsgTime();
|
||||
}
|
||||
|
||||
void addHeader(ForumPostHeader h) {
|
||||
postCount++;
|
||||
if (!h.isRead()) unread++;
|
||||
if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
|
||||
ForumListItem(ForumListItem item, ForumPostHeader h) {
|
||||
this.forum = item.forum;
|
||||
this.postCount = item.postCount + 1;
|
||||
this.unread = item.unread + (h.isRead() ? 0 : 1);
|
||||
this.timestamp = Math.max(item.timestamp, h.getTimestamp());
|
||||
}
|
||||
|
||||
Forum getForum() {
|
||||
@@ -43,4 +48,29 @@ class ForumListItem {
|
||||
int getUnreadCount() {
|
||||
return unread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return forum.getId().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
return o instanceof ForumListItem && getForum().equals(
|
||||
((ForumListItem) o).getForum());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ForumListItem o) {
|
||||
if (this == o) return 0;
|
||||
// The forum with the newest message comes first
|
||||
long aTime = getTimestamp(), bTime = o.getTimestamp();
|
||||
if (aTime > bTime) return -1;
|
||||
if (aTime < bTime) return 1;
|
||||
// Break ties by forum name
|
||||
String aName = getForum().getName();
|
||||
String bName = o.getForum().getName();
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
import org.briarproject.briar.android.viewmodel.LiveResult;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.api.forum.ForumManager;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
import org.briarproject.briar.api.forum.ForumSharingManager;
|
||||
import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent;
|
||||
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class ForumListViewModel extends DbViewModel implements EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ForumListViewModel.class.getName());
|
||||
|
||||
private final ForumManager forumManager;
|
||||
private final ForumSharingManager forumSharingManager;
|
||||
private final AndroidNotificationManager notificationManager;
|
||||
private final EventBus eventBus;
|
||||
|
||||
private final MutableLiveData<LiveResult<List<ForumListItem>>> forumItems =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Integer> numInvitations =
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
ForumListViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
ForumManager forumManager,
|
||||
ForumSharingManager forumSharingManager,
|
||||
AndroidNotificationManager notificationManager, EventBus eventBus) {
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.forumManager = forumManager;
|
||||
this.forumSharingManager = forumSharingManager;
|
||||
this.notificationManager = notificationManager;
|
||||
this.eventBus = eventBus;
|
||||
this.eventBus.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
void clearAllForumPostNotifications() {
|
||||
notificationManager.clearAllForumPostNotifications();
|
||||
}
|
||||
|
||||
void blockAllForumPostNotifications() {
|
||||
notificationManager.blockAllForumPostNotifications();
|
||||
}
|
||||
|
||||
void unblockAllForumPostNotifications() {
|
||||
notificationManager.unblockAllForumPostNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof ContactRemovedEvent) {
|
||||
LOG.info("Contact removed, reloading available forums");
|
||||
loadForumInvitations();
|
||||
} else if (e instanceof ForumInvitationRequestReceivedEvent) {
|
||||
LOG.info("Forum invitation received, reloading available forums");
|
||||
loadForumInvitations();
|
||||
} else if (e instanceof GroupAddedEvent) {
|
||||
GroupAddedEvent g = (GroupAddedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
|
||||
LOG.info("Forum added, reloading forums");
|
||||
loadForums();
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
|
||||
LOG.info("Forum removed, removing from list");
|
||||
onGroupRemoved(g.getGroup().getId());
|
||||
}
|
||||
} else if (e instanceof ForumPostReceivedEvent) {
|
||||
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
||||
LOG.info("Forum post added, updating item");
|
||||
onForumPostReceived(f.getGroupId(), f.getHeader());
|
||||
}
|
||||
}
|
||||
|
||||
public void loadForums() {
|
||||
loadList(this::loadForums, forumItems::setValue);
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
private List<ForumListItem> loadForums(Transaction txn) throws DbException {
|
||||
long start = now();
|
||||
List<ForumListItem> forums = new ArrayList<>();
|
||||
for (Forum f : forumManager.getForums(txn)) {
|
||||
GroupCount count = forumManager.getGroupCount(txn, f.getId());
|
||||
forums.add(new ForumListItem(f, count));
|
||||
}
|
||||
Collections.sort(forums);
|
||||
logDuration(LOG, "Loading forums", start);
|
||||
return forums;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onForumPostReceived(GroupId g, ForumPostHeader header) {
|
||||
List<ForumListItem> list = updateListItems(forumItems,
|
||||
itemToTest -> itemToTest.getForum().getId().equals(g),
|
||||
itemToUpdate -> new ForumListItem(itemToUpdate, header));
|
||||
if (list == null) return;
|
||||
// re-sort as the order of items may have changed
|
||||
Collections.sort(list);
|
||||
forumItems.setValue(new LiveResult<>(list));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onGroupRemoved(GroupId groupId) {
|
||||
List<ForumListItem> list = removeListItems(forumItems, i ->
|
||||
i.getForum().getId().equals(groupId)
|
||||
);
|
||||
if (list == null) return;
|
||||
forumItems.setValue(new LiveResult<>(list));
|
||||
}
|
||||
|
||||
void loadForumInvitations() {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
int available = forumSharingManager.getInvitations().size();
|
||||
logDuration(LOG, "Loading available", start);
|
||||
numInvitations.postValue(available);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
LiveData<LiveResult<List<ForumListItem>>> getForumListItems() {
|
||||
return forumItems;
|
||||
}
|
||||
|
||||
LiveData<Integer> getNumInvitations() {
|
||||
return numInvitations;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,13 +2,25 @@ package org.briarproject.briar.android.forum;
|
||||
|
||||
import org.briarproject.briar.android.activity.ActivityScope;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
@Module
|
||||
public class ForumModule {
|
||||
|
||||
@Module
|
||||
public interface BindsModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ForumListViewModel.class)
|
||||
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ForumController provideForumController(BaseActivity activity,
|
||||
|
||||
@@ -10,15 +10,15 @@ import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
@NotThreadSafe
|
||||
class ForumItem extends ThreadItem {
|
||||
class ForumPostItem extends ThreadItem {
|
||||
|
||||
ForumItem(ForumPostHeader h, String text) {
|
||||
ForumPostItem(ForumPostHeader h, String text) {
|
||||
super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
|
||||
h.getAuthorInfo(), h.isRead());
|
||||
}
|
||||
|
||||
ForumItem(MessageId messageId, @Nullable MessageId parentId, String text,
|
||||
long timestamp, Author author, AuthorInfo authorInfo) {
|
||||
ForumPostItem(MessageId messageId, @Nullable MessageId parentId,
|
||||
String text, long timestamp, Author author, AuthorInfo authorInfo) {
|
||||
super(messageId, parentId, text, timestamp, author, authorInfo, true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
import org.briarproject.briar.android.view.TextAvatarView;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME;
|
||||
|
||||
class ForumViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final Context ctx;
|
||||
private final ViewGroup layout;
|
||||
private final TextAvatarView avatar;
|
||||
private final TextView name;
|
||||
private final TextView postCount;
|
||||
private final TextView date;
|
||||
|
||||
ForumViewHolder(View v) {
|
||||
super(v);
|
||||
ctx = v.getContext();
|
||||
layout = (ViewGroup) v;
|
||||
avatar = v.findViewById(R.id.avatarView);
|
||||
name = v.findViewById(R.id.forumNameView);
|
||||
postCount = v.findViewById(R.id.postCountView);
|
||||
date = v.findViewById(R.id.dateView);
|
||||
}
|
||||
|
||||
void bind(ForumListItem item) {
|
||||
// Avatar
|
||||
avatar.setText(item.getForum().getName().substring(0, 1));
|
||||
avatar.setBackgroundBytes(item.getForum().getId().getBytes());
|
||||
avatar.setUnreadCount(item.getUnreadCount());
|
||||
|
||||
// Forum Name
|
||||
name.setText(item.getForum().getName());
|
||||
|
||||
// Post Count
|
||||
int count = item.getPostCount();
|
||||
if (count > 0) {
|
||||
postCount.setText(ctx.getResources()
|
||||
.getQuantityString(R.plurals.posts, count, count));
|
||||
} else {
|
||||
postCount.setText(ctx.getString(R.string.no_posts));
|
||||
}
|
||||
|
||||
// Date
|
||||
if (item.isEmpty()) {
|
||||
date.setVisibility(GONE);
|
||||
} else {
|
||||
long timestamp = item.getTimestamp();
|
||||
date.setText(UiUtils.formatDate(ctx, timestamp));
|
||||
date.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
// Open Forum on Click
|
||||
layout.setOnClickListener(v -> {
|
||||
Intent i = new Intent(ctx, ForumActivity.class);
|
||||
Forum f = item.getForum();
|
||||
i.putExtra(GROUP_ID, f.getId().getBytes());
|
||||
i.putExtra(GROUP_NAME, f.getName());
|
||||
ctx.startActivity(i);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.android.DestroyableContext;
|
||||
@@ -77,7 +76,7 @@ public abstract class BaseFragment extends Fragment
|
||||
void showNextFragment(BaseFragment f);
|
||||
|
||||
@UiThread
|
||||
void handleDbException(DbException e);
|
||||
void handleException(Exception e);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@@ -100,8 +99,8 @@ public abstract class BaseFragment extends Fragment
|
||||
}
|
||||
|
||||
@UiThread
|
||||
protected void handleDbException(DbException e) {
|
||||
listener.handleDbException(e);
|
||||
protected void handleException(Exception e) {
|
||||
listener.handleException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -28,6 +29,10 @@ import javax.inject.Inject;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION;
|
||||
import static android.view.View.GONE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ScreenFilterDialogFragment extends DialogFragment {
|
||||
@@ -37,7 +42,7 @@ public class ScreenFilterDialogFragment extends DialogFragment {
|
||||
@Inject
|
||||
ScreenFilterMonitor screenFilterMonitor;
|
||||
|
||||
DismissListener dismissListener = null;
|
||||
private DismissListener dismissListener = null;
|
||||
|
||||
public static ScreenFilterDialogFragment newInstance(
|
||||
Collection<AppDetails> apps) {
|
||||
@@ -83,10 +88,20 @@ public class ScreenFilterDialogFragment extends DialogFragment {
|
||||
View dialogView = inflater.inflate(R.layout.dialog_screen_filter, null);
|
||||
builder.setView(dialogView);
|
||||
TextView message = dialogView.findViewById(R.id.screen_filter_message);
|
||||
message.setText(getString(R.string.screen_filter_body,
|
||||
TextUtils.join("\n", appNames)));
|
||||
CheckBox allow = dialogView.findViewById(R.id.screen_filter_checkbox);
|
||||
builder.setNeutralButton(R.string.continue_button, (dialog, which) -> {
|
||||
if (SDK_INT <= 29) {
|
||||
message.setText(getString(R.string.screen_filter_body,
|
||||
TextUtils.join("\n", appNames)));
|
||||
} else {
|
||||
message.setText(R.string.screen_filter_body_api_30);
|
||||
allow.setVisibility(GONE);
|
||||
builder.setNeutralButton(R.string.screen_filter_review_apps,
|
||||
(dialog, which) -> {
|
||||
Intent i = new Intent(ACTION_MANAGE_OVERLAY_PERMISSION);
|
||||
startActivity(i);
|
||||
});
|
||||
}
|
||||
builder.setPositiveButton(R.string.continue_button, (dialog, which) -> {
|
||||
if (allow.isChecked()) screenFilterMonitor.allowApps(packageNames);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
@@ -10,14 +10,11 @@ import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
@@ -41,9 +38,6 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
||||
return f;
|
||||
}
|
||||
|
||||
@Inject
|
||||
AndroidExecutor androidExecutor;
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
@@ -88,8 +82,8 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
||||
}
|
||||
|
||||
private void triggerFeedback() {
|
||||
UiUtils.triggerFeedback(requireContext());
|
||||
finish();
|
||||
UiUtils.triggerFeedback(androidExecutor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import android.widget.TextView;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
@@ -365,7 +364,7 @@ public class NavDrawerActivity extends BriarActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDbException(DbException e) {
|
||||
public void handleException(Exception e) {
|
||||
// Do nothing for now
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@ import android.app.Application;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
@@ -14,7 +18,6 @@ import java.util.logging.Logger;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
@@ -28,7 +31,7 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_
|
||||
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
|
||||
|
||||
@NotNullByDefault
|
||||
public class NavDrawerViewModel extends AndroidViewModel {
|
||||
public class NavDrawerViewModel extends DbViewModel {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(NavDrawerViewModel.class.getName());
|
||||
@@ -37,8 +40,6 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
||||
private static final String SHOW_TRANSPORTS_ONBOARDING =
|
||||
"showTransportsOnboarding";
|
||||
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
private final SettingsManager settingsManager;
|
||||
|
||||
private final MutableLiveData<Boolean> showExpiryWarning =
|
||||
@@ -49,10 +50,13 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
NavDrawerViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
|
||||
NavDrawerViewModel(Application app,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
SettingsManager settingsManager) {
|
||||
super(app);
|
||||
this.dbExecutor = dbExecutor;
|
||||
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.settingsManager = settingsManager;
|
||||
}
|
||||
|
||||
@@ -62,7 +66,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
||||
|
||||
@UiThread
|
||||
void checkExpiryWarning() {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Settings settings =
|
||||
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||
@@ -97,7 +101,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
||||
@UiThread
|
||||
void expiryWarningDismissed() {
|
||||
showExpiryWarning.setValue(false);
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Settings settings = new Settings();
|
||||
int date = (int) (System.currentTimeMillis() / 1000L);
|
||||
@@ -120,7 +124,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
||||
shouldAskForDozeWhitelisting.setValue(false);
|
||||
return;
|
||||
}
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Settings settings =
|
||||
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||
@@ -141,7 +145,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
||||
@UiThread
|
||||
void checkTransportsOnboarding() {
|
||||
if (showTransportsOnboarding.getValue() != null) return;
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Settings settings =
|
||||
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||
@@ -157,7 +161,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
||||
@UiThread
|
||||
void transportsOnboardingShown() {
|
||||
showTransportsOnboarding.setValue(false);
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Settings settings = new Settings();
|
||||
settings.putBoolean(SHOW_TRANSPORTS_ONBOARDING, false);
|
||||
|
||||
@@ -9,9 +9,11 @@ import android.content.IntentFilter;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.network.NetworkManager;
|
||||
import org.briarproject.bramble.api.network.NetworkStatus;
|
||||
import org.briarproject.bramble.api.network.event.NetworkStatusEvent;
|
||||
@@ -27,6 +29,8 @@ import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
@@ -34,7 +38,6 @@ import java.util.logging.Logger;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
@@ -51,13 +54,12 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
|
||||
@NotNullByDefault
|
||||
public class PluginViewModel extends AndroidViewModel implements EventListener {
|
||||
public class PluginViewModel extends DbViewModel implements EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(PluginViewModel.class.getName());
|
||||
|
||||
private final Application app;
|
||||
private final Executor dbExecutor;
|
||||
private final SettingsManager settingsManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final EventBus eventBus;
|
||||
@@ -85,11 +87,12 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
||||
|
||||
@Inject
|
||||
PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
|
||||
SettingsManager settingsManager, PluginManager pluginManager,
|
||||
EventBus eventBus, NetworkManager networkManager) {
|
||||
super(app);
|
||||
LifecycleManager lifecycleManager, TransactionManager db,
|
||||
AndroidExecutor androidExecutor, SettingsManager settingsManager,
|
||||
PluginManager pluginManager, EventBus eventBus,
|
||||
NetworkManager networkManager) {
|
||||
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.app = app;
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.settingsManager = settingsManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.eventBus = eventBus;
|
||||
@@ -182,7 +185,7 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
boolean tor = isPluginEnabled(TorConstants.ID,
|
||||
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
|
||||
@@ -219,7 +222,7 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
||||
}
|
||||
|
||||
private void mergeSettings(Settings s, String namespace) {
|
||||
dbExecutor.execute(() -> {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
settingsManager.mergeSettings(s, namespace);
|
||||
@@ -235,8 +238,7 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||
if (state == STATE_ON) bluetoothTurnedOn.postValue(true);
|
||||
else bluetoothTurnedOn.postValue(false);
|
||||
bluetoothTurnedOn.postValue(state == STATE_ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ public class PanicPreferencesFragment extends PreferenceFragmentCompat
|
||||
entries.add(0, getString(R.string.panic_app_setting_none));
|
||||
entryValues.add(0, PACKAGE_NAME_NONE);
|
||||
|
||||
// only info.guardianproject.ripple is whitelisted in manifest
|
||||
for (ResolveInfo resolveInfo : PanicResponder.resolveTriggerApps(pm)) {
|
||||
if (resolveInfo.activityInfo == null)
|
||||
continue;
|
||||
|
||||
@@ -106,7 +106,7 @@ public class GroupActivity extends
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public class GroupActivity extends
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -264,7 +264,7 @@ public class GroupActivity extends
|
||||
// GroupRemovedEvent being fired
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class CreateGroupActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
setResult(RESULT_CANCELED);
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,15 +8,19 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
|
||||
// This class is not thread-safe
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class GroupItem {
|
||||
class GroupItem implements Comparable<GroupItem> {
|
||||
|
||||
private final PrivateGroup privateGroup;
|
||||
private final AuthorInfo authorInfo;
|
||||
private int messageCount, unreadCount;
|
||||
private long timestamp;
|
||||
private boolean dissolved;
|
||||
private final int messageCount, unreadCount;
|
||||
private final long timestamp;
|
||||
private final boolean dissolved;
|
||||
|
||||
GroupItem(PrivateGroup privateGroup, AuthorInfo authorInfo,
|
||||
GroupCount count, boolean dissolved) {
|
||||
@@ -28,18 +32,22 @@ class GroupItem {
|
||||
this.dissolved = dissolved;
|
||||
}
|
||||
|
||||
void addMessageHeader(GroupMessageHeader header) {
|
||||
messageCount++;
|
||||
if (header.getTimestamp() > timestamp) {
|
||||
timestamp = header.getTimestamp();
|
||||
}
|
||||
if (!header.isRead()) {
|
||||
unreadCount++;
|
||||
}
|
||||
GroupItem(GroupItem item, GroupMessageHeader header) {
|
||||
this.privateGroup = item.privateGroup;
|
||||
this.authorInfo = item.authorInfo;
|
||||
this.messageCount = item.messageCount + 1;
|
||||
this.unreadCount = item.unreadCount + (header.isRead() ? 0 : 1);
|
||||
this.timestamp = Math.max(header.getTimestamp(), item.timestamp);
|
||||
this.dissolved = item.dissolved;
|
||||
}
|
||||
|
||||
PrivateGroup getPrivateGroup() {
|
||||
return privateGroup;
|
||||
GroupItem(GroupItem item, boolean isDissolved) {
|
||||
this.privateGroup = item.privateGroup;
|
||||
this.authorInfo = item.authorInfo;
|
||||
this.messageCount = item.messageCount;
|
||||
this.unreadCount = item.unreadCount;
|
||||
this.timestamp = item.timestamp;
|
||||
this.dissolved = isDissolved;
|
||||
}
|
||||
|
||||
GroupId getId() {
|
||||
@@ -78,8 +86,27 @@ class GroupItem {
|
||||
return dissolved;
|
||||
}
|
||||
|
||||
void setDissolved() {
|
||||
dissolved = true;
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getId().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
return o instanceof GroupItem &&
|
||||
getId().equals(((GroupItem) o).getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(GroupItem o) {
|
||||
if (this == o) return 0;
|
||||
// The group with the latest message comes first
|
||||
long aTime = getTimestamp(), bTime = o.getTimestamp();
|
||||
if (aTime > bTime) return -1;
|
||||
if (aTime < bTime) return 1;
|
||||
// Break ties by group name
|
||||
String aName = getName();
|
||||
String bName = o.getName();
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,52 @@
|
||||
package org.briarproject.briar.android.privategroup.list;
|
||||
|
||||
import android.content.Context;
|
||||
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.bramble.api.sync.GroupId;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
|
||||
import org.briarproject.briar.android.util.BriarAdapter;
|
||||
|
||||
import static androidx.recyclerview.widget.SortedList.INVALID_POSITION;
|
||||
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class GroupListAdapter extends BriarAdapter<GroupItem, GroupViewHolder> {
|
||||
class GroupListAdapter extends ListAdapter<GroupItem, GroupViewHolder> {
|
||||
|
||||
private final OnGroupRemoveClickListener listener;
|
||||
|
||||
GroupListAdapter(Context ctx, OnGroupRemoveClickListener listener) {
|
||||
super(ctx, GroupItem.class);
|
||||
GroupListAdapter(OnGroupRemoveClickListener listener) {
|
||||
super(new GroupItemCallback());
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(ctx).inflate(
|
||||
R.layout.list_item_group, parent, false);
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.list_item_group, parent, false);
|
||||
return new GroupViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(GroupViewHolder ui, int position) {
|
||||
ui.bindView(ctx, items.get(position), listener);
|
||||
ui.bindView(getItem(position), listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(GroupItem a, GroupItem b) {
|
||||
if (a == b) return 0;
|
||||
// The group with the latest message comes first
|
||||
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
|
||||
if (aTime > bTime) return -1;
|
||||
if (aTime < bTime) return 1;
|
||||
// Break ties by group name
|
||||
String aName = a.getName();
|
||||
String bName = b.getName();
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(GroupItem a, GroupItem b) {
|
||||
return a.getMessageCount() == b.getMessageCount() &&
|
||||
a.getTimestamp() == b.getTimestamp() &&
|
||||
a.getUnreadCount() == b.getUnreadCount() &&
|
||||
a.isDissolved() == b.isDissolved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(GroupItem a, GroupItem b) {
|
||||
return a.getId().equals(b.getId());
|
||||
}
|
||||
|
||||
int findItemPosition(GroupId g) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
GroupItem item = items.get(i);
|
||||
if (item.getId().equals(g)) {
|
||||
return i;
|
||||
}
|
||||
private static class GroupItemCallback extends ItemCallback<GroupItem> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(GroupItem a, GroupItem b) {
|
||||
return a.equals(b);
|
||||
}
|
||||
return INVALID_POSITION;
|
||||
}
|
||||
|
||||
void removeItem(GroupId groupId) {
|
||||
int pos = findItemPosition(groupId);
|
||||
if (pos != INVALID_POSITION) items.removeItemAt(pos);
|
||||
@Override
|
||||
public boolean areContentsTheSame(GroupItem a, GroupItem b) {
|
||||
return a.getMessageCount() == b.getMessageCount() &&
|
||||
a.getTimestamp() == b.getTimestamp() &&
|
||||
a.getUnreadCount() == b.getUnreadCount() &&
|
||||
a.isDissolved() == b.isDissolved();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.briarproject.briar.android.privategroup.list;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.briar.android.controller.DbController;
|
||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@NotNullByDefault
|
||||
interface GroupListController extends DbController {
|
||||
|
||||
/**
|
||||
* The listener must be set right after the controller was injected
|
||||
*/
|
||||
@UiThread
|
||||
void setGroupListListener(GroupListListener listener);
|
||||
|
||||
@UiThread
|
||||
void unsetGroupListListener(GroupListListener listener);
|
||||
|
||||
@UiThread
|
||||
void onStart();
|
||||
|
||||
@UiThread
|
||||
void onStop();
|
||||
|
||||
void loadGroups(
|
||||
ResultExceptionHandler<Collection<GroupItem>, DbException> result);
|
||||
|
||||
void removeGroup(GroupId g, ExceptionHandler<DbException> result);
|
||||
|
||||
void loadAvailableGroups(
|
||||
ResultExceptionHandler<Integer, DbException> result);
|
||||
|
||||
interface GroupListListener {
|
||||
|
||||
@UiThread
|
||||
void onGroupMessageAdded(GroupMessageHeader header);
|
||||
|
||||
@UiThread
|
||||
void onGroupInvitationReceived();
|
||||
|
||||
@UiThread
|
||||
void onGroupAdded(GroupId groupId);
|
||||
|
||||
@UiThread
|
||||
void onGroupRemoved(GroupId groupId);
|
||||
|
||||
@UiThread
|
||||
void onGroupDissolved(GroupId groupId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,57 +12,50 @@ import android.view.ViewGroup;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
|
||||
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupListController.GroupListListener;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
|
||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class GroupListFragment extends BaseFragment implements
|
||||
GroupListListener, OnGroupRemoveClickListener, OnClickListener {
|
||||
OnGroupRemoveClickListener, OnClickListener {
|
||||
|
||||
public final static String TAG = GroupListFragment.class.getName();
|
||||
private static final Logger LOG = Logger.getLogger(TAG);
|
||||
|
||||
public static GroupListFragment newInstance() {
|
||||
return new GroupListFragment();
|
||||
}
|
||||
|
||||
@Inject
|
||||
GroupListController controller;
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private GroupListViewModel viewModel;
|
||||
private BriarRecyclerView list;
|
||||
private GroupListAdapter adapter;
|
||||
private Snackbar snackbar;
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
controller.setGroupListListener(this);
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||
.get(GroupListViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -75,17 +68,32 @@ public class GroupListFragment extends BaseFragment implements
|
||||
|
||||
View v = inflater.inflate(R.layout.list, container, false);
|
||||
|
||||
adapter = new GroupListAdapter(getActivity(), this);
|
||||
adapter = new GroupListAdapter(this);
|
||||
list = v.findViewById(R.id.list);
|
||||
list.setEmptyImage(R.drawable.ic_empty_state_group_list);
|
||||
list.setEmptyText(R.string.groups_list_empty);
|
||||
list.setEmptyAction(R.string.groups_list_empty_action);
|
||||
list.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
list.setAdapter(adapter);
|
||||
viewModel.getGroupItems().observe(getViewLifecycleOwner(), result ->
|
||||
result.onError(this::handleException).onSuccess(items -> {
|
||||
adapter.submitList(items);
|
||||
if (requireNonNull(items).size() == 0) list.showData();
|
||||
})
|
||||
);
|
||||
|
||||
snackbar = new BriarSnackbarBuilder()
|
||||
Snackbar snackbar = new BriarSnackbarBuilder()
|
||||
.setAction(R.string.show, this)
|
||||
.make(list, "", LENGTH_INDEFINITE);
|
||||
viewModel.getNumInvitations().observe(getViewLifecycleOwner(), num -> {
|
||||
if (num == 0) {
|
||||
snackbar.dismiss();
|
||||
} else {
|
||||
snackbar.setText(getResources().getQuantityString(
|
||||
R.plurals.groups_invitations_open, num, num));
|
||||
if (!snackbar.isShownOrQueued()) snackbar.show();
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
@@ -93,25 +101,23 @@ public class GroupListFragment extends BaseFragment implements
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
controller.onStart();
|
||||
viewModel.blockAllGroupMessageNotifications();
|
||||
viewModel.clearAllGroupMessageNotifications();
|
||||
// The attributes and sorting of the groups may have changed while we
|
||||
// were stopped and we have no way finding out about them, so re-load
|
||||
// e.g. less unread messages in a group after viewing it.
|
||||
viewModel.loadGroups();
|
||||
// The number of invitations might have changed while we were stopped
|
||||
// e.g. because of accepting an invitation which does not trigger event
|
||||
viewModel.loadNumInvitations();
|
||||
list.startPeriodicUpdate();
|
||||
loadGroups();
|
||||
loadAvailableGroups();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
controller.onStop();
|
||||
list.stopPeriodicUpdate();
|
||||
adapter.clear();
|
||||
list.showProgressBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
controller.unsetGroupListListener(this);
|
||||
viewModel.unblockAllGroupMessageNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,68 +128,18 @@ public class GroupListFragment extends BaseFragment implements
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_add_group:
|
||||
Intent i = new Intent(getContext(), CreateGroupActivity.class);
|
||||
startActivity(i);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == R.id.action_add_group) {
|
||||
Intent i = new Intent(getContext(), CreateGroupActivity.class);
|
||||
startActivity(i);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onGroupRemoveClick(GroupItem item) {
|
||||
controller.removeGroup(item.getId(),
|
||||
new UiExceptionHandler<DbException>(this) {
|
||||
// result handled by GroupRemovedEvent and onGroupRemoved()
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onGroupMessageAdded(GroupMessageHeader header) {
|
||||
adapter.incrementRevision();
|
||||
int position = adapter.findItemPosition(header.getGroupId());
|
||||
GroupItem item = adapter.getItemAt(position);
|
||||
if (item != null) {
|
||||
item.addMessageHeader(header);
|
||||
adapter.updateItemAt(position, item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupInvitationReceived() {
|
||||
loadAvailableGroups();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onGroupAdded(GroupId groupId) {
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onGroupRemoved(GroupId groupId) {
|
||||
adapter.incrementRevision();
|
||||
adapter.removeItem(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupDissolved(GroupId groupId) {
|
||||
adapter.incrementRevision();
|
||||
int position = adapter.findItemPosition(groupId);
|
||||
GroupItem item = adapter.getItemAt(position);
|
||||
if (item != null) {
|
||||
item.setDissolved();
|
||||
adapter.updateItemAt(position, item);
|
||||
}
|
||||
viewModel.removeGroup(item.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,52 +147,6 @@ public class GroupListFragment extends BaseFragment implements
|
||||
return TAG;
|
||||
}
|
||||
|
||||
private void loadGroups() {
|
||||
int revision = adapter.getRevision();
|
||||
controller.loadGroups(
|
||||
new UiResultExceptionHandler<Collection<GroupItem>, DbException>(
|
||||
this) {
|
||||
@Override
|
||||
public void onResultUi(Collection<GroupItem> groups) {
|
||||
if (revision == adapter.getRevision()) {
|
||||
adapter.incrementRevision();
|
||||
if (groups.isEmpty()) list.showData();
|
||||
else adapter.replaceAll(groups);
|
||||
} else {
|
||||
LOG.info("Concurrent update, reloading");
|
||||
loadGroups();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAvailableGroups() {
|
||||
controller.loadAvailableGroups(
|
||||
new UiResultExceptionHandler<Integer, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(Integer num) {
|
||||
if (num == 0) {
|
||||
snackbar.dismiss();
|
||||
} else {
|
||||
snackbar.setText(getResources().getQuantityString(
|
||||
R.plurals.groups_invitations_open, num,
|
||||
num));
|
||||
if (!snackbar.isShownOrQueued()) snackbar.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is handling the available groups snackbar action
|
||||
*/
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package org.briarproject.briar.android.privategroup.list;
|
||||
|
||||
import org.briarproject.briar.android.activity.ActivityScope;
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
@Module
|
||||
public class GroupListModule {
|
||||
public abstract class GroupListModule {
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
GroupListController provideGroupListController(
|
||||
GroupListControllerImpl groupListController) {
|
||||
return groupListController;
|
||||
}
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(GroupListViewModel.class)
|
||||
abstract ViewModel bindGroupListViewModel(
|
||||
GroupListViewModel groupListViewModel);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package org.briarproject.briar.android.privategroup.list;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchGroupException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
@@ -16,11 +19,12 @@ import org.briarproject.bramble.api.sync.ClientId;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||
import org.briarproject.briar.android.controller.DbControllerImpl;
|
||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||
import org.briarproject.briar.android.viewmodel.LiveResult;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
|
||||
@@ -30,6 +34,7 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -38,10 +43,13 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
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.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
@@ -49,11 +57,10 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class GroupListControllerImpl extends DbControllerImpl
|
||||
implements GroupListController, EventListener {
|
||||
class GroupListViewModel extends DbViewModel implements EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(GroupListControllerImpl.class.getName());
|
||||
getLogger(GroupListViewModel.class.getName());
|
||||
|
||||
private final PrivateGroupManager groupManager;
|
||||
private final GroupInvitationManager groupInvitationManager;
|
||||
@@ -61,120 +68,137 @@ class GroupListControllerImpl extends DbControllerImpl
|
||||
private final AndroidNotificationManager notificationManager;
|
||||
private final EventBus eventBus;
|
||||
|
||||
// UI thread
|
||||
@Nullable
|
||||
private GroupListListener listener;
|
||||
private final MutableLiveData<LiveResult<List<GroupItem>>> groupItems =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Integer> numInvitations =
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
GroupListControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager, PrivateGroupManager groupManager,
|
||||
GroupListViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
PrivateGroupManager groupManager,
|
||||
GroupInvitationManager groupInvitationManager,
|
||||
ContactManager contactManager,
|
||||
AndroidNotificationManager notificationManager, EventBus eventBus) {
|
||||
super(dbExecutor, lifecycleManager);
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.groupManager = groupManager;
|
||||
this.groupInvitationManager = groupInvitationManager;
|
||||
this.contactManager = contactManager;
|
||||
this.notificationManager = notificationManager;
|
||||
this.eventBus = eventBus;
|
||||
this.eventBus.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGroupListListener(GroupListListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsetGroupListListener(GroupListListener listener) {
|
||||
if (this.listener == listener) this.listener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onStart() {
|
||||
if (listener == null) throw new IllegalStateException();
|
||||
eventBus.addListener(this);
|
||||
notificationManager.clearAllGroupMessageNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onStop() {
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
void clearAllGroupMessageNotifications() {
|
||||
notificationManager.clearAllGroupMessageNotifications();
|
||||
}
|
||||
|
||||
void blockAllGroupMessageNotifications() {
|
||||
notificationManager.blockAllGroupMessageNotifications();
|
||||
}
|
||||
|
||||
void unblockAllGroupMessageNotifications() {
|
||||
notificationManager.unblockAllGroupMessageNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void eventOccurred(Event e) {
|
||||
if (listener == null) throw new IllegalStateException();
|
||||
if (e instanceof GroupMessageAddedEvent) {
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
LOG.info("Private group message added");
|
||||
listener.onGroupMessageAdded(g.getHeader());
|
||||
onGroupMessageAdded(g.getHeader());
|
||||
} else if (e instanceof GroupInvitationRequestReceivedEvent) {
|
||||
LOG.info("Private group invitation received");
|
||||
listener.onGroupInvitationReceived();
|
||||
loadNumInvitations();
|
||||
} else if (e instanceof GroupAddedEvent) {
|
||||
GroupAddedEvent g = (GroupAddedEvent) e;
|
||||
ClientId id = g.getGroup().getClientId();
|
||||
if (id.equals(CLIENT_ID)) {
|
||||
LOG.info("Private group added");
|
||||
listener.onGroupAdded(g.getGroup().getId());
|
||||
loadGroups();
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||
ClientId id = g.getGroup().getClientId();
|
||||
if (id.equals(CLIENT_ID)) {
|
||||
LOG.info("Private group removed");
|
||||
listener.onGroupRemoved(g.getGroup().getId());
|
||||
onGroupRemoved(g.getGroup().getId());
|
||||
}
|
||||
} else if (e instanceof GroupDissolvedEvent) {
|
||||
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
|
||||
LOG.info("Private group dissolved");
|
||||
listener.onGroupDissolved(g.getGroupId());
|
||||
onGroupDissolved(g.getGroupId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadGroups(
|
||||
ResultExceptionHandler<Collection<GroupItem>, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
Collection<PrivateGroup> groups =
|
||||
groupManager.getPrivateGroups();
|
||||
List<GroupItem> items = new ArrayList<>(groups.size());
|
||||
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
|
||||
for (PrivateGroup g : groups) {
|
||||
try {
|
||||
GroupId id = g.getId();
|
||||
AuthorId authorId = g.getCreator().getId();
|
||||
AuthorInfo authorInfo;
|
||||
if (authorInfos.containsKey(authorId)) {
|
||||
authorInfo = authorInfos.get(authorId);
|
||||
} else {
|
||||
authorInfo = contactManager.getAuthorInfo(authorId);
|
||||
authorInfos.put(authorId, authorInfo);
|
||||
}
|
||||
GroupCount count = groupManager.getGroupCount(id);
|
||||
boolean dissolved = groupManager.isDissolved(id);
|
||||
items.add(
|
||||
new GroupItem(g, authorInfo, count, dissolved));
|
||||
} catch (NoSuchGroupException e) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
logDuration(LOG, "Loading groups", start);
|
||||
handler.onResult(items);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
void loadGroups() {
|
||||
loadList(this::loadGroups, groupItems::setValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeGroup(GroupId g, ExceptionHandler<DbException> handler) {
|
||||
@DatabaseExecutor
|
||||
private List<GroupItem> loadGroups(Transaction txn) throws DbException {
|
||||
long start = now();
|
||||
Collection<PrivateGroup> groups = groupManager.getPrivateGroups(txn);
|
||||
List<GroupItem> items = new ArrayList<>(groups.size());
|
||||
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
|
||||
for (PrivateGroup g : groups) {
|
||||
GroupId id = g.getId();
|
||||
AuthorId authorId = g.getCreator().getId();
|
||||
AuthorInfo authorInfo;
|
||||
if (authorInfos.containsKey(authorId)) {
|
||||
authorInfo = requireNonNull(authorInfos.get(authorId));
|
||||
} else {
|
||||
authorInfo = contactManager.getAuthorInfo(txn, authorId);
|
||||
authorInfos.put(authorId, authorInfo);
|
||||
}
|
||||
GroupCount count = groupManager.getGroupCount(txn, id);
|
||||
boolean dissolved = groupManager.isDissolved(txn, id);
|
||||
items.add(new GroupItem(g, authorInfo, count, dissolved));
|
||||
}
|
||||
Collections.sort(items);
|
||||
logDuration(LOG, "Loading groups", start);
|
||||
return items;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onGroupMessageAdded(GroupMessageHeader header) {
|
||||
GroupId g = header.getGroupId();
|
||||
List<GroupItem> list = updateListItems(groupItems,
|
||||
itemToTest -> itemToTest.getId().equals(g),
|
||||
itemToUpdate -> new GroupItem(itemToUpdate, header));
|
||||
if (list == null) return;
|
||||
// re-sort as the order of items may have changed
|
||||
Collections.sort(list);
|
||||
groupItems.setValue(new LiveResult<>(list));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onGroupDissolved(GroupId groupId) {
|
||||
List<GroupItem> list = updateListItems(groupItems,
|
||||
itemToTest -> itemToTest.getId().equals(groupId),
|
||||
itemToUpdate -> new GroupItem(itemToUpdate, true));
|
||||
if (list == null) return;
|
||||
groupItems.setValue(new LiveResult<>(list));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void onGroupRemoved(GroupId groupId) {
|
||||
List<GroupItem> list =
|
||||
removeListItems(groupItems, i -> i.getId().equals(groupId));
|
||||
if (list == null) return;
|
||||
groupItems.setValue(new LiveResult<>(list));
|
||||
}
|
||||
|
||||
void removeGroup(GroupId g) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
@@ -182,23 +206,26 @@ class GroupListControllerImpl extends DbControllerImpl
|
||||
logDuration(LOG, "Removing group", start);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAvailableGroups(
|
||||
ResultExceptionHandler<Integer, DbException> handler) {
|
||||
void loadNumInvitations() {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
handler.onResult(
|
||||
groupInvitationManager.getInvitations().size());
|
||||
int i = groupInvitationManager.getInvitations().size();
|
||||
numInvitations.postValue(i);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
LiveData<LiveResult<List<GroupItem>>> getGroupItems() {
|
||||
return groupItems;
|
||||
}
|
||||
|
||||
LiveData<Integer> getNumInvitations() {
|
||||
return numInvitations;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final static float ALPHA = 0.42f;
|
||||
|
||||
private final Context ctx;
|
||||
private final ViewGroup layout;
|
||||
private final TextAvatarView avatar;
|
||||
private final TextView name;
|
||||
@@ -40,7 +41,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
GroupViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
ctx = v.getContext();
|
||||
layout = (ViewGroup) v;
|
||||
avatar = v.findViewById(R.id.avatarView);
|
||||
name = v.findViewById(R.id.nameView);
|
||||
@@ -51,8 +52,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
||||
remove = v.findViewById(R.id.removeButton);
|
||||
}
|
||||
|
||||
void bindView(Context ctx, GroupItem group,
|
||||
OnGroupRemoveClickListener listener) {
|
||||
void bindView(GroupItem group, OnGroupRemoveClickListener listener) {
|
||||
// Avatar
|
||||
avatar.setText(group.getName().substring(0, 1));
|
||||
avatar.setBackgroundBytes(group.getId().getBytes());
|
||||
|
||||
@@ -124,7 +124,7 @@ public class GroupMemberListActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -120,7 +120,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
|
||||
new UiExceptionHandler<DbException>(this) {
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
|
||||
new UiExceptionHandler<DbException>(this) {
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
supportFinishAfterTransition();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Process;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
|
||||
import static org.briarproject.briar.android.util.UiUtils.startDevReportActivity;
|
||||
|
||||
@NotNullByDefault
|
||||
public class BriarExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
private final Context ctx;
|
||||
private final long appStartTime;
|
||||
|
||||
public BriarExceptionHandler(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
this.appStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
// activity runs in its own process, so we can kill the old one
|
||||
startDevReportActivity(ctx, CrashReportActivity.class, e, appStartTime);
|
||||
Process.killProcess(Process.myPid());
|
||||
System.exit(10);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
Some of the code in this file was copied from or inspired by ACRA
|
||||
which is licenced under Apache 2.0 and authored by F43nd1r.
|
||||
https://github.com/ACRA/acra/blob/3b9034/acra-core/src/main/java/org/acra/collector/
|
||||
*/
|
||||
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityManager;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.content.pm.FeatureInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.BuildConfig;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.BriarApplication;
|
||||
import org.briarproject.briar.android.logging.BriefLogFormatter;
|
||||
import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo;
|
||||
import org.briarproject.briar.android.reporting.ReportData.ReportItem;
|
||||
import org.briarproject.briar.android.reporting.ReportData.SingleReportInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
import static android.content.Context.WIFI_P2P_SERVICE;
|
||||
import static android.net.ConnectivityManager.TYPE_MOBILE;
|
||||
import static android.net.ConnectivityManager.TYPE_WIFI;
|
||||
import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static androidx.core.content.ContextCompat.getSystemService;
|
||||
import static java.util.Locale.US;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.TimeZone.getTimeZone;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod;
|
||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress;
|
||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class BriarReportCollector {
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
BriarReportCollector(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
public ReportData collectReportData(@Nullable Throwable t,
|
||||
long appStartTime) {
|
||||
ReportData reportData = new ReportData()
|
||||
.add(getBasicInfo(t))
|
||||
.add(getDeviceInfo());
|
||||
if (t != null) reportData.add(getStacktrace(t));
|
||||
return reportData
|
||||
.add(getTimeInfo(appStartTime))
|
||||
.add(getMemory())
|
||||
.add(getStorage())
|
||||
.add(getConnectivity())
|
||||
.add(getBuildConfig())
|
||||
.add(getLogcat())
|
||||
.add(getDeviceFeatures());
|
||||
}
|
||||
|
||||
private ReportItem getBasicInfo(@Nullable Throwable t) {
|
||||
String packageName = ctx.getPackageName();
|
||||
PackageManager pm = ctx.getPackageManager();
|
||||
String versionName;
|
||||
int versionCode;
|
||||
try {
|
||||
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
|
||||
versionName = packageInfo.versionName;
|
||||
versionCode = packageInfo.versionCode;
|
||||
} catch (NameNotFoundException e) {
|
||||
versionName = e.toString();
|
||||
versionCode = 0;
|
||||
}
|
||||
MultiReportInfo basicInfo = new MultiReportInfo()
|
||||
.add("PackageName", packageName)
|
||||
.add("VersionName", versionName)
|
||||
.add("VersionCode", versionCode)
|
||||
.add("IsCrashReport", t != null);
|
||||
return new ReportItem("BasicInfo", R.string.dev_report_basic_info,
|
||||
basicInfo, false);
|
||||
}
|
||||
|
||||
private ReportItem getDeviceInfo() {
|
||||
MultiReportInfo deviceInfo = new MultiReportInfo()
|
||||
.add("AndroidVersion", Build.VERSION.RELEASE)
|
||||
.add("AndroidApi", SDK_INT)
|
||||
.add("Product", Build.PRODUCT)
|
||||
.add("Model", Build.MODEL)
|
||||
.add("Brand", Build.BRAND);
|
||||
return new ReportItem("DeviceInfo", R.string.dev_report_device_info,
|
||||
deviceInfo);
|
||||
}
|
||||
|
||||
private ReportItem getStacktrace(Throwable t) {
|
||||
final Writer sw = new StringWriter();
|
||||
final PrintWriter printWriter = new PrintWriter(sw);
|
||||
if (!isNullOrEmpty(t.getMessage())) {
|
||||
printWriter.println(t.getMessage());
|
||||
}
|
||||
t.printStackTrace(printWriter);
|
||||
SingleReportInfo stacktrace = new SingleReportInfo(sw.toString());
|
||||
return new ReportItem("Stacktrace", R.string.dev_report_stacktrace,
|
||||
stacktrace);
|
||||
}
|
||||
|
||||
private ReportItem getTimeInfo(long startTime) {
|
||||
MultiReportInfo timeInfo = new MultiReportInfo()
|
||||
.add("ReportTime", formatTime(System.currentTimeMillis()));
|
||||
if (startTime > -1) {
|
||||
timeInfo.add("AppStartTime", formatTime(startTime));
|
||||
}
|
||||
return new ReportItem("TimeInfo", R.string.dev_report_time_info,
|
||||
timeInfo);
|
||||
}
|
||||
|
||||
private String formatTime(long time) {
|
||||
SimpleDateFormat format =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", US);
|
||||
format.setTimeZone(getTimeZone("UTC"));
|
||||
return format.format(new Date(time));
|
||||
}
|
||||
|
||||
private ReportItem getMemory() {
|
||||
MultiReportInfo memInfo = new MultiReportInfo();
|
||||
|
||||
// System memory
|
||||
ActivityManager am = getSystemService(ctx, ActivityManager.class);
|
||||
ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo();
|
||||
requireNonNull(am).getMemoryInfo(mem);
|
||||
memInfo.add("SystemMemoryTotal", mem.totalMem);
|
||||
memInfo.add("SystemMemoryFree", mem.availMem);
|
||||
memInfo.add("SystemMemoryThreshold", mem.threshold);
|
||||
|
||||
// Virtual machine memory
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
memInfo.add("VirtualMachineMemoryAllocated", runtime.totalMemory());
|
||||
memInfo.add("VirtualMachineMemoryFree", runtime.freeMemory());
|
||||
memInfo.add("VirtualMachineMemoryMaximum", runtime.maxMemory());
|
||||
|
||||
return new ReportItem("Memory", R.string.dev_report_memory, memInfo);
|
||||
}
|
||||
|
||||
private ReportItem getStorage() {
|
||||
MultiReportInfo storageInfo = new MultiReportInfo();
|
||||
|
||||
// Internal storage
|
||||
File root = Environment.getRootDirectory();
|
||||
storageInfo.add("InternalStorageTotal", root.getTotalSpace());
|
||||
storageInfo.add("InternalStorageFree", root.getFreeSpace());
|
||||
|
||||
// External storage (SD card)
|
||||
File sd = Environment.getExternalStorageDirectory();
|
||||
storageInfo.add("ExternalStorageTotal", sd.getTotalSpace());
|
||||
storageInfo.add("ExternalStorageFree", sd.getFreeSpace());
|
||||
|
||||
return new ReportItem("Storage", R.string.dev_report_storage,
|
||||
storageInfo);
|
||||
}
|
||||
|
||||
private ReportItem getConnectivity() {
|
||||
MultiReportInfo connectivityInfo = new MultiReportInfo();
|
||||
|
||||
// Is mobile data available?
|
||||
ConnectivityManager cm = requireNonNull(
|
||||
getSystemService(ctx, ConnectivityManager.class));
|
||||
NetworkInfo mobile = cm.getNetworkInfo(TYPE_MOBILE);
|
||||
boolean mobileAvailable = mobile != null && mobile.isAvailable();
|
||||
connectivityInfo.add("MobileDataAvailable", mobileAvailable);
|
||||
|
||||
// Is mobile data enabled?
|
||||
boolean mobileEnabled = false;
|
||||
try {
|
||||
Class<?> clazz = Class.forName(cm.getClass().getName());
|
||||
Method method = clazz.getDeclaredMethod("getMobileDataEnabled");
|
||||
method.setAccessible(true);
|
||||
mobileEnabled = (Boolean) requireNonNull(method.invoke(cm));
|
||||
} catch (ClassNotFoundException
|
||||
| NoSuchMethodException
|
||||
| IllegalArgumentException
|
||||
| InvocationTargetException
|
||||
| IllegalAccessException e) {
|
||||
connectivityInfo
|
||||
.add("MobileDataReflectionException", e.toString());
|
||||
}
|
||||
connectivityInfo.add("MobileDataEnabled", mobileEnabled);
|
||||
|
||||
// Is mobile data connected ?
|
||||
boolean mobileConnected = mobile != null && mobile.isConnected();
|
||||
connectivityInfo.add("MobileDataConnected", mobileConnected);
|
||||
|
||||
// Is wifi available?
|
||||
NetworkInfo wifi = cm.getNetworkInfo(TYPE_WIFI);
|
||||
boolean wifiAvailable = wifi != null && wifi.isAvailable();
|
||||
connectivityInfo.add("WifiAvailable", wifiAvailable);
|
||||
|
||||
// Is wifi enabled?
|
||||
WifiManager wm = getSystemService(ctx, WifiManager.class);
|
||||
boolean wifiEnabled = wm != null &&
|
||||
wm.getWifiState() == WIFI_STATE_ENABLED;
|
||||
connectivityInfo.add("WifiEnabled", wifiEnabled);
|
||||
|
||||
// Is wifi connected?
|
||||
boolean wifiConnected = wifi != null && wifi.isConnected();
|
||||
connectivityInfo.add("WifiConnected", wifiConnected);
|
||||
|
||||
// Is wifi direct supported?
|
||||
boolean wifiDirect = ctx.getSystemService(WIFI_P2P_SERVICE) != null;
|
||||
connectivityInfo.add("WiFiDirectSupported", wifiDirect);
|
||||
|
||||
if (wm != null) {
|
||||
WifiInfo wifiInfo = wm.getConnectionInfo();
|
||||
if (wifiInfo != null) {
|
||||
int ip = wifiInfo.getIpAddress(); // Nice API, Google
|
||||
byte[] ipBytes = new byte[4];
|
||||
ipBytes[0] = (byte) (ip & 0xFF);
|
||||
ipBytes[1] = (byte) ((ip >> 8) & 0xFF);
|
||||
ipBytes[2] = (byte) ((ip >> 16) & 0xFF);
|
||||
ipBytes[3] = (byte) ((ip >> 24) & 0xFF);
|
||||
try {
|
||||
InetAddress address = InetAddress.getByAddress(ipBytes);
|
||||
connectivityInfo.add("WiFiAddress",
|
||||
scrubInetAddress(address));
|
||||
} catch (UnknownHostException ignored) {
|
||||
// Should only be thrown if address has illegal length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is Bluetooth available?
|
||||
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
|
||||
if (bt == null) {
|
||||
connectivityInfo.add("BluetoothAvailable", false);
|
||||
} else {
|
||||
connectivityInfo.add("BluetoothAvailable", true);
|
||||
|
||||
// Is Bluetooth enabled?
|
||||
@SuppressLint("HardwareIds")
|
||||
boolean btEnabled = bt.isEnabled()
|
||||
&& !isNullOrEmpty(bt.getAddress());
|
||||
connectivityInfo.add("BluetoothEnabled", btEnabled);
|
||||
|
||||
// Is Bluetooth connectable?
|
||||
int scanMode = bt.getScanMode();
|
||||
boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE ||
|
||||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
connectivityInfo.add("BluetoothConnectable", btConnectable);
|
||||
|
||||
// Is Bluetooth discoverable?
|
||||
boolean btDiscoverable =
|
||||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
connectivityInfo.add("BluetoothDiscoverable", btDiscoverable);
|
||||
|
||||
if (SDK_INT >= 21) {
|
||||
// Is Bluetooth LE scanning and advertising supported?
|
||||
boolean btLeScan = bt.getBluetoothLeScanner() != null;
|
||||
connectivityInfo.add("BluetoothLeScanningSupported", btLeScan);
|
||||
boolean btLeAdvertise =
|
||||
bt.getBluetoothLeAdvertiser() != null;
|
||||
connectivityInfo.add("BluetoothLeAdvertisingSupported",
|
||||
btLeAdvertise);
|
||||
}
|
||||
|
||||
Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt);
|
||||
String address = p.getFirst();
|
||||
String method = p.getSecond();
|
||||
connectivityInfo.add("BluetoothAddress", scrubMacAddress(address));
|
||||
connectivityInfo.add("BluetoothAddressMethod", method);
|
||||
}
|
||||
return new ReportItem("Connectivity", R.string.dev_report_connectivity,
|
||||
connectivityInfo);
|
||||
}
|
||||
|
||||
private ReportItem getBuildConfig() {
|
||||
MultiReportInfo buildConfig = new MultiReportInfo()
|
||||
.add("GitHash", BuildConfig.GitHash)
|
||||
.add("BuildType", BuildConfig.BUILD_TYPE)
|
||||
.add("Flavor", BuildConfig.FLAVOR)
|
||||
.add("Debug", BuildConfig.DEBUG)
|
||||
.add("BuildTimestamp", formatTime(BuildConfig.BuildTimestamp));
|
||||
return new ReportItem("BuildConfig", R.string.dev_report_build_config,
|
||||
buildConfig);
|
||||
}
|
||||
|
||||
private ReportItem getLogcat() {
|
||||
BriarApplication app = (BriarApplication) ctx.getApplicationContext();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Formatter formatter = new BriefLogFormatter();
|
||||
for (LogRecord record : app.getRecentLogRecords()) {
|
||||
sb.append(formatter.format(record)).append('\n');
|
||||
}
|
||||
return new ReportItem("Logcat", R.string.dev_report_logcat,
|
||||
sb.toString());
|
||||
}
|
||||
|
||||
private ReportItem getDeviceFeatures() {
|
||||
PackageManager pm = ctx.getPackageManager();
|
||||
FeatureInfo[] features = pm.getSystemAvailableFeatures();
|
||||
MultiReportInfo deviceFeatures = new MultiReportInfo();
|
||||
for (FeatureInfo feature : features) {
|
||||
String featureName = feature.name;
|
||||
if (featureName != null) {
|
||||
deviceFeatures.add(featureName, true);
|
||||
} else {
|
||||
deviceFeatures.add("glEsVersion", feature.getGlEsVersion());
|
||||
}
|
||||
}
|
||||
return new ReportItem("DeviceFeatures",
|
||||
R.string.dev_report_device_features, deviceFeatures);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import org.acra.builder.ReportBuilder;
|
||||
import org.acra.builder.ReportPrimer;
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.briar.BuildConfig;
|
||||
import org.briarproject.briar.android.BriarApplication;
|
||||
import org.briarproject.briar.android.logging.BriefLogFormatter;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
import static android.content.Context.ACTIVITY_SERVICE;
|
||||
import static android.content.Context.CONNECTIVITY_SERVICE;
|
||||
import static android.content.Context.WIFI_P2P_SERVICE;
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
import static android.net.ConnectivityManager.TYPE_MOBILE;
|
||||
import static android.net.ConnectivityManager.TYPE_WIFI;
|
||||
import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static java.util.Collections.unmodifiableMap;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod;
|
||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress;
|
||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
|
||||
public class BriarReportPrimer implements ReportPrimer {
|
||||
|
||||
@Override
|
||||
public void primeReport(@NonNull Context ctx,
|
||||
@NonNull ReportBuilder builder) {
|
||||
CustomDataTask task = new CustomDataTask(ctx);
|
||||
FutureTask<Map<String, String>> futureTask = new FutureTask<>(task);
|
||||
// Use a new thread as the Android executor thread may have died
|
||||
new SingleShotAndroidExecutor(futureTask).start();
|
||||
try {
|
||||
builder.customData(futureTask.get());
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
builder.customData("Custom data exception", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class CustomDataTask
|
||||
implements Callable<Map<String, String>> {
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
private CustomDataTask(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> call() {
|
||||
Map<String, String> customData = new LinkedHashMap<>();
|
||||
|
||||
// Log
|
||||
BriarApplication app =
|
||||
(BriarApplication) ctx.getApplicationContext();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Formatter formatter = new BriefLogFormatter();
|
||||
for (LogRecord record : app.getRecentLogRecords()) {
|
||||
sb.append(formatter.format(record)).append('\n');
|
||||
}
|
||||
customData.put("Log", sb.toString());
|
||||
|
||||
// System memory
|
||||
Object o = ctx.getSystemService(ACTIVITY_SERVICE);
|
||||
ActivityManager am = (ActivityManager) o;
|
||||
ActivityManager.MemoryInfo mem = new ActivityManager.MemoryInfo();
|
||||
am.getMemoryInfo(mem);
|
||||
String systemMemory;
|
||||
systemMemory = (mem.totalMem / 1024 / 1024) + " MiB total, "
|
||||
+ (mem.availMem / 1024 / 1204) + " MiB free, "
|
||||
+ (mem.threshold / 1024 / 1024) + " MiB threshold";
|
||||
customData.put("System memory", systemMemory);
|
||||
|
||||
// Virtual machine memory
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long heap = runtime.totalMemory();
|
||||
long heapFree = runtime.freeMemory();
|
||||
long heapMax = runtime.maxMemory();
|
||||
String vmMemory = (heap / 1024 / 1024) + " MiB allocated, "
|
||||
+ (heapFree / 1024 / 1024) + " MiB free, "
|
||||
+ (heapMax / 1024 / 1024) + " MiB maximum";
|
||||
customData.put("Virtual machine memory", vmMemory);
|
||||
|
||||
// Internal storage
|
||||
File root = Environment.getRootDirectory();
|
||||
long rootTotal = root.getTotalSpace();
|
||||
long rootFree = root.getFreeSpace();
|
||||
String internal = (rootTotal / 1024 / 1024) + " MiB total, "
|
||||
+ (rootFree / 1024 / 1024) + " MiB free";
|
||||
customData.put("Internal storage", internal);
|
||||
|
||||
// External storage (SD card)
|
||||
File sd = Environment.getExternalStorageDirectory();
|
||||
long sdTotal = sd.getTotalSpace();
|
||||
long sdFree = sd.getFreeSpace();
|
||||
String external = (sdTotal / 1024 / 1024) + " MiB total, "
|
||||
+ (sdFree / 1024 / 1024) + " MiB free";
|
||||
customData.put("External storage", external);
|
||||
|
||||
// Is mobile data available?
|
||||
o = ctx.getSystemService(CONNECTIVITY_SERVICE);
|
||||
ConnectivityManager cm = (ConnectivityManager) o;
|
||||
NetworkInfo mobile = cm.getNetworkInfo(TYPE_MOBILE);
|
||||
boolean mobileAvailable = mobile != null && mobile.isAvailable();
|
||||
// Is mobile data enabled?
|
||||
boolean mobileEnabled = false;
|
||||
try {
|
||||
Class<?> clazz = Class.forName(cm.getClass().getName());
|
||||
Method method = clazz.getDeclaredMethod("getMobileDataEnabled");
|
||||
method.setAccessible(true);
|
||||
mobileEnabled = (Boolean) method.invoke(cm);
|
||||
} catch (ClassNotFoundException
|
||||
| NoSuchMethodException
|
||||
| IllegalArgumentException
|
||||
| InvocationTargetException
|
||||
| IllegalAccessException e) {
|
||||
customData.put("Mobile data reflection exception",
|
||||
e.toString());
|
||||
}
|
||||
// Is mobile data connected ?
|
||||
boolean mobileConnected = mobile != null && mobile.isConnected();
|
||||
|
||||
String mobileStatus;
|
||||
if (mobileAvailable) mobileStatus = "Available, ";
|
||||
else mobileStatus = "Not available, ";
|
||||
if (mobileEnabled) mobileStatus += "enabled, ";
|
||||
else mobileStatus += "not enabled, ";
|
||||
if (mobileConnected) mobileStatus += "connected";
|
||||
else mobileStatus += "not connected";
|
||||
customData.put("Mobile data status", mobileStatus);
|
||||
|
||||
// Is wifi available?
|
||||
NetworkInfo wifi = cm.getNetworkInfo(TYPE_WIFI);
|
||||
boolean wifiAvailable = wifi != null && wifi.isAvailable();
|
||||
// Is wifi enabled?
|
||||
o = ctx.getApplicationContext().getSystemService(WIFI_SERVICE);
|
||||
WifiManager wm = (WifiManager) o;
|
||||
boolean wifiEnabled = wm != null &&
|
||||
wm.getWifiState() == WIFI_STATE_ENABLED;
|
||||
// Is wifi connected?
|
||||
boolean wifiConnected = wifi != null && wifi.isConnected();
|
||||
|
||||
String wifiStatus;
|
||||
if (wifiAvailable) wifiStatus = "Available, ";
|
||||
else wifiStatus = "Not available, ";
|
||||
if (wifiEnabled) wifiStatus += "enabled, ";
|
||||
else wifiStatus += "not enabled, ";
|
||||
if (wifiConnected) wifiStatus += "connected";
|
||||
else wifiStatus += "not connected";
|
||||
customData.put("Wi-Fi status", wifiStatus);
|
||||
|
||||
// Is wifi direct supported?
|
||||
String wifiDirectStatus = "Supported";
|
||||
if (ctx.getSystemService(WIFI_P2P_SERVICE) == null)
|
||||
wifiDirectStatus = "Not supported";
|
||||
customData.put("Wi-Fi Direct", wifiDirectStatus);
|
||||
|
||||
if (wm != null) {
|
||||
WifiInfo wifiInfo = wm.getConnectionInfo();
|
||||
if (wifiInfo != null) {
|
||||
int ip = wifiInfo.getIpAddress(); // Nice API, Google
|
||||
byte[] ipBytes = new byte[4];
|
||||
ipBytes[0] = (byte) (ip & 0xFF);
|
||||
ipBytes[1] = (byte) ((ip >> 8) & 0xFF);
|
||||
ipBytes[2] = (byte) ((ip >> 16) & 0xFF);
|
||||
ipBytes[3] = (byte) ((ip >> 24) & 0xFF);
|
||||
try {
|
||||
InetAddress address = InetAddress.getByAddress(ipBytes);
|
||||
customData.put("Wi-Fi address",
|
||||
scrubInetAddress(address));
|
||||
} catch (UnknownHostException ignored) {
|
||||
// Should only be thrown if address has illegal length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is Bluetooth available?
|
||||
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
|
||||
if (bt == null) {
|
||||
customData.put("Bluetooth status", "Not available");
|
||||
} else {
|
||||
// Is Bluetooth enabled?
|
||||
boolean btEnabled = bt.isEnabled()
|
||||
&& !isNullOrEmpty(bt.getAddress());
|
||||
// Is Bluetooth connectable?
|
||||
int scanMode = bt.getScanMode();
|
||||
boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE ||
|
||||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
// Is Bluetooth discoverable?
|
||||
boolean btDiscoverable =
|
||||
scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
|
||||
String btStatus;
|
||||
if (btEnabled) btStatus = "Available, enabled, ";
|
||||
else btStatus = "Available, not enabled, ";
|
||||
if (btConnectable) btStatus += "connectable, ";
|
||||
else btStatus += "not connectable, ";
|
||||
if (btDiscoverable) btStatus += "discoverable";
|
||||
else btStatus += "not discoverable";
|
||||
customData.put("Bluetooth status", btStatus);
|
||||
|
||||
if (SDK_INT >= 21) {
|
||||
// Is Bluetooth LE scanning and advertising supported?
|
||||
boolean btLeScan = bt.getBluetoothLeScanner() != null;
|
||||
boolean btLeAdvertise =
|
||||
bt.getBluetoothLeAdvertiser() != null;
|
||||
String btLeStatus;
|
||||
if (btLeScan) btLeStatus = "Scanning, ";
|
||||
else btLeStatus = "No scanning, ";
|
||||
if (btLeAdvertise) btLeStatus += "advertising";
|
||||
else btLeStatus += "no advertising";
|
||||
customData.put("Bluetooth LE status", btLeStatus);
|
||||
}
|
||||
|
||||
Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt);
|
||||
String address = p.getFirst();
|
||||
String method = p.getSecond();
|
||||
customData.put("Bluetooth address", scrubMacAddress(address));
|
||||
customData.put("Bluetooth address method", method);
|
||||
}
|
||||
|
||||
// Git commit ID
|
||||
customData.put("Commit ID", BuildConfig.GitHash);
|
||||
|
||||
return unmodifiableMap(customData);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleShotAndroidExecutor extends Thread {
|
||||
|
||||
private final Runnable runnable;
|
||||
|
||||
private SingleShotAndroidExecutor(Runnable runnable) {
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
Handler handler = new Handler();
|
||||
handler.post(runnable);
|
||||
handler.post(() -> {
|
||||
Looper looper = Looper.myLooper();
|
||||
if (looper != null) looper.quit();
|
||||
});
|
||||
Looper.loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.acra.collector.CrashReportData;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.acra.sender.ReportSenderException;
|
||||
import org.briarproject.bramble.api.reporting.DevReporter;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
import org.briarproject.briar.android.AndroidComponent;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static org.acra.ReportField.REPORT_ID;
|
||||
|
||||
public class BriarReportSender implements ReportSender {
|
||||
|
||||
private final AndroidComponent component;
|
||||
|
||||
@Inject
|
||||
DevReporter reporter;
|
||||
|
||||
BriarReportSender(AndroidComponent component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(@NonNull Context ctx,
|
||||
@NonNull CrashReportData errorContent)
|
||||
throws ReportSenderException {
|
||||
component.inject(this);
|
||||
String crashReport = errorContent.toJSON().toString();
|
||||
try {
|
||||
File reportDir = AndroidUtils.getReportDir(ctx);
|
||||
String reportId = errorContent.getProperty(REPORT_ID);
|
||||
reporter.encryptReportToFile(reportDir, reportId, crashReport);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new ReportSenderException("Failed to encrypt report", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.acra.config.ACRAConfiguration;
|
||||
import org.acra.sender.ReportSender;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.briarproject.briar.android.BriarApplication;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class BriarReportSenderFactory implements ReportSenderFactory {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ReportSender create(@NonNull Context ctx,
|
||||
@NonNull ACRAConfiguration config) {
|
||||
// ACRA passes in the Application as context
|
||||
BriarApplication app = (BriarApplication) ctx;
|
||||
return new BriarReportSender(app.getApplicationComponent());
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,36 @@ import android.view.ViewGroup;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class CrashFragment extends Fragment {
|
||||
public class CrashFragment extends BaseFragment {
|
||||
|
||||
public final static String TAG = CrashFragment.class.getName();
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
private ReportViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(ReportViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -25,15 +48,16 @@ public class CrashFragment extends Fragment {
|
||||
.inflate(R.layout.fragment_crash, container, false);
|
||||
|
||||
v.findViewById(R.id.acceptButton).setOnClickListener(view ->
|
||||
getDevReportActivity().displayFragment(true));
|
||||
viewModel.showReport());
|
||||
v.findViewById(R.id.declineButton).setOnClickListener(view ->
|
||||
getDevReportActivity().closeReport());
|
||||
viewModel.closeReport());
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
private DevReportActivity getDevReportActivity() {
|
||||
return (DevReportActivity) requireActivity();
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
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.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||
import org.briarproject.briar.android.logout.HideUiActivity;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class CrashReportActivity extends BaseActivity
|
||||
implements BaseFragmentListener {
|
||||
|
||||
public static final String EXTRA_THROWABLE = "throwable";
|
||||
public static final String EXTRA_APP_START_TIME = "appStartTime";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private ReportViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_dev_report);
|
||||
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||
.get(ReportViewModel.class);
|
||||
Intent intent = getIntent();
|
||||
Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE);
|
||||
long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, -1);
|
||||
viewModel.init(t, appStartTime);
|
||||
viewModel.getShowReport().observeEvent(this, show -> {
|
||||
if (show) displayFragment(true);
|
||||
});
|
||||
viewModel.getCloseReport().observeEvent(this, res -> {
|
||||
if (res != 0) {
|
||||
Toast.makeText(this, res, LENGTH_LONG).show();
|
||||
}
|
||||
exit();
|
||||
});
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
if (savedInstanceState == null) displayFragment(viewModel.isFeedback());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runOnDbThread(Runnable runnable) {
|
||||
throw new AssertionError("deprecated!!!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
exit();
|
||||
}
|
||||
|
||||
void displayFragment(boolean showReportForm) {
|
||||
BaseFragment f;
|
||||
if (showReportForm) {
|
||||
f = new ReportFormFragment();
|
||||
requireNonNull(getSupportActionBar()).show();
|
||||
} else {
|
||||
f = new CrashFragment();
|
||||
requireNonNull(getSupportActionBar()).hide();
|
||||
}
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
|
||||
.commit();
|
||||
}
|
||||
|
||||
void exit() {
|
||||
if (!viewModel.isFeedback()) {
|
||||
Intent i = new Intent(this, HideUiActivity.class);
|
||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK
|
||||
| FLAG_ACTIVITY_NO_ANIMATION
|
||||
| FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(i);
|
||||
// crash reports run in their own process that we should kill now
|
||||
// otherwise it keeps running and e.g. doesn't pick up theme changes
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
Process.killProcess(Process.myPid());
|
||||
// kill the process with some delay to keep the Toast visible
|
||||
}, 5000);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.acra.dialog.BaseCrashReportDialog;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.Localizer;
|
||||
import org.briarproject.briar.android.logout.HideUiActivity;
|
||||
import org.briarproject.briar.android.util.UserFeedback;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.acra.ACRAConstants.EXTRA_REPORT_FILE;
|
||||
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class DevReportActivity extends BaseCrashReportDialog {
|
||||
|
||||
private AppCompatDelegate delegate;
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
if (delegate == null) {
|
||||
delegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preInit(@Nullable Bundle savedInstanceState) {
|
||||
super.preInit(savedInstanceState);
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
getDelegate().applyDayNight();
|
||||
// We always need to re-apply the theme
|
||||
// for day/night the changes to take effect.
|
||||
// On API 23+, we should bypass setTheme(), which will no-op
|
||||
// if the theme ID is identical to the current theme ID.
|
||||
int theme = R.style.BriarTheme_NoActionBar;
|
||||
if (SDK_INT >= 23) {
|
||||
onApplyThemeResource(getTheme(), theme, false);
|
||||
} else {
|
||||
setTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(@Nullable Bundle state) {
|
||||
super.init(state);
|
||||
|
||||
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
|
||||
|
||||
getDelegate().setContentView(R.layout.activity_dev_report);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
getDelegate().setSupportActionBar(toolbar);
|
||||
|
||||
String title = getString(isFeedback() ? R.string.feedback_title :
|
||||
R.string.crash_report_title);
|
||||
requireNonNull(getDelegate().getSupportActionBar()).setTitle(title);
|
||||
|
||||
if (state == null) displayFragment(isFeedback());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(
|
||||
Localizer.getInstance().setLocale(base));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostCreate(@Nullable Bundle state) {
|
||||
super.onPostCreate(state);
|
||||
getDelegate().onPostCreate(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
getDelegate().onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
getDelegate().onPostResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(CharSequence title, int color) {
|
||||
super.onTitleChanged(title, color);
|
||||
getDelegate().setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
getDelegate().onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
getDelegate().onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
getDelegate().onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
getDelegate().onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
closeReport();
|
||||
}
|
||||
|
||||
void sendCrashReport(String comment, String email) {
|
||||
sendCrash(comment, email);
|
||||
}
|
||||
|
||||
private boolean isFeedback() {
|
||||
return getException() instanceof UserFeedback;
|
||||
}
|
||||
|
||||
void displayFragment(boolean showReportForm) {
|
||||
Fragment f;
|
||||
if (showReportForm) {
|
||||
File file =
|
||||
(File) getIntent().getSerializableExtra(EXTRA_REPORT_FILE);
|
||||
f = ReportFormFragment.newInstance(isFeedback(), file);
|
||||
requireNonNull(getDelegate().getSupportActionBar()).show();
|
||||
} else {
|
||||
f = new CrashFragment();
|
||||
requireNonNull(getDelegate().getSupportActionBar()).hide();
|
||||
}
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragmentContainer, f, f.getTag())
|
||||
.commit();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateOptionsMenu() {
|
||||
super.invalidateOptionsMenu();
|
||||
getDelegate().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
void closeReport() {
|
||||
cancelReports();
|
||||
exit();
|
||||
}
|
||||
|
||||
void exit() {
|
||||
if (!isFeedback()) {
|
||||
Intent i = new Intent(this, HideUiActivity.class);
|
||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK
|
||||
| FLAG_ACTIVITY_NO_ANIMATION
|
||||
| FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(i);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
@Module
|
||||
public abstract class DevReportModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ReportViewModel.class)
|
||||
abstract ViewModel bindReportViewModel(ReportViewModel reportViewModel);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
public class FeedbackActivity extends CrashReportActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setHomeButtonEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
@NotThreadSafe
|
||||
@NotNullByDefault
|
||||
class ReportData {
|
||||
|
||||
private final ArrayList<ReportItem> items = new ArrayList<>();
|
||||
|
||||
ReportData add(ReportItem item) {
|
||||
items.add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
List<ReportItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public JSONObject toJson(boolean includeReport) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
for (ReportItem item : items) {
|
||||
// only include required items when report not added
|
||||
if (!includeReport && item.isOptional) continue;
|
||||
// only include what should be included
|
||||
if (!item.isIncluded) continue;
|
||||
json.put(item.name, item.info.toJson());
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
static class ReportItem {
|
||||
final String name;
|
||||
@StringRes
|
||||
final int nameRes;
|
||||
final ReportInfo info;
|
||||
final boolean isOptional;
|
||||
boolean isIncluded = true;
|
||||
|
||||
ReportItem(String name, int nameRes, ReportInfo info) {
|
||||
this(name, nameRes, info, true);
|
||||
}
|
||||
|
||||
ReportItem(String name, int nameRes, String info) {
|
||||
this(name, nameRes, new SingleReportInfo(info), true);
|
||||
}
|
||||
|
||||
ReportItem(String name, int nameRes, ReportInfo info,
|
||||
boolean isOptional) {
|
||||
this.name = name;
|
||||
this.nameRes = nameRes;
|
||||
this.info = info;
|
||||
this.isOptional = isOptional;
|
||||
}
|
||||
}
|
||||
|
||||
interface ReportInfo {
|
||||
Object toJson();
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
static class SingleReportInfo implements ReportInfo {
|
||||
private final String string;
|
||||
|
||||
SingleReportInfo(String string) {
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toJson() {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
static class MultiReportInfo implements ReportInfo {
|
||||
private final Map<String, Object> map = new TreeMap<>();
|
||||
|
||||
MultiReportInfo add(String key, @Nullable Object value) {
|
||||
map.put(key, value == null ? "null" : value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||
sb
|
||||
.append(entry.getKey())
|
||||
.append(": ")
|
||||
.append(entry.getValue())
|
||||
.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object toJson() {
|
||||
return new JSONObject(map);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.reporting.ReportData.ReportItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
|
||||
@NotNullByDefault
|
||||
class ReportDataAdapter
|
||||
extends Adapter<ReportDataAdapter.ReportDataViewHolder> {
|
||||
|
||||
private final List<ReportItem> items;
|
||||
|
||||
ReportDataAdapter(List<ReportItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReportDataViewHolder onCreateViewHolder(ViewGroup parent,
|
||||
int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.list_item_crash, parent, false);
|
||||
return new ReportDataViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ReportDataViewHolder holder, int position) {
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
static class ReportDataViewHolder extends RecyclerView.ViewHolder {
|
||||
private final CheckBox cb;
|
||||
private final TextView content;
|
||||
|
||||
private ReportDataViewHolder(View v) {
|
||||
super(v);
|
||||
cb = v.findViewById(R.id.include_in_report);
|
||||
content = v.findViewById(R.id.content);
|
||||
}
|
||||
|
||||
public void bind(ReportItem item) {
|
||||
cb.setChecked(!item.isOptional || item.isIncluded);
|
||||
cb.setEnabled(item.isOptional);
|
||||
cb.setOnCheckedChangeListener((buttonView, isChecked) ->
|
||||
item.isIncluded = isChecked
|
||||
);
|
||||
cb.setText(item.nameRes);
|
||||
content.setText(item.info.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -10,92 +9,58 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.acra.ReportField;
|
||||
import org.acra.collector.CrashReportData;
|
||||
import org.acra.file.CrashReportPersister;
|
||||
import org.acra.model.Element;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.json.JSONException;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.acra.ACRAConstants.EXTRA_REPORT_FILE;
|
||||
import static org.acra.ReportField.ANDROID_VERSION;
|
||||
import static org.acra.ReportField.APP_VERSION_CODE;
|
||||
import static org.acra.ReportField.APP_VERSION_NAME;
|
||||
import static org.acra.ReportField.PACKAGE_NAME;
|
||||
import static org.acra.ReportField.REPORT_ID;
|
||||
import static org.acra.ReportField.STACK_TRACE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ReportFormFragment extends Fragment
|
||||
implements OnCheckedChangeListener {
|
||||
public class ReportFormFragment extends BaseFragment {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ReportFormFragment.class.getName());
|
||||
private static final String IS_FEEDBACK = "isFeedback";
|
||||
private static final Set<ReportField> requiredFields = new HashSet<>();
|
||||
private static final Set<ReportField> excludedFields = new HashSet<>();
|
||||
static {
|
||||
requiredFields.add(REPORT_ID);
|
||||
requiredFields.add(APP_VERSION_CODE);
|
||||
requiredFields.add(APP_VERSION_NAME);
|
||||
requiredFields.add(PACKAGE_NAME);
|
||||
requiredFields.add(ANDROID_VERSION);
|
||||
requiredFields.add(STACK_TRACE);
|
||||
}
|
||||
public final static String TAG = ReportFormFragment.class.getName();
|
||||
|
||||
private boolean isFeedback;
|
||||
private File reportFile;
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private ReportViewModel viewModel;
|
||||
|
||||
private EditText userCommentView;
|
||||
private EditText userEmailView;
|
||||
private CheckBox includeDebugReport;
|
||||
private Button chevron;
|
||||
private LinearLayout report;
|
||||
private RecyclerView list;
|
||||
private View progress;
|
||||
@Nullable
|
||||
private MenuItem sendReport;
|
||||
|
||||
static ReportFormFragment newInstance(boolean isFeedback,
|
||||
File reportFile) {
|
||||
ReportFormFragment f = new ReportFormFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putBoolean(IS_FEEDBACK, isFeedback);
|
||||
args.putSerializable(EXTRA_REPORT_FILE, reportFile);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(ReportViewModel.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -110,15 +75,10 @@ public class ReportFormFragment extends Fragment
|
||||
userEmailView = v.findViewById(R.id.user_email);
|
||||
includeDebugReport = v.findViewById(R.id.include_debug_report);
|
||||
chevron = v.findViewById(R.id.chevron);
|
||||
report = v.findViewById(R.id.report_content);
|
||||
list = v.findViewById(R.id.list);
|
||||
progress = v.findViewById(R.id.progress_wheel);
|
||||
|
||||
Bundle args = requireArguments();
|
||||
isFeedback = args.getBoolean(IS_FEEDBACK);
|
||||
reportFile =
|
||||
(File) requireNonNull(args.getSerializable(EXTRA_REPORT_FILE));
|
||||
|
||||
if (isFeedback) {
|
||||
if (viewModel.isFeedback()) {
|
||||
includeDebugReport
|
||||
.setText(getString(R.string.include_debug_report_feedback));
|
||||
userCommentView.setHint(R.string.enter_feedback);
|
||||
@@ -129,163 +89,73 @@ public class ReportFormFragment extends Fragment
|
||||
|
||||
chevron.setOnClickListener(view -> {
|
||||
boolean show = chevron.getText().equals(getString(R.string.show));
|
||||
if (show) {
|
||||
chevron.setText(R.string.hide);
|
||||
refresh();
|
||||
} else {
|
||||
chevron.setText(R.string.show);
|
||||
report.setVisibility(GONE);
|
||||
}
|
||||
viewModel.showReportData(show);
|
||||
});
|
||||
|
||||
viewModel.getShowReportData().observe(getViewLifecycleOwner(), show -> {
|
||||
if (show) {
|
||||
chevron.setText(R.string.hide);
|
||||
list.setVisibility(VISIBLE);
|
||||
if (list.getAdapter() == null) {
|
||||
progress.setVisibility(VISIBLE);
|
||||
} else {
|
||||
progress.setVisibility(INVISIBLE);
|
||||
}
|
||||
} else {
|
||||
chevron.setText(R.string.show);
|
||||
list.setVisibility(GONE);
|
||||
progress.setVisibility(INVISIBLE);
|
||||
}
|
||||
});
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
if (chevron.isSelected()) refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.dev_report_actions, menu);
|
||||
sendReport = menu.findItem(R.id.action_send_report);
|
||||
// calling setShowAsAction() shouldn't be needed, but for some reason is
|
||||
sendReport.setShowAsAction(SHOW_AS_ACTION_ALWAYS);
|
||||
sendReport.setEnabled(false);
|
||||
viewModel.getReportData().observe(getViewLifecycleOwner(), data -> {
|
||||
list.setAdapter(new ReportDataAdapter(data.getItems()));
|
||||
sendReport.setEnabled(true);
|
||||
progress.setVisibility(INVISIBLE);
|
||||
});
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_send_report) {
|
||||
processReport();
|
||||
sendReport();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
ReportField field = (ReportField) buttonView.getTag();
|
||||
if (field != null) {
|
||||
if (isChecked) excludedFields.remove(field);
|
||||
else excludedFields.add(field);
|
||||
}
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
report.setVisibility(INVISIBLE);
|
||||
progress.setVisibility(VISIBLE);
|
||||
report.removeAllViews();
|
||||
new AsyncTask<Void, Void, CrashReportData>() {
|
||||
|
||||
@Override
|
||||
protected CrashReportData doInBackground(Void... args) {
|
||||
CrashReportPersister persister = new CrashReportPersister();
|
||||
try {
|
||||
return persister.load(reportFile);
|
||||
} catch (IOException | JSONException e) {
|
||||
LOG.log(WARNING, "Could not load report file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(CrashReportData crashData) {
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
if (crashData != null) {
|
||||
for (Map.Entry<ReportField, Element> e : crashData
|
||||
.entrySet()) {
|
||||
ReportField field = e.getKey();
|
||||
StringBuilder valueBuilder = new StringBuilder();
|
||||
for (String pair : e.getValue().flatten()) {
|
||||
valueBuilder.append(pair).append("\n");
|
||||
}
|
||||
String value = valueBuilder.toString();
|
||||
boolean required = requiredFields.contains(field);
|
||||
boolean excluded = excludedFields.contains(field);
|
||||
View v = inflater.inflate(R.layout.list_item_crash,
|
||||
report, false);
|
||||
CheckBox cb = v.findViewById(R.id.include_in_report);
|
||||
cb.setTag(field);
|
||||
cb.setChecked(required || !excluded);
|
||||
cb.setEnabled(!required);
|
||||
cb.setOnCheckedChangeListener(ReportFormFragment.this);
|
||||
cb.setText(field.toString());
|
||||
TextView content = v.findViewById(R.id.content);
|
||||
content.setText(value);
|
||||
report.addView(v);
|
||||
}
|
||||
} else {
|
||||
View v = inflater.inflate(
|
||||
android.R.layout.simple_list_item_1, report, false);
|
||||
TextView error = v.findViewById(android.R.id.text1);
|
||||
error.setText(R.string.could_not_load_report_data);
|
||||
report.addView(v);
|
||||
}
|
||||
report.setVisibility(VISIBLE);
|
||||
progress.setVisibility(GONE);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void processReport() {
|
||||
private void sendReport() {
|
||||
userCommentView.setEnabled(false);
|
||||
userEmailView.setEnabled(false);
|
||||
requireNonNull(sendReport).setEnabled(false);
|
||||
list.setVisibility(GONE); // ensures that progress fits on screen
|
||||
progress.setVisibility(VISIBLE);
|
||||
boolean includeReport = !isFeedback || includeDebugReport.isChecked();
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... args) {
|
||||
CrashReportPersister persister = new CrashReportPersister();
|
||||
try {
|
||||
CrashReportData data = persister.load(reportFile);
|
||||
if (includeReport) {
|
||||
for (ReportField field : excludedFields) {
|
||||
LOG.info("Removing field " + field.name());
|
||||
data.remove(field);
|
||||
}
|
||||
} else {
|
||||
Iterator<Map.Entry<ReportField, Element>> iter =
|
||||
data.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
Map.Entry<ReportField, Element> e = iter.next();
|
||||
if (!requiredFields.contains(e.getKey())) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
persister.store(data, reportFile);
|
||||
return true;
|
||||
} catch (IOException | JSONException e) {
|
||||
LOG.log(WARNING, "Error processing report file", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Retrieve user's comment and email address, if any
|
||||
String comment = userCommentView.getText().toString();
|
||||
String email = userEmailView.getText().toString();
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean success) {
|
||||
if (success) {
|
||||
// Retrieve user's comment and email address, if any
|
||||
String comment = "";
|
||||
if (userCommentView != null)
|
||||
comment = userCommentView.getText().toString();
|
||||
String email = "";
|
||||
if (userEmailView != null) {
|
||||
email = userEmailView.getText().toString();
|
||||
}
|
||||
getDevReportActivity().sendCrashReport(comment, email);
|
||||
}
|
||||
if (getActivity() != null) getDevReportActivity().exit();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
boolean includeReport = includeDebugReport.isChecked();
|
||||
|
||||
private DevReportActivity getDevReportActivity() {
|
||||
return (DevReportActivity) requireActivity();
|
||||
// Send report (now or after next sign-in)
|
||||
if (viewModel.sendReport(comment, email, includeReport)) {
|
||||
// trying to send now
|
||||
Toast.makeText(requireContext(), R.string.dev_report_sending,
|
||||
LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.plugin.Plugin;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
import org.briarproject.bramble.api.reporting.DevReporter;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo;
|
||||
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
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.api.plugin.Plugin.State.ACTIVE;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ReportViewModel extends AndroidViewModel {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ReportViewModel.class.getName());
|
||||
|
||||
private final BriarReportCollector collector;
|
||||
private final DevReporter reporter;
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
private final MutableLiveEvent<Boolean> showReport =
|
||||
new MutableLiveEvent<>();
|
||||
private final MutableLiveData<Boolean> showReportData =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<ReportData> reportData =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveEvent<Integer> closeReport =
|
||||
new MutableLiveEvent<>();
|
||||
private boolean isFeedback;
|
||||
|
||||
@Inject
|
||||
public ReportViewModel(@NonNull Application application,
|
||||
DevReporter reporter, PluginManager pluginManager) {
|
||||
super(application);
|
||||
this.collector = new BriarReportCollector(application);
|
||||
this.reporter = reporter;
|
||||
this.pluginManager = pluginManager;
|
||||
}
|
||||
|
||||
void init(@Nullable Throwable t, long appStartTime) {
|
||||
isFeedback = t == null;
|
||||
if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> {
|
||||
ReportData data = collector.collectReportData(t, appStartTime);
|
||||
reportData.postValue(data);
|
||||
}).start();
|
||||
}
|
||||
|
||||
boolean isFeedback() {
|
||||
return isFeedback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this from the crash screen, if the user wants to report a crash.
|
||||
*/
|
||||
@UiThread
|
||||
void showReport() {
|
||||
showReport.setEvent(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be set to true when the user wants to report a crash.
|
||||
*/
|
||||
LiveEvent<Boolean> getShowReport() {
|
||||
return showReport;
|
||||
}
|
||||
|
||||
/**
|
||||
* The report data will be made visible in the UI when visible is true,
|
||||
* otherwise hidden.
|
||||
*/
|
||||
@UiThread
|
||||
void showReportData(boolean visible) {
|
||||
showReportData.setValue(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be set to true when the user wants to see report data.
|
||||
*/
|
||||
LiveData<Boolean> getShowReportData() {
|
||||
return showReportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The content of the report
|
||||
* that will be loaded after {@link #init(Throwable, long)} was called.
|
||||
*/
|
||||
LiveData<ReportData> getReportData() {
|
||||
return reportData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends reports and returns now if reports are being sent now
|
||||
* or false, if reports will be sent next time TorPlugin becomes active.
|
||||
*/
|
||||
@UiThread
|
||||
boolean sendReport(String comment, String email, boolean includeReport) {
|
||||
ReportData data = requireNonNull(reportData.getValue());
|
||||
if (!isNullOrEmpty(comment) || isNullOrEmpty(email)) {
|
||||
MultiReportInfo userInfo = new MultiReportInfo();
|
||||
if (!isNullOrEmpty(comment)) userInfo.add("Comment", comment);
|
||||
if (!isNullOrEmpty(email)) userInfo.add("Email", email);
|
||||
data.add(new ReportData.ReportItem("UserInfo", 0, userInfo, false));
|
||||
}
|
||||
|
||||
// check the state of the TorPlugin, if this is feedback
|
||||
boolean sendFeedbackNow;
|
||||
if (isFeedback) {
|
||||
Plugin plugin = pluginManager.getPlugin(TorConstants.ID);
|
||||
sendFeedbackNow = plugin != null && plugin.getState() == ACTIVE;
|
||||
} else {
|
||||
sendFeedbackNow = false;
|
||||
}
|
||||
|
||||
Runnable reportSender =
|
||||
getReportSender(includeReport, data, sendFeedbackNow);
|
||||
new SingleShotAndroidExecutor(reportSender).start();
|
||||
return sendFeedbackNow;
|
||||
}
|
||||
|
||||
private Runnable getReportSender(boolean includeReport, ReportData data,
|
||||
boolean sendFeedbackNow) {
|
||||
return () -> {
|
||||
boolean error = false;
|
||||
try {
|
||||
File reportDir = AndroidUtils.getReportDir(getApplication());
|
||||
String reportId = UUID.randomUUID().toString();
|
||||
String report = data.toJson(includeReport).toString();
|
||||
reporter.encryptReportToFile(reportDir, reportId, report);
|
||||
} catch (FileNotFoundException | JSONException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
error = true;
|
||||
}
|
||||
|
||||
int stringRes;
|
||||
if (error) {
|
||||
stringRes = R.string.dev_report_error;
|
||||
} else if (sendFeedbackNow) {
|
||||
boolean sent = reporter.sendReports() > 0;
|
||||
stringRes = sent ?
|
||||
R.string.dev_report_sent : R.string.dev_report_saved;
|
||||
} else {
|
||||
stringRes = R.string.dev_report_saved;
|
||||
}
|
||||
closeReport.postEvent(stringRes);
|
||||
};
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void closeReport() {
|
||||
closeReport.setEvent(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* An integer representing a string resource
|
||||
* informing about the outcome of the report
|
||||
* or 0 if no information is required, such as when back button was pressed.
|
||||
*/
|
||||
LiveEvent<Integer> getCloseReport() {
|
||||
return closeReport;
|
||||
}
|
||||
|
||||
// Used for a new thread as the Android executor thread may have died
|
||||
private static class SingleShotAndroidExecutor extends Thread {
|
||||
|
||||
private final Runnable runnable;
|
||||
|
||||
private SingleShotAndroidExecutor(Runnable runnable) {
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Looper.prepare();
|
||||
Handler handler = new Handler();
|
||||
handler.post(runnable);
|
||||
handler.post(() -> {
|
||||
Looper looper = Looper.myLooper();
|
||||
if (looper != null) looper.quit();
|
||||
});
|
||||
Looper.loop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.api.system.LocationUtils;
|
||||
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
|
||||
import org.briarproject.bramble.util.StringUtils;
|
||||
@@ -72,6 +71,7 @@ import static android.provider.Settings.EXTRA_CHANNEL_ID;
|
||||
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
|
||||
@@ -166,9 +166,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
@Inject
|
||||
CircumventionProvider circumventionProvider;
|
||||
|
||||
@Inject
|
||||
AndroidExecutor androidExecutor;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
@@ -226,11 +223,12 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
screenLock.setOnPreferenceChangeListener(this);
|
||||
screenLockTimeout.setOnPreferenceChangeListener(this);
|
||||
|
||||
findPreference("pref_key_send_feedback").setOnPreferenceClickListener(
|
||||
preference -> {
|
||||
triggerFeedback(androidExecutor);
|
||||
return true;
|
||||
});
|
||||
Preference prefFeedback =
|
||||
requireNonNull(findPreference("pref_key_send_feedback"));
|
||||
prefFeedback.setOnPreferenceClickListener(preference -> {
|
||||
triggerFeedback(requireContext());
|
||||
return true;
|
||||
});
|
||||
|
||||
if (SDK_INT < 27) {
|
||||
// remove System Default Theme option from preference entries
|
||||
@@ -245,17 +243,15 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
values.remove(getString(R.string.pref_theme_system_value));
|
||||
theme.setEntryValues(values.toArray(new CharSequence[0]));
|
||||
}
|
||||
Preference explode = requireNonNull(findPreference("pref_key_explode"));
|
||||
if (IS_DEBUG_BUILD) {
|
||||
findPreference("pref_key_explode").setOnPreferenceClickListener(
|
||||
preference -> {
|
||||
throw new RuntimeException("Boom!");
|
||||
}
|
||||
);
|
||||
explode.setOnPreferenceClickListener(preference -> {
|
||||
throw new RuntimeException("Boom!");
|
||||
});
|
||||
} else {
|
||||
findPreference("pref_key_explode").setVisible(false);
|
||||
explode.setVisible(false);
|
||||
findPreference("pref_key_test_data").setVisible(false);
|
||||
PreferenceGroup testing =
|
||||
findPreference("pref_key_explode").getParent();
|
||||
PreferenceGroup testing = explode.getParent();
|
||||
if (testing == null) throw new AssertionError();
|
||||
testing.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -110,7 +110,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
|
||||
new UiExceptionHandler<DbException>(this) {
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class ShareBlogActivity extends ShareActivity {
|
||||
Toast.makeText(ShareBlogActivity.this,
|
||||
R.string.blogs_sharing_error, LENGTH_SHORT)
|
||||
.show();
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ public class ShareForumActivity extends ShareActivity {
|
||||
Toast.makeText(ShareForumActivity.this,
|
||||
R.string.forum_share_error, LENGTH_SHORT)
|
||||
.show();
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -183,7 +183,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -214,7 +214,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -351,7 +351,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleDbException(exception);
|
||||
handleException(exception);
|
||||
}
|
||||
};
|
||||
getController().createAndStoreMessage(text, replyItem, handler);
|
||||
|
||||
@@ -27,14 +27,13 @@ import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.reporting.FeedbackActivity;
|
||||
import org.briarproject.briar.android.view.ArticleMovementMethod;
|
||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
||||
|
||||
@@ -51,6 +50,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
@@ -92,6 +92,8 @@ import static java.util.Objects.requireNonNull;
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
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_START_TIME;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
@@ -345,10 +347,18 @@ public class UiUtils {
|
||||
return fm.hasEnrolledFingerprints() && fm.isHardwareDetected();
|
||||
}
|
||||
|
||||
public static void triggerFeedback(AndroidExecutor androidExecutor) {
|
||||
androidExecutor.runOnBackgroundThread(
|
||||
() -> ACRA.getErrorReporter()
|
||||
.handleException(new UserFeedback(), false));
|
||||
public static void triggerFeedback(Context ctx) {
|
||||
startDevReportActivity(ctx, FeedbackActivity.class, null, null);
|
||||
}
|
||||
|
||||
public static void startDevReportActivity(Context ctx,
|
||||
Class<? extends FragmentActivity> activity, @Nullable Throwable t,
|
||||
@Nullable Long appStartTime) {
|
||||
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);
|
||||
ctx.startActivity(dialogIntent);
|
||||
}
|
||||
|
||||
public static boolean enterPressed(int actionId,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.briarproject.briar.android.util;
|
||||
|
||||
public class UserFeedback extends Exception {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package org.briarproject.briar.android.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbCallable;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public abstract class DbViewModel extends AndroidViewModel {
|
||||
|
||||
private static final Logger LOG = getLogger(DbViewModel.class.getName());
|
||||
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
private final LifecycleManager lifecycleManager;
|
||||
private final TransactionManager db;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
|
||||
public DbViewModel(
|
||||
@NonNull Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor) {
|
||||
super(application);
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.lifecycleManager = lifecycleManager;
|
||||
this.db = db;
|
||||
this.androidExecutor = androidExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given task on the {@link DatabaseExecutor}
|
||||
* and waits for the DB to open.
|
||||
* <p>
|
||||
* If you need a list of items to be displayed in a
|
||||
* {@link RecyclerView.Adapter},
|
||||
* use {@link #loadList(DbCallable, UiConsumer)} instead.
|
||||
*/
|
||||
protected void runOnDbThread(Runnable task) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
lifecycleManager.waitForDatabase();
|
||||
task.run();
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting for database");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a list of items on the {@link DatabaseExecutor} within a single
|
||||
* {@link Transaction} and publishes it as a {@link LiveResult}
|
||||
* to the {@link UiThread}.
|
||||
* <p>
|
||||
* Use this to ensure that modifications to your local list do not get
|
||||
* overridden by database loads that were in progress while the modification
|
||||
* was made.
|
||||
* E.g. An event about the removal of a message causes the message item to
|
||||
* be removed from the local list while all messages are reloaded.
|
||||
* This method ensures that those operations can be processed on the
|
||||
* UiThread in the correct order so that the removed message will not be
|
||||
* re-added when the re-load completes.
|
||||
*/
|
||||
protected <T extends List<?>> void loadList(
|
||||
DbCallable<T, DbException> task,
|
||||
UiConsumer<LiveResult<T>> uiConsumer) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
lifecycleManager.waitForDatabase();
|
||||
db.transaction(true, txn -> {
|
||||
T t = task.call(txn);
|
||||
txn.attach(() -> uiConsumer.accept(new LiveResult<>(t)));
|
||||
});
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting for database");
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
androidExecutor.runOnUiThread(
|
||||
() -> uiConsumer.accept(new LiveResult<>(e)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
public interface UiConsumer<T> {
|
||||
@UiThread
|
||||
void accept(T t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the list available in the given LiveData
|
||||
* and replaces items where the given test function returns true.
|
||||
*
|
||||
* @return a copy of the list in the LiveData with item(s) replaced
|
||||
* or null when the
|
||||
* <ul>
|
||||
* <li> LiveData does not have a value
|
||||
* <li> LiveResult in the LiveData has an error
|
||||
* <li> test function did return false for all items in the list
|
||||
* </ul>
|
||||
*/
|
||||
@Nullable
|
||||
protected <T> List<T> updateListItems(
|
||||
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test,
|
||||
Function<T, T> replacer) {
|
||||
List<T> items = getListCopy(liveData);
|
||||
if (items == null) return null;
|
||||
|
||||
ListIterator<T> iterator = items.listIterator();
|
||||
boolean changed = false;
|
||||
while (iterator.hasNext()) {
|
||||
T item = iterator.next();
|
||||
if (test.apply(item)) {
|
||||
changed = true;
|
||||
iterator.set(replacer.apply(item));
|
||||
}
|
||||
}
|
||||
return changed ? items : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the list available in the given LiveData
|
||||
* and removes the items from it where the given test function returns true.
|
||||
*
|
||||
* @return a copy of the list in the LiveData with item(s) removed
|
||||
* or null when the
|
||||
* <ul>
|
||||
* <li> LiveData does not have a value
|
||||
* <li> LiveResult in the LiveData has an error
|
||||
* <li> test function did return false for all items in the list
|
||||
* </ul>
|
||||
*/
|
||||
@Nullable
|
||||
protected <T> List<T> removeListItems(
|
||||
LiveData<LiveResult<List<T>>> liveData, Function<T, Boolean> test) {
|
||||
List<T> items = getListCopy(liveData);
|
||||
if (items == null) return null;
|
||||
|
||||
ListIterator<T> iterator = items.listIterator();
|
||||
boolean changed = false;
|
||||
while (iterator.hasNext()) {
|
||||
T item = iterator.next();
|
||||
if (test.apply(item)) {
|
||||
changed = true;
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
return changed ? items : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a copy of the list of items from the given LiveData
|
||||
* or null if it is not available.
|
||||
* The list copy can be safely mutated.
|
||||
*/
|
||||
@Nullable
|
||||
private <T> List<T> getListCopy(LiveData<LiveResult<List<T>>> liveData) {
|
||||
LiveResult<List<T>> value = liveData.getValue();
|
||||
if (value == null) return null;
|
||||
List<T> list = value.getResultOrNull();
|
||||
if (list == null) return null;
|
||||
return new ArrayList<>(list);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,14 +3,15 @@ package org.briarproject.briar.android.viewmodel;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
@NotNullByDefault
|
||||
public class LiveResult<T> {
|
||||
|
||||
@Nullable
|
||||
private T result;
|
||||
private final T result;
|
||||
@Nullable
|
||||
private Exception exception;
|
||||
private final Exception exception;
|
||||
|
||||
public LiveResult(T result) {
|
||||
this.result = result;
|
||||
@@ -36,4 +37,20 @@ public class LiveResult<T> {
|
||||
return exception != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given function, if {@link #hasError()} is true.
|
||||
*/
|
||||
public LiveResult<T> onError(Consumer<Exception> fun) {
|
||||
if (exception != null) fun.accept(exception);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given function, if {@link #hasError()} is false.
|
||||
*/
|
||||
public LiveResult<T> onSuccess(Consumer<T> fun) {
|
||||
if (result != null) fun.accept(result);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,6 +82,14 @@ public interface AndroidNotificationManager {
|
||||
|
||||
void unblockNotification(GroupId g);
|
||||
|
||||
void blockAllForumPostNotifications();
|
||||
|
||||
void unblockAllForumPostNotifications();
|
||||
|
||||
void blockAllGroupMessageNotifications();
|
||||
|
||||
void unblockAllGroupMessageNotifications();
|
||||
|
||||
void blockAllBlogPostNotifications();
|
||||
|
||||
void unblockAllBlogPostNotifications();
|
||||
|
||||
@@ -14,6 +14,8 @@ public interface ScreenFilterMonitor {
|
||||
* SYSTEM_ALERT_WINDOW permission, excluding system apps, Google Play
|
||||
* Services, and any apps that have been allowed by calling
|
||||
* {@link #allowApps(Collection)}.
|
||||
*
|
||||
* Only works on SDK_INT 29 and below.
|
||||
*/
|
||||
@UiThread
|
||||
Collection<AppDetails> getApps();
|
||||
@@ -21,6 +23,8 @@ public interface ScreenFilterMonitor {
|
||||
/**
|
||||
* Allows the apps with the given package names to use overlay windows.
|
||||
* They will not be returned by future calls to {@link #getApps()}.
|
||||
*
|
||||
* Only works on SDK_INT 29 and below.
|
||||
*/
|
||||
@UiThread
|
||||
void allowApps(Collection<String> packageNames);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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"
|
||||
@@ -79,19 +79,20 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/user_email_layout" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/report_content"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/margin_large"
|
||||
android:paddingTop="@dimen/margin_small"
|
||||
android:paddingEnd="@dimen/margin_large"
|
||||
android:paddingBottom="@dimen/listitem_height_one_line_avatar"
|
||||
android:paddingBottom="@dimen/margin_large"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/include_debug_report"
|
||||
tools:listitem="@layout/list_item_crash"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
@@ -99,8 +100,7 @@
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -109,4 +109,4 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -572,9 +572,12 @@
|
||||
<string name="optional_contact_email">بريدك الالكتروني (إختياري)</string>
|
||||
<string name="include_debug_report_crash">تضمين بيانات مجهولة عن الإنهيار</string>
|
||||
<string name="include_debug_report_feedback">تضمين بيانات مجهولة عن هذا الجهاز</string>
|
||||
<string name="could_not_load_report_data">لم يمكن تحميل بيانات التقرير.</string>
|
||||
<string name="dev_report_basic_info">المعلومات الأساسية</string>
|
||||
<string name="dev_report_storage">تخزين</string>
|
||||
<string name="dev_report_connectivity">الاتصال</string>
|
||||
<string name="send_report">ارسال التقرير</string>
|
||||
<string name="close">إغلاق</string>
|
||||
<string name="dev_report_sending">ارسال التعليقات</string>
|
||||
<string name="dev_report_saved">تم حفظ التقرير. سيتم إرساله عند تسجيل الدخول إلى Briar (براير) في المرة القادمة.</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">تسجيل الخروج من Briar (براير)...</string>
|
||||
|
||||
@@ -451,7 +451,6 @@
|
||||
<string name="optional_contact_email">Sizim elektron ünvanınız (istəyinizə bağlıdır)</string>
|
||||
<string name="include_debug_report_crash">Cədvəl haqqında anonim məlumatları əlavə edin</string>
|
||||
<string name="include_debug_report_feedback">Bu cihaz haqqında anonim məlumatları əlavə edin</string>
|
||||
<string name="could_not_load_report_data">Hesabat yüklənə bilmədi.</string>
|
||||
<string name="send_report">Hesabat göndər</string>
|
||||
<string name="close">Bağla</string>
|
||||
<string name="dev_report_saved">Hesabat saxlandı. Briar-a növbəti dəfə daxil olduqda göndəriləcək.</string>
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
<string name="optional_contact_email">Vaša email adresa (opciono)</string>
|
||||
<string name="include_debug_report_crash">Uključite anonimne podatke o rušenju</string>
|
||||
<string name="include_debug_report_feedback">Uključite anonimne podatke o ovom uređaju</string>
|
||||
<string name="could_not_load_report_data">Nije bilo moguće učitati podatke izvještaja</string>
|
||||
<string name="send_report">Pošalji izvještaj</string>
|
||||
<string name="close">Zatvori</string>
|
||||
<string name="dev_report_saved">Izvještaj je sačuvan. Biće poslat slijedeći put kada se ulogujete u Briar.</string>
|
||||
|
||||
@@ -532,7 +532,8 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
|
||||
<string name="optional_contact_email">La vostra adreça de correu (opcional)</string>
|
||||
<string name="include_debug_report_crash">Inclou dades anònimes sobre la fallida</string>
|
||||
<string name="include_debug_report_feedback">Inclou dades anònimes sobre el dispositiu</string>
|
||||
<string name="could_not_load_report_data">No s\'han pogut carregar les dades de l\'informe.</string>
|
||||
<string name="dev_report_basic_info">Informació bàsica</string>
|
||||
<string name="dev_report_storage">Emmagatzematge</string>
|
||||
<string name="send_report">Envia l\'informe</string>
|
||||
<string name="close">Tanca</string>
|
||||
<string name="dev_report_saved">S\'ha desat l\'informe. Se us enviarà la propera vegada que inicieu sessió a Briar.</string>
|
||||
|
||||
@@ -531,10 +531,22 @@
|
||||
<string name="optional_contact_email">Deine E-Mail-Adresse (optional)</string>
|
||||
<string name="include_debug_report_crash">Anonymisierte Daten über den Absturz anhängen</string>
|
||||
<string name="include_debug_report_feedback">Anonymisierte Daten über dieses Gerät anhängen</string>
|
||||
<string name="could_not_load_report_data">Konnte Daten des Berichts nicht laden</string>
|
||||
<string name="dev_report_basic_info">Basisinformationen</string>
|
||||
<string name="dev_report_device_info">Geräteinformationen</string>
|
||||
<string name="dev_report_stacktrace">Stacktrace</string>
|
||||
<string name="dev_report_time_info">Zeitangaben</string>
|
||||
<string name="dev_report_memory">Speicher</string>
|
||||
<string name="dev_report_storage">Speicher</string>
|
||||
<string name="dev_report_connectivity">Konnektivität</string>
|
||||
<string name="dev_report_build_config">Buildkonfiguration</string>
|
||||
<string name="dev_report_logcat">App-Log</string>
|
||||
<string name="dev_report_device_features">Geräteeigenschaften</string>
|
||||
<string name="send_report">Bericht senden</string>
|
||||
<string name="close">Schließen</string>
|
||||
<string name="dev_report_sending">Rückmeldung wird gesendet…</string>
|
||||
<string name="dev_report_sent">Feedback senden</string>
|
||||
<string name="dev_report_saved">Der Bericht wurde gespeichert. Er wird verschickt, wenn du dich das nächste Mal bei Briar anmeldest.</string>
|
||||
<string name="dev_report_error">Fehler: Senden des Reports fehlgeschlagen</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Von Briar abmelden...</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
|
||||
@@ -531,16 +531,30 @@
|
||||
<string name="optional_contact_email">Tu correo electrónico (opcional)</string>
|
||||
<string name="include_debug_report_crash">Incluir datos anónimos sobre la falla</string>
|
||||
<string name="include_debug_report_feedback">Incluir datos anónimos sobre este dispositivo</string>
|
||||
<string name="could_not_load_report_data">No se pudieron cargar los datos del informe.</string>
|
||||
<string name="dev_report_basic_info">Información básica</string>
|
||||
<string name="dev_report_device_info">Información del dispositivo</string>
|
||||
<string name="dev_report_stacktrace">Traza de pila</string>
|
||||
<string name="dev_report_time_info">Información temporal</string>
|
||||
<string name="dev_report_memory">Memoria</string>
|
||||
<string name="dev_report_storage">Almacenamiento</string>
|
||||
<string name="dev_report_connectivity">Conectividad</string>
|
||||
<string name="dev_report_build_config">Configuración de compilación</string>
|
||||
<string name="dev_report_logcat">Registro de la aplicación</string>
|
||||
<string name="dev_report_device_features">Características del dispositivo</string>
|
||||
<string name="send_report">Enviar informe</string>
|
||||
<string name="close">Cerrar</string>
|
||||
<string name="dev_report_sending">Enviando sus comentarios...</string>
|
||||
<string name="dev_report_sent">Comentarios enviados</string>
|
||||
<string name="dev_report_saved">Informe guardado. Se enviará la próxima vez que inicies sesión en Briar.</string>
|
||||
<string name="dev_report_error">Error: El envío del informe falló</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Cerrando sesión de Briar…</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
<string name="screen_filter_title">Superposición de pantalla detectada</string>
|
||||
<string name="screen_filter_body">Otra aplicación se está mostrando por encima de Briar. Por seguridad, Briar no reaccionará a los toques mientras otras aplicaciones se muestren por encima.\n\nLas siguientes aplis pueden ser las causantes:\n\n%1$s</string>
|
||||
<string name="screen_filter_body_api_30">Otra aplicación se está mostrando por encima de Briar. Para proteger tu seguridad, Briar no responderá a las pulsaciones cuando otra aplicación se muestre por encima.\n\nRevisa las aplicaciones aquí abajo para descubrir cuál puede ser la causante.</string>
|
||||
<string name="screen_filter_allow">Permitir a estas aplicaciones a mostrarse por encima</string>
|
||||
<string name="screen_filter_review_apps">Revisar aplicaciones</string>
|
||||
<!--Permission Requests-->
|
||||
<string name="permission_camera_title">Permiso de cámara</string>
|
||||
<string name="permission_camera_request_body">Para escanear el código QR, Briar necesita acceso a la cámara.</string>
|
||||
|
||||
@@ -497,7 +497,6 @@
|
||||
<string name="optional_contact_email">Zure e-mail helbidea (aukerakoa)</string>
|
||||
<string name="include_debug_report_crash">Gehitu kraskatzeari buruzko datu anonimoak</string>
|
||||
<string name="include_debug_report_feedback">Gehitu gailu honi buruzko datu anonimoak</string>
|
||||
<string name="could_not_load_report_data">Ezin izan dira txostenaren datuak kargatu.</string>
|
||||
<string name="send_report">Bidali txostena</string>
|
||||
<string name="close">Itxi</string>
|
||||
<string name="dev_report_saved">Txostena gordeta. Briar-en saioa hasten duzun hurrengoan bidaliko da.</string>
|
||||
|
||||
@@ -69,19 +69,29 @@
|
||||
<string name="sign_out_button">خروج</string>
|
||||
<!--Transports: Tor-->
|
||||
<string name="transport_tor">اینترنت</string>
|
||||
<string name="tor_device_status_offline">تلفن شما دارای دسترسی اینترنتی نیست</string>
|
||||
<string name="tor_plugin_status_enabling">Briar در حال اتصال به اینترنت می باشد</string>
|
||||
<string name="tor_plugin_status_active">Briar به اینترنت متصل شد</string>
|
||||
<string name="tor_plugin_status_inactive">Briar نمی تواند به اینترنت متصل شود</string>
|
||||
<string name="tor_plugin_status_disabled">Briar طوری پیکربندی شده تا از اینترنت استفاده نکند</string>
|
||||
<string name="tor_plugin_status_disabled_mobile_data">Briar طوری پیکربندی شده تا از داده موبایل استفاده نکند</string>
|
||||
<string name="tor_plugin_status_disabled_battery">Briar طوری پیکربندی شده تا از اینترنت در هنگام مصرف باتری استفاده نکند</string>
|
||||
<string name="tor_plugin_status_disabled_country_blocked">Briar طوری پیکربندی شده تا از اینترنت در این کشور استفاده نکند</string>
|
||||
<!--Transports: Wi-Fi-->
|
||||
<string name="transport_lan">وای فای</string>
|
||||
<string name="transport_lan_long">همان شبکه وای-فای</string>
|
||||
<string name="lan_device_status_on">موبایل شما به وای-فای وصل می باشد</string>
|
||||
<string name="lan_device_status_off">موبایل شما به وای-فای وصل نیست</string>
|
||||
<string name="lan_plugin_status_enabling">Briar در حال اتصال به شبکه وای-فای می باشد</string>
|
||||
<string name="lan_plugin_status_active">Briar به شبکه وای-فای متصل می باشد</string>
|
||||
<string name="lan_plugin_status_inactive">Briar نمیتواند به شبکه وای-فای وصل شود</string>
|
||||
<string name="lan_plugin_status_disabled">Briar طوری پیکربندی شده تا از شبکه وای-فای استفاده نکند</string>
|
||||
<!--Transports: Bluetooth-->
|
||||
<string name="transport_bt">بلوتوث</string>
|
||||
<string name="bt_device_status_on">بلوتوث موبایل شما روشن می باشد</string>
|
||||
<string name="bt_device_status_off">بلوتوث موبایل شما خاموش می باشد</string>
|
||||
<string name="bt_plugin_status_enabling">Briar در حال اتصال به بلوتوث می باشد</string>
|
||||
<string name="bt_plugin_status_active">Briar به بلوتوث وصل می باشد</string>
|
||||
<string name="bt_plugin_status_inactive">Briar نمی تواند به بلوتوث وصل شود</string>
|
||||
<string name="bt_plugin_status_disabled">Briar طوری پیکربندی شده که از بلوتوث استفاده نکند</string>
|
||||
<!--Notifications-->
|
||||
@@ -558,9 +568,18 @@
|
||||
<string name="optional_contact_email">آدرس ایمیل شما (اختیاری)</string>
|
||||
<string name="include_debug_report_crash">قرار دادن داده های ناشناس مربوط به خرابی</string>
|
||||
<string name="include_debug_report_feedback">قرار دادن داده های ناشناس درباره این دستگاه</string>
|
||||
<string name="could_not_load_report_data">امکان بارگذاری داده های گزارش وجود ندارد.</string>
|
||||
<string name="dev_report_basic_info">اطلاعات پایه</string>
|
||||
<string name="dev_report_device_info">اطلاعات دستگاه</string>
|
||||
<string name="dev_report_time_info">اطلاعات زمانی</string>
|
||||
<string name="dev_report_memory">حافظه</string>
|
||||
<string name="dev_report_storage">حافظه</string>
|
||||
<string name="dev_report_connectivity">اتصال</string>
|
||||
<string name="dev_report_build_config">پیکربندی ساخت</string>
|
||||
<string name="dev_report_device_features">ویژگیهای دستگاه</string>
|
||||
<string name="send_report">ارسال گزارش</string>
|
||||
<string name="close">بستن</string>
|
||||
<string name="dev_report_sending">در حال فرستادن نظر...</string>
|
||||
<string name="dev_report_sent">بازخورد ارسال شد</string>
|
||||
<string name="dev_report_saved">گزارش ذخیره شد. دفعه بعدی که وارد Briar (برایر) شدید فرستاده خواهد شد.</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">خروج از Briar (برایر)...</string>
|
||||
|
||||
@@ -531,10 +531,19 @@
|
||||
<string name="optional_contact_email">Votre adresse courriel (facultative)</string>
|
||||
<string name="include_debug_report_crash">Inclure des données anonymes concernant le plantage</string>
|
||||
<string name="include_debug_report_feedback">Inclure des données anonymes concernant cet appareil</string>
|
||||
<string name="could_not_load_report_data">Impossible de charger les données du rapport.</string>
|
||||
<string name="dev_report_basic_info">Informations de base</string>
|
||||
<string name="dev_report_device_info">Informations sur l\'appareil</string>
|
||||
<string name="dev_report_time_info">Informations temporelles</string>
|
||||
<string name="dev_report_memory">Mémoire</string>
|
||||
<string name="dev_report_storage">Stockage</string>
|
||||
<string name="dev_report_connectivity">Connectivité</string>
|
||||
<string name="dev_report_device_features">Fonctionnalités de l\'appareil</string>
|
||||
<string name="send_report">Envoyer le rapport</string>
|
||||
<string name="close">Fermer</string>
|
||||
<string name="dev_report_sending">Envoi de la rétroaction…</string>
|
||||
<string name="dev_report_sent">Rétroaction envoyée avec succès</string>
|
||||
<string name="dev_report_saved">Le rapport a été enregistré. Il sera envoyé lors de votre prochaine connexion à Briar.</string>
|
||||
<string name="dev_report_error">Erreur : l\'envoi du signalement a échoué</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Déconnexion de Briar…</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
|
||||
@@ -531,16 +531,30 @@
|
||||
<string name="optional_contact_email">O seu enderezo e-mail (optativo)</string>
|
||||
<string name="include_debug_report_crash">Incluír datos anónimos sobre o fallo</string>
|
||||
<string name="include_debug_report_feedback">Incluír datos anónimos sobre este dispositivo</string>
|
||||
<string name="could_not_load_report_data">Non se puideron cargar os datos do informe.</string>
|
||||
<string name="dev_report_basic_info">Información básica</string>
|
||||
<string name="dev_report_device_info">Información do dispositivo</string>
|
||||
<string name="dev_report_stacktrace">Trazas</string>
|
||||
<string name="dev_report_time_info">Información da hora</string>
|
||||
<string name="dev_report_memory">Memoria</string>
|
||||
<string name="dev_report_storage">Almacenaxe</string>
|
||||
<string name="dev_report_connectivity">Conectividade</string>
|
||||
<string name="dev_report_build_config">Configuración da compilación</string>
|
||||
<string name="dev_report_logcat">Rexistro da app</string>
|
||||
<string name="dev_report_device_features">Características do dispositivo</string>
|
||||
<string name="send_report">Enviar informe</string>
|
||||
<string name="close">Pechar</string>
|
||||
<string name="dev_report_sending">Enviando comentarios...</string>
|
||||
<string name="dev_report_sent">Comentarios enviados</string>
|
||||
<string name="dev_report_saved">Informe gardado. Enviarase a seguinte vez que se conecte con Briar.</string>
|
||||
<string name="dev_report_error">Erro: fallou o envío do informe</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Desconectando de Briar...</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
<string name="screen_filter_title">Detectouse unha sobreescrita da pantalla</string>
|
||||
<string name="screen_filter_body">Outra aplicación estase a amosar enriba de Briar. Para protexer a súa seguridade, Briar non responderá a toques cando outra aplicación está debuxando enriba.\n\nAs seguintes aplicacións poderían estar debuxando enriba:\n\n%1$s</string>
|
||||
<string name="screen_filter_body_api_30">Outra app ten acceso a ver a pantalla enriba de Briar. Para protexer a túa seguridade, Briar non vai responder aos toques cando outra app ten acceso a pantalla.\n\nRevisa as app de abaixo para atopara a resposable.</string>
|
||||
<string name="screen_filter_allow">Permitir a estas aplicación amosarse enriba</string>
|
||||
<string name="screen_filter_review_apps">Revisar apps</string>
|
||||
<!--Permission Requests-->
|
||||
<string name="permission_camera_title">Permiso da cámara</string>
|
||||
<string name="permission_camera_request_body">Para escanear códigos QR, Briar precisa acceso a cámara.</string>
|
||||
|
||||
@@ -555,16 +555,29 @@
|
||||
<string name="optional_contact_email">כתובת הדוא״ל שלך (רשותי)</string>
|
||||
<string name="include_debug_report_crash">כלול נתונים אלמוניים לגבי הקריסה</string>
|
||||
<string name="include_debug_report_feedback">כלול נתונים אלמוניים לגבי מכשיר זה</string>
|
||||
<string name="could_not_load_report_data">לא היה ניתן לטעון נתוני דוח.</string>
|
||||
<string name="dev_report_basic_info">מידע בסיסי</string>
|
||||
<string name="dev_report_device_info">מידע מכשיר</string>
|
||||
<string name="dev_report_time_info">מידע זמן</string>
|
||||
<string name="dev_report_memory">זיכרון</string>
|
||||
<string name="dev_report_storage">אחסון</string>
|
||||
<string name="dev_report_connectivity">קישוריות</string>
|
||||
<string name="dev_report_build_config">תצורת בנייה</string>
|
||||
<string name="dev_report_logcat">יומן יישום</string>
|
||||
<string name="dev_report_device_features">מאפייני מכשיר</string>
|
||||
<string name="send_report">שלח דוח</string>
|
||||
<string name="close">סגור</string>
|
||||
<string name="dev_report_sending">שולח משוב…</string>
|
||||
<string name="dev_report_sent">משוב נשלח</string>
|
||||
<string name="dev_report_saved">הדוח נשמר. הוא יישלח בפעם הבאה שתתחבר אל Briar.</string>
|
||||
<string name="dev_report_error">שגיאה: שליחת דוח נכשלה</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">מתנתק מן Briar…</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
<string name="screen_filter_title">ציפוי מסך התגלה</string>
|
||||
<string name="screen_filter_body">יישום אחר מציירת מעל Briar. כדי להגן על אבטחתך, Briar לא יגיב לנגיעות כאשר יישום אחר מצייר מעל.\n\nהיישומים הבאים יכולים לצייר מעל:\n\n%1$s</string>
|
||||
<string name="screen_filter_body_api_30">יישום אחר מצייר מעל Briar. כדי להגן על אבטחתך, Briar לא יגיב לנגיעות כאשר יישום אחר מצייר מעל.\n\nסקור יישומים למטה כדי למצוא את היישום האחראי.</string>
|
||||
<string name="screen_filter_allow">התר ליישומים אלו לצייר מעל</string>
|
||||
<string name="screen_filter_review_apps">סקור יישומים</string>
|
||||
<!--Permission Requests-->
|
||||
<string name="permission_camera_title">הרשאת מצלמה</string>
|
||||
<string name="permission_camera_request_body">כדי לסרוק את קוד ה־QR, היישום Briar צריך גישה אל המצלמה.</string>
|
||||
@@ -573,6 +586,7 @@
|
||||
<string name="permission_camera_location_title">מצלמה ומיקום</string>
|
||||
<string name="permission_camera_location_request_body">כדי לסרוק את קוד ה־QR, היישום Briar צריך הרשאה אל המצלמה.\n\nכדי לגלות מכשירי שן־כחולה, Briar צריך הרשאה להשיג גישה אל מיקומך.\n\nBriar אינו מאחסן את מיקומך או משתף אותו עם אף אחד.</string>
|
||||
<string name="permission_camera_denied_body">דחית גישה אל המצלמה, אבל הוספת אנשי קשר דורשת שימוש במצלמה.\n\nאנא שקול הענקת גישה.</string>
|
||||
<string name="permission_location_denied_body">דחית גישה אל המיקום שלך, אבל Briar צריך הרשאה זו כדי לגלות מכשירי Bluetooth.\n\nאנא שקול להעניק גישה.</string>
|
||||
<string name="qr_code">קוד QR</string>
|
||||
<string name="show_qr_code_fullscreen">הראה קוד QR במסך מלא</string>
|
||||
<!--App Locking-->
|
||||
|
||||
@@ -479,7 +479,6 @@
|
||||
<string name="optional_contact_email">आपका ईमेल पता (वैकल्पिक)</string>
|
||||
<string name="include_debug_report_crash">दुर्घटना के बारे में अनाम डेटा शामिल करें</string>
|
||||
<string name="include_debug_report_feedback">इस डिवाइस के बारे में अनाम डेटा शामिल करें</string>
|
||||
<string name="could_not_load_report_data">रिपोर्ट डेटा लोड नहीं किया जा सका</string>
|
||||
<string name="send_report">रिपोर्ट भेजो</string>
|
||||
<string name="close">बंद करे</string>
|
||||
<string name="dev_report_saved">रिपोर्ट सहेजी गई अगली बार जब आप Briar में प्रवेश करेंगे तो उसे भेजा जाएगा।</string>
|
||||
|
||||
@@ -540,16 +540,30 @@ Vigyázat: Ez végleg törli az identitásait, kapcsolatait és üzeneteit</stri
|
||||
<string name="optional_contact_email">Email címe (opcionális)</string>
|
||||
<string name="include_debug_report_crash">Névtelen adat beágyazása az összeomlásról</string>
|
||||
<string name="include_debug_report_feedback">Névtelen adat beágyazása az eszközről</string>
|
||||
<string name="could_not_load_report_data">Nem sikerült a jelentés adatot betölteni.</string>
|
||||
<string name="dev_report_basic_info">Alapinformáció</string>
|
||||
<string name="dev_report_device_info">Eszköz információ</string>
|
||||
<string name="dev_report_stacktrace">Stacktrace</string>
|
||||
<string name="dev_report_time_info">Idő információ</string>
|
||||
<string name="dev_report_memory">Memória</string>
|
||||
<string name="dev_report_storage">Tárhely</string>
|
||||
<string name="dev_report_connectivity">Csatlakozódás</string>
|
||||
<string name="dev_report_build_config">Build konfiguráció</string>
|
||||
<string name="dev_report_logcat">App log</string>
|
||||
<string name="dev_report_device_features">Eszköz szolgáltatások</string>
|
||||
<string name="send_report">Jelentés elküldése</string>
|
||||
<string name="close">Bezár</string>
|
||||
<string name="dev_report_sending">Visszajelzés küldése...</string>
|
||||
<string name="dev_report_sent">Visszajelzés elküldve</string>
|
||||
<string name="dev_report_saved">A jelentés mentve. Elküldésre kerül akkor, amikor legközelebb bejelentkezik a Briar-ba.</string>
|
||||
<string name="dev_report_error">Hiba: Riport küldése sikertelen</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Kilépés a Briar-ból...</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
<string name="screen_filter_title">Képernyő felülírás észlelve </string>
|
||||
<string name="screen_filter_body">Egy másik app a Briar tetejére rajzol. A biztonsága védelme érdekében a Briar nem válaszol az érintésre, ha egy másik app rajzol a tetejére.\n\nA következő app lehet, ami a tetejére rajzol:\n\n%1$s</string>
|
||||
<string name="screen_filter_body_api_30">Egy másik app a Briar tetejére rajzol. A biztonsága védelme érdekében a Briar nem válaszol az érintésre, ha egy másik app rajzol a tetejére.\n\nTekintsd át az appokat, hogy megtaláld az ezért felelőst.</string>
|
||||
<string name="screen_filter_allow">Az appok engedélyezése a felé rajzolásra</string>
|
||||
<string name="screen_filter_review_apps">Appok áttekintése</string>
|
||||
<!--Permission Requests-->
|
||||
<string name="permission_camera_title">Kamera jogosultságok</string>
|
||||
<string name="permission_camera_request_body">A QR kód olvasáshoz a Briar-nak szüksége van kamera hozzáférésre.</string>
|
||||
@@ -558,6 +572,7 @@ Vigyázat: Ez végleg törli az identitásait, kapcsolatait és üzeneteit</stri
|
||||
<string name="permission_camera_location_title">Kamera és lokáció</string>
|
||||
<string name="permission_camera_location_request_body">A QR kód beszkenneléséhez a Briar-nak szüksége van a Kamerához hozzáférésre.\n\nA Bluetooth eszközök észleléséhez a Briar-nak szükségve van a lokációhoz hozzáférésre.\n\nA Briar nem tárolja lokációját vagy ossza meg bárkivel.</string>
|
||||
<string name="permission_camera_denied_body">Megtiltotta hozzáférést a kamerához, de a kapcsolatok hozzáadásához szükséges a kamera.\n\nKérjük gondolja meg a jog megadását.</string>
|
||||
<string name="permission_location_denied_body">Megtiltotta hozzáférést a helyhez, azonban a Briar-nak szüksége van erre, hogy detektálja a Bluetooth eszközöket.\n\nKérjük gondolja meg a jog megadását.</string>
|
||||
<string name="qr_code">QR kód</string>
|
||||
<string name="show_qr_code_fullscreen">A QR kód teljes képernyősen</string>
|
||||
<!--App Locking-->
|
||||
@@ -571,15 +586,15 @@ Vigyázat: Ez végleg törli az identitásait, kapcsolatait és üzeneteit</stri
|
||||
<string name="transports_help_text">A Briar Interneten, Wi-Fi-n vagy Bluetooth-on keresztül csatlakozhat kapcsolataihoz.\n\nAz összes internetkapcsolat a Tor hálózaton megy keresztül megy az adatvédelem érdekében.\n\nHa egy kapcsolatot több módszerrel is el lehet érni, Briar párhuzamosan használja azokat.</string>
|
||||
<!--Screenshots-->
|
||||
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
|
||||
<string name="screenshot_alice">Alice</string>
|
||||
<string name="screenshot_alice">Alíz</string>
|
||||
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
|
||||
<string name="screenshot_bob">Bob</string>
|
||||
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
|
||||
<string name="screenshot_carol">Carol</string>
|
||||
<!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
|
||||
<string name="screenshot_message_1">Hi Bob!</string>
|
||||
<string name="screenshot_message_1">Szia Bob!</string>
|
||||
<!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
|
||||
<string name="screenshot_message_2">Hi Alice! Köszönöm hogy említette nekem a Briar-t!</string>
|
||||
<string name="screenshot_message_2">Szia Alíz! Köszönöm, hogy megemlítetted nekem a Briar-t!</string>
|
||||
<!--This is a message to be used in screenshots.-->
|
||||
<string name="screenshot_message_3">Szívesen, remélem tetszeni fog 😀</string>
|
||||
</resources>
|
||||
|
||||
@@ -531,16 +531,30 @@
|
||||
<string name="optional_contact_email">Tölvupóstfangið þitt (valfrjálst)</string>
|
||||
<string name="include_debug_report_crash">Senda nafnlaus gögn um hrunið</string>
|
||||
<string name="include_debug_report_feedback">Senda nafnlaus gögn um tækið</string>
|
||||
<string name="could_not_load_report_data">Gat ekki hlaðið inn gögnum skýrslunnar.</string>
|
||||
<string name="dev_report_basic_info">Grunnupplýsingar</string>
|
||||
<string name="dev_report_device_info">Upplýsingar um tæki</string>
|
||||
<string name="dev_report_stacktrace">Stacktrace-rakning</string>
|
||||
<string name="dev_report_time_info">Tímaupplýsingar</string>
|
||||
<string name="dev_report_memory">Minni</string>
|
||||
<string name="dev_report_storage">Geymslurými</string>
|
||||
<string name="dev_report_connectivity">Tengingar</string>
|
||||
<string name="dev_report_build_config">Byggingaruppsetning</string>
|
||||
<string name="dev_report_logcat">Atvikaskrá forrits</string>
|
||||
<string name="dev_report_device_features">Eiginleikar tækis</string>
|
||||
<string name="send_report">Senda skýrslu</string>
|
||||
<string name="close">Loka</string>
|
||||
<string name="dev_report_sending">Sendi umsögn…</string>
|
||||
<string name="dev_report_sent">Umsögn send</string>
|
||||
<string name="dev_report_saved">Skýrsla vistuð. Hún verður send næst þegar þú skráir þig inn í Briar.</string>
|
||||
<string name="dev_report_error">Villa: Sending skýrslu mistókst</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">Skrái út úr Briar…</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
<string name="screen_filter_title">Skjáyfirlag fannst</string>
|
||||
<string name="screen_filter_body">Annað forrit er að birta upplýsingar ofan á Briar. Í öryggisskyni mun Briar ekki bregðast við snertingum þegar annað forrit teiknar ofaná það.\n\nEftirfarandi forrit gætu verið að birta upplýsingar ofan á:\n\n%1$s</string>
|
||||
<string name="screen_filter_body_api_30">Annað forrit er að birta upplýsingar ofan á Briar. Í öryggisskyni mun Briar ekki bregðast við snertingum þegar annað forrit teiknar ofaná það.\n\nSkoðaðu eftirfarandi forrit til að finna það þeirra sem gæti valdið þessu.</string>
|
||||
<string name="screen_filter_allow">Leyfa þessum forritum að birta upplýsingar efst</string>
|
||||
<string name="screen_filter_review_apps">Yfirfara forrit</string>
|
||||
<!--Permission Requests-->
|
||||
<string name="permission_camera_title">Heimildir á myndavél</string>
|
||||
<string name="permission_camera_request_body">Til að skanna QR-kóðann þarf Briar heimild til að nota myndavélina.</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user