mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-16 04:39:54 +01:00
Compare commits
39 Commits
1837-conve
...
release-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;
|
package org.briarproject.bramble.network;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.LinkAddress;
|
||||||
|
import android.net.LinkProperties;
|
||||||
|
import android.net.Network;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
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;
|
||||||
import org.briarproject.bramble.api.system.TaskScheduler.Cancellable;
|
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.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION;
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
|
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.MINUTES;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class AndroidNetworkManager implements NetworkManager, Service {
|
class AndroidNetworkManager implements NetworkManager, Service {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(AndroidNetworkManager.class.getName());
|
getLogger(AndroidNetworkManager.class.getName());
|
||||||
|
|
||||||
// See android.net.wifi.WifiManager
|
// See android.net.wifi.WifiManager
|
||||||
private static final String WIFI_AP_STATE_CHANGED_ACTION =
|
private static final String WIFI_AP_STATE_CHANGED_ACTION =
|
||||||
@@ -54,7 +69,8 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
|||||||
private final TaskScheduler scheduler;
|
private final TaskScheduler scheduler;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
private final Executor eventExecutor;
|
private final Executor eventExecutor;
|
||||||
private final Context appContext;
|
private final Application app;
|
||||||
|
private final ConnectivityManager connectivityManager;
|
||||||
private final AtomicReference<Cancellable> connectivityCheck =
|
private final AtomicReference<Cancellable> connectivityCheck =
|
||||||
new AtomicReference<>();
|
new AtomicReference<>();
|
||||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||||
@@ -67,7 +83,9 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
|||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.eventExecutor = eventExecutor;
|
this.eventExecutor = eventExecutor;
|
||||||
this.appContext = app.getApplicationContext();
|
this.app = app;
|
||||||
|
connectivityManager = (ConnectivityManager)
|
||||||
|
requireNonNull(app.getSystemService(CONNECTIVITY_SERVICE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -82,24 +100,82 @@ class AndroidNetworkManager implements NetworkManager, Service {
|
|||||||
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
|
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
|
||||||
filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
|
filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
|
||||||
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
|
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
|
||||||
appContext.registerReceiver(networkStateReceiver, filter);
|
app.registerReceiver(networkStateReceiver, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stopService() {
|
public void stopService() {
|
||||||
if (networkStateReceiver != null)
|
if (networkStateReceiver != null)
|
||||||
appContext.unregisterReceiver(networkStateReceiver);
|
app.unregisterReceiver(networkStateReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NetworkStatus getNetworkStatus() {
|
public NetworkStatus getNetworkStatus() {
|
||||||
ConnectivityManager cm = (ConnectivityManager)
|
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
|
||||||
appContext.getSystemService(CONNECTIVITY_SERVICE);
|
|
||||||
if (cm == null) throw new AssertionError();
|
|
||||||
NetworkInfo net = cm.getActiveNetworkInfo();
|
|
||||||
boolean connected = net != null && net.isConnected();
|
boolean connected = net != null && net.isConnected();
|
||||||
boolean wifi = connected && net.getType() == TYPE_WIFI;
|
boolean wifi = false, ipv6Only = false;
|
||||||
return new NetworkStatus(connected, wifi);
|
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() {
|
private void updateConnectionStatus() {
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import android.bluetooth.BluetoothAdapter;
|
|||||||
import android.bluetooth.BluetoothDevice;
|
import android.bluetooth.BluetoothDevice;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.wifi.WifiConfiguration;
|
|
||||||
import android.net.wifi.WifiManager;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.StrictMode;
|
import android.os.StrictMode;
|
||||||
@@ -17,12 +15,10 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
|||||||
|
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static android.content.Context.WIFI_SERVICE;
|
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
import static android.provider.Settings.Secure.ANDROID_ID;
|
import static android.provider.Settings.Secure.ANDROID_ID;
|
||||||
|
|
||||||
@@ -52,15 +48,6 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
|
|||||||
String id = Settings.Secure.getString(contentResolver, ANDROID_ID);
|
String id = Settings.Secure.getString(contentResolver, ANDROID_ID);
|
||||||
if (id != null) out.writeUTF(id);
|
if (id != null) out.writeUTF(id);
|
||||||
Parcel parcel = Parcel.obtain();
|
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();
|
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
|
||||||
if (bt != null) {
|
if (bt != null) {
|
||||||
for (BluetoothDevice device : bt.getBondedDevices())
|
for (BluetoothDevice device : bt.getBondedDevices())
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.briarproject.bramble.api.client;
|
package org.briarproject.bramble.api.client;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
|
||||||
import org.briarproject.bramble.api.crypto.PrivateKey;
|
import org.briarproject.bramble.api.crypto.PrivateKey;
|
||||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||||
@@ -120,17 +119,4 @@ public interface ClientHelper {
|
|||||||
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
|
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
|
||||||
BdfDictionary properties) throws FormatException;
|
BdfDictionary properties) throws FormatException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the contact ID from the group metadata of the given contact
|
|
||||||
* group.
|
|
||||||
*/
|
|
||||||
ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
|
||||||
throws DbException, FormatException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the given contact ID in the group metadata of the given contact
|
|
||||||
* group.
|
|
||||||
*/
|
|
||||||
void setContactId(Transaction txn, GroupId contactGroupId, ContactId c)
|
|
||||||
throws DbException;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.briarproject.bramble.api.client;
|
|
||||||
|
|
||||||
public interface ContactGroupConstants {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group metadata key for associating a contact ID with a contact group.
|
|
||||||
*/
|
|
||||||
String GROUP_KEY_CONTACT_ID = "contactId";
|
|
||||||
}
|
|
||||||
@@ -8,11 +8,12 @@ import javax.annotation.concurrent.Immutable;
|
|||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class NetworkStatus {
|
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.connected = connected;
|
||||||
this.wifi = wifi;
|
this.wifi = wifi;
|
||||||
|
this.ipv6Only = ipv6Only;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isConnected() {
|
public boolean isConnected() {
|
||||||
@@ -22,4 +23,8 @@ public class NetworkStatus {
|
|||||||
public boolean isWifi() {
|
public boolean isWifi() {
|
||||||
return wifi;
|
return wifi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isIpv6Only() {
|
||||||
|
return ipv6Only;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public interface DevReporter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends any reports previously stored on disk.
|
* Sends any reports previously stored on disk.
|
||||||
|
*
|
||||||
|
* @return The number of reports that were sent.
|
||||||
*/
|
*/
|
||||||
void sendReports();
|
int sendReports();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import org.briarproject.bramble.api.data.BdfList;
|
|||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.Immutable;
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class ValidationUtils {
|
public class ValidationUtils {
|
||||||
|
|
||||||
@@ -66,9 +64,4 @@ public class ValidationUtils {
|
|||||||
if (dictionary != null && dictionary.size() != size)
|
if (dictionary != null && dictionary.size() != size)
|
||||||
throw new FormatException();
|
throw new FormatException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void checkRange(@Nullable Long l, long min, long max)
|
|
||||||
throws FormatException {
|
|
||||||
if (l != null && (l < min || l > max)) throw new FormatException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ package org.briarproject.bramble.client;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
import org.briarproject.bramble.api.client.ClientHelper;
|
import org.briarproject.bramble.api.client.ClientHelper;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
|
||||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||||
import org.briarproject.bramble.api.crypto.KeyParser;
|
import org.briarproject.bramble.api.crypto.KeyParser;
|
||||||
import org.briarproject.bramble.api.crypto.PrivateKey;
|
import org.briarproject.bramble.api.crypto.PrivateKey;
|
||||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||||
import org.briarproject.bramble.api.data.BdfEntry;
|
|
||||||
import org.briarproject.bramble.api.data.BdfList;
|
import org.briarproject.bramble.api.data.BdfList;
|
||||||
import org.briarproject.bramble.api.data.BdfReader;
|
import org.briarproject.bramble.api.data.BdfReader;
|
||||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||||
@@ -41,7 +39,6 @@ import java.util.Map.Entry;
|
|||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
|
|
||||||
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||||
@@ -392,27 +389,4 @@ class ClientHelperImpl implements ClientHelper {
|
|||||||
return tpMap;
|
return tpMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
|
||||||
throws DbException {
|
|
||||||
try {
|
|
||||||
BdfDictionary meta =
|
|
||||||
getGroupMetadataAsDictionary(txn, contactGroupId);
|
|
||||||
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
|
||||||
} catch (FormatException e) {
|
|
||||||
throw new DbException(e); // Invalid group metadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setContactId(Transaction txn, GroupId contactGroupId,
|
|
||||||
ContactId c) throws DbException {
|
|
||||||
BdfDictionary meta = BdfDictionary.of(
|
|
||||||
new BdfEntry(GROUP_KEY_CONTACT_ID, c.getInt()));
|
|
||||||
try {
|
|
||||||
mergeGroupMetadata(txn, contactGroupId, meta);
|
|
||||||
} catch (FormatException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.system.Clock;
|
|||||||
import org.briarproject.bramble.util.Base32;
|
import org.briarproject.bramble.util.Base32;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -52,7 +53,7 @@ class PendingContactFactoryImpl implements PendingContactFactory {
|
|||||||
byte[] raw = new byte[RAW_LINK_BYTES];
|
byte[] raw = new byte[RAW_LINK_BYTES];
|
||||||
raw[0] = FORMAT_VERSION;
|
raw[0] = FORMAT_VERSION;
|
||||||
arraycopy(encoded, 0, raw, 1, encoded.length);
|
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 {
|
private PublicKey parseHandshakeLink(String link) throws FormatException {
|
||||||
|
|||||||
@@ -863,6 +863,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
|||||||
if (!state.isTorRunning()) return;
|
if (!state.isTorRunning()) return;
|
||||||
boolean online = status.isConnected();
|
boolean online = status.isConnected();
|
||||||
boolean wifi = status.isWifi();
|
boolean wifi = status.isWifi();
|
||||||
|
boolean ipv6Only = status.isIpv6Only();
|
||||||
String country = locationUtils.getCurrentCountry();
|
String country = locationUtils.getCurrentCountry();
|
||||||
boolean blocked =
|
boolean blocked =
|
||||||
circumventionProvider.isTorProbablyBlocked(country);
|
circumventionProvider.isTorProbablyBlocked(country);
|
||||||
@@ -879,7 +880,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
|||||||
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
|
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
|
||||||
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
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");
|
if (country.isEmpty()) LOG.info("Country code unknown");
|
||||||
else LOG.info("Country code: " + country);
|
else LOG.info("Country code: " + country);
|
||||||
LOG.info("Charging: " + charging);
|
LOG.info("Charging: " + charging);
|
||||||
@@ -916,7 +918,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
|||||||
enableNetwork = true;
|
enableNetwork = true;
|
||||||
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
|
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
|
||||||
(automatic && bridgesWork)) {
|
(automatic && bridgesWork)) {
|
||||||
if (circumventionProvider.needsMeek(country)) {
|
if (ipv6Only ||
|
||||||
|
circumventionProvider.needsMeek(country)) {
|
||||||
LOG.info("Using meek bridges");
|
LOG.info("Using meek bridges");
|
||||||
enableBridges = true;
|
enableBridges = true;
|
||||||
useMeek = true;
|
useMeek = true;
|
||||||
@@ -942,6 +945,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
|||||||
if (enableNetwork) {
|
if (enableNetwork) {
|
||||||
enableBridges(enableBridges, useMeek);
|
enableBridges(enableBridges, useMeek);
|
||||||
enableConnectionPadding(enableConnectionPadding);
|
enableConnectionPadding(enableConnectionPadding);
|
||||||
|
useIpv6(ipv6Only);
|
||||||
}
|
}
|
||||||
enableNetwork(enableNetwork);
|
enableNetwork(enableNetwork);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -954,6 +958,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
|
|||||||
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
|
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
|
@ThreadSafe
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
protected class PluginState {
|
protected class PluginState {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import javax.annotation.concurrent.Immutable;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.net.SocketFactory;
|
import javax.net.SocketFactory;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||||
|
|
||||||
@@ -100,11 +101,12 @@ class DevReporterImpl implements DevReporter, EventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendReports() {
|
public int sendReports() {
|
||||||
File reportDir = devConfig.getReportDir();
|
File reportDir = devConfig.getReportDir();
|
||||||
File[] reports = reportDir.listFiles();
|
File[] reports = reportDir.listFiles();
|
||||||
|
int reportsSent = 0;
|
||||||
if (reports == null || reports.length == 0)
|
if (reports == null || reports.length == 0)
|
||||||
return; // No reports to send
|
return reportsSent; // No reports to send
|
||||||
|
|
||||||
LOG.info("Sending reports to developers");
|
LOG.info("Sending reports to developers");
|
||||||
for (File f : reports) {
|
for (File f : reports) {
|
||||||
@@ -116,13 +118,15 @@ class DevReporterImpl implements DevReporter, EventListener {
|
|||||||
in = new FileInputStream(f);
|
in = new FileInputStream(f);
|
||||||
IoUtils.copyAndClose(in, out);
|
IoUtils.copyAndClose(in, out);
|
||||||
f.delete();
|
f.delete();
|
||||||
|
reportsSent++;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.log(WARNING, "Failed to send reports", e);
|
LOG.log(WARNING, "Failed to send reports", e);
|
||||||
tryToClose(out, LOG, WARNING);
|
tryToClose(out, LOG, WARNING);
|
||||||
tryToClose(in, 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,5 +5,6 @@ interface ClientVersioningConstants {
|
|||||||
// Metadata keys
|
// Metadata keys
|
||||||
String MSG_KEY_UPDATE_VERSION = "version";
|
String MSG_KEY_UPDATE_VERSION = "version";
|
||||||
String MSG_KEY_LOCAL = "local";
|
String MSG_KEY_LOCAL = "local";
|
||||||
|
String GROUP_KEY_CONTACT_ID = "contactId";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import static java.util.Collections.emptyList;
|
|||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||||
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||||
|
|
||||||
@@ -160,7 +161,13 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
db.addGroup(txn, g);
|
db.addGroup(txn, g);
|
||||||
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
||||||
// Attach the contact ID to the group
|
// Attach the contact ID to the group
|
||||||
clientHelper.setContactId(txn, g.getId(), c.getId());
|
BdfDictionary meta = new BdfDictionary();
|
||||||
|
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
||||||
|
try {
|
||||||
|
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
// Create and store the first local update
|
// Create and store the first local update
|
||||||
List<ClientVersion> versions = new ArrayList<>(clients);
|
List<ClientVersion> versions = new ArrayList<>(clients);
|
||||||
Collections.sort(versions);
|
Collections.sort(versions);
|
||||||
@@ -222,7 +229,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
Map<ClientMajorVersion, Visibility> after =
|
Map<ClientMajorVersion, Visibility> after =
|
||||||
getVisibilities(newLocalStates, newRemoteStates);
|
getVisibilities(newLocalStates, newRemoteStates);
|
||||||
// Call hooks for any visibilities that have changed
|
// Call hooks for any visibilities that have changed
|
||||||
ContactId c = clientHelper.getContactId(txn, m.getGroupId());
|
ContactId c = getContactId(txn, m.getGroupId());
|
||||||
if (!before.equals(after)) {
|
if (!before.equals(after)) {
|
||||||
Contact contact = db.getContact(txn, c);
|
Contact contact = db.getContact(txn, c);
|
||||||
callVisibilityHooks(txn, contact, before, after);
|
callVisibilityHooks(txn, contact, before, after);
|
||||||
@@ -514,6 +521,17 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
storeUpdate(txn, g, states, 1);
|
storeUpdate(txn, g, states, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ContactId getContactId(Transaction txn, GroupId g)
|
||||||
|
throws DbException {
|
||||||
|
try {
|
||||||
|
BdfDictionary meta =
|
||||||
|
clientHelper.getGroupMetadataAsDictionary(txn, g);
|
||||||
|
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<ClientState> updateStatesFromRemoteStates(
|
private List<ClientState> updateStatesFromRemoteStates(
|
||||||
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
|
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
|
||||||
Set<ClientMajorVersion> remoteSet = new HashSet<>();
|
Set<ClientMajorVersion> remoteSet = new HashSet<>();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import static org.briarproject.bramble.test.TestUtils.getContact;
|
|||||||
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||||
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -59,6 +60,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
private final ClientId clientId = getClientId();
|
private final ClientId clientId = getClientId();
|
||||||
private final long now = System.currentTimeMillis();
|
private final long now = System.currentTimeMillis();
|
||||||
private final Transaction txn = new Transaction(null, false);
|
private final Transaction txn = new Transaction(null, false);
|
||||||
|
private final BdfDictionary groupMeta = BdfDictionary.of(
|
||||||
|
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
||||||
|
|
||||||
private ClientVersioningManagerImpl createInstance() {
|
private ClientVersioningManagerImpl createInstance() {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
@@ -120,8 +123,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(db).addGroup(txn, contactGroup);
|
oneOf(db).addGroup(txn, contactGroup);
|
||||||
oneOf(db).setGroupVisibility(txn, contact.getId(),
|
oneOf(db).setGroupVisibility(txn, contact.getId(),
|
||||||
contactGroup.getId(), SHARED);
|
contactGroup.getId(), SHARED);
|
||||||
oneOf(clientHelper).setContactId(txn, contactGroup.getId(),
|
oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(),
|
||||||
contact.getId());
|
groupMeta);
|
||||||
oneOf(clock).currentTimeMillis();
|
oneOf(clock).currentTimeMillis();
|
||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
|
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
|
||||||
@@ -457,8 +460,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
|
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
|
||||||
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
|
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
|
||||||
// Get contact ID
|
// Get contact ID
|
||||||
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
||||||
will(returnValue(contact.getId()));
|
contactGroup.getId());
|
||||||
|
will(returnValue(groupMeta));
|
||||||
// No states or visibilities have changed
|
// No states or visibilities have changed
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -488,9 +492,10 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
// Load the latest local update
|
// Load the latest local update
|
||||||
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
|
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
|
||||||
will(returnValue(oldLocalUpdateBody));
|
will(returnValue(oldLocalUpdateBody));
|
||||||
// Get contact ID
|
// Get client ID
|
||||||
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
||||||
will(returnValue(contact.getId()));
|
contactGroup.getId());
|
||||||
|
will(returnValue(groupMeta));
|
||||||
// No states or visibilities have changed
|
// No states or visibilities have changed
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -541,6 +546,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
||||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
||||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||||
|
BdfDictionary groupMeta = BdfDictionary.of(
|
||||||
|
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(clientHelper).toList(newRemoteUpdate);
|
oneOf(clientHelper).toList(newRemoteUpdate);
|
||||||
@@ -570,8 +577,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
||||||
newLocalUpdateMeta, true, false);
|
newLocalUpdateMeta, true, false);
|
||||||
// The client's visibility has changed
|
// The client's visibility has changed
|
||||||
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
||||||
will(returnValue(contact.getId()));
|
contactGroup.getId());
|
||||||
|
will(returnValue(groupMeta));
|
||||||
oneOf(db).getContact(txn, contact.getId());
|
oneOf(db).getContact(txn, contact.getId());
|
||||||
will(returnValue(contact));
|
will(returnValue(contact));
|
||||||
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
|
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
|
||||||
@@ -611,6 +619,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
||||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
||||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||||
|
BdfDictionary groupMeta = BdfDictionary.of(
|
||||||
|
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(clientHelper).toList(newRemoteUpdate);
|
oneOf(clientHelper).toList(newRemoteUpdate);
|
||||||
@@ -640,8 +650,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
||||||
newLocalUpdateMeta, true, false);
|
newLocalUpdateMeta, true, false);
|
||||||
// The client's visibility has changed
|
// The client's visibility has changed
|
||||||
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
||||||
will(returnValue(contact.getId()));
|
contactGroup.getId());
|
||||||
|
will(returnValue(groupMeta));
|
||||||
oneOf(db).getContact(txn, contact.getId());
|
oneOf(db).getContact(txn, contact.getId());
|
||||||
will(returnValue(contact));
|
will(returnValue(contact));
|
||||||
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
|
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import org.briarproject.bramble.api.network.NetworkStatus;
|
|||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.NetworkInterface;
|
import java.net.NetworkInterface;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
@@ -14,8 +16,8 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
import static java.net.NetworkInterface.getNetworkInterfaces;
|
import static java.net.NetworkInterface.getNetworkInterfaces;
|
||||||
import static java.util.Collections.list;
|
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.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@@ -23,7 +25,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
|||||||
class JavaNetworkManager implements NetworkManager {
|
class JavaNetworkManager implements NetworkManager {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(JavaNetworkManager.class.getName());
|
getLogger(JavaNetworkManager.class.getName());
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
JavaNetworkManager() {
|
JavaNetworkManager() {
|
||||||
@@ -31,26 +33,28 @@ class JavaNetworkManager implements NetworkManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NetworkStatus getNetworkStatus() {
|
public NetworkStatus getNetworkStatus() {
|
||||||
boolean connected = false;
|
boolean connected = false, hasIpv4 = false, hasIpv6Unicast = false;
|
||||||
try {
|
try {
|
||||||
Enumeration<NetworkInterface> interfaces = getNetworkInterfaces();
|
Enumeration<NetworkInterface> interfaces = getNetworkInterfaces();
|
||||||
if (interfaces != null) {
|
if (interfaces == null) {
|
||||||
|
LOG.info("No network interfaces");
|
||||||
|
} else {
|
||||||
for (NetworkInterface i : list(interfaces)) {
|
for (NetworkInterface i : list(interfaces)) {
|
||||||
if (i.isLoopback()) continue;
|
if (i.isLoopback() || !i.isUp()) continue;
|
||||||
if (i.isUp() && i.getInetAddresses().hasMoreElements()) {
|
for (InetAddress addr : list(i.getInetAddresses())) {
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Interface " + i.getDisplayName() +
|
|
||||||
" is up with at least one address.");
|
|
||||||
}
|
|
||||||
connected = true;
|
connected = true;
|
||||||
break;
|
if (addr instanceof Inet4Address) {
|
||||||
|
hasIpv4 = true;
|
||||||
|
} else if (!addr.isMulticastAddress()) {
|
||||||
|
hasIpv6Unicast = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
logException(LOG, WARNING, 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 'com.google.android.material:material:1.2.1'
|
||||||
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc03'
|
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.panic:panic:1.0'
|
||||||
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
|
||||||
implementation 'de.hdodenhof:circleimageview:3.0.1'
|
implementation 'de.hdodenhof:circleimageview:3.0.1'
|
||||||
@@ -127,10 +126,11 @@ dependencies {
|
|||||||
testImplementation 'androidx.test:runner:1.3.0'
|
testImplementation 'androidx.test:runner:1.3.0'
|
||||||
testImplementation 'androidx.test.ext:junit:1.1.2'
|
testImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
testImplementation 'androidx.fragment:fragment-testing:1.2.5'
|
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 "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||||
testImplementation 'org.robolectric:robolectric:4.3.1'
|
testImplementation 'org.robolectric:robolectric:4.3.1'
|
||||||
testImplementation 'org.mockito:mockito-core:3.1.0'
|
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:$jmockVersion"
|
||||||
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
|
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
|
||||||
testImplementation "org.jmock:jmock-legacy:$jmockVersion"
|
testImplementation "org.jmock:jmock-legacy:$jmockVersion"
|
||||||
|
|||||||
@@ -65,30 +65,43 @@
|
|||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="org.briarproject.briar.android.NotificationCleanupService"
|
android:name="org.briarproject.briar.android.NotificationCleanupService"
|
||||||
android:exported="false"></service>
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.reporting.DevReportActivity"
|
android:name="org.briarproject.briar.android.reporting.CrashReportActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:finishOnTaskLaunch="true"
|
android:finishOnTaskLaunch="true"
|
||||||
android:label="@string/crash_report_title"
|
android:label="@string/crash_report_title"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
|
android:process=":briar_error_handler"
|
||||||
android:theme="@style/BriarTheme.NoActionBar"
|
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
|
<activity
|
||||||
android:name="org.briarproject.briar.android.splash.ExpiredActivity"
|
android:name="org.briarproject.briar.android.splash.ExpiredActivity"
|
||||||
android:label="@string/app_name"></activity>
|
android:label="@string/app_name" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.login.StartupActivity"
|
android:name="org.briarproject.briar.android.login.StartupActivity"
|
||||||
android:label="@string/app_name"></activity>
|
android:label="@string/app_name" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.account.SetupActivity"
|
android:name="org.briarproject.briar.android.account.SetupActivity"
|
||||||
android:label="@string/setup_title"
|
android:label="@string/setup_title"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"></activity>
|
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.splash.SplashScreenActivity"
|
android:name="org.briarproject.briar.android.splash.SplashScreenActivity"
|
||||||
@@ -141,16 +154,6 @@
|
|||||||
android:value="org.briarproject.briar.android.conversation.ConversationActivity" />
|
android:value="org.briarproject.briar.android.conversation.ConversationActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".android.conversation.ConversationSettingsActivity"
|
|
||||||
android:parentActivityName="org.briarproject.briar.android.conversation.ConversationActivity"
|
|
||||||
android:label="@string/disappearing_messages_title"
|
|
||||||
android:theme="@style/BriarTheme">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value="org.briarproject.briar.android.conversation.ConversationActivity" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
|
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
|
||||||
android:label="@string/groups_create_group_title"
|
android:label="@string/groups_create_group_title"
|
||||||
@@ -356,7 +359,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.StartupFailureActivity"
|
android:name="org.briarproject.briar.android.StartupFailureActivity"
|
||||||
android:label="@string/startup_failed_activity_title"></activity>
|
android:label="@string/startup_failed_activity_title" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.settings.SettingsActivity"
|
android:name="org.briarproject.briar.android.settings.SettingsActivity"
|
||||||
@@ -422,11 +425,11 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.briarproject.briar.android.logout.ExitActivity"
|
android:name="org.briarproject.briar.android.logout.ExitActivity"
|
||||||
android:theme="@android:style/Theme.NoDisplay"></activity>
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".android.logout.HideUiActivity"
|
android:name=".android.logout.HideUiActivity"
|
||||||
android:theme="@android:style/Theme.NoDisplay"></activity>
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".android.account.UnlockActivity"
|
android:name=".android.account.UnlockActivity"
|
||||||
@@ -446,4 +449,27 @@
|
|||||||
android:theme="@style/BriarTheme" />
|
android:theme="@style/BriarTheme" />
|
||||||
|
|
||||||
</application>
|
</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>
|
</manifest>
|
||||||
|
|||||||
@@ -34,13 +34,11 @@ import org.briarproject.briar.BriarCoreModule;
|
|||||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||||
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
||||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||||
import org.briarproject.briar.android.reporting.BriarReportSender;
|
|
||||||
import org.briarproject.briar.android.view.EmojiTextInputView;
|
import org.briarproject.briar.android.view.EmojiTextInputView;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||||
import org.briarproject.briar.api.android.LockManager;
|
import org.briarproject.briar.api.android.LockManager;
|
||||||
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
||||||
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
|
|
||||||
import org.briarproject.briar.api.blog.BlogManager;
|
import org.briarproject.briar.api.blog.BlogManager;
|
||||||
import org.briarproject.briar.api.blog.BlogPostFactory;
|
import org.briarproject.briar.api.blog.BlogPostFactory;
|
||||||
import org.briarproject.briar.api.blog.BlogSharingManager;
|
import org.briarproject.briar.api.blog.BlogSharingManager;
|
||||||
@@ -88,6 +86,8 @@ public interface AndroidComponent
|
|||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
Executor databaseExecutor();
|
Executor databaseExecutor();
|
||||||
|
|
||||||
|
TransactionManager transactionManager();
|
||||||
|
|
||||||
MessageTracker messageTracker();
|
MessageTracker messageTracker();
|
||||||
|
|
||||||
LifecycleManager lifecycleManager();
|
LifecycleManager lifecycleManager();
|
||||||
@@ -171,16 +171,10 @@ public interface AndroidComponent
|
|||||||
|
|
||||||
AndroidWakeLockManager wakeLockManager();
|
AndroidWakeLockManager wakeLockManager();
|
||||||
|
|
||||||
TransactionManager transactionManager();
|
|
||||||
|
|
||||||
AutoDeleteManager autoDeleteManager();
|
|
||||||
|
|
||||||
void inject(SignInReminderReceiver briarService);
|
void inject(SignInReminderReceiver briarService);
|
||||||
|
|
||||||
void inject(BriarService briarService);
|
void inject(BriarService briarService);
|
||||||
|
|
||||||
void inject(BriarReportSender briarReportSender);
|
|
||||||
|
|
||||||
void inject(NotificationCleanupService notificationCleanupService);
|
void inject(NotificationCleanupService notificationCleanupService);
|
||||||
|
|
||||||
void inject(EmojiTextInputView textInputView);
|
void inject(EmojiTextInputView textInputView);
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
|||||||
@Nullable
|
@Nullable
|
||||||
private GroupId blockedGroup = null;
|
private GroupId blockedGroup = null;
|
||||||
private boolean blockSignInReminder = false;
|
private boolean blockSignInReminder = false;
|
||||||
private boolean blockBlogs = false;
|
private boolean blockForums = false, blockGroups = false,
|
||||||
|
blockBlogs = false;
|
||||||
private long lastSound = 0;
|
private long lastSound = 0;
|
||||||
|
|
||||||
private volatile Settings settings = new Settings();
|
private volatile Settings settings = new Settings();
|
||||||
@@ -223,8 +224,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
|||||||
if (s.getNamespace().equals(SETTINGS_NAMESPACE))
|
if (s.getNamespace().equals(SETTINGS_NAMESPACE))
|
||||||
settings = s.getSettings();
|
settings = s.getSettings();
|
||||||
} else if (e instanceof ConversationMessageReceivedEvent) {
|
} else if (e instanceof ConversationMessageReceivedEvent) {
|
||||||
ConversationMessageReceivedEvent p =
|
ConversationMessageReceivedEvent<?> p =
|
||||||
(ConversationMessageReceivedEvent) e;
|
(ConversationMessageReceivedEvent<?>) e;
|
||||||
showContactNotification(p.getContactId());
|
showContactNotification(p.getContactId());
|
||||||
} else if (e instanceof GroupMessageAddedEvent) {
|
} else if (e instanceof GroupMessageAddedEvent) {
|
||||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||||
@@ -385,6 +386,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void showGroupMessageNotification(GroupId g) {
|
private void showGroupMessageNotification(GroupId g) {
|
||||||
|
if (blockGroups) return;
|
||||||
if (g.equals(blockedGroup)) return;
|
if (g.equals(blockedGroup)) return;
|
||||||
groupCounts.add(g);
|
groupCounts.add(g);
|
||||||
updateGroupMessageNotification(true);
|
updateGroupMessageNotification(true);
|
||||||
@@ -452,6 +454,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void showForumPostNotification(GroupId g) {
|
private void showForumPostNotification(GroupId g) {
|
||||||
|
if (blockForums) return;
|
||||||
if (g.equals(blockedGroup)) return;
|
if (g.equals(blockedGroup)) return;
|
||||||
forumCounts.add(g);
|
forumCounts.add(g);
|
||||||
updateForumPostNotification(true);
|
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
|
@Override
|
||||||
public void blockAllBlogPostNotifications() {
|
public void blockAllBlogPostNotifications() {
|
||||||
androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = true);
|
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.AndroidUtils;
|
||||||
import org.briarproject.bramble.util.StringUtils;
|
import org.briarproject.bramble.util.StringUtils;
|
||||||
import org.briarproject.briar.android.account.LockManagerImpl;
|
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.keyagreement.ContactExchangeModule;
|
||||||
import org.briarproject.briar.android.login.LoginModule;
|
import org.briarproject.briar.android.login.LoginModule;
|
||||||
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
|
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.android.viewmodel.ViewModelModule;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.android.DozeWatchdog;
|
import org.briarproject.briar.api.android.DozeWatchdog;
|
||||||
@@ -63,7 +66,11 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
|||||||
ContactExchangeModule.class,
|
ContactExchangeModule.class,
|
||||||
LoginModule.class,
|
LoginModule.class,
|
||||||
NavDrawerModule.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 {
|
public class AppModule {
|
||||||
|
|
||||||
@@ -196,7 +203,10 @@ public class AppModule {
|
|||||||
ScreenFilterMonitor provideScreenFilterMonitor(
|
ScreenFilterMonitor provideScreenFilterMonitor(
|
||||||
LifecycleManager lifecycleManager,
|
LifecycleManager lifecycleManager,
|
||||||
ScreenFilterMonitorImpl screenFilterMonitor) {
|
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;
|
return screenFilterMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,13 @@ import android.preference.PreferenceManager;
|
|||||||
import com.vanniktech.emoji.EmojiManager;
|
import com.vanniktech.emoji.EmojiManager;
|
||||||
import com.vanniktech.emoji.google.GoogleEmojiProvider;
|
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.BrambleAndroidEagerSingletons;
|
||||||
import org.briarproject.bramble.BrambleAppComponent;
|
import org.briarproject.bramble.BrambleAppComponent;
|
||||||
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
||||||
import org.briarproject.briar.BriarCoreEagerSingletons;
|
import org.briarproject.briar.BriarCoreEagerSingletons;
|
||||||
import org.briarproject.briar.BuildConfig;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.logging.CachingLogHandler;
|
import org.briarproject.briar.android.logging.CachingLogHandler;
|
||||||
import org.briarproject.briar.android.reporting.BriarReportPrimer;
|
import org.briarproject.briar.android.reporting.BriarExceptionHandler;
|
||||||
import org.briarproject.briar.android.reporting.BriarReportSenderFactory;
|
|
||||||
import org.briarproject.briar.android.reporting.DevReportActivity;
|
|
||||||
import org.briarproject.briar.android.util.UiUtils;
|
import org.briarproject.briar.android.util.UiUtils;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -34,50 +28,14 @@ import java.util.logging.Handler;
|
|||||||
import java.util.logging.LogRecord;
|
import java.util.logging.LogRecord;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
|
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
|
||||||
import static java.util.logging.Level.FINE;
|
import static java.util.logging.Level.FINE;
|
||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static 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;
|
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
|
public class BriarApplicationImpl extends Application
|
||||||
implements BriarApplication {
|
implements BriarApplication {
|
||||||
|
|
||||||
@@ -85,12 +43,15 @@ public class BriarApplicationImpl extends Application
|
|||||||
getLogger(BriarApplicationImpl.class.getName());
|
getLogger(BriarApplicationImpl.class.getName());
|
||||||
|
|
||||||
private final CachingLogHandler logHandler = new CachingLogHandler();
|
private final CachingLogHandler logHandler = new CachingLogHandler();
|
||||||
|
private final BriarExceptionHandler exceptionHandler =
|
||||||
|
new BriarExceptionHandler(this);
|
||||||
|
|
||||||
private AndroidComponent applicationComponent;
|
private AndroidComponent applicationComponent;
|
||||||
private volatile SharedPreferences prefs;
|
private volatile SharedPreferences prefs;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void attachBaseContext(Context base) {
|
protected void attachBaseContext(Context base) {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
|
||||||
if (prefs == null)
|
if (prefs == null)
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(base);
|
prefs = PreferenceManager.getDefaultSharedPreferences(base);
|
||||||
// Loading the language needs to be done here.
|
// Loading the language needs to be done here.
|
||||||
@@ -98,7 +59,6 @@ public class BriarApplicationImpl extends Application
|
|||||||
super.attachBaseContext(
|
super.attachBaseContext(
|
||||||
Localizer.getInstance().setLocale(base));
|
Localizer.getInstance().setLocale(base));
|
||||||
setTheme(base, prefs);
|
setTheme(base, prefs);
|
||||||
ACRA.init(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -144,7 +104,7 @@ public class BriarApplicationImpl extends Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
Localizer.getInstance().setLocale(this);
|
Localizer.getInstance().setLocale(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
|
|||||||
Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
|
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
|
* effectively a system app, but not flagged as such on older systems
|
||||||
*/
|
*/
|
||||||
private static final String PLAY_SERVICES_PACKAGE =
|
private static final String PLAY_SERVICES_PACKAGE =
|
||||||
@@ -108,7 +108,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
|
|||||||
Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
|
Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
|
||||||
Collections.emptySet());
|
Collections.emptySet());
|
||||||
List<AppDetails> apps = new ArrayList<>();
|
List<AppDetails> apps = new ArrayList<>();
|
||||||
List<PackageInfo> packageInfos =
|
@SuppressLint("QueryPermissionsNeeded") List<PackageInfo> packageInfos =
|
||||||
pm.getInstalledPackages(GET_PERMISSIONS);
|
pm.getInstalledPackages(GET_PERMISSIONS);
|
||||||
for (PackageInfo packageInfo : packageInfos) {
|
for (PackageInfo packageInfo : packageInfos) {
|
||||||
if (!allowed.contains(packageInfo.packageName)
|
if (!allowed.contains(packageInfo.packageName)
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
|
|||||||
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
||||||
import org.briarproject.briar.android.conversation.AliasDialogFragment;
|
import org.briarproject.briar.android.conversation.AliasDialogFragment;
|
||||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||||
import org.briarproject.briar.android.conversation.ConversationSettingsActivity;
|
|
||||||
import org.briarproject.briar.android.conversation.ConversationSettingsFragment;
|
|
||||||
import org.briarproject.briar.android.conversation.ImageActivity;
|
import org.briarproject.briar.android.conversation.ImageActivity;
|
||||||
import org.briarproject.briar.android.conversation.ImageFragment;
|
import org.briarproject.briar.android.conversation.ImageFragment;
|
||||||
import org.briarproject.briar.android.forum.CreateForumActivity;
|
import org.briarproject.briar.android.forum.CreateForumActivity;
|
||||||
@@ -62,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.GroupInvitationActivity;
|
||||||
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationModule;
|
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationModule;
|
||||||
import org.briarproject.briar.android.privategroup.list.GroupListFragment;
|
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.GroupMemberListActivity;
|
||||||
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule;
|
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule;
|
||||||
import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule;
|
import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule;
|
||||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
||||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsFragment;
|
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.SettingsActivity;
|
||||||
import org.briarproject.briar.android.settings.SettingsFragment;
|
import org.briarproject.briar.android.settings.SettingsFragment;
|
||||||
import org.briarproject.briar.android.sharing.BlogInvitationActivity;
|
import org.briarproject.briar.android.sharing.BlogInvitationActivity;
|
||||||
@@ -93,7 +93,6 @@ import dagger.Component;
|
|||||||
ForumModule.class,
|
ForumModule.class,
|
||||||
GroupInvitationModule.class,
|
GroupInvitationModule.class,
|
||||||
GroupConversationModule.class,
|
GroupConversationModule.class,
|
||||||
GroupListModule.class,
|
|
||||||
GroupMemberModule.class,
|
GroupMemberModule.class,
|
||||||
GroupRevealModule.class,
|
GroupRevealModule.class,
|
||||||
SharingModule.class
|
SharingModule.class
|
||||||
@@ -186,7 +185,7 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(PendingContactListActivity activity);
|
void inject(PendingContactListActivity activity);
|
||||||
|
|
||||||
void inject(ConversationSettingsActivity activity);
|
void inject(CrashReportActivity crashReportActivity);
|
||||||
|
|
||||||
// Fragments
|
// Fragments
|
||||||
|
|
||||||
@@ -238,6 +237,8 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(ImageFragment imageFragment);
|
void inject(ImageFragment imageFragment);
|
||||||
|
|
||||||
void inject(ConversationSettingsFragment conversationSettingsFragment);
|
void inject(ReportFormFragment reportFormFragment);
|
||||||
|
|
||||||
|
void inject(CrashFragment crashFragment);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewGroup.LayoutParams;
|
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.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
@@ -18,7 +17,6 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
|
|||||||
import org.briarproject.briar.android.forum.ForumModule;
|
import org.briarproject.briar.android.forum.ForumModule;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
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.util.UiUtils;
|
||||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout;
|
import org.briarproject.briar.android.widget.TapSafeFrameLayout;
|
||||||
import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener;
|
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.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
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.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
||||||
import static androidx.lifecycle.Lifecycle.State.STARTED;
|
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.Level.INFO;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
|
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
|
||||||
@@ -50,7 +50,6 @@ import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Warning: Some activities don't extend {@link BaseActivity}.
|
* Warning: Some activities don't extend {@link BaseActivity}.
|
||||||
* E.g. {@link DevReportActivity}
|
|
||||||
*/
|
*/
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
@@ -123,6 +122,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
return new ActivityModule(this);
|
return new ActivityModule(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO use a test module where this is used in tests
|
||||||
protected ForumModule getForumModule() {
|
protected ForumModule getForumModule() {
|
||||||
return new ForumModule();
|
return new ForumModule();
|
||||||
}
|
}
|
||||||
@@ -202,9 +202,15 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
// If the dialog is already visible, filter the tap
|
// If the dialog is already visible, filter the tap
|
||||||
ScreenFilterDialogFragment f = findDialogFragment();
|
ScreenFilterDialogFragment f = findDialogFragment();
|
||||||
if (f != null && f.isVisible()) return false;
|
if (f != null && f.isVisible()) return false;
|
||||||
Collection<AppDetails> apps = screenFilterMonitor.getApps();
|
Collection<AppDetails> apps;
|
||||||
// If all overlay apps have been allowed, allow the tap
|
// querying all apps is only possible at API 29 and below
|
||||||
if (apps.isEmpty()) return true;
|
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
|
// Show dialog unless onSaveInstanceState() has been called, see #1112
|
||||||
FragmentManager fm = getSupportFragmentManager();
|
FragmentManager fm = getSupportFragmentManager();
|
||||||
if (!fm.isStateSaved()) {
|
if (!fm.isStateSaved()) {
|
||||||
@@ -241,7 +247,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
public void handleDbException(DbException e) {
|
public void handleException(Exception e) {
|
||||||
supportFinishAfterTransition();
|
supportFinishAfterTransition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +272,12 @@ public abstract class BaseActivity extends AppCompatActivity
|
|||||||
private void protectToolbar() {
|
private void protectToolbar() {
|
||||||
findToolbar();
|
findToolbar();
|
||||||
if (toolbar != null) {
|
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);
|
UiUtils.setFilterTouchesWhenObscured(toolbar, filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,10 +129,6 @@ public abstract class BriarActivity extends BaseActivity {
|
|||||||
lockManager.onActivityStop();
|
lockManager.onActivityStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean signedIn() {
|
|
||||||
return briarController.accountSignedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the transition animations.
|
* Sets the transition animations.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ public class BlogFragment extends BaseFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -277,7 +277,7 @@ public class BlogFragment extends BaseFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -296,7 +296,7 @@ public class BlogFragment extends BaseFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ public class BlogFragment extends BaseFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -398,7 +398,7 @@ public class BlogFragment extends BaseFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class BlogPostFragment extends BasePostFragment implements BlogListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public class FeedFragment extends BaseFragment implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ public class FeedFragment extends BaseFragment implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -242,7 +242,7 @@ public class FeedFragment extends BaseFragment implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class FeedPostFragment extends BasePostFragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
new UiExceptionHandler<DbException>(this) {
|
new UiExceptionHandler<DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
finish();
|
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.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.NoSuchPendingContactException;
|
import org.briarproject.bramble.api.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.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.LiveEvent;
|
||||||
import org.briarproject.briar.android.viewmodel.LiveResult;
|
import org.briarproject.briar.android.viewmodel.LiveResult;
|
||||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||||
@@ -21,7 +25,6 @@ import java.util.logging.Logger;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
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;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class AddContactViewModel extends AndroidViewModel {
|
public class AddContactViewModel extends DbViewModel {
|
||||||
|
|
||||||
private final static Logger LOG =
|
private final static Logger LOG =
|
||||||
getLogger(AddContactViewModel.class.getName());
|
getLogger(AddContactViewModel.class.getName());
|
||||||
|
|
||||||
private final ContactManager contactManager;
|
private final ContactManager contactManager;
|
||||||
@DatabaseExecutor
|
|
||||||
private final Executor dbExecutor;
|
|
||||||
|
|
||||||
private final MutableLiveData<String> handshakeLink =
|
private final MutableLiveData<String> handshakeLink =
|
||||||
new MutableLiveData<>();
|
new MutableLiveData<>();
|
||||||
@@ -52,10 +53,12 @@ public class AddContactViewModel extends AndroidViewModel {
|
|||||||
@Inject
|
@Inject
|
||||||
AddContactViewModel(Application application,
|
AddContactViewModel(Application application,
|
||||||
ContactManager contactManager,
|
ContactManager contactManager,
|
||||||
@DatabaseExecutor Executor dbExecutor) {
|
@DatabaseExecutor Executor dbExecutor,
|
||||||
super(application);
|
LifecycleManager lifecycleManager,
|
||||||
|
TransactionManager db,
|
||||||
|
AndroidExecutor androidExecutor) {
|
||||||
|
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
this.contactManager = contactManager;
|
this.contactManager = contactManager;
|
||||||
this.dbExecutor = dbExecutor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCreate() {
|
void onCreate() {
|
||||||
@@ -63,7 +66,7 @@ public class AddContactViewModel extends AndroidViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadHandshakeLink() {
|
private void loadHandshakeLink() {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
handshakeLink.postValue(contactManager.getHandshakeLink());
|
handshakeLink.postValue(contactManager.getHandshakeLink());
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
@@ -102,7 +105,7 @@ public class AddContactViewModel extends AndroidViewModel {
|
|||||||
|
|
||||||
void addContact(String nickname) {
|
void addContact(String nickname) {
|
||||||
if (remoteHandshakeLink == null) throw new IllegalStateException();
|
if (remoteHandshakeLink == null) throw new IllegalStateException();
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
contactManager.addPendingContact(remoteHandshakeLink, nickname);
|
contactManager.addPendingContact(remoteHandshakeLink, nickname);
|
||||||
addContactResult.postValue(new LiveResult<>(true));
|
addContactResult.postValue(new LiveResult<>(true));
|
||||||
@@ -122,11 +125,11 @@ public class AddContactViewModel extends AndroidViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updatePendingContact(String name, PendingContact p) {
|
public void updatePendingContact(String name, PendingContact p) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
contactManager.removePendingContact(p.getId());
|
contactManager.removePendingContact(p.getId());
|
||||||
addContact(name);
|
addContact(name);
|
||||||
} catch(NoSuchPendingContactException e) {
|
} catch (NoSuchPendingContactException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
// no error in UI as pending contact was converted into contact
|
// no error in UI as pending contact was converted into contact
|
||||||
} catch (DbException e) {
|
} 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.contact.event.PendingContactStateChangedEvent;
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.db.TransactionManager;
|
||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.rendezvous.RendezvousPoller;
|
import org.briarproject.bramble.api.rendezvous.RendezvousPoller;
|
||||||
import org.briarproject.bramble.api.rendezvous.event.RendezvousPollEvent;
|
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.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -26,7 +30,6 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
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;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class PendingContactListViewModel extends AndroidViewModel
|
public class PendingContactListViewModel extends DbViewModel
|
||||||
implements EventListener {
|
implements EventListener {
|
||||||
|
|
||||||
private final Logger LOG =
|
private final Logger LOG =
|
||||||
getLogger(PendingContactListViewModel.class.getName());
|
getLogger(PendingContactListViewModel.class.getName());
|
||||||
|
|
||||||
@DatabaseExecutor
|
|
||||||
private final Executor dbExecutor;
|
|
||||||
private final ContactManager contactManager;
|
private final ContactManager contactManager;
|
||||||
private final RendezvousPoller rendezvousPoller;
|
private final RendezvousPoller rendezvousPoller;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
@@ -56,11 +57,13 @@ public class PendingContactListViewModel extends AndroidViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
PendingContactListViewModel(Application application,
|
PendingContactListViewModel(Application application,
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
@DatabaseExecutor Executor dbExecutor,
|
||||||
|
LifecycleManager lifecycleManager,
|
||||||
|
TransactionManager db,
|
||||||
|
AndroidExecutor androidExecutor,
|
||||||
ContactManager contactManager,
|
ContactManager contactManager,
|
||||||
RendezvousPoller rendezvousPoller,
|
RendezvousPoller rendezvousPoller,
|
||||||
EventBus eventBus) {
|
EventBus eventBus) {
|
||||||
super(application);
|
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
this.dbExecutor = dbExecutor;
|
|
||||||
this.contactManager = contactManager;
|
this.contactManager = contactManager;
|
||||||
this.rendezvousPoller = rendezvousPoller;
|
this.rendezvousPoller = rendezvousPoller;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
@@ -87,7 +90,7 @@ public class PendingContactListViewModel extends AndroidViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadPendingContacts() {
|
private void loadPendingContacts() {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
Collection<Pair<PendingContact, PendingContactState>> pairs =
|
Collection<Pair<PendingContact, PendingContactState>> pairs =
|
||||||
contactManager.getPendingContacts();
|
contactManager.getPendingContacts();
|
||||||
@@ -113,7 +116,7 @@ public class PendingContactListViewModel extends AndroidViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
void removePendingContact(PendingContactId id) {
|
void removePendingContact(PendingContactId id) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
contactManager.removePendingContact(id);
|
contactManager.removePendingContact(id);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
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;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public interface DbController {
|
public interface DbController {
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import javax.annotation.concurrent.Immutable;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@Deprecated
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class DbControllerImpl implements DbController {
|
public class DbControllerImpl implements DbController {
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
|
||||||
import androidx.recyclerview.selection.Selection;
|
import androidx.recyclerview.selection.Selection;
|
||||||
import androidx.recyclerview.selection.SelectionPredicates;
|
import androidx.recyclerview.selection.SelectionPredicates;
|
||||||
import androidx.recyclerview.selection.SelectionTracker;
|
import androidx.recyclerview.selection.SelectionTracker;
|
||||||
@@ -140,8 +139,6 @@ import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
|
|||||||
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
|
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
|
||||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
|
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
|
||||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
|
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
|
||||||
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE;
|
|
||||||
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
|
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
@@ -226,8 +223,9 @@ public class ConversationActivity extends BriarActivity
|
|||||||
if (id == -1) throw new IllegalStateException();
|
if (id == -1) throw new IllegalStateException();
|
||||||
contactId = new ContactId(id);
|
contactId = new ContactId(id);
|
||||||
|
|
||||||
viewModel = ViewModelProviders.of(this, viewModelFactory)
|
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||||
.get(ConversationViewModel.class);
|
.get(ConversationViewModel.class);
|
||||||
|
viewModel.setContactId(contactId);
|
||||||
attachmentRetriever = viewModel.getAttachmentRetriever();
|
attachmentRetriever = viewModel.getAttachmentRetriever();
|
||||||
|
|
||||||
setContentView(R.layout.activity_conversation);
|
setContentView(R.layout.activity_conversation);
|
||||||
@@ -275,11 +273,15 @@ public class ConversationActivity extends BriarActivity
|
|||||||
ImagePreview imagePreview = findViewById(R.id.imagePreview);
|
ImagePreview imagePreview = findViewById(R.id.imagePreview);
|
||||||
sendController = new TextAttachmentController(textInputView,
|
sendController = new TextAttachmentController(textInputView,
|
||||||
imagePreview, this, viewModel);
|
imagePreview, this, viewModel);
|
||||||
observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
|
viewModel.hasImageSupport().observe(this, new Observer<Boolean>() {
|
||||||
if (format != TEXT_ONLY) {
|
@Override
|
||||||
// TODO: remove cast when removing feature flag
|
public void onChanged(@Nullable Boolean hasSupport) {
|
||||||
((TextAttachmentController) sendController)
|
if (hasSupport != null && hasSupport) {
|
||||||
.setImagesSupported();
|
// TODO: remove cast when removing feature flag
|
||||||
|
((TextAttachmentController) sendController)
|
||||||
|
.setImagesSupported();
|
||||||
|
viewModel.hasImageSupport().removeObserver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -328,16 +330,6 @@ public class ConversationActivity extends BriarActivity
|
|||||||
list.startPeriodicUpdate();
|
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
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
@@ -382,12 +374,6 @@ public class ConversationActivity extends BriarActivity
|
|||||||
// enable alias action if available
|
// enable alias action if available
|
||||||
observeOnce(viewModel.getContact(), this, contact ->
|
observeOnce(viewModel.getContact(), this, contact ->
|
||||||
menu.findItem(R.id.action_set_alias).setEnabled(true));
|
menu.findItem(R.id.action_set_alias).setEnabled(true));
|
||||||
// show auto-delete timer setting only, if contacts supports it
|
|
||||||
observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
|
|
||||||
boolean visible = format == TEXT_IMAGES_AUTO_DELETE;
|
|
||||||
menu.findItem(R.id.action_conversation_settings)
|
|
||||||
.setVisible(visible);
|
|
||||||
});
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
return super.onCreateOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
@@ -409,12 +395,6 @@ public class ConversationActivity extends BriarActivity
|
|||||||
AliasDialogFragment.newInstance().show(
|
AliasDialogFragment.newInstance().show(
|
||||||
getSupportFragmentManager(), AliasDialogFragment.TAG);
|
getSupportFragmentManager(), AliasDialogFragment.TAG);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_conversation_settings:
|
|
||||||
if (contactId == null) return false;
|
|
||||||
intent = new Intent(this, ConversationSettingsActivity.class);
|
|
||||||
intent.putExtra(CONTACT_ID, contactId.getInt());
|
|
||||||
startActivity(intent);
|
|
||||||
return true;
|
|
||||||
case R.id.action_delete_all_messages:
|
case R.id.action_delete_all_messages:
|
||||||
askToDeleteAllMessages();
|
askToDeleteAllMessages();
|
||||||
return true;
|
return true;
|
||||||
@@ -667,8 +647,8 @@ public class ConversationActivity extends BriarActivity
|
|||||||
supportFinishAfterTransition();
|
supportFinishAfterTransition();
|
||||||
}
|
}
|
||||||
} else if (e instanceof ConversationMessageReceivedEvent) {
|
} else if (e instanceof ConversationMessageReceivedEvent) {
|
||||||
ConversationMessageReceivedEvent<?> p =
|
ConversationMessageReceivedEvent p =
|
||||||
(ConversationMessageReceivedEvent<?>) e;
|
(ConversationMessageReceivedEvent) e;
|
||||||
if (p.getContactId().equals(contactId)) {
|
if (p.getContactId().equals(contactId)) {
|
||||||
LOG.info("Message received, adding");
|
LOG.info("Message received, adding");
|
||||||
onNewConversationMessage(p.getMessageHeader());
|
onNewConversationMessage(p.getMessageHeader());
|
||||||
@@ -766,10 +746,18 @@ public class ConversationActivity extends BriarActivity
|
|||||||
List<AttachmentHeader> attachmentHeaders) {
|
List<AttachmentHeader> attachmentHeaders) {
|
||||||
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
|
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
viewModel.sendMessage(text, attachmentHeaders);
|
long timestamp = System.currentTimeMillis();
|
||||||
|
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
||||||
|
viewModel.sendMessage(text, attachmentHeaders, timestamp);
|
||||||
textInputView.clearText();
|
textInputView.clearText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long getMinTimestampForNewMessage() {
|
||||||
|
// Don't use an earlier timestamp than the newest message
|
||||||
|
ConversationItem item = adapter.getLastItem();
|
||||||
|
return item == null ? 0 : item.getTime() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
|
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
|
||||||
if (h == null) return;
|
if (h == null) return;
|
||||||
addConversationItem(h.accept(visitor));
|
addConversationItem(h.accept(visitor));
|
||||||
@@ -974,11 +962,13 @@ public class ConversationActivity extends BriarActivity
|
|||||||
adapter.notifyItemChanged(position, item);
|
adapter.notifyItemChanged(position, item);
|
||||||
}
|
}
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
|
long timestamp = System.currentTimeMillis();
|
||||||
|
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
||||||
try {
|
try {
|
||||||
switch (item.getRequestType()) {
|
switch (item.getRequestType()) {
|
||||||
case INTRODUCTION:
|
case INTRODUCTION:
|
||||||
respondToIntroductionRequest(item.getSessionId(),
|
respondToIntroductionRequest(item.getSessionId(),
|
||||||
accept);
|
accept, timestamp);
|
||||||
break;
|
break;
|
||||||
case FORUM:
|
case FORUM:
|
||||||
respondToForumRequest(item.getSessionId(), accept);
|
respondToForumRequest(item.getSessionId(), accept);
|
||||||
@@ -1054,8 +1044,9 @@ public class ConversationActivity extends BriarActivity
|
|||||||
|
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
private void respondToIntroductionRequest(SessionId sessionId,
|
private void respondToIntroductionRequest(SessionId sessionId,
|
||||||
boolean accept) throws DbException {
|
boolean accept, long time) throws DbException {
|
||||||
introductionManager.respondToIntroduction(contactId, sessionId, accept);
|
introductionManager.respondToIntroduction(contactId, sessionId, time,
|
||||||
|
accept);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ abstract class ConversationItem {
|
|||||||
protected String text;
|
protected String text;
|
||||||
private final MessageId id;
|
private final MessageId id;
|
||||||
private final GroupId groupId;
|
private final GroupId groupId;
|
||||||
private final long time, autoDeleteTimer;
|
private final long time;
|
||||||
private final boolean isIncoming;
|
private final boolean isIncoming;
|
||||||
private boolean read, sent, seen;
|
private boolean read, sent, seen;
|
||||||
|
|
||||||
@@ -32,7 +32,6 @@ abstract class ConversationItem {
|
|||||||
this.id = h.getId();
|
this.id = h.getId();
|
||||||
this.groupId = h.getGroupId();
|
this.groupId = h.getGroupId();
|
||||||
this.time = h.getTimestamp();
|
this.time = h.getTimestamp();
|
||||||
this.autoDeleteTimer = h.getAutoDeleteTimer();
|
|
||||||
this.read = h.isRead();
|
this.read = h.isRead();
|
||||||
this.sent = h.isSent();
|
this.sent = h.isSent();
|
||||||
this.seen = h.isSeen();
|
this.seen = h.isSeen();
|
||||||
@@ -69,10 +68,6 @@ abstract class ConversationItem {
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getAutoDeleteTimer() {
|
|
||||||
return autoDeleteTimer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only useful for incoming messages.
|
* Only useful for incoming messages.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ import androidx.annotation.UiThread;
|
|||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.trim;
|
import static org.briarproject.bramble.util.StringUtils.trim;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.formatDate;
|
import static org.briarproject.briar.android.util.UiUtils.formatDate;
|
||||||
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
|
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@@ -29,7 +26,6 @@ abstract class ConversationItemViewHolder extends ViewHolder {
|
|||||||
private final OutItemViewHolder outViewHolder;
|
private final OutItemViewHolder outViewHolder;
|
||||||
private final TextView text;
|
private final TextView text;
|
||||||
protected final TextView time;
|
protected final TextView time;
|
||||||
private final View bomb;
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String itemKey = null;
|
private String itemKey = null;
|
||||||
|
|
||||||
@@ -42,7 +38,6 @@ abstract class ConversationItemViewHolder extends ViewHolder {
|
|||||||
layout = v.findViewById(R.id.layout);
|
layout = v.findViewById(R.id.layout);
|
||||||
text = v.findViewById(R.id.text);
|
text = v.findViewById(R.id.text);
|
||||||
time = v.findViewById(R.id.time);
|
time = v.findViewById(R.id.time);
|
||||||
bomb = v.findViewById(R.id.bomb);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
@@ -57,9 +52,6 @@ abstract class ConversationItemViewHolder extends ViewHolder {
|
|||||||
long timestamp = item.getTime();
|
long timestamp = item.getTime();
|
||||||
time.setText(formatDate(time.getContext(), timestamp));
|
time.setText(formatDate(time.getContext(), timestamp));
|
||||||
|
|
||||||
boolean showBomb = item.getAutoDeleteTimer() != NO_AUTO_DELETE_TIMER;
|
|
||||||
bomb.setVisibility(showBomb ? VISIBLE : GONE);
|
|
||||||
|
|
||||||
if (outViewHolder != null) outViewHolder.bind(item);
|
if (outViewHolder != null) outViewHolder.bind(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package org.briarproject.briar.android.conversation;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
|
||||||
import org.briarproject.briar.R;
|
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
|
||||||
import org.briarproject.briar.android.activity.BriarActivity;
|
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
|
||||||
|
|
||||||
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
|
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
|
||||||
@ParametersNotNullByDefault
|
|
||||||
public class ConversationSettingsActivity extends BriarActivity implements
|
|
||||||
BaseFragmentListener {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
|
||||||
|
|
||||||
private ConversationViewModel viewModel;
|
|
||||||
private ContactId contactId;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle bundle) {
|
|
||||||
super.onCreate(bundle);
|
|
||||||
|
|
||||||
Intent i = getIntent();
|
|
||||||
int id = i.getIntExtra(CONTACT_ID, -1);
|
|
||||||
if (id == -1) throw new IllegalStateException();
|
|
||||||
contactId = new ContactId(id);
|
|
||||||
|
|
||||||
ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
actionBar.setHomeButtonEnabled(true);
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_conversation_settings);
|
|
||||||
|
|
||||||
viewModel = ViewModelProviders.of(this, viewModelFactory)
|
|
||||||
.get(ConversationViewModel.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 injectActivity(ActivityComponent component) {
|
|
||||||
component.inject(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == android.R.id.home) {
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
package org.briarproject.briar.android.conversation;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
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.nullsafety.MethodsNotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
|
||||||
import org.briarproject.briar.R;
|
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
|
||||||
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
|
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
|
||||||
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
|
|
||||||
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
|
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
|
||||||
@ParametersNotNullByDefault
|
|
||||||
public class ConversationSettingsFragment extends BaseFragment {
|
|
||||||
|
|
||||||
public static final String TAG =
|
|
||||||
ConversationSettingsFragment.class.getName();
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
|
||||||
Logger.getLogger(ConversationSettingsFragment.class.getName());
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
|
||||||
@Inject
|
|
||||||
@DatabaseExecutor
|
|
||||||
Executor dbExecutor;
|
|
||||||
@Inject
|
|
||||||
TransactionManager db;
|
|
||||||
@Inject
|
|
||||||
AutoDeleteManager autoDeleteManager;
|
|
||||||
|
|
||||||
private ConversationSettingsActivity listener;
|
|
||||||
private ConversationViewModel viewModel;
|
|
||||||
private SwitchCompat switchDisappearingMessages;
|
|
||||||
private volatile boolean disappearingMessages = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUniqueTag() {
|
|
||||||
return TAG;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
listener = (ConversationSettingsActivity) context;
|
|
||||||
listener.getActivityComponent().inject(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater,
|
|
||||||
@Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
View contentView =
|
|
||||||
inflater.inflate(R.layout.fragment_conversation_settings,
|
|
||||||
container, false);
|
|
||||||
|
|
||||||
switchDisappearingMessages =
|
|
||||||
contentView.findViewById(R.id.switchDisappearingMessages);
|
|
||||||
|
|
||||||
switchDisappearingMessages
|
|
||||||
.setOnCheckedChangeListener((button, value) -> viewModel
|
|
||||||
.setAutoDeleteTimerEnabled(value));
|
|
||||||
|
|
||||||
TextView buttonLearnMore =
|
|
||||||
contentView.findViewById(R.id.buttonLearnMore);
|
|
||||||
buttonLearnMore.setOnClickListener(e -> showLearnMoreDialog());
|
|
||||||
|
|
||||||
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)
|
|
||||||
.get(ConversationViewModel.class);
|
|
||||||
|
|
||||||
return contentView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
switchDisappearingMessages.setEnabled(false);
|
|
||||||
loadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadSettings() {
|
|
||||||
observeOnce(viewModel.getContact(), this, c -> {
|
|
||||||
dbExecutor.execute(() -> {
|
|
||||||
try {
|
|
||||||
db.transaction(false, txn -> {
|
|
||||||
long timer = autoDeleteManager
|
|
||||||
.getAutoDeleteTimer(txn, c.getId());
|
|
||||||
disappearingMessages = timer != NO_AUTO_DELETE_TIMER;
|
|
||||||
});
|
|
||||||
listener.runOnUiThreadUnlessDestroyed(
|
|
||||||
this::displaySettings);
|
|
||||||
} catch (DbException e) {
|
|
||||||
logException(LOG, WARNING, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displaySettings() {
|
|
||||||
switchDisappearingMessages.setChecked(disappearingMessages);
|
|
||||||
switchDisappearingMessages.setEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.help_action, menu);
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == R.id.action_help) {
|
|
||||||
showLearnMoreDialog();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showLearnMoreDialog() {
|
|
||||||
ConversationSettingsLearnMoreDialog
|
|
||||||
dialog = new ConversationSettingsLearnMoreDialog();
|
|
||||||
dialog.show(requireActivity().getSupportFragmentManager(),
|
|
||||||
ConversationSettingsLearnMoreDialog.TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package org.briarproject.briar.android.conversation;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
|
||||||
import org.briarproject.briar.R;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
|
||||||
public class ConversationSettingsLearnMoreDialog extends DialogFragment {
|
|
||||||
|
|
||||||
final static String TAG =
|
|
||||||
ConversationSettingsLearnMoreDialog.class.getName();
|
|
||||||
|
|
||||||
static ConversationSettingsLearnMoreDialog newInstance() {
|
|
||||||
return new ConversationSettingsLearnMoreDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
FragmentActivity activity = requireActivity();
|
|
||||||
|
|
||||||
AlertDialog.Builder builder =
|
|
||||||
new AlertDialog.Builder(activity, R.style.BriarDialogTheme);
|
|
||||||
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
|
||||||
View view = inflater.inflate(
|
|
||||||
R.layout.fragment_conversation_settings_learn_more, null);
|
|
||||||
builder.setView(view);
|
|
||||||
|
|
||||||
builder.setTitle(R.string.disappearing_messages_title);
|
|
||||||
builder.setNeutralButton(R.string.ok, null);
|
|
||||||
|
|
||||||
return builder.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -10,32 +10,31 @@ import org.briarproject.bramble.api.contact.ContactManager;
|
|||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||||
import org.briarproject.bramble.api.db.Transaction;
|
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
import org.briarproject.bramble.api.db.TransactionManager;
|
||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
import org.briarproject.bramble.api.identity.AuthorId;
|
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.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.settings.Settings;
|
import org.briarproject.bramble.api.settings.Settings;
|
||||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
import org.briarproject.bramble.api.sync.Message;
|
import org.briarproject.bramble.api.sync.Message;
|
||||||
import org.briarproject.bramble.api.sync.MessageId;
|
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.AttachmentCreator;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentManager;
|
import org.briarproject.briar.android.attachment.AttachmentManager;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentResult;
|
import org.briarproject.briar.android.attachment.AttachmentResult;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentRetriever;
|
import org.briarproject.briar.android.attachment.AttachmentRetriever;
|
||||||
import org.briarproject.briar.android.util.UiUtils;
|
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.LiveEvent;
|
||||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||||
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
|
|
||||||
import org.briarproject.briar.api.conversation.ConversationManager;
|
|
||||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||||
import org.briarproject.briar.api.messaging.PrivateMessage;
|
import org.briarproject.briar.api.messaging.PrivateMessage;
|
||||||
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
|
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
|
||||||
import org.briarproject.briar.api.messaging.PrivateMessageFormat;
|
|
||||||
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||||
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
|
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
|
||||||
|
|
||||||
@@ -48,13 +47,11 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.Transformations;
|
import androidx.lifecycle.Transformations;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static java.util.concurrent.TimeUnit.DAYS;
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
@@ -62,12 +59,9 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
|||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
|
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
|
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
|
||||||
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
|
|
||||||
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES;
|
|
||||||
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
|
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class ConversationViewModel extends AndroidViewModel
|
public class ConversationViewModel extends DbViewModel
|
||||||
implements EventListener, AttachmentManager {
|
implements EventListener, AttachmentManager {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
@@ -78,8 +72,6 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
private static final String SHOW_ONBOARDING_INTRODUCTION =
|
private static final String SHOW_ONBOARDING_INTRODUCTION =
|
||||||
"showOnboardingIntroduction";
|
"showOnboardingIntroduction";
|
||||||
|
|
||||||
@DatabaseExecutor
|
|
||||||
private final Executor dbExecutor;
|
|
||||||
private final TransactionManager db;
|
private final TransactionManager db;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
private final MessagingManager messagingManager;
|
private final MessagingManager messagingManager;
|
||||||
@@ -88,8 +80,6 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
private final PrivateMessageFactory privateMessageFactory;
|
private final PrivateMessageFactory privateMessageFactory;
|
||||||
private final AttachmentRetriever attachmentRetriever;
|
private final AttachmentRetriever attachmentRetriever;
|
||||||
private final AttachmentCreator attachmentCreator;
|
private final AttachmentCreator attachmentCreator;
|
||||||
private final AutoDeleteManager autoDeleteManager;
|
|
||||||
private final ConversationManager conversationManager;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private ContactId contactId = null;
|
private ContactId contactId = null;
|
||||||
@@ -99,7 +89,7 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
private final LiveData<String> contactName =
|
private final LiveData<String> contactName =
|
||||||
Transformations.map(contact, UiUtils::getContactDisplayName);
|
Transformations.map(contact, UiUtils::getContactDisplayName);
|
||||||
private final LiveData<GroupId> messagingGroupId;
|
private final LiveData<GroupId> messagingGroupId;
|
||||||
private final MutableLiveData<PrivateMessageFormat> privateMessageFormat =
|
private final MutableLiveData<Boolean> imageSupport =
|
||||||
new MutableLiveData<>();
|
new MutableLiveData<>();
|
||||||
private final MutableLiveEvent<Boolean> showImageOnboarding =
|
private final MutableLiveEvent<Boolean> showImageOnboarding =
|
||||||
new MutableLiveEvent<>();
|
new MutableLiveEvent<>();
|
||||||
@@ -115,18 +105,17 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
@Inject
|
@Inject
|
||||||
ConversationViewModel(Application application,
|
ConversationViewModel(Application application,
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
@DatabaseExecutor Executor dbExecutor,
|
||||||
|
LifecycleManager lifecycleManager,
|
||||||
TransactionManager db,
|
TransactionManager db,
|
||||||
|
AndroidExecutor androidExecutor,
|
||||||
EventBus eventBus,
|
EventBus eventBus,
|
||||||
MessagingManager messagingManager,
|
MessagingManager messagingManager,
|
||||||
ContactManager contactManager,
|
ContactManager contactManager,
|
||||||
SettingsManager settingsManager,
|
SettingsManager settingsManager,
|
||||||
PrivateMessageFactory privateMessageFactory,
|
PrivateMessageFactory privateMessageFactory,
|
||||||
AttachmentRetriever attachmentRetriever,
|
AttachmentRetriever attachmentRetriever,
|
||||||
AttachmentCreator attachmentCreator,
|
AttachmentCreator attachmentCreator) {
|
||||||
AutoDeleteManager autoDeleteManager,
|
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
ConversationManager conversationManager) {
|
|
||||||
super(application);
|
|
||||||
this.dbExecutor = dbExecutor;
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.messagingManager = messagingManager;
|
this.messagingManager = messagingManager;
|
||||||
@@ -135,8 +124,6 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
this.privateMessageFactory = privateMessageFactory;
|
this.privateMessageFactory = privateMessageFactory;
|
||||||
this.attachmentRetriever = attachmentRetriever;
|
this.attachmentRetriever = attachmentRetriever;
|
||||||
this.attachmentCreator = attachmentCreator;
|
this.attachmentCreator = attachmentCreator;
|
||||||
this.autoDeleteManager = autoDeleteManager;
|
|
||||||
this.conversationManager = conversationManager;
|
|
||||||
messagingGroupId = Transformations
|
messagingGroupId = Transformations
|
||||||
.map(contact, c -> messagingManager.getContactGroup(c).getId());
|
.map(contact, c -> messagingManager.getContactGroup(c).getId());
|
||||||
contactDeleted.setValue(false);
|
contactDeleted.setValue(false);
|
||||||
@@ -157,7 +144,7 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
|
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
|
||||||
if (a.getContactId().equals(contactId)) {
|
if (a.getContactId().equals(contactId)) {
|
||||||
LOG.info("Attachment received");
|
LOG.info("Attachment received");
|
||||||
dbExecutor.execute(() -> attachmentRetriever
|
runOnDbThread(() -> attachmentRetriever
|
||||||
.loadAttachmentItem(a.getMessageId()));
|
.loadAttachmentItem(a.getMessageId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +164,7 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadContact(ContactId contactId) {
|
private void loadContact(ContactId contactId) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
long start = now();
|
long start = now();
|
||||||
Contact c = contactManager.getContact(contactId);
|
Contact c = contactManager.getContact(contactId);
|
||||||
@@ -195,7 +182,7 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
void markMessageRead(GroupId g, MessageId m) {
|
void markMessageRead(GroupId g, MessageId m) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
long start = now();
|
long start = now();
|
||||||
messagingManager.setReadFlag(g, m, true);
|
messagingManager.setReadFlag(g, m, true);
|
||||||
@@ -207,7 +194,7 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setContactAlias(String alias) {
|
void setContactAlias(String alias) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
contactManager.setContactAlias(requireNonNull(contactId),
|
contactManager.setContactAlias(requireNonNull(contactId),
|
||||||
alias.isEmpty() ? null : alias);
|
alias.isEmpty() ? null : alias);
|
||||||
@@ -219,13 +206,16 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
void sendMessage(@Nullable String text, List<AttachmentHeader> headers) {
|
void sendMessage(@Nullable String text,
|
||||||
|
List<AttachmentHeader> headers, long timestamp) {
|
||||||
// messagingGroupId is loaded with the contact
|
// messagingGroupId is loaded with the contact
|
||||||
observeForeverOnce(messagingGroupId, groupId -> {
|
observeForeverOnce(messagingGroupId, groupId -> {
|
||||||
requireNonNull(groupId);
|
requireNonNull(groupId);
|
||||||
observeForeverOnce(privateMessageFormat, format ->
|
observeForeverOnce(imageSupport, hasImageSupport -> {
|
||||||
storeMessage(requireNonNull(contactId), groupId, text,
|
requireNonNull(hasImageSupport);
|
||||||
headers, format));
|
createMessage(groupId, text, headers, timestamp,
|
||||||
|
hasImageSupport);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,10 +245,10 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
|
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
private void checkFeaturesAndOnboarding(ContactId c) throws DbException {
|
private void checkFeaturesAndOnboarding(ContactId c) throws DbException {
|
||||||
// check if images and auto-deletion are supported
|
// check if images are supported
|
||||||
PrivateMessageFormat format = db.transactionWithResult(true, txn ->
|
boolean imagesSupported = db.transactionWithResult(true, txn ->
|
||||||
messagingManager.getContactMessageFormat(txn, c));
|
messagingManager.contactSupportsImages(txn, c));
|
||||||
privateMessageFormat.postValue(format);
|
imageSupport.postValue(imagesSupported);
|
||||||
|
|
||||||
// check if introductions are supported
|
// check if introductions are supported
|
||||||
Collection<Contact> contacts = contactManager.getContacts();
|
Collection<Contact> contacts = contactManager.getContacts();
|
||||||
@@ -267,7 +257,7 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
|
|
||||||
// we only show one onboarding dialog at a time
|
// we only show one onboarding dialog at a time
|
||||||
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
|
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||||
if (format != TEXT_ONLY &&
|
if (imagesSupported &&
|
||||||
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
|
settings.getBoolean(SHOW_ONBOARDING_IMAGE, true)) {
|
||||||
onOnboardingShown(SHOW_ONBOARDING_IMAGE);
|
onOnboardingShown(SHOW_ONBOARDING_IMAGE);
|
||||||
showImageOnboarding.postEvent(true);
|
showImageOnboarding.postEvent(true);
|
||||||
@@ -285,67 +275,40 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
|
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PrivateMessage createMessage(Transaction txn, ContactId c,
|
@UiThread
|
||||||
GroupId groupId, @Nullable String text,
|
private void createMessage(GroupId groupId, @Nullable String text,
|
||||||
List<AttachmentHeader> headers, PrivateMessageFormat format)
|
List<AttachmentHeader> headers, long timestamp,
|
||||||
throws DbException {
|
boolean hasImageSupport) {
|
||||||
long timestamp =
|
|
||||||
conversationManager.getTimestampForOutgoingMessage(txn, c);
|
|
||||||
try {
|
try {
|
||||||
if (format == TEXT_ONLY) {
|
PrivateMessage pm;
|
||||||
return privateMessageFactory.createLegacyPrivateMessage(
|
if (hasImageSupport) {
|
||||||
groupId, timestamp, requireNonNull(text));
|
pm = privateMessageFactory.createPrivateMessage(groupId,
|
||||||
} else if (format == TEXT_IMAGES) {
|
|
||||||
return privateMessageFactory.createPrivateMessage(groupId,
|
|
||||||
timestamp, text, headers);
|
timestamp, text, headers);
|
||||||
} else {
|
} else {
|
||||||
long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
|
pm = privateMessageFactory.createLegacyPrivateMessage(
|
||||||
timestamp);
|
groupId, timestamp, requireNonNull(text));
|
||||||
return privateMessageFactory.createPrivateMessage(groupId,
|
|
||||||
timestamp, text, headers, timer);
|
|
||||||
}
|
}
|
||||||
|
storeMessage(pm);
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void storeMessage(ContactId c, GroupId groupId,
|
private void storeMessage(PrivateMessage m) {
|
||||||
@Nullable String text, List<AttachmentHeader> headers,
|
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
|
||||||
PrivateMessageFormat format) {
|
runOnDbThread(() -> {
|
||||||
dbExecutor.execute(() -> {
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, txn -> {
|
long start = now();
|
||||||
long start = now();
|
messagingManager.addLocalMessage(m);
|
||||||
PrivateMessage m = createMessage(txn, c, groupId, text,
|
logDuration(LOG, "Storing message", start);
|
||||||
headers, format);
|
Message message = m.getMessage();
|
||||||
messagingManager.addLocalMessage(txn, m);
|
PrivateMessageHeader h = new PrivateMessageHeader(
|
||||||
logDuration(LOG, "Storing message", start);
|
message.getId(), message.getGroupId(),
|
||||||
Message message = m.getMessage();
|
message.getTimestamp(), true, true, false, false,
|
||||||
PrivateMessageHeader h = new PrivateMessageHeader(
|
m.hasText(), m.getAttachmentHeaders());
|
||||||
message.getId(), message.getGroupId(),
|
// TODO add text to cache when available here
|
||||||
message.getTimestamp(), true, true, false, false,
|
addedHeader.postEvent(h);
|
||||||
m.hasText(), m.getAttachmentHeaders(),
|
|
||||||
m.getAutoDeleteTimer());
|
|
||||||
// TODO add text to cache when available here
|
|
||||||
MessageId id = message.getId();
|
|
||||||
txn.attach(() -> attachmentCreator.onAttachmentsSent(id));
|
|
||||||
addedHeader.postEvent(h);
|
|
||||||
});
|
|
||||||
} catch (DbException e) {
|
|
||||||
logException(LOG, WARNING, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAutoDeleteTimerEnabled(boolean enabled) {
|
|
||||||
final long timer = enabled ? DAYS.toMillis(7) : NO_AUTO_DELETE_TIMER;
|
|
||||||
// ContactId is set before menu gets inflated and UI interaction
|
|
||||||
final ContactId c = requireNonNull(contactId);
|
|
||||||
dbExecutor.execute(() -> {
|
|
||||||
try {
|
|
||||||
db.transaction(false, txn ->
|
|
||||||
autoDeleteManager.setAutoDeleteTimer(txn, c, timer));
|
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
}
|
}
|
||||||
@@ -368,8 +331,8 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
return contactName;
|
return contactName;
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveData<PrivateMessageFormat> getPrivateMessageFormat() {
|
LiveData<Boolean> hasImageSupport() {
|
||||||
return privateMessageFormat;
|
return imageSupport;
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveEvent<Boolean> showImageOnboarding() {
|
LiveEvent<Boolean> showImageOnboarding() {
|
||||||
@@ -394,7 +357,7 @@ public class ConversationViewModel extends AndroidViewModel
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
void recheckFeaturesAndOnboarding(ContactId contactId) {
|
void recheckFeaturesAndOnboarding(ContactId contactId) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
checkFeaturesAndOnboarding(contactId);
|
checkFeaturesAndOnboarding(contactId);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import android.view.View;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.db.TransactionManager;
|
||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
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.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.MessageId;
|
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.attachment.AttachmentItem;
|
||||||
|
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||||
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
||||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||||
import org.briarproject.briar.api.messaging.Attachment;
|
import org.briarproject.briar.api.messaging.Attachment;
|
||||||
@@ -37,7 +41,6 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
|
|
||||||
import static android.media.MediaScannerConnection.scanFile;
|
import static android.media.MediaScannerConnection.scanFile;
|
||||||
import static android.os.Environment.DIRECTORY_PICTURES;
|
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;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@NotNullByDefault
|
@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 MessagingManager messagingManager;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
@DatabaseExecutor
|
|
||||||
private final Executor dbExecutor;
|
|
||||||
@IoExecutor
|
@IoExecutor
|
||||||
private final Executor ioExecutor;
|
private final Executor ioExecutor;
|
||||||
|
|
||||||
private boolean receivedAttachmentsInitialized = false;
|
private boolean receivedAttachmentsInitialized = false;
|
||||||
private HashMap<MessageId, MutableLiveEvent<Boolean>> receivedAttachments =
|
private final HashMap<MessageId, MutableLiveEvent<Boolean>>
|
||||||
new HashMap<>();
|
receivedAttachments = new HashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* true means there was an error saving the image, false if image was saved.
|
* 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
|
@Inject
|
||||||
ImageViewModel(Application application,
|
ImageViewModel(Application application,
|
||||||
MessagingManager messagingManager, EventBus eventBus,
|
MessagingManager messagingManager,
|
||||||
|
EventBus eventBus,
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
@DatabaseExecutor Executor dbExecutor,
|
||||||
|
LifecycleManager lifecycleManager,
|
||||||
|
TransactionManager db,
|
||||||
|
AndroidExecutor androidExecutor,
|
||||||
@IoExecutor Executor ioExecutor) {
|
@IoExecutor Executor ioExecutor) {
|
||||||
super(application);
|
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
this.messagingManager = messagingManager;
|
this.messagingManager = messagingManager;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.dbExecutor = dbExecutor;
|
|
||||||
this.ioExecutor = ioExecutor;
|
this.ioExecutor = ioExecutor;
|
||||||
|
|
||||||
eventBus.addListener(this);
|
eventBus.addListener(this);
|
||||||
@@ -195,7 +199,7 @@ public class ImageViewModel extends AndroidViewModel implements EventListener {
|
|||||||
|
|
||||||
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
|
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
|
||||||
@Nullable Runnable afterCopy) {
|
@Nullable Runnable afterCopy) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
Attachment a =
|
Attachment a =
|
||||||
messagingManager.getAttachment(attachment.getHeader());
|
messagingManager.getAttachment(attachment.getHeader());
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEX
|
|||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ForumActivity extends
|
public class ForumActivity extends
|
||||||
ThreadListActivity<Forum, ForumItem, ThreadItemAdapter<ForumItem>>
|
ThreadListActivity<Forum, ForumPostItem, ThreadItemAdapter<ForumPostItem>>
|
||||||
implements ForumListener {
|
implements ForumListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -50,7 +50,7 @@ public class ForumActivity extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ThreadListController<Forum, ForumItem> getController() {
|
protected ThreadListController<Forum, ForumPostItem> getController() {
|
||||||
return forumController;
|
return forumController;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ public class ForumActivity extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ThreadItemAdapter<ForumItem> createAdapter(
|
protected ThreadItemAdapter<ForumPostItem> createAdapter(
|
||||||
LinearLayoutManager layoutManager) {
|
LinearLayoutManager layoutManager) {
|
||||||
return new ThreadItemAdapter<>(this, layoutManager);
|
return new ThreadItemAdapter<>(this, layoutManager);
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ public class ForumActivity extends
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import org.briarproject.briar.api.forum.Forum;
|
|||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
interface ForumController extends ThreadListController<Forum, ForumItem> {
|
interface ForumController extends ThreadListController<Forum, ForumPostItem> {
|
||||||
|
|
||||||
interface ForumListener extends ThreadListListener<ForumItem> {
|
interface ForumListener extends ThreadListListener<ForumPostItem> {
|
||||||
@UiThread
|
@UiThread
|
||||||
void onForumLeft(ContactId c);
|
void onForumLeft(ContactId c);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
|||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
class ForumControllerImpl extends
|
class ForumControllerImpl extends
|
||||||
ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost, ForumListener>
|
ThreadListControllerImpl<Forum, ForumPostItem, ForumPostHeader, ForumPost, ForumListener>
|
||||||
implements ForumController {
|
implements ForumController {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
@@ -138,8 +138,8 @@ class ForumControllerImpl extends
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createAndStoreMessage(String text,
|
public void createAndStoreMessage(String text,
|
||||||
@Nullable ForumItem parentItem,
|
@Nullable ForumPostItem parentItem,
|
||||||
ResultExceptionHandler<ForumItem, DbException> handler) {
|
ResultExceptionHandler<ForumPostItem, DbException> handler) {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
LocalAuthor author = identityManager.getLocalAuthor();
|
LocalAuthor author = identityManager.getLocalAuthor();
|
||||||
@@ -158,7 +158,7 @@ class ForumControllerImpl extends
|
|||||||
|
|
||||||
private void createMessage(String text, long timestamp,
|
private void createMessage(String text, long timestamp,
|
||||||
@Nullable MessageId parentId, LocalAuthor author,
|
@Nullable MessageId parentId, LocalAuthor author,
|
||||||
ResultExceptionHandler<ForumItem, DbException> handler) {
|
ResultExceptionHandler<ForumPostItem, DbException> handler) {
|
||||||
cryptoExecutor.execute(() -> {
|
cryptoExecutor.execute(() -> {
|
||||||
LOG.info("Creating forum post...");
|
LOG.info("Creating forum post...");
|
||||||
ForumPost msg = forumManager.createLocalPost(getGroupId(), text,
|
ForumPost msg = forumManager.createLocalPost(getGroupId(), text,
|
||||||
@@ -178,8 +178,8 @@ class ForumControllerImpl extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ForumItem buildItem(ForumPostHeader header, String text) {
|
protected ForumPostItem buildItem(ForumPostHeader header, String text) {
|
||||||
return new ForumItem(header, text);
|
return new ForumPostItem(header, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +1,47 @@
|
|||||||
package org.briarproject.briar.android.forum;
|
package org.briarproject.briar.android.forum;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.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.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;
|
@NotNullByDefault
|
||||||
import static android.view.View.VISIBLE;
|
class ForumListAdapter extends ListAdapter<ForumListItem, ForumViewHolder> {
|
||||||
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;
|
|
||||||
|
|
||||||
class ForumListAdapter
|
ForumListAdapter() {
|
||||||
extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> {
|
super(new ForumListCallback());
|
||||||
|
|
||||||
ForumListAdapter(Context ctx) {
|
|
||||||
super(ctx, ForumListItem.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
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);
|
R.layout.list_item_forum, parent, false);
|
||||||
return new ForumViewHolder(v);
|
return new ForumViewHolder(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ForumViewHolder ui, int position) {
|
public void onBindViewHolder(ForumViewHolder viewHolder, int position) {
|
||||||
ForumListItem item = getItemAt(position);
|
viewHolder.bind(getItem(position));
|
||||||
if (item == null) return;
|
}
|
||||||
|
|
||||||
// Avatar
|
@NotNullByDefault
|
||||||
ui.avatar.setText(item.getForum().getName().substring(0, 1));
|
private static class ForumListCallback extends ItemCallback<ForumListItem> {
|
||||||
ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes());
|
@Override
|
||||||
ui.avatar.setUnreadCount(item.getUnreadCount());
|
public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
|
||||||
|
return a.equals(b);
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date
|
@Override
|
||||||
if (item.isEmpty()) {
|
public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
|
||||||
ui.date.setVisibility(GONE);
|
return a.isEmpty() == b.isEmpty() &&
|
||||||
} else {
|
a.getTimestamp() == b.getTimestamp() &&
|
||||||
long timestamp = item.getTimestamp();
|
a.getUnreadCount() == b.getUnreadCount();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,80 +12,48 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
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.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
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.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
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.sharing.ForumInvitationActivity;
|
||||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
import org.briarproject.briar.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.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
|
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.Objects.requireNonNull;
|
||||||
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
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ForumListFragment extends BaseEventFragment implements
|
public class ForumListFragment extends BaseFragment implements
|
||||||
OnClickListener {
|
OnClickListener {
|
||||||
|
|
||||||
public final static String TAG = ForumListFragment.class.getName();
|
public final static String TAG = ForumListFragment.class.getName();
|
||||||
private final static Logger LOG = Logger.getLogger(TAG);
|
|
||||||
|
|
||||||
|
private ForumListViewModel viewModel;
|
||||||
private BriarRecyclerView list;
|
private BriarRecyclerView list;
|
||||||
private ForumListAdapter adapter;
|
|
||||||
private Snackbar snackbar;
|
private Snackbar snackbar;
|
||||||
|
private final ForumListAdapter adapter = new ForumListAdapter();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
AndroidNotificationManager notificationManager;
|
ViewModelProvider.Factory viewModelFactory;
|
||||||
|
|
||||||
// Fields that are accessed from background threads must be volatile
|
|
||||||
@Inject
|
|
||||||
volatile ForumManager forumManager;
|
|
||||||
@Inject
|
|
||||||
volatile ForumSharingManager forumSharingManager;
|
|
||||||
|
|
||||||
public static ForumListFragment newInstance() {
|
public static ForumListFragment newInstance() {
|
||||||
|
return new ForumListFragment();
|
||||||
Bundle args = new Bundle();
|
|
||||||
|
|
||||||
ForumListFragment fragment = new ForumListFragment();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void injectFragment(ActivityComponent component) {
|
public void injectFragment(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
|
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||||
|
.get(ForumListViewModel.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -93,24 +61,35 @@ public class ForumListFragment extends BaseEventFragment implements
|
|||||||
public View onCreateView(LayoutInflater inflater,
|
public View onCreateView(LayoutInflater inflater,
|
||||||
@Nullable ViewGroup container,
|
@Nullable ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
|
||||||
requireActivity().setTitle(R.string.forums_button);
|
requireActivity().setTitle(R.string.forums_button);
|
||||||
|
|
||||||
View contentView =
|
View v = inflater.inflate(R.layout.fragment_forum_list, container,
|
||||||
inflater.inflate(R.layout.fragment_forum_list, container,
|
false);
|
||||||
false);
|
|
||||||
|
|
||||||
adapter = new ForumListAdapter(getActivity());
|
list = v.findViewById(R.id.forumList);
|
||||||
|
|
||||||
list = contentView.findViewById(R.id.forumList);
|
|
||||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||||
list.setAdapter(adapter);
|
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()
|
snackbar = new BriarSnackbarBuilder()
|
||||||
.setAction(R.string.show, this)
|
.setAction(R.string.show, this)
|
||||||
.make(list, "", LENGTH_INDEFINITE);
|
.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
|
@Override
|
||||||
@@ -121,18 +100,23 @@ public class ForumListFragment extends BaseEventFragment implements
|
|||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
notificationManager.clearAllForumPostNotifications();
|
viewModel.blockAllForumPostNotifications();
|
||||||
loadForums();
|
viewModel.clearAllForumPostNotifications();
|
||||||
loadAvailableForums();
|
// 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();
|
list.startPeriodicUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
adapter.clear();
|
|
||||||
list.showProgressBar();
|
|
||||||
list.stopPeriodicUpdate();
|
list.stopPeriodicUpdate();
|
||||||
|
viewModel.unblockAllForumPostNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -144,123 +128,12 @@ public class ForumListFragment extends BaseEventFragment implements
|
|||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
// Handle presses on the action bar items
|
// Handle presses on the action bar items
|
||||||
switch (item.getItemId()) {
|
if (item.getItemId() == R.id.action_create_forum) {
|
||||||
case R.id.action_create_forum:
|
Intent intent = new Intent(getContext(), CreateForumActivity.class);
|
||||||
Intent intent =
|
startActivity(intent);
|
||||||
new Intent(getContext(), CreateForumActivity.class);
|
return true;
|
||||||
startActivity(intent);
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
}
|
||||||
}
|
return super.onOptionsItemSelected(item);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.Forum;
|
||||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||||
|
|
||||||
// This class is NOT thread-safe
|
import javax.annotation.concurrent.Immutable;
|
||||||
class ForumListItem {
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class ForumListItem implements Comparable<ForumListItem> {
|
||||||
|
|
||||||
private final Forum forum;
|
private final Forum forum;
|
||||||
private int postCount, unread;
|
private final int postCount, unread;
|
||||||
private long timestamp;
|
private final long timestamp;
|
||||||
|
|
||||||
ForumListItem(Forum forum, GroupCount count) {
|
ForumListItem(Forum forum, GroupCount count) {
|
||||||
this.forum = forum;
|
this.forum = forum;
|
||||||
@@ -18,10 +22,11 @@ class ForumListItem {
|
|||||||
this.timestamp = count.getLatestMsgTime();
|
this.timestamp = count.getLatestMsgTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addHeader(ForumPostHeader h) {
|
ForumListItem(ForumListItem item, ForumPostHeader h) {
|
||||||
postCount++;
|
this.forum = item.forum;
|
||||||
if (!h.isRead()) unread++;
|
this.postCount = item.postCount + 1;
|
||||||
if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
|
this.unread = item.unread + (h.isRead() ? 0 : 1);
|
||||||
|
this.timestamp = Math.max(item.timestamp, h.getTimestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
Forum getForum() {
|
Forum getForum() {
|
||||||
@@ -43,4 +48,29 @@ class ForumListItem {
|
|||||||
int getUnreadCount() {
|
int getUnreadCount() {
|
||||||
return unread;
|
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.ActivityScope;
|
||||||
import org.briarproject.briar.android.activity.BaseActivity;
|
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.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
public class ForumModule {
|
public class ForumModule {
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public interface BindsModule {
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(ForumListViewModel.class)
|
||||||
|
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
@Provides
|
@Provides
|
||||||
ForumController provideForumController(BaseActivity activity,
|
ForumController provideForumController(BaseActivity activity,
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import javax.annotation.Nullable;
|
|||||||
import javax.annotation.concurrent.NotThreadSafe;
|
import javax.annotation.concurrent.NotThreadSafe;
|
||||||
|
|
||||||
@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(),
|
super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
|
||||||
h.getAuthorInfo(), h.isRead());
|
h.getAuthorInfo(), h.isRead());
|
||||||
}
|
}
|
||||||
|
|
||||||
ForumItem(MessageId messageId, @Nullable MessageId parentId, String text,
|
ForumPostItem(MessageId messageId, @Nullable MessageId parentId,
|
||||||
long timestamp, Author author, AuthorInfo authorInfo) {
|
String text, long timestamp, Author author, AuthorInfo authorInfo) {
|
||||||
super(messageId, parentId, text, timestamp, author, authorInfo, true);
|
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.os.Bundle;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.android.DestroyableContext;
|
import org.briarproject.briar.android.DestroyableContext;
|
||||||
@@ -77,7 +76,7 @@ public abstract class BaseFragment extends Fragment
|
|||||||
void showNextFragment(BaseFragment f);
|
void showNextFragment(BaseFragment f);
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
void handleDbException(DbException e);
|
void handleException(Exception e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
@@ -100,8 +99,8 @@ public abstract class BaseFragment extends Fragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
protected void handleDbException(DbException e) {
|
protected void handleException(Exception e) {
|
||||||
listener.handleDbException(e);
|
listener.handleException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.app.Activity;
|
|||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -28,6 +29,10 @@ import javax.inject.Inject;
|
|||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.DialogFragment;
|
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
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ScreenFilterDialogFragment extends DialogFragment {
|
public class ScreenFilterDialogFragment extends DialogFragment {
|
||||||
@@ -37,7 +42,7 @@ public class ScreenFilterDialogFragment extends DialogFragment {
|
|||||||
@Inject
|
@Inject
|
||||||
ScreenFilterMonitor screenFilterMonitor;
|
ScreenFilterMonitor screenFilterMonitor;
|
||||||
|
|
||||||
DismissListener dismissListener = null;
|
private DismissListener dismissListener = null;
|
||||||
|
|
||||||
public static ScreenFilterDialogFragment newInstance(
|
public static ScreenFilterDialogFragment newInstance(
|
||||||
Collection<AppDetails> apps) {
|
Collection<AppDetails> apps) {
|
||||||
@@ -83,10 +88,20 @@ public class ScreenFilterDialogFragment extends DialogFragment {
|
|||||||
View dialogView = inflater.inflate(R.layout.dialog_screen_filter, null);
|
View dialogView = inflater.inflate(R.layout.dialog_screen_filter, null);
|
||||||
builder.setView(dialogView);
|
builder.setView(dialogView);
|
||||||
TextView message = dialogView.findViewById(R.id.screen_filter_message);
|
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);
|
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);
|
if (allow.isChecked()) screenFilterMonitor.allowApps(packageNames);
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -211,7 +211,8 @@ public class IntroductionMessageFragment extends BaseFragment
|
|||||||
introductionActivity.runOnDbThread(() -> {
|
introductionActivity.runOnDbThread(() -> {
|
||||||
// actually make the introduction
|
// actually make the introduction
|
||||||
try {
|
try {
|
||||||
introductionManager.makeIntroduction(c1, c2, text);
|
long timestamp = System.currentTimeMillis();
|
||||||
|
introductionManager.makeIntroduction(c1, c2, text, timestamp);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
introductionError();
|
introductionError();
|
||||||
|
|||||||
@@ -10,14 +10,11 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.util.UiUtils;
|
import org.briarproject.briar.android.util.UiUtils;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
@@ -41,9 +38,6 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
|
||||||
AndroidExecutor androidExecutor;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUniqueTag() {
|
public String getUniqueTag() {
|
||||||
return TAG;
|
return TAG;
|
||||||
@@ -88,8 +82,8 @@ public class ContactExchangeErrorFragment extends BaseFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void triggerFeedback() {
|
private void triggerFeedback() {
|
||||||
|
UiUtils.triggerFeedback(requireContext());
|
||||||
finish();
|
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;
|
||||||
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener;
|
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.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
@@ -365,7 +364,7 @@ public class NavDrawerActivity extends BriarActivity implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleDbException(DbException e) {
|
public void handleException(Exception e) {
|
||||||
// Do nothing for now
|
// 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.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
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.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.settings.Settings;
|
import org.briarproject.bramble.api.settings.Settings;
|
||||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||||
|
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||||
|
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -14,7 +18,6 @@ import java.util.logging.Logger;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
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;
|
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class NavDrawerViewModel extends AndroidViewModel {
|
public class NavDrawerViewModel extends DbViewModel {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(NavDrawerViewModel.class.getName());
|
getLogger(NavDrawerViewModel.class.getName());
|
||||||
@@ -37,8 +40,6 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
|||||||
private static final String SHOW_TRANSPORTS_ONBOARDING =
|
private static final String SHOW_TRANSPORTS_ONBOARDING =
|
||||||
"showTransportsOnboarding";
|
"showTransportsOnboarding";
|
||||||
|
|
||||||
@DatabaseExecutor
|
|
||||||
private final Executor dbExecutor;
|
|
||||||
private final SettingsManager settingsManager;
|
private final SettingsManager settingsManager;
|
||||||
|
|
||||||
private final MutableLiveData<Boolean> showExpiryWarning =
|
private final MutableLiveData<Boolean> showExpiryWarning =
|
||||||
@@ -49,10 +50,13 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
|||||||
new MutableLiveData<>();
|
new MutableLiveData<>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
NavDrawerViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
|
NavDrawerViewModel(Application app,
|
||||||
|
@DatabaseExecutor Executor dbExecutor,
|
||||||
|
LifecycleManager lifecycleManager,
|
||||||
|
TransactionManager db,
|
||||||
|
AndroidExecutor androidExecutor,
|
||||||
SettingsManager settingsManager) {
|
SettingsManager settingsManager) {
|
||||||
super(app);
|
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
this.dbExecutor = dbExecutor;
|
|
||||||
this.settingsManager = settingsManager;
|
this.settingsManager = settingsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
void checkExpiryWarning() {
|
void checkExpiryWarning() {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
Settings settings =
|
Settings settings =
|
||||||
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||||
@@ -97,7 +101,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
|||||||
@UiThread
|
@UiThread
|
||||||
void expiryWarningDismissed() {
|
void expiryWarningDismissed() {
|
||||||
showExpiryWarning.setValue(false);
|
showExpiryWarning.setValue(false);
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
Settings settings = new Settings();
|
Settings settings = new Settings();
|
||||||
int date = (int) (System.currentTimeMillis() / 1000L);
|
int date = (int) (System.currentTimeMillis() / 1000L);
|
||||||
@@ -120,7 +124,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
|||||||
shouldAskForDozeWhitelisting.setValue(false);
|
shouldAskForDozeWhitelisting.setValue(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
Settings settings =
|
Settings settings =
|
||||||
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||||
@@ -141,7 +145,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
|||||||
@UiThread
|
@UiThread
|
||||||
void checkTransportsOnboarding() {
|
void checkTransportsOnboarding() {
|
||||||
if (showTransportsOnboarding.getValue() != null) return;
|
if (showTransportsOnboarding.getValue() != null) return;
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
Settings settings =
|
Settings settings =
|
||||||
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
settingsManager.getSettings(SETTINGS_NAMESPACE);
|
||||||
@@ -157,7 +161,7 @@ public class NavDrawerViewModel extends AndroidViewModel {
|
|||||||
@UiThread
|
@UiThread
|
||||||
void transportsOnboardingShown() {
|
void transportsOnboardingShown() {
|
||||||
showTransportsOnboarding.setValue(false);
|
showTransportsOnboarding.setValue(false);
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
Settings settings = new Settings();
|
Settings settings = new Settings();
|
||||||
settings.putBoolean(SHOW_TRANSPORTS_ONBOARDING, false);
|
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.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
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.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.network.NetworkManager;
|
import org.briarproject.bramble.api.network.NetworkManager;
|
||||||
import org.briarproject.bramble.api.network.NetworkStatus;
|
import org.briarproject.bramble.api.network.NetworkStatus;
|
||||||
import org.briarproject.bramble.api.network.event.NetworkStatusEvent;
|
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.Settings;
|
||||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
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.concurrent.Executor;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -34,7 +38,6 @@ import java.util.logging.Logger;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
@@ -51,13 +54,12 @@ import static org.briarproject.bramble.util.LogUtils.logException;
|
|||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class PluginViewModel extends AndroidViewModel implements EventListener {
|
public class PluginViewModel extends DbViewModel implements EventListener {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(PluginViewModel.class.getName());
|
getLogger(PluginViewModel.class.getName());
|
||||||
|
|
||||||
private final Application app;
|
private final Application app;
|
||||||
private final Executor dbExecutor;
|
|
||||||
private final SettingsManager settingsManager;
|
private final SettingsManager settingsManager;
|
||||||
private final PluginManager pluginManager;
|
private final PluginManager pluginManager;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
@@ -85,11 +87,12 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
|
PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
|
||||||
SettingsManager settingsManager, PluginManager pluginManager,
|
LifecycleManager lifecycleManager, TransactionManager db,
|
||||||
EventBus eventBus, NetworkManager networkManager) {
|
AndroidExecutor androidExecutor, SettingsManager settingsManager,
|
||||||
super(app);
|
PluginManager pluginManager, EventBus eventBus,
|
||||||
|
NetworkManager networkManager) {
|
||||||
|
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.dbExecutor = dbExecutor;
|
|
||||||
this.settingsManager = settingsManager;
|
this.settingsManager = settingsManager;
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
@@ -182,7 +185,7 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadSettings() {
|
private void loadSettings() {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
boolean tor = isPluginEnabled(TorConstants.ID,
|
boolean tor = isPluginEnabled(TorConstants.ID,
|
||||||
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
|
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
|
||||||
@@ -219,7 +222,7 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void mergeSettings(Settings s, String namespace) {
|
private void mergeSettings(Settings s, String namespace) {
|
||||||
dbExecutor.execute(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
long start = now();
|
long start = now();
|
||||||
settingsManager.mergeSettings(s, namespace);
|
settingsManager.mergeSettings(s, namespace);
|
||||||
@@ -235,8 +238,7 @@ public class PluginViewModel extends AndroidViewModel implements EventListener {
|
|||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||||
if (state == STATE_ON) bluetoothTurnedOn.postValue(true);
|
bluetoothTurnedOn.postValue(state == STATE_ON);
|
||||||
else bluetoothTurnedOn.postValue(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ public class PanicPreferencesFragment extends PreferenceFragmentCompat
|
|||||||
entries.add(0, getString(R.string.panic_app_setting_none));
|
entries.add(0, getString(R.string.panic_app_setting_none));
|
||||||
entryValues.add(0, PACKAGE_NAME_NONE);
|
entryValues.add(0, PACKAGE_NAME_NONE);
|
||||||
|
|
||||||
|
// only info.guardianproject.ripple is whitelisted in manifest
|
||||||
for (ResolveInfo resolveInfo : PanicResponder.resolveTriggerApps(pm)) {
|
for (ResolveInfo resolveInfo : PanicResponder.resolveTriggerApps(pm)) {
|
||||||
if (resolveInfo.activityInfo == null)
|
if (resolveInfo.activityInfo == null)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ public class GroupActivity extends
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ public class GroupActivity extends
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -264,7 +264,7 @@ public class GroupActivity extends
|
|||||||
// GroupRemovedEvent being fired
|
// GroupRemovedEvent being fired
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class CreateGroupActivity extends BriarActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
|||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||||
import org.briarproject.bramble.api.db.Transaction;
|
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
|
||||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
@@ -17,8 +15,6 @@ import org.briarproject.bramble.api.sync.GroupId;
|
|||||||
import org.briarproject.bramble.api.system.Clock;
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
||||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
|
|
||||||
import org.briarproject.briar.api.conversation.ConversationManager;
|
|
||||||
import org.briarproject.briar.api.privategroup.GroupMessage;
|
import org.briarproject.briar.api.privategroup.GroupMessage;
|
||||||
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
|
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
|
||||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||||
@@ -39,8 +35,6 @@ import javax.inject.Inject;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -49,12 +43,9 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
implements CreateGroupController {
|
implements CreateGroupController {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(CreateGroupControllerImpl.class.getName());
|
Logger.getLogger(CreateGroupControllerImpl.class.getName());
|
||||||
|
|
||||||
private final Executor cryptoExecutor;
|
private final Executor cryptoExecutor;
|
||||||
private final TransactionManager db;
|
|
||||||
private final AutoDeleteManager autoDeleteManager;
|
|
||||||
private final ConversationManager conversationManager;
|
|
||||||
private final ContactManager contactManager;
|
private final ContactManager contactManager;
|
||||||
private final IdentityManager identityManager;
|
private final IdentityManager identityManager;
|
||||||
private final PrivateGroupFactory groupFactory;
|
private final PrivateGroupFactory groupFactory;
|
||||||
@@ -65,26 +56,16 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CreateGroupControllerImpl(
|
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
|
||||||
@CryptoExecutor Executor cryptoExecutor,
|
@CryptoExecutor Executor cryptoExecutor,
|
||||||
TransactionManager db,
|
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||||
AutoDeleteManager autoDeleteManager,
|
IdentityManager identityManager, PrivateGroupFactory groupFactory,
|
||||||
ConversationManager conversationManager,
|
|
||||||
LifecycleManager lifecycleManager,
|
|
||||||
ContactManager contactManager,
|
|
||||||
IdentityManager identityManager,
|
|
||||||
PrivateGroupFactory groupFactory,
|
|
||||||
GroupMessageFactory groupMessageFactory,
|
GroupMessageFactory groupMessageFactory,
|
||||||
PrivateGroupManager groupManager,
|
PrivateGroupManager groupManager,
|
||||||
GroupInvitationFactory groupInvitationFactory,
|
GroupInvitationFactory groupInvitationFactory,
|
||||||
GroupInvitationManager groupInvitationManager,
|
GroupInvitationManager groupInvitationManager, Clock clock) {
|
||||||
Clock clock) {
|
|
||||||
super(dbExecutor, lifecycleManager, contactManager);
|
super(dbExecutor, lifecycleManager, contactManager);
|
||||||
this.cryptoExecutor = cryptoExecutor;
|
this.cryptoExecutor = cryptoExecutor;
|
||||||
this.db = db;
|
|
||||||
this.autoDeleteManager = autoDeleteManager;
|
|
||||||
this.conversationManager = conversationManager;
|
|
||||||
this.contactManager = contactManager;
|
this.contactManager = contactManager;
|
||||||
this.identityManager = identityManager;
|
this.identityManager = identityManager;
|
||||||
this.groupFactory = groupFactory;
|
this.groupFactory = groupFactory;
|
||||||
@@ -148,14 +129,16 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
ResultExceptionHandler<Void, DbException> handler) {
|
ResultExceptionHandler<Void, DbException> handler) {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
db.transaction(true, txn -> {
|
LocalAuthor localAuthor = identityManager.getLocalAuthor();
|
||||||
LocalAuthor localAuthor =
|
List<Contact> contacts = new ArrayList<>();
|
||||||
identityManager.getLocalAuthor(txn);
|
for (ContactId c : contactIds) {
|
||||||
List<InvitationContext> contexts =
|
try {
|
||||||
createInvitationContexts(txn, contactIds);
|
contacts.add(contactManager.getContact(c));
|
||||||
txn.attach(() -> signInvitations(g, localAuthor, contexts,
|
} catch (NoSuchContactException e) {
|
||||||
text, handler));
|
// Continue
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
signInvitations(g, localAuthor, contacts, text, handler);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
handler.onException(e);
|
handler.onException(e);
|
||||||
@@ -163,32 +146,17 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<InvitationContext> createInvitationContexts(Transaction txn,
|
|
||||||
Collection<ContactId> contactIds) throws DbException {
|
|
||||||
List<InvitationContext> contexts = new ArrayList<>();
|
|
||||||
for (ContactId c : contactIds) {
|
|
||||||
try {
|
|
||||||
Contact contact = contactManager.getContact(txn, c);
|
|
||||||
long timestamp = conversationManager
|
|
||||||
.getTimestampForOutgoingMessage(txn, c);
|
|
||||||
long timer = autoDeleteManager.getAutoDeleteTimer(txn, c,
|
|
||||||
timestamp);
|
|
||||||
contexts.add(new InvitationContext(contact, timestamp, timer));
|
|
||||||
} catch (NoSuchContactException e) {
|
|
||||||
// Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return contexts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void signInvitations(GroupId g, LocalAuthor localAuthor,
|
private void signInvitations(GroupId g, LocalAuthor localAuthor,
|
||||||
List<InvitationContext> contexts, @Nullable String text,
|
Collection<Contact> contacts, @Nullable String text,
|
||||||
ResultExceptionHandler<Void, DbException> handler) {
|
ResultExceptionHandler<Void, DbException> handler) {
|
||||||
cryptoExecutor.execute(() -> {
|
cryptoExecutor.execute(() -> {
|
||||||
for (InvitationContext ctx : contexts) {
|
long timestamp = clock.currentTimeMillis();
|
||||||
ctx.signature = groupInvitationFactory.signInvitation(
|
List<InvitationContext> contexts = new ArrayList<>();
|
||||||
ctx.contact, g, ctx.timestamp,
|
for (Contact c : contacts) {
|
||||||
localAuthor.getPrivateKey());
|
byte[] signature = groupInvitationFactory.signInvitation(c, g,
|
||||||
|
timestamp, localAuthor.getPrivateKey());
|
||||||
|
contexts.add(new InvitationContext(c.getId(), timestamp,
|
||||||
|
signature));
|
||||||
}
|
}
|
||||||
sendInvitations(g, contexts, text, handler);
|
sendInvitations(g, contexts, text, handler);
|
||||||
});
|
});
|
||||||
@@ -199,16 +167,16 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
ResultExceptionHandler<Void, DbException> handler) {
|
ResultExceptionHandler<Void, DbException> handler) {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
for (InvitationContext ctx : contexts) {
|
for (InvitationContext context : contexts) {
|
||||||
try {
|
try {
|
||||||
groupInvitationManager.sendInvitation(g,
|
groupInvitationManager.sendInvitation(g,
|
||||||
ctx.contact.getId(), text, ctx.timestamp,
|
context.contactId, text, context.timestamp,
|
||||||
requireNonNull(ctx.signature),
|
context.signature);
|
||||||
ctx.autoDeleteTimer);
|
|
||||||
} catch (NoSuchContactException e) {
|
} catch (NoSuchContactException e) {
|
||||||
// Continue
|
// Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//noinspection ConstantConditions
|
||||||
handler.onResult(null);
|
handler.onResult(null);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
@@ -219,16 +187,15 @@ class CreateGroupControllerImpl extends ContactSelectorControllerImpl
|
|||||||
|
|
||||||
private static class InvitationContext {
|
private static class InvitationContext {
|
||||||
|
|
||||||
private final Contact contact;
|
private final ContactId contactId;
|
||||||
private final long timestamp, autoDeleteTimer;
|
private final long timestamp;
|
||||||
@Nullable
|
private final byte[] signature;
|
||||||
private byte[] signature = null;
|
|
||||||
|
|
||||||
private InvitationContext(Contact contact, long timestamp,
|
private InvitationContext(ContactId contactId, long timestamp,
|
||||||
long autoDeleteTimer) {
|
byte[] signature) {
|
||||||
this.contact = contact;
|
this.contactId = contactId;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.autoDeleteTimer = autoDeleteTimer;
|
this.signature = signature;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
setResult(RESULT_CANCELED);
|
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.GroupMessageHeader;
|
||||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||||
|
|
||||||
// This class is not thread-safe
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
class GroupItem {
|
class GroupItem implements Comparable<GroupItem> {
|
||||||
|
|
||||||
private final PrivateGroup privateGroup;
|
private final PrivateGroup privateGroup;
|
||||||
private final AuthorInfo authorInfo;
|
private final AuthorInfo authorInfo;
|
||||||
private int messageCount, unreadCount;
|
private final int messageCount, unreadCount;
|
||||||
private long timestamp;
|
private final long timestamp;
|
||||||
private boolean dissolved;
|
private final boolean dissolved;
|
||||||
|
|
||||||
GroupItem(PrivateGroup privateGroup, AuthorInfo authorInfo,
|
GroupItem(PrivateGroup privateGroup, AuthorInfo authorInfo,
|
||||||
GroupCount count, boolean dissolved) {
|
GroupCount count, boolean dissolved) {
|
||||||
@@ -28,18 +32,22 @@ class GroupItem {
|
|||||||
this.dissolved = dissolved;
|
this.dissolved = dissolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addMessageHeader(GroupMessageHeader header) {
|
GroupItem(GroupItem item, GroupMessageHeader header) {
|
||||||
messageCount++;
|
this.privateGroup = item.privateGroup;
|
||||||
if (header.getTimestamp() > timestamp) {
|
this.authorInfo = item.authorInfo;
|
||||||
timestamp = header.getTimestamp();
|
this.messageCount = item.messageCount + 1;
|
||||||
}
|
this.unreadCount = item.unreadCount + (header.isRead() ? 0 : 1);
|
||||||
if (!header.isRead()) {
|
this.timestamp = Math.max(header.getTimestamp(), item.timestamp);
|
||||||
unreadCount++;
|
this.dissolved = item.dissolved;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PrivateGroup getPrivateGroup() {
|
GroupItem(GroupItem item, boolean isDissolved) {
|
||||||
return privateGroup;
|
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() {
|
GroupId getId() {
|
||||||
@@ -78,8 +86,27 @@ class GroupItem {
|
|||||||
return dissolved;
|
return dissolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setDissolved() {
|
@Override
|
||||||
dissolved = true;
|
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;
|
package org.briarproject.briar.android.privategroup.list;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
|
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
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class GroupListAdapter extends BriarAdapter<GroupItem, GroupViewHolder> {
|
class GroupListAdapter extends ListAdapter<GroupItem, GroupViewHolder> {
|
||||||
|
|
||||||
private final OnGroupRemoveClickListener listener;
|
private final OnGroupRemoveClickListener listener;
|
||||||
|
|
||||||
GroupListAdapter(Context ctx, OnGroupRemoveClickListener listener) {
|
GroupListAdapter(OnGroupRemoveClickListener listener) {
|
||||||
super(ctx, GroupItem.class);
|
super(new GroupItemCallback());
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public GroupViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
View v = LayoutInflater.from(ctx).inflate(
|
View v = LayoutInflater.from(parent.getContext())
|
||||||
R.layout.list_item_group, parent, false);
|
.inflate(R.layout.list_item_group, parent, false);
|
||||||
return new GroupViewHolder(v);
|
return new GroupViewHolder(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(GroupViewHolder ui, int position) {
|
public void onBindViewHolder(GroupViewHolder ui, int position) {
|
||||||
ui.bindView(ctx, items.get(position), listener);
|
ui.bindView(getItem(position), listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static class GroupItemCallback extends ItemCallback<GroupItem> {
|
||||||
public int compare(GroupItem a, GroupItem b) {
|
@Override
|
||||||
if (a == b) return 0;
|
public boolean areItemsTheSame(GroupItem a, GroupItem b) {
|
||||||
// The group with the latest message comes first
|
return a.equals(b);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return INVALID_POSITION;
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeItem(GroupId groupId) {
|
@Override
|
||||||
int pos = findItemPosition(groupId);
|
public boolean areContentsTheSame(GroupItem a, GroupItem b) {
|
||||||
if (pos != INVALID_POSITION) items.removeItemAt(pos);
|
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 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.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import 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.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
|
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
|
||||||
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity;
|
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.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
|
||||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
|
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class GroupListFragment extends BaseFragment implements
|
public class GroupListFragment extends BaseFragment implements
|
||||||
GroupListListener, OnGroupRemoveClickListener, OnClickListener {
|
OnGroupRemoveClickListener, OnClickListener {
|
||||||
|
|
||||||
public final static String TAG = GroupListFragment.class.getName();
|
public final static String TAG = GroupListFragment.class.getName();
|
||||||
private static final Logger LOG = Logger.getLogger(TAG);
|
|
||||||
|
|
||||||
public static GroupListFragment newInstance() {
|
public static GroupListFragment newInstance() {
|
||||||
return new GroupListFragment();
|
return new GroupListFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
GroupListController controller;
|
ViewModelProvider.Factory viewModelFactory;
|
||||||
|
|
||||||
|
private GroupListViewModel viewModel;
|
||||||
private BriarRecyclerView list;
|
private BriarRecyclerView list;
|
||||||
private GroupListAdapter adapter;
|
private GroupListAdapter adapter;
|
||||||
private Snackbar snackbar;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void injectFragment(ActivityComponent component) {
|
public void injectFragment(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
controller.setGroupListListener(this);
|
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||||
|
.get(GroupListViewModel.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -75,17 +68,32 @@ public class GroupListFragment extends BaseFragment implements
|
|||||||
|
|
||||||
View v = inflater.inflate(R.layout.list, container, false);
|
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 = v.findViewById(R.id.list);
|
||||||
list.setEmptyImage(R.drawable.ic_empty_state_group_list);
|
list.setEmptyImage(R.drawable.ic_empty_state_group_list);
|
||||||
list.setEmptyText(R.string.groups_list_empty);
|
list.setEmptyText(R.string.groups_list_empty);
|
||||||
list.setEmptyAction(R.string.groups_list_empty_action);
|
list.setEmptyAction(R.string.groups_list_empty_action);
|
||||||
list.setLayoutManager(new LinearLayoutManager(getContext()));
|
list.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
list.setAdapter(adapter);
|
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)
|
.setAction(R.string.show, this)
|
||||||
.make(list, "", LENGTH_INDEFINITE);
|
.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;
|
return v;
|
||||||
}
|
}
|
||||||
@@ -93,25 +101,23 @@ public class GroupListFragment extends BaseFragment implements
|
|||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.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();
|
list.startPeriodicUpdate();
|
||||||
loadGroups();
|
|
||||||
loadAvailableGroups();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
controller.onStop();
|
|
||||||
list.stopPeriodicUpdate();
|
list.stopPeriodicUpdate();
|
||||||
adapter.clear();
|
viewModel.unblockAllGroupMessageNotifications();
|
||||||
list.showProgressBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
controller.unsetGroupListListener(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -122,68 +128,18 @@ public class GroupListFragment extends BaseFragment implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
if (item.getItemId() == R.id.action_add_group) {
|
||||||
case R.id.action_add_group:
|
Intent i = new Intent(getContext(), CreateGroupActivity.class);
|
||||||
Intent i = new Intent(getContext(), CreateGroupActivity.class);
|
startActivity(i);
|
||||||
startActivity(i);
|
return true;
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@Override
|
@Override
|
||||||
public void onGroupRemoveClick(GroupItem item) {
|
public void onGroupRemoveClick(GroupItem item) {
|
||||||
controller.removeGroup(item.getId(),
|
viewModel.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -191,52 +147,6 @@ public class GroupListFragment extends BaseFragment implements
|
|||||||
return TAG;
|
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
|
* This method is handling the available groups snackbar action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package org.briarproject.briar.android.privategroup.list;
|
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.Module;
|
||||||
import dagger.Provides;
|
import dagger.multibindings.IntoMap;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
public class GroupListModule {
|
public abstract class GroupListModule {
|
||||||
|
|
||||||
@ActivityScope
|
@Binds
|
||||||
@Provides
|
@IntoMap
|
||||||
GroupListController provideGroupListController(
|
@ViewModelKey(GroupListViewModel.class)
|
||||||
GroupListControllerImpl groupListController) {
|
abstract ViewModel bindGroupListViewModel(
|
||||||
return groupListController;
|
GroupListViewModel groupListViewModel);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.briarproject.briar.android.privategroup.list;
|
package org.briarproject.briar.android.privategroup.list;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.contact.ContactManager;
|
import org.briarproject.bramble.api.contact.ContactManager;
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.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.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
@@ -16,11 +19,12 @@ import org.briarproject.bramble.api.sync.ClientId;
|
|||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
|
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
|
||||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||||
import org.briarproject.briar.android.controller.DbControllerImpl;
|
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
import org.briarproject.briar.android.viewmodel.LiveResult;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
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.PrivateGroup;
|
||||||
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
|
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
|
||||||
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
|
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.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -38,10 +43,13 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.annotation.Nullable;
|
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.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
@@ -49,11 +57,10 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class GroupListControllerImpl extends DbControllerImpl
|
class GroupListViewModel extends DbViewModel implements EventListener {
|
||||||
implements GroupListController, EventListener {
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(GroupListControllerImpl.class.getName());
|
getLogger(GroupListViewModel.class.getName());
|
||||||
|
|
||||||
private final PrivateGroupManager groupManager;
|
private final PrivateGroupManager groupManager;
|
||||||
private final GroupInvitationManager groupInvitationManager;
|
private final GroupInvitationManager groupInvitationManager;
|
||||||
@@ -61,120 +68,137 @@ class GroupListControllerImpl extends DbControllerImpl
|
|||||||
private final AndroidNotificationManager notificationManager;
|
private final AndroidNotificationManager notificationManager;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
|
|
||||||
// UI thread
|
private final MutableLiveData<LiveResult<List<GroupItem>>> groupItems =
|
||||||
@Nullable
|
new MutableLiveData<>();
|
||||||
private GroupListListener listener;
|
private final MutableLiveData<Integer> numInvitations =
|
||||||
|
new MutableLiveData<>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
GroupListControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
GroupListViewModel(Application application,
|
||||||
LifecycleManager lifecycleManager, PrivateGroupManager groupManager,
|
@DatabaseExecutor Executor dbExecutor,
|
||||||
|
LifecycleManager lifecycleManager,
|
||||||
|
TransactionManager db,
|
||||||
|
AndroidExecutor androidExecutor,
|
||||||
|
PrivateGroupManager groupManager,
|
||||||
GroupInvitationManager groupInvitationManager,
|
GroupInvitationManager groupInvitationManager,
|
||||||
ContactManager contactManager,
|
ContactManager contactManager,
|
||||||
AndroidNotificationManager notificationManager, EventBus eventBus) {
|
AndroidNotificationManager notificationManager, EventBus eventBus) {
|
||||||
super(dbExecutor, lifecycleManager);
|
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
this.groupManager = groupManager;
|
this.groupManager = groupManager;
|
||||||
this.groupInvitationManager = groupInvitationManager;
|
this.groupInvitationManager = groupInvitationManager;
|
||||||
this.contactManager = contactManager;
|
this.contactManager = contactManager;
|
||||||
this.notificationManager = notificationManager;
|
this.notificationManager = notificationManager;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
|
this.eventBus.addListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setGroupListListener(GroupListListener listener) {
|
protected void onCleared() {
|
||||||
this.listener = listener;
|
super.onCleared();
|
||||||
}
|
|
||||||
|
|
||||||
@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() {
|
|
||||||
eventBus.removeListener(this);
|
eventBus.removeListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearAllGroupMessageNotifications() {
|
||||||
|
notificationManager.clearAllGroupMessageNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
void blockAllGroupMessageNotifications() {
|
||||||
|
notificationManager.blockAllGroupMessageNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
void unblockAllGroupMessageNotifications() {
|
||||||
|
notificationManager.unblockAllGroupMessageNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@CallSuper
|
|
||||||
public void eventOccurred(Event e) {
|
public void eventOccurred(Event e) {
|
||||||
if (listener == null) throw new IllegalStateException();
|
|
||||||
if (e instanceof GroupMessageAddedEvent) {
|
if (e instanceof GroupMessageAddedEvent) {
|
||||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||||
LOG.info("Private group message added");
|
LOG.info("Private group message added");
|
||||||
listener.onGroupMessageAdded(g.getHeader());
|
onGroupMessageAdded(g.getHeader());
|
||||||
} else if (e instanceof GroupInvitationRequestReceivedEvent) {
|
} else if (e instanceof GroupInvitationRequestReceivedEvent) {
|
||||||
LOG.info("Private group invitation received");
|
LOG.info("Private group invitation received");
|
||||||
listener.onGroupInvitationReceived();
|
loadNumInvitations();
|
||||||
} else if (e instanceof GroupAddedEvent) {
|
} else if (e instanceof GroupAddedEvent) {
|
||||||
GroupAddedEvent g = (GroupAddedEvent) e;
|
GroupAddedEvent g = (GroupAddedEvent) e;
|
||||||
ClientId id = g.getGroup().getClientId();
|
ClientId id = g.getGroup().getClientId();
|
||||||
if (id.equals(CLIENT_ID)) {
|
if (id.equals(CLIENT_ID)) {
|
||||||
LOG.info("Private group added");
|
LOG.info("Private group added");
|
||||||
listener.onGroupAdded(g.getGroup().getId());
|
loadGroups();
|
||||||
}
|
}
|
||||||
} else if (e instanceof GroupRemovedEvent) {
|
} else if (e instanceof GroupRemovedEvent) {
|
||||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||||
ClientId id = g.getGroup().getClientId();
|
ClientId id = g.getGroup().getClientId();
|
||||||
if (id.equals(CLIENT_ID)) {
|
if (id.equals(CLIENT_ID)) {
|
||||||
LOG.info("Private group removed");
|
LOG.info("Private group removed");
|
||||||
listener.onGroupRemoved(g.getGroup().getId());
|
onGroupRemoved(g.getGroup().getId());
|
||||||
}
|
}
|
||||||
} else if (e instanceof GroupDissolvedEvent) {
|
} else if (e instanceof GroupDissolvedEvent) {
|
||||||
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
|
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
|
||||||
LOG.info("Private group dissolved");
|
LOG.info("Private group dissolved");
|
||||||
listener.onGroupDissolved(g.getGroupId());
|
onGroupDissolved(g.getGroupId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
void loadGroups() {
|
||||||
public void loadGroups(
|
loadList(this::loadGroups, groupItems::setValue);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@DatabaseExecutor
|
||||||
public void removeGroup(GroupId g, ExceptionHandler<DbException> handler) {
|
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(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
long start = now();
|
long start = now();
|
||||||
@@ -182,23 +206,26 @@ class GroupListControllerImpl extends DbControllerImpl
|
|||||||
logDuration(LOG, "Removing group", start);
|
logDuration(LOG, "Removing group", start);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
handler.onException(e);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
void loadNumInvitations() {
|
||||||
public void loadAvailableGroups(
|
|
||||||
ResultExceptionHandler<Integer, DbException> handler) {
|
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
handler.onResult(
|
int i = groupInvitationManager.getInvitations().size();
|
||||||
groupInvitationManager.getInvitations().size());
|
numInvitations.postValue(i);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
logException(LOG, WARNING, 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 static float ALPHA = 0.42f;
|
||||||
|
|
||||||
|
private final Context ctx;
|
||||||
private final ViewGroup layout;
|
private final ViewGroup layout;
|
||||||
private final TextAvatarView avatar;
|
private final TextAvatarView avatar;
|
||||||
private final TextView name;
|
private final TextView name;
|
||||||
@@ -40,7 +41,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
|||||||
|
|
||||||
GroupViewHolder(View v) {
|
GroupViewHolder(View v) {
|
||||||
super(v);
|
super(v);
|
||||||
|
ctx = v.getContext();
|
||||||
layout = (ViewGroup) v;
|
layout = (ViewGroup) v;
|
||||||
avatar = v.findViewById(R.id.avatarView);
|
avatar = v.findViewById(R.id.avatarView);
|
||||||
name = v.findViewById(R.id.nameView);
|
name = v.findViewById(R.id.nameView);
|
||||||
@@ -51,8 +52,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
|
|||||||
remove = v.findViewById(R.id.removeButton);
|
remove = v.findViewById(R.id.removeButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bindView(Context ctx, GroupItem group,
|
void bindView(GroupItem group, OnGroupRemoveClickListener listener) {
|
||||||
OnGroupRemoveClickListener listener) {
|
|
||||||
// Avatar
|
// Avatar
|
||||||
avatar.setText(group.getName().substring(0, 1));
|
avatar.setText(group.getName().substring(0, 1));
|
||||||
avatar.setBackgroundBytes(group.getId().getBytes());
|
avatar.setBackgroundBytes(group.getId().getBytes());
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ public class GroupMemberListActivity extends BriarActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
|
|||||||
new UiExceptionHandler<DbException>(this) {
|
new UiExceptionHandler<DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
|
|||||||
new UiExceptionHandler<DbException>(this) {
|
new UiExceptionHandler<DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
supportFinishAfterTransition();
|
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.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@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
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
@@ -25,15 +48,16 @@ public class CrashFragment extends Fragment {
|
|||||||
.inflate(R.layout.fragment_crash, container, false);
|
.inflate(R.layout.fragment_crash, container, false);
|
||||||
|
|
||||||
v.findViewById(R.id.acceptButton).setOnClickListener(view ->
|
v.findViewById(R.id.acceptButton).setOnClickListener(view ->
|
||||||
getDevReportActivity().displayFragment(true));
|
viewModel.showReport());
|
||||||
v.findViewById(R.id.declineButton).setOnClickListener(view ->
|
v.findViewById(R.id.declineButton).setOnClickListener(view ->
|
||||||
getDevReportActivity().closeReport());
|
viewModel.closeReport());
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DevReportActivity getDevReportActivity() {
|
@Override
|
||||||
return (DevReportActivity) requireActivity();
|
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;
|
package org.briarproject.briar.android.reporting;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@@ -10,92 +9,58 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.Toast;
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
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.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.json.JSONException;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
|
|
||||||
import java.io.File;
|
import javax.inject.Inject;
|
||||||
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 androidx.annotation.Nullable;
|
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.GONE;
|
||||||
import static android.view.View.INVISIBLE;
|
import static android.view.View.INVISIBLE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
import static java.util.Objects.requireNonNull;
|
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
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ReportFormFragment extends Fragment
|
public class ReportFormFragment extends BaseFragment {
|
||||||
implements OnCheckedChangeListener {
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
public final static String TAG = ReportFormFragment.class.getName();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFeedback;
|
@Inject
|
||||||
private File reportFile;
|
ViewModelProvider.Factory viewModelFactory;
|
||||||
|
|
||||||
|
private ReportViewModel viewModel;
|
||||||
|
|
||||||
private EditText userCommentView;
|
private EditText userCommentView;
|
||||||
private EditText userEmailView;
|
private EditText userEmailView;
|
||||||
private CheckBox includeDebugReport;
|
private CheckBox includeDebugReport;
|
||||||
private Button chevron;
|
private Button chevron;
|
||||||
private LinearLayout report;
|
private RecyclerView list;
|
||||||
private View progress;
|
private View progress;
|
||||||
@Nullable
|
@Nullable
|
||||||
private MenuItem sendReport;
|
private MenuItem sendReport;
|
||||||
|
|
||||||
static ReportFormFragment newInstance(boolean isFeedback,
|
@Override
|
||||||
File reportFile) {
|
public void injectFragment(ActivityComponent component) {
|
||||||
ReportFormFragment f = new ReportFormFragment();
|
component.inject(this);
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putBoolean(IS_FEEDBACK, isFeedback);
|
|
||||||
args.putSerializable(EXTRA_REPORT_FILE, reportFile);
|
|
||||||
f.setArguments(args);
|
|
||||||
return f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||||
|
.get(ReportViewModel.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -110,15 +75,10 @@ public class ReportFormFragment extends Fragment
|
|||||||
userEmailView = v.findViewById(R.id.user_email);
|
userEmailView = v.findViewById(R.id.user_email);
|
||||||
includeDebugReport = v.findViewById(R.id.include_debug_report);
|
includeDebugReport = v.findViewById(R.id.include_debug_report);
|
||||||
chevron = v.findViewById(R.id.chevron);
|
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);
|
progress = v.findViewById(R.id.progress_wheel);
|
||||||
|
|
||||||
Bundle args = requireArguments();
|
if (viewModel.isFeedback()) {
|
||||||
isFeedback = args.getBoolean(IS_FEEDBACK);
|
|
||||||
reportFile =
|
|
||||||
(File) requireNonNull(args.getSerializable(EXTRA_REPORT_FILE));
|
|
||||||
|
|
||||||
if (isFeedback) {
|
|
||||||
includeDebugReport
|
includeDebugReport
|
||||||
.setText(getString(R.string.include_debug_report_feedback));
|
.setText(getString(R.string.include_debug_report_feedback));
|
||||||
userCommentView.setHint(R.string.enter_feedback);
|
userCommentView.setHint(R.string.enter_feedback);
|
||||||
@@ -129,163 +89,73 @@ public class ReportFormFragment extends Fragment
|
|||||||
|
|
||||||
chevron.setOnClickListener(view -> {
|
chevron.setOnClickListener(view -> {
|
||||||
boolean show = chevron.getText().equals(getString(R.string.show));
|
boolean show = chevron.getText().equals(getString(R.string.show));
|
||||||
if (show) {
|
viewModel.showReportData(show);
|
||||||
chevron.setText(R.string.hide);
|
|
||||||
refresh();
|
|
||||||
} else {
|
|
||||||
chevron.setText(R.string.show);
|
|
||||||
report.setVisibility(GONE);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
if (chevron.isSelected()) refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.dev_report_actions, menu);
|
inflater.inflate(R.menu.dev_report_actions, menu);
|
||||||
sendReport = menu.findItem(R.id.action_send_report);
|
sendReport = menu.findItem(R.id.action_send_report);
|
||||||
// calling setShowAsAction() shouldn't be needed, but for some reason is
|
sendReport.setEnabled(false);
|
||||||
sendReport.setShowAsAction(SHOW_AS_ACTION_ALWAYS);
|
viewModel.getReportData().observe(getViewLifecycleOwner(), data -> {
|
||||||
|
list.setAdapter(new ReportDataAdapter(data.getItems()));
|
||||||
|
sendReport.setEnabled(true);
|
||||||
|
progress.setVisibility(INVISIBLE);
|
||||||
|
});
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
if (item.getItemId() == R.id.action_send_report) {
|
if (item.getItemId() == R.id.action_send_report) {
|
||||||
processReport();
|
sendReport();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
public String getUniqueTag() {
|
||||||
ReportField field = (ReportField) buttonView.getTag();
|
return TAG;
|
||||||
if (field != null) {
|
|
||||||
if (isChecked) excludedFields.remove(field);
|
|
||||||
else excludedFields.add(field);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refresh() {
|
private void sendReport() {
|
||||||
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() {
|
|
||||||
userCommentView.setEnabled(false);
|
userCommentView.setEnabled(false);
|
||||||
userEmailView.setEnabled(false);
|
userEmailView.setEnabled(false);
|
||||||
requireNonNull(sendReport).setEnabled(false);
|
requireNonNull(sendReport).setEnabled(false);
|
||||||
|
list.setVisibility(GONE); // ensures that progress fits on screen
|
||||||
progress.setVisibility(VISIBLE);
|
progress.setVisibility(VISIBLE);
|
||||||
boolean includeReport = !isFeedback || includeDebugReport.isChecked();
|
|
||||||
new AsyncTask<Void, Void, Boolean>() {
|
|
||||||
|
|
||||||
@Override
|
// Retrieve user's comment and email address, if any
|
||||||
protected Boolean doInBackground(Void... args) {
|
String comment = userCommentView.getText().toString();
|
||||||
CrashReportPersister persister = new CrashReportPersister();
|
String email = userEmailView.getText().toString();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
boolean includeReport = includeDebugReport.isChecked();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private DevReportActivity getDevReportActivity() {
|
// Send report (now or after next sign-in)
|
||||||
return (DevReportActivity) requireActivity();
|
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.Settings;
|
||||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
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.api.system.LocationUtils;
|
||||||
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
|
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
|
||||||
import org.briarproject.bramble.util.StringUtils;
|
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.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
import static android.widget.Toast.LENGTH_SHORT;
|
||||||
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
|
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.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
|
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
|
||||||
@@ -166,9 +166,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
|||||||
@Inject
|
@Inject
|
||||||
CircumventionProvider circumventionProvider;
|
CircumventionProvider circumventionProvider;
|
||||||
|
|
||||||
@Inject
|
|
||||||
AndroidExecutor androidExecutor;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
@@ -226,11 +223,12 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
|||||||
screenLock.setOnPreferenceChangeListener(this);
|
screenLock.setOnPreferenceChangeListener(this);
|
||||||
screenLockTimeout.setOnPreferenceChangeListener(this);
|
screenLockTimeout.setOnPreferenceChangeListener(this);
|
||||||
|
|
||||||
findPreference("pref_key_send_feedback").setOnPreferenceClickListener(
|
Preference prefFeedback =
|
||||||
preference -> {
|
requireNonNull(findPreference("pref_key_send_feedback"));
|
||||||
triggerFeedback(androidExecutor);
|
prefFeedback.setOnPreferenceClickListener(preference -> {
|
||||||
return true;
|
triggerFeedback(requireContext());
|
||||||
});
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
if (SDK_INT < 27) {
|
if (SDK_INT < 27) {
|
||||||
// remove System Default Theme option from preference entries
|
// 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));
|
values.remove(getString(R.string.pref_theme_system_value));
|
||||||
theme.setEntryValues(values.toArray(new CharSequence[0]));
|
theme.setEntryValues(values.toArray(new CharSequence[0]));
|
||||||
}
|
}
|
||||||
|
Preference explode = requireNonNull(findPreference("pref_key_explode"));
|
||||||
if (IS_DEBUG_BUILD) {
|
if (IS_DEBUG_BUILD) {
|
||||||
findPreference("pref_key_explode").setOnPreferenceClickListener(
|
explode.setOnPreferenceClickListener(preference -> {
|
||||||
preference -> {
|
throw new RuntimeException("Boom!");
|
||||||
throw new RuntimeException("Boom!");
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
findPreference("pref_key_explode").setVisible(false);
|
explode.setVisible(false);
|
||||||
findPreference("pref_key_test_data").setVisible(false);
|
findPreference("pref_key_test_data").setVisible(false);
|
||||||
PreferenceGroup testing =
|
PreferenceGroup testing = explode.getParent();
|
||||||
findPreference("pref_key_explode").getParent();
|
|
||||||
if (testing == null) throw new AssertionError();
|
if (testing == null) throw new AssertionError();
|
||||||
testing.setVisible(false);
|
testing.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
|
|||||||
new UiExceptionHandler<DbException>(this) {
|
new UiExceptionHandler<DbException>(this) {
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class ShareBlogActivity extends ShareActivity {
|
|||||||
Toast.makeText(ShareBlogActivity.this,
|
Toast.makeText(ShareBlogActivity.this,
|
||||||
R.string.blogs_sharing_error, LENGTH_SHORT)
|
R.string.blogs_sharing_error, LENGTH_SHORT)
|
||||||
.show();
|
.show();
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
|
|||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
||||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||||
import org.briarproject.briar.api.blog.BlogSharingManager;
|
import org.briarproject.briar.api.blog.BlogSharingManager;
|
||||||
|
import org.briarproject.briar.api.conversation.ConversationManager;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@@ -23,7 +25,6 @@ import javax.annotation.concurrent.Immutable;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -32,16 +33,21 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
|
|||||||
implements ShareBlogController {
|
implements ShareBlogController {
|
||||||
|
|
||||||
private final static Logger LOG =
|
private final static Logger LOG =
|
||||||
getLogger(ShareBlogControllerImpl.class.getName());
|
Logger.getLogger(ShareBlogControllerImpl.class.getName());
|
||||||
|
|
||||||
|
private final ConversationManager conversationManager;
|
||||||
private final BlogSharingManager blogSharingManager;
|
private final BlogSharingManager blogSharingManager;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
ShareBlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
LifecycleManager lifecycleManager, ContactManager contactManager,
|
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||||
BlogSharingManager blogSharingManager) {
|
ConversationManager conversationManager,
|
||||||
|
BlogSharingManager blogSharingManager, Clock clock) {
|
||||||
super(dbExecutor, lifecycleManager, contactManager);
|
super(dbExecutor, lifecycleManager, contactManager);
|
||||||
|
this.conversationManager = conversationManager;
|
||||||
this.blogSharingManager = blogSharingManager;
|
this.blogSharingManager = blogSharingManager;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -56,7 +62,10 @@ class ShareBlogControllerImpl extends ContactSelectorControllerImpl
|
|||||||
try {
|
try {
|
||||||
for (ContactId c : contacts) {
|
for (ContactId c : contacts) {
|
||||||
try {
|
try {
|
||||||
blogSharingManager.sendInvitation(g, c, text);
|
long time = Math.max(clock.currentTimeMillis(),
|
||||||
|
conversationManager.getGroupCount(c)
|
||||||
|
.getLatestMsgTime() + 1);
|
||||||
|
blogSharingManager.sendInvitation(g, c, text, time);
|
||||||
} catch (NoSuchContactException | NoSuchGroupException e) {
|
} catch (NoSuchContactException | NoSuchGroupException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class ShareForumActivity extends ShareActivity {
|
|||||||
Toast.makeText(ShareForumActivity.this,
|
Toast.makeText(ShareForumActivity.this,
|
||||||
R.string.forum_share_error, LENGTH_SHORT)
|
R.string.forum_share_error, LENGTH_SHORT)
|
||||||
.show();
|
.show();
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import org.briarproject.bramble.api.db.NoSuchGroupException;
|
|||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
import org.briarproject.briar.android.contactselection.ContactSelectorControllerImpl;
|
||||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||||
|
import org.briarproject.briar.api.conversation.ConversationManager;
|
||||||
import org.briarproject.briar.api.forum.ForumSharingManager;
|
import org.briarproject.briar.api.forum.ForumSharingManager;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -23,7 +25,6 @@ import javax.annotation.concurrent.Immutable;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -32,16 +33,21 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
|
|||||||
implements ShareForumController {
|
implements ShareForumController {
|
||||||
|
|
||||||
private final static Logger LOG =
|
private final static Logger LOG =
|
||||||
getLogger(ShareForumControllerImpl.class.getName());
|
Logger.getLogger(ShareForumControllerImpl.class.getName());
|
||||||
|
|
||||||
|
private final ConversationManager conversationManager;
|
||||||
private final ForumSharingManager forumSharingManager;
|
private final ForumSharingManager forumSharingManager;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
ShareForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
LifecycleManager lifecycleManager, ContactManager contactManager,
|
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||||
ForumSharingManager forumSharingManager) {
|
ConversationManager conversationManager,
|
||||||
|
ForumSharingManager forumSharingManager, Clock clock) {
|
||||||
super(dbExecutor, lifecycleManager, contactManager);
|
super(dbExecutor, lifecycleManager, contactManager);
|
||||||
|
this.conversationManager = conversationManager;
|
||||||
this.forumSharingManager = forumSharingManager;
|
this.forumSharingManager = forumSharingManager;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -56,7 +62,10 @@ class ShareForumControllerImpl extends ContactSelectorControllerImpl
|
|||||||
try {
|
try {
|
||||||
for (ContactId c : contacts) {
|
for (ContactId c : contacts) {
|
||||||
try {
|
try {
|
||||||
forumSharingManager.sendInvitation(g, c, text);
|
long time = Math.max(clock.currentTimeMillis(),
|
||||||
|
conversationManager.getGroupCount(c)
|
||||||
|
.getLatestMsgTime() + 1);
|
||||||
|
forumSharingManager.sendInvitation(g, c, text, time);
|
||||||
} catch (NoSuchContactException | NoSuchGroupException e) {
|
} catch (NoSuchContactException | NoSuchGroupException e) {
|
||||||
logException(LOG, WARNING, e);
|
logException(LOG, WARNING, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -351,7 +351,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExceptionUi(DbException exception) {
|
public void onExceptionUi(DbException exception) {
|
||||||
handleDbException(exception);
|
handleException(exception);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
getController().createAndStoreMessage(text, replyItem, handler);
|
getController().createAndStoreMessage(text, replyItem, handler);
|
||||||
|
|||||||
@@ -27,14 +27,13 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.identity.Author;
|
import org.briarproject.bramble.api.identity.Author;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.reporting.FeedbackActivity;
|
||||||
import org.briarproject.briar.android.view.ArticleMovementMethod;
|
import org.briarproject.briar.android.view.ArticleMovementMethod;
|
||||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
||||||
|
|
||||||
@@ -51,6 +50,7 @@ import androidx.appcompat.app.AlertDialog;
|
|||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||||
import androidx.core.text.HtmlCompat;
|
import androidx.core.text.HtmlCompat;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
@@ -92,6 +92,8 @@ import static java.util.Objects.requireNonNull;
|
|||||||
import static java.util.concurrent.TimeUnit.DAYS;
|
import static java.util.concurrent.TimeUnit.DAYS;
|
||||||
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
||||||
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
|
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
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
@@ -345,10 +347,18 @@ public class UiUtils {
|
|||||||
return fm.hasEnrolledFingerprints() && fm.isHardwareDetected();
|
return fm.hasEnrolledFingerprints() && fm.isHardwareDetected();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void triggerFeedback(AndroidExecutor androidExecutor) {
|
public static void triggerFeedback(Context ctx) {
|
||||||
androidExecutor.runOnBackgroundThread(
|
startDevReportActivity(ctx, FeedbackActivity.class, null, null);
|
||||||
() -> ACRA.getErrorReporter()
|
}
|
||||||
.handleException(new UserFeedback(), false));
|
|
||||||
|
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,
|
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 org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.util.Consumer;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class LiveResult<T> {
|
public class LiveResult<T> {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private T result;
|
private final T result;
|
||||||
@Nullable
|
@Nullable
|
||||||
private Exception exception;
|
private final Exception exception;
|
||||||
|
|
||||||
public LiveResult(T result) {
|
public LiveResult(T result) {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
@@ -36,4 +37,20 @@ public class LiveResult<T> {
|
|||||||
return exception != null;
|
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 unblockNotification(GroupId g);
|
||||||
|
|
||||||
|
void blockAllForumPostNotifications();
|
||||||
|
|
||||||
|
void unblockAllForumPostNotifications();
|
||||||
|
|
||||||
|
void blockAllGroupMessageNotifications();
|
||||||
|
|
||||||
|
void unblockAllGroupMessageNotifications();
|
||||||
|
|
||||||
void blockAllBlogPostNotifications();
|
void blockAllBlogPostNotifications();
|
||||||
|
|
||||||
void unblockAllBlogPostNotifications();
|
void unblockAllBlogPostNotifications();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user