Compare commits

..

1 Commits

Author SHA1 Message Date
akwizgran
b0928089ec Deliver test messages as though they arrived from contacts. 2020-11-27 11:30:40 +00:00
134 changed files with 1937 additions and 3136 deletions

View File

@@ -1,15 +1,11 @@
package org.briarproject.bramble.network;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
import org.briarproject.bramble.api.event.EventBus;
@@ -23,11 +19,6 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.TaskScheduler;
import org.briarproject.bramble.api.system.TaskScheduler.Cancellable;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -45,22 +36,16 @@ import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.wifi.p2p.WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.list;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.util.LogUtils.logException;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class AndroidNetworkManager implements NetworkManager, Service {
private static final Logger LOG =
getLogger(AndroidNetworkManager.class.getName());
Logger.getLogger(AndroidNetworkManager.class.getName());
// See android.net.wifi.WifiManager
private static final String WIFI_AP_STATE_CHANGED_ACTION =
@@ -69,8 +54,7 @@ class AndroidNetworkManager implements NetworkManager, Service {
private final TaskScheduler scheduler;
private final EventBus eventBus;
private final Executor eventExecutor;
private final Application app;
private final ConnectivityManager connectivityManager;
private final Context appContext;
private final AtomicReference<Cancellable> connectivityCheck =
new AtomicReference<>();
private final AtomicBoolean used = new AtomicBoolean(false);
@@ -83,9 +67,7 @@ class AndroidNetworkManager implements NetworkManager, Service {
this.scheduler = scheduler;
this.eventBus = eventBus;
this.eventExecutor = eventExecutor;
this.app = app;
connectivityManager = (ConnectivityManager)
requireNonNull(app.getSystemService(CONNECTIVITY_SERVICE));
this.appContext = app.getApplicationContext();
}
@Override
@@ -100,82 +82,24 @@ class AndroidNetworkManager implements NetworkManager, Service {
filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
app.registerReceiver(networkStateReceiver, filter);
appContext.registerReceiver(networkStateReceiver, filter);
}
@Override
public void stopService() {
if (networkStateReceiver != null)
app.unregisterReceiver(networkStateReceiver);
appContext.unregisterReceiver(networkStateReceiver);
}
@Override
public NetworkStatus getNetworkStatus() {
NetworkInfo net = connectivityManager.getActiveNetworkInfo();
ConnectivityManager cm = (ConnectivityManager)
appContext.getSystemService(CONNECTIVITY_SERVICE);
if (cm == null) throw new AssertionError();
NetworkInfo net = cm.getActiveNetworkInfo();
boolean connected = net != null && net.isConnected();
boolean wifi = false, ipv6Only = false;
if (connected) {
wifi = net.getType() == TYPE_WIFI;
if (SDK_INT >= 23) ipv6Only = isActiveNetworkIpv6Only();
else ipv6Only = areAllAvailableNetworksIpv6Only();
}
return new NetworkStatus(connected, wifi, ipv6Only);
}
/**
* Returns true if the
* {@link ConnectivityManager#getActiveNetwork() active network} has an
* IPv6 unicast address and no IPv4 addresses. The active network is
* assumed not to be a loopback interface.
*/
@TargetApi(23)
private boolean isActiveNetworkIpv6Only() {
Network net = connectivityManager.getActiveNetwork();
if (net == null) {
LOG.info("No active network");
return false;
}
LinkProperties props = connectivityManager.getLinkProperties(net);
if (props == null) {
LOG.info("No link properties for active network");
return false;
}
boolean hasIpv6Unicast = false;
for (LinkAddress linkAddress : props.getLinkAddresses()) {
InetAddress addr = linkAddress.getAddress();
if (addr instanceof Inet4Address) return false;
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
}
return hasIpv6Unicast;
}
/**
* Returns true if the device has at least one network interface with an
* IPv6 unicast address and no interfaces with IPv4 addresses, excluding
* loopback interfaces and interfaces that are
* {@link NetworkInterface#isUp() down}. If this method returns true and
* the device has internet access then it's via IPv6 only.
*/
private boolean areAllAvailableNetworksIpv6Only() {
try {
Enumeration<NetworkInterface> interfaces = getNetworkInterfaces();
if (interfaces == null) {
LOG.info("No network interfaces");
return false;
}
boolean hasIpv6Unicast = false;
for (NetworkInterface i : list(interfaces)) {
if (i.isLoopback() || !i.isUp()) continue;
for (InetAddress addr : list(i.getInetAddresses())) {
if (addr instanceof Inet4Address) return false;
if (!addr.isMulticastAddress()) hasIpv6Unicast = true;
}
}
return hasIpv6Unicast;
} catch (SocketException e) {
logException(LOG, WARNING, e);
return false;
}
boolean wifi = connected && net.getType() == TYPE_WIFI;
return new NetworkStatus(connected, wifi);
}
private void updateConnectionStatus() {

View File

@@ -6,6 +6,8 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ContentResolver;
import android.content.Context;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Parcel;
import android.os.StrictMode;
@@ -15,10 +17,12 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.List;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import static android.content.Context.WIFI_SERVICE;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.Secure.ANDROID_ID;
@@ -48,6 +52,15 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
String id = Settings.Secure.getString(contentResolver, ANDROID_ID);
if (id != null) out.writeUTF(id);
Parcel parcel = Parcel.obtain();
WifiManager wm = (WifiManager) appContext.getApplicationContext()
.getSystemService(WIFI_SERVICE);
if (wm != null) {
List<WifiConfiguration> configs = wm.getConfiguredNetworks();
if (configs != null) {
for (WifiConfiguration config : configs)
parcel.writeParcelable(config, 0);
}
}
BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
if (bt != null) {
for (BluetoothDevice device : bt.getBondedDevices())

View File

@@ -8,12 +8,11 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault
public class NetworkStatus {
private final boolean connected, wifi, ipv6Only;
private final boolean connected, wifi;
public NetworkStatus(boolean connected, boolean wifi, boolean ipv6Only) {
public NetworkStatus(boolean connected, boolean wifi) {
this.connected = connected;
this.wifi = wifi;
this.ipv6Only = ipv6Only;
}
public boolean isConnected() {
@@ -23,8 +22,4 @@ public class NetworkStatus {
public boolean isWifi() {
return wifi;
}
public boolean isIpv6Only() {
return ipv6Only;
}
}

View File

@@ -23,8 +23,6 @@ public interface DevReporter {
/**
* Sends any reports previously stored on disk.
*
* @return The number of reports that were sent.
*/
int sendReports();
void sendReports();
}

View File

@@ -11,7 +11,6 @@ import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.util.Base32;
import java.security.GeneralSecurityException;
import java.util.Locale;
import java.util.regex.Matcher;
import javax.inject.Inject;
@@ -53,7 +52,7 @@ class PendingContactFactoryImpl implements PendingContactFactory {
byte[] raw = new byte[RAW_LINK_BYTES];
raw[0] = FORMAT_VERSION;
arraycopy(encoded, 0, raw, 1, encoded.length);
return "briar://" + Base32.encode(raw).toLowerCase(Locale.US);
return "briar://" + Base32.encode(raw).toLowerCase();
}
private PublicKey parseHandshakeLink(String link) throws FormatException {

View File

@@ -863,7 +863,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (!state.isTorRunning()) return;
boolean online = status.isConnected();
boolean wifi = status.isWifi();
boolean ipv6Only = status.isIpv6Only();
String country = locationUtils.getCurrentCountry();
boolean blocked =
circumventionProvider.isTorProbablyBlocked(country);
@@ -880,8 +879,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
boolean automatic = network == PREF_TOR_NETWORK_AUTOMATIC;
if (LOG.isLoggable(INFO)) {
LOG.info("Online: " + online + ", wifi: " + wifi
+ ", IPv6 only: " + ipv6Only);
LOG.info("Online: " + online + ", wifi: " + wifi);
if (country.isEmpty()) LOG.info("Country code unknown");
else LOG.info("Country code: " + country);
LOG.info("Charging: " + charging);
@@ -918,8 +916,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
enableNetwork = true;
if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
(automatic && bridgesWork)) {
if (ipv6Only ||
circumventionProvider.needsMeek(country)) {
if (circumventionProvider.needsMeek(country)) {
LOG.info("Using meek bridges");
enableBridges = true;
useMeek = true;
@@ -945,7 +942,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
if (enableNetwork) {
enableBridges(enableBridges, useMeek);
enableConnectionPadding(enableConnectionPadding);
useIpv6(ipv6Only);
}
enableNetwork(enableNetwork);
} catch (IOException e) {
@@ -958,11 +954,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setConf("ConnectionPadding", enable ? "1" : "0");
}
private void useIpv6(boolean ipv6Only) throws IOException {
controlConnection.setConf("ClientUseIPv4", ipv6Only ? "0" : "1");
controlConnection.setConf("ClientUseIPv6", ipv6Only ? "1" : "0");
}
@ThreadSafe
@NotNullByDefault
protected class PluginState {

View File

@@ -29,7 +29,6 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
import javax.net.SocketFactory;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -101,12 +100,11 @@ class DevReporterImpl implements DevReporter, EventListener {
}
@Override
public int sendReports() {
public void sendReports() {
File reportDir = devConfig.getReportDir();
File[] reports = reportDir.listFiles();
int reportsSent = 0;
if (reports == null || reports.length == 0)
return reportsSent; // No reports to send
return; // No reports to send
LOG.info("Sending reports to developers");
for (File f : reports) {
@@ -118,15 +116,13 @@ class DevReporterImpl implements DevReporter, EventListener {
in = new FileInputStream(f);
IoUtils.copyAndClose(in, out);
f.delete();
reportsSent++;
} catch (IOException e) {
LOG.log(WARNING, "Failed to send reports", e);
tryToClose(out, LOG, WARNING);
tryToClose(in, LOG, WARNING);
return reportsSent;
return;
}
}
if (LOG.isLoggable(INFO)) LOG.info(reportsSent + " report(s) sent");
return reportsSent;
LOG.info("Reports sent");
}
}

View File

@@ -5,8 +5,6 @@ import org.briarproject.bramble.api.network.NetworkStatus;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
@@ -16,8 +14,8 @@ import javax.inject.Inject;
import static java.net.NetworkInterface.getNetworkInterfaces;
import static java.util.Collections.list;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@MethodsNotNullByDefault
@@ -25,7 +23,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
class JavaNetworkManager implements NetworkManager {
private static final Logger LOG =
getLogger(JavaNetworkManager.class.getName());
Logger.getLogger(JavaNetworkManager.class.getName());
@Inject
JavaNetworkManager() {
@@ -33,28 +31,26 @@ class JavaNetworkManager implements NetworkManager {
@Override
public NetworkStatus getNetworkStatus() {
boolean connected = false, hasIpv4 = false, hasIpv6Unicast = false;
boolean connected = false;
try {
Enumeration<NetworkInterface> interfaces = getNetworkInterfaces();
if (interfaces == null) {
LOG.info("No network interfaces");
} else {
if (interfaces != null) {
for (NetworkInterface i : list(interfaces)) {
if (i.isLoopback() || !i.isUp()) continue;
for (InetAddress addr : list(i.getInetAddresses())) {
connected = true;
if (addr instanceof Inet4Address) {
hasIpv4 = true;
} else if (!addr.isMulticastAddress()) {
hasIpv6Unicast = true;
if (i.isLoopback()) continue;
if (i.isUp() && i.getInetAddresses().hasMoreElements()) {
if (LOG.isLoggable(INFO)) {
LOG.info("Interface " + i.getDisplayName() +
" is up with at least one address.");
}
connected = true;
break;
}
}
}
} catch (SocketException e) {
logException(LOG, WARNING, e);
}
return new NetworkStatus(connected, false, !hasIpv4 && hasIpv6Unicast);
return new NetworkStatus(connected, false);
}
}

View File

@@ -100,6 +100,7 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc03'
implementation 'ch.acra:acra:4.11'
implementation 'info.guardianproject.panic:panic:1.0'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'de.hdodenhof:circleimageview:3.0.1'
@@ -126,11 +127,10 @@ dependencies {
testImplementation 'androidx.test:runner:1.3.0'
testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation 'androidx.fragment:fragment-testing:1.2.5'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'org.mockito:mockito-core:3.1.0'
testImplementation 'junit:junit:4.13.1'
testImplementation 'junit:junit:4.12'
testImplementation "org.jmock:jmock:$jmockVersion"
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
testImplementation "org.jmock:jmock-legacy:$jmockVersion"

View File

@@ -65,43 +65,30 @@
<service
android:name="org.briarproject.briar.android.NotificationCleanupService"
android:exported="false" />
android:exported="false"></service>
<activity
android:name="org.briarproject.briar.android.reporting.CrashReportActivity"
android:name="org.briarproject.briar.android.reporting.DevReportActivity"
android:excludeFromRecents="true"
android:exported="false"
android:finishOnTaskLaunch="true"
android:label="@string/crash_report_title"
android:launchMode="singleInstance"
android:process=":briar_error_handler"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android: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>
android:windowSoftInputMode="adjustResize|stateHidden"></activity>
<activity
android:name="org.briarproject.briar.android.splash.ExpiredActivity"
android:label="@string/app_name" />
android:label="@string/app_name"></activity>
<activity
android:name="org.briarproject.briar.android.login.StartupActivity"
android:label="@string/app_name" />
android:label="@string/app_name"></activity>
<activity
android:name="org.briarproject.briar.android.account.SetupActivity"
android:label="@string/setup_title"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" />
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"></activity>
<activity
android:name="org.briarproject.briar.android.splash.SplashScreenActivity"
@@ -359,7 +346,7 @@
<activity
android:name="org.briarproject.briar.android.StartupFailureActivity"
android:label="@string/startup_failed_activity_title" />
android:label="@string/startup_failed_activity_title"></activity>
<activity
android:name="org.briarproject.briar.android.settings.SettingsActivity"
@@ -425,11 +412,11 @@
<activity
android:name="org.briarproject.briar.android.logout.ExitActivity"
android:theme="@android:style/Theme.NoDisplay" />
android:theme="@android:style/Theme.NoDisplay"></activity>
<activity
android:name=".android.logout.HideUiActivity"
android:theme="@android:style/Theme.NoDisplay" />
android:theme="@android:style/Theme.NoDisplay"></activity>
<activity
android:name=".android.account.UnlockActivity"
@@ -449,27 +436,4 @@
android:theme="@style/BriarTheme" />
</application>
<queries>
<package android:name="info.guardianproject.ripple" />
<package android:name="com.huawei.systemmanager" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<!-- white-listing the intents below does not seem necessary,
but they are still included in case modified Android versions require it -->
<intent>
<action android:name="android.bluetooth.adapter.action.REQUEST_DISCOVERABLE" />
</intent>
<intent>
<action android:name="android.settings.CHANNEL_NOTIFICATION_SETTINGS" />
</intent>
</queries>
</manifest>

View File

@@ -14,7 +14,6 @@ import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
@@ -34,6 +33,7 @@ import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
import org.briarproject.briar.android.login.SignInReminderReceiver;
import org.briarproject.briar.android.reporting.BriarReportSender;
import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog;
@@ -86,8 +86,6 @@ public interface AndroidComponent
@DatabaseExecutor
Executor databaseExecutor();
TransactionManager transactionManager();
MessageTracker messageTracker();
LifecycleManager lifecycleManager();
@@ -175,6 +173,8 @@ public interface AndroidComponent
void inject(BriarService briarService);
void inject(BriarReportSender briarReportSender);
void inject(NotificationCleanupService notificationCleanupService);
void inject(EmojiTextInputView textInputView);

View File

@@ -109,8 +109,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@Nullable
private GroupId blockedGroup = null;
private boolean blockSignInReminder = false;
private boolean blockForums = false, blockGroups = false,
blockBlogs = false;
private boolean blockBlogs = false;
private long lastSound = 0;
private volatile Settings settings = new Settings();
@@ -224,8 +223,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
if (s.getNamespace().equals(SETTINGS_NAMESPACE))
settings = s.getSettings();
} else if (e instanceof ConversationMessageReceivedEvent) {
ConversationMessageReceivedEvent<?> p =
(ConversationMessageReceivedEvent<?>) e;
ConversationMessageReceivedEvent p =
(ConversationMessageReceivedEvent) e;
showContactNotification(p.getContactId());
} else if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
@@ -386,7 +385,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@UiThread
private void showGroupMessageNotification(GroupId g) {
if (blockGroups) return;
if (g.equals(blockedGroup)) return;
groupCounts.add(g);
updateGroupMessageNotification(true);
@@ -454,7 +452,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@UiThread
private void showForumPostNotification(GroupId g) {
if (blockForums) return;
if (g.equals(blockedGroup)) return;
forumCounts.add(g);
updateForumPostNotification(true);
@@ -684,26 +681,6 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
});
}
@Override
public void blockAllForumPostNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockForums = true);
}
@Override
public void unblockAllForumPostNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockForums = false);
}
@Override
public void blockAllGroupMessageNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockGroups = true);
}
@Override
public void unblockAllGroupMessageNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockGroups = false);
}
@Override
public void blockAllBlogPostNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = true);

View File

@@ -28,12 +28,9 @@ import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory;
import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.reporting.DevReportModule;
import org.briarproject.briar.android.viewmodel.ViewModelModule;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog;
@@ -66,11 +63,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
ContactExchangeModule.class,
LoginModule.class,
NavDrawerModule.class,
ViewModelModule.class,
DevReportModule.class,
// below need to be within same scope as ViewModelProvider.Factory
ForumModule.BindsModule.class,
GroupListModule.class,
ViewModelModule.class
})
public class AppModule {
@@ -203,10 +196,7 @@ public class AppModule {
ScreenFilterMonitor provideScreenFilterMonitor(
LifecycleManager lifecycleManager,
ScreenFilterMonitorImpl screenFilterMonitor) {
if (SDK_INT <= 29) {
// this keeps track of installed apps and does not work on API 30+
lifecycleManager.registerService(screenFilterMonitor);
}
lifecycleManager.registerService(screenFilterMonitor);
return screenFilterMonitor;
}

View File

@@ -14,13 +14,19 @@ import android.preference.PreferenceManager;
import com.vanniktech.emoji.EmojiManager;
import com.vanniktech.emoji.google.GoogleEmojiProvider;
import org.acra.ACRA;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import org.briarproject.bramble.BrambleAndroidEagerSingletons;
import org.briarproject.bramble.BrambleAppComponent;
import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.R;
import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.reporting.BriarExceptionHandler;
import org.briarproject.briar.android.reporting.BriarReportPrimer;
import org.briarproject.briar.android.reporting.BriarReportSenderFactory;
import org.briarproject.briar.android.reporting.DevReportActivity;
import org.briarproject.briar.android.util.UiUtils;
import java.util.Collection;
@@ -28,14 +34,50 @@ import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import androidx.annotation.NonNull;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.BRAND;
import static org.acra.ReportField.BUILD_CONFIG;
import static org.acra.ReportField.CRASH_CONFIGURATION;
import static org.acra.ReportField.CUSTOM_DATA;
import static org.acra.ReportField.DEVICE_FEATURES;
import static org.acra.ReportField.DISPLAY;
import static org.acra.ReportField.INITIAL_CONFIGURATION;
import static org.acra.ReportField.PACKAGE_NAME;
import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.PRODUCT;
import static org.acra.ReportField.REPORT_ID;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_APP_START_DATE;
import static org.acra.ReportField.USER_CRASH_DATE;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
@ReportsCrashes(
reportPrimerClass = BriarReportPrimer.class,
logcatArguments = {"-d", "-v", "time", "*:I"},
reportSenderFactoryClasses = {BriarReportSenderFactory.class},
mode = ReportingInteractionMode.DIALOG,
reportDialogClass = DevReportActivity.class,
resDialogOkToast = R.string.dev_report_saved,
deleteOldUnsentReportsOnApplicationStart = false,
buildConfigClass = BuildConfig.class,
customReportContent = {
REPORT_ID,
APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME,
PHONE_MODEL, ANDROID_VERSION, BRAND, PRODUCT,
BUILD_CONFIG,
CUSTOM_DATA,
STACK_TRACE,
INITIAL_CONFIGURATION, CRASH_CONFIGURATION,
DISPLAY, DEVICE_FEATURES,
USER_APP_START_DATE, USER_CRASH_DATE
}
)
public class BriarApplicationImpl extends Application
implements BriarApplication {
@@ -43,15 +85,12 @@ public class BriarApplicationImpl extends Application
getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler();
private final BriarExceptionHandler exceptionHandler =
new BriarExceptionHandler(this);
private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs;
@Override
protected void attachBaseContext(Context base) {
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
if (prefs == null)
prefs = PreferenceManager.getDefaultSharedPreferences(base);
// Loading the language needs to be done here.
@@ -59,6 +98,7 @@ public class BriarApplicationImpl extends Application
super.attachBaseContext(
Localizer.getInstance().setLocale(base));
setTheme(base, prefs);
ACRA.init(this);
}
@Override
@@ -104,7 +144,7 @@ public class BriarApplicationImpl extends Application
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Localizer.getInstance().setLocale(this);
}

View File

@@ -58,7 +58,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
Logger.getLogger(ScreenFilterMonitorImpl.class.getName());
/*
* Ignore Play Services if it uses this package name and public key - it's
* Ignore Play Services if it uses this package name and public key - it's
* effectively a system app, but not flagged as such on older systems
*/
private static final String PLAY_SERVICES_PACKAGE =
@@ -108,7 +108,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service {
Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED,
Collections.emptySet());
List<AppDetails> apps = new ArrayList<>();
@SuppressLint("QueryPermissionsNeeded") List<PackageInfo> packageInfos =
List<PackageInfo> packageInfos =
pm.getInstalledPackages(GET_PERMISSIONS);
for (PackageInfo packageInfo : packageInfos) {
if (!allowed.contains(packageInfo.packageName)

View File

@@ -60,14 +60,12 @@ import org.briarproject.briar.android.privategroup.creation.GroupInviteFragment;
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity;
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationModule;
import org.briarproject.briar.android.privategroup.list.GroupListFragment;
import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity;
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberModule;
import org.briarproject.briar.android.privategroup.reveal.GroupRevealModule;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsFragment;
import org.briarproject.briar.android.reporting.CrashFragment;
import org.briarproject.briar.android.reporting.CrashReportActivity;
import org.briarproject.briar.android.reporting.ReportFormFragment;
import org.briarproject.briar.android.settings.SettingsActivity;
import org.briarproject.briar.android.settings.SettingsFragment;
import org.briarproject.briar.android.sharing.BlogInvitationActivity;
@@ -93,6 +91,7 @@ import dagger.Component;
ForumModule.class,
GroupInvitationModule.class,
GroupConversationModule.class,
GroupListModule.class,
GroupMemberModule.class,
GroupRevealModule.class,
SharingModule.class
@@ -185,8 +184,6 @@ public interface ActivityComponent {
void inject(PendingContactListActivity activity);
void inject(CrashReportActivity crashReportActivity);
// Fragments
void inject(AuthorNameFragment fragment);
@@ -237,8 +234,4 @@ public interface ActivityComponent {
void inject(ImageFragment imageFragment);
void inject(ReportFormFragment reportFormFragment);
void inject(CrashFragment crashFragment);
}

View File

@@ -6,6 +6,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
@@ -17,6 +18,7 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.reporting.DevReportActivity;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.widget.TapSafeFrameLayout;
import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener;
@@ -38,11 +40,9 @@ import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import static android.os.Build.VERSION.SDK_INT;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static androidx.lifecycle.Lifecycle.State.STARTED;
import static java.util.Collections.emptyList;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
@@ -50,6 +50,7 @@ import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
/**
* Warning: Some activities don't extend {@link BaseActivity}.
* E.g. {@link DevReportActivity}
*/
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -122,7 +123,6 @@ public abstract class BaseActivity extends AppCompatActivity
return new ActivityModule(this);
}
// TODO use a test module where this is used in tests
protected ForumModule getForumModule() {
return new ForumModule();
}
@@ -202,15 +202,9 @@ public abstract class BaseActivity extends AppCompatActivity
// If the dialog is already visible, filter the tap
ScreenFilterDialogFragment f = findDialogFragment();
if (f != null && f.isVisible()) return false;
Collection<AppDetails> apps;
// querying all apps is only possible at API 29 and below
if (SDK_INT <= 29) {
apps = screenFilterMonitor.getApps();
// If all overlay apps have been allowed, allow the tap
if (apps.isEmpty()) return true;
} else {
apps = emptyList();
}
Collection<AppDetails> apps = screenFilterMonitor.getApps();
// If all overlay apps have been allowed, allow the tap
if (apps.isEmpty()) return true;
// Show dialog unless onSaveInstanceState() has been called, see #1112
FragmentManager fm = getSupportFragmentManager();
if (!fm.isStateSaved()) {
@@ -247,7 +241,7 @@ public abstract class BaseActivity extends AppCompatActivity
}
@UiThread
public void handleException(Exception e) {
public void handleDbException(DbException e) {
supportFinishAfterTransition();
}
@@ -272,12 +266,7 @@ public abstract class BaseActivity extends AppCompatActivity
private void protectToolbar() {
findToolbar();
if (toolbar != null) {
boolean filter;
if (SDK_INT <= 29) {
filter = !screenFilterMonitor.getApps().isEmpty();
} else {
filter = true;
}
boolean filter = !screenFilterMonitor.getApps().isEmpty();
UiUtils.setFilterTouchesWhenObscured(toolbar, filter);
}
}

View File

@@ -129,6 +129,10 @@ public abstract class BriarActivity extends BaseActivity {
lockManager.onActivityStop();
}
protected boolean signedIn() {
return briarController.accountSignedIn();
}
/**
* Sets the transition animations.
*

View File

@@ -232,7 +232,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
}
);
@@ -277,7 +277,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -296,7 +296,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -318,7 +318,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -398,7 +398,7 @@ public class BlogFragment extends BaseFragment
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -58,7 +58,7 @@ public class BlogPostFragment extends BasePostFragment implements BlogListener {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -156,7 +156,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -187,7 +187,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -242,7 +242,7 @@ public class FeedFragment extends BaseFragment implements
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
}
);

View File

@@ -79,7 +79,7 @@ public class FeedPostFragment extends BasePostFragment {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -101,7 +101,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
@@ -128,7 +128,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
finish();

View File

@@ -9,11 +9,7 @@ import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchPendingContactException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
@@ -25,6 +21,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -34,12 +31,14 @@ import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_R
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class AddContactViewModel extends DbViewModel {
public class AddContactViewModel extends AndroidViewModel {
private final static Logger LOG =
getLogger(AddContactViewModel.class.getName());
private final ContactManager contactManager;
@DatabaseExecutor
private final Executor dbExecutor;
private final MutableLiveData<String> handshakeLink =
new MutableLiveData<>();
@@ -53,12 +52,10 @@ public class AddContactViewModel extends DbViewModel {
@Inject
AddContactViewModel(Application application,
ContactManager contactManager,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
@DatabaseExecutor Executor dbExecutor) {
super(application);
this.contactManager = contactManager;
this.dbExecutor = dbExecutor;
}
void onCreate() {
@@ -66,7 +63,7 @@ public class AddContactViewModel extends DbViewModel {
}
private void loadHandshakeLink() {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
handshakeLink.postValue(contactManager.getHandshakeLink());
} catch (DbException e) {
@@ -105,7 +102,7 @@ public class AddContactViewModel extends DbViewModel {
void addContact(String nickname) {
if (remoteHandshakeLink == null) throw new IllegalStateException();
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
contactManager.addPendingContact(remoteHandshakeLink, nickname);
addContactResult.postValue(new LiveResult<>(true));
@@ -125,11 +122,11 @@ public class AddContactViewModel extends DbViewModel {
}
public void updatePendingContact(String name, PendingContact p) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
contactManager.removePendingContact(p.getId());
addContact(name);
} catch (NoSuchPendingContactException e) {
} catch(NoSuchPendingContactException e) {
logException(LOG, WARNING, e);
// no error in UI as pending contact was converted into contact
} catch (DbException e) {

View File

@@ -11,16 +11,12 @@ import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.rendezvous.RendezvousPoller;
import org.briarproject.bramble.api.rendezvous.event.RendezvousPollEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.ArrayList;
import java.util.Collection;
@@ -30,6 +26,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -39,12 +36,14 @@ import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class PendingContactListViewModel extends DbViewModel
public class PendingContactListViewModel extends AndroidViewModel
implements EventListener {
private final Logger LOG =
getLogger(PendingContactListViewModel.class.getName());
@DatabaseExecutor
private final Executor dbExecutor;
private final ContactManager contactManager;
private final RendezvousPoller rendezvousPoller;
private final EventBus eventBus;
@@ -57,13 +56,11 @@ public class PendingContactListViewModel extends DbViewModel
@Inject
PendingContactListViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
ContactManager contactManager,
RendezvousPoller rendezvousPoller,
EventBus eventBus) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
super(application);
this.dbExecutor = dbExecutor;
this.contactManager = contactManager;
this.rendezvousPoller = rendezvousPoller;
this.eventBus = eventBus;
@@ -90,7 +87,7 @@ public class PendingContactListViewModel extends DbViewModel
}
private void loadPendingContacts() {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts();
@@ -116,7 +113,7 @@ public class PendingContactListViewModel extends DbViewModel
}
void removePendingContact(PendingContactId id) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
contactManager.removePendingContact(id);
} catch (DbException e) {

View File

@@ -133,7 +133,7 @@ public abstract class BaseContactSelectorFragment<I extends SelectableContactIte
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.controller;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@Deprecated
@NotNullByDefault
public interface DbController {

View File

@@ -11,7 +11,6 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
@Immutable
@Deprecated
@NotNullByDefault
public class DbControllerImpl implements DbController {

View File

@@ -98,6 +98,7 @@ import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.selection.Selection;
import androidx.recyclerview.selection.SelectionPredicates;
import androidx.recyclerview.selection.SelectionTracker;
@@ -223,9 +224,8 @@ public class ConversationActivity extends BriarActivity
if (id == -1) throw new IllegalStateException();
contactId = new ContactId(id);
viewModel = new ViewModelProvider(this, viewModelFactory)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ConversationViewModel.class);
viewModel.setContactId(contactId);
attachmentRetriever = viewModel.getAttachmentRetriever();
setContentView(R.layout.activity_conversation);
@@ -330,6 +330,16 @@ public class ConversationActivity extends BriarActivity
list.startPeriodicUpdate();
}
@Override
public void onResume() {
super.onResume();
// Trigger loading of contact data, noop if data was loaded already.
//
// We can only start loading data *after* we are sure
// the user has signed in. After sign-in, onCreate() isn't run again.
if (signedIn()) viewModel.setContactId(contactId);
}
@Override
public void onStop() {
super.onStop();

View File

@@ -15,20 +15,17 @@ import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.attachment.AttachmentCreator;
import org.briarproject.briar.android.attachment.AttachmentManager;
import org.briarproject.briar.android.attachment.AttachmentResult;
import org.briarproject.briar.android.attachment.AttachmentRetriever;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.AttachmentHeader;
@@ -47,6 +44,7 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
@@ -61,10 +59,10 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault
public class ConversationViewModel extends DbViewModel
public class ConversationViewModel extends AndroidViewModel
implements EventListener, AttachmentManager {
private static final Logger LOG =
private static Logger LOG =
getLogger(ConversationViewModel.class.getName());
private static final String SHOW_ONBOARDING_IMAGE =
@@ -72,6 +70,8 @@ public class ConversationViewModel extends DbViewModel
private static final String SHOW_ONBOARDING_INTRODUCTION =
"showOnboardingIntroduction";
@DatabaseExecutor
private final Executor dbExecutor;
private final TransactionManager db;
private final EventBus eventBus;
private final MessagingManager messagingManager;
@@ -105,9 +105,7 @@ public class ConversationViewModel extends DbViewModel
@Inject
ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
MessagingManager messagingManager,
ContactManager contactManager,
@@ -115,7 +113,8 @@ public class ConversationViewModel extends DbViewModel
PrivateMessageFactory privateMessageFactory,
AttachmentRetriever attachmentRetriever,
AttachmentCreator attachmentCreator) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
super(application);
this.dbExecutor = dbExecutor;
this.db = db;
this.eventBus = eventBus;
this.messagingManager = messagingManager;
@@ -144,7 +143,7 @@ public class ConversationViewModel extends DbViewModel
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
runOnDbThread(() -> attachmentRetriever
dbExecutor.execute(() -> attachmentRetriever
.loadAttachmentItem(a.getMessageId()));
}
}
@@ -164,7 +163,7 @@ public class ConversationViewModel extends DbViewModel
}
private void loadContact(ContactId contactId) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
long start = now();
Contact c = contactManager.getContact(contactId);
@@ -182,7 +181,7 @@ public class ConversationViewModel extends DbViewModel
}
void markMessageRead(GroupId g, MessageId m) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
long start = now();
messagingManager.setReadFlag(g, m, true);
@@ -194,7 +193,7 @@ public class ConversationViewModel extends DbViewModel
}
void setContactAlias(String alias) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
contactManager.setContactAlias(requireNonNull(contactId),
alias.isEmpty() ? null : alias);
@@ -297,7 +296,7 @@ public class ConversationViewModel extends DbViewModel
@UiThread
private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
@@ -357,7 +356,7 @@ public class ConversationViewModel extends DbViewModel
@UiThread
void recheckFeaturesAndOnboarding(ContactId contactId) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
checkFeaturesAndOnboarding(contactId);
} catch (DbException e) {

View File

@@ -7,17 +7,13 @@ import android.view.View;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.attachment.AttachmentItem;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.messaging.Attachment;
@@ -41,6 +37,7 @@ import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.lifecycle.AndroidViewModel;
import static android.media.MediaScannerConnection.scanFile;
import static android.os.Environment.DIRECTORY_PICTURES;
@@ -52,18 +49,20 @@ import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class ImageViewModel extends DbViewModel implements EventListener {
public class ImageViewModel extends AndroidViewModel implements EventListener {
private static final Logger LOG = getLogger(ImageViewModel.class.getName());
private static Logger LOG = getLogger(ImageViewModel.class.getName());
private final MessagingManager messagingManager;
private final EventBus eventBus;
@DatabaseExecutor
private final Executor dbExecutor;
@IoExecutor
private final Executor ioExecutor;
private boolean receivedAttachmentsInitialized = false;
private final HashMap<MessageId, MutableLiveEvent<Boolean>>
receivedAttachments = new HashMap<>();
private HashMap<MessageId, MutableLiveEvent<Boolean>> receivedAttachments =
new HashMap<>();
/**
* true means there was an error saving the image, false if image was saved.
@@ -76,16 +75,13 @@ public class ImageViewModel extends DbViewModel implements EventListener {
@Inject
ImageViewModel(Application application,
MessagingManager messagingManager,
EventBus eventBus,
MessagingManager messagingManager, EventBus eventBus,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
@IoExecutor Executor ioExecutor) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
super(application);
this.messagingManager = messagingManager;
this.eventBus = eventBus;
this.dbExecutor = dbExecutor;
this.ioExecutor = ioExecutor;
eventBus.addListener(this);
@@ -199,7 +195,7 @@ public class ImageViewModel extends DbViewModel implements EventListener {
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
Attachment a =
messagingManager.getAttachment(attachment.getHeader());

View File

@@ -38,7 +38,7 @@ import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEX
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ForumActivity extends
ThreadListActivity<Forum, ForumPostItem, ThreadItemAdapter<ForumPostItem>>
ThreadListActivity<Forum, ForumItem, ThreadItemAdapter<ForumItem>>
implements ForumListener {
@Inject
@@ -50,7 +50,7 @@ public class ForumActivity extends
}
@Override
protected ThreadListController<Forum, ForumPostItem> getController() {
protected ThreadListController<Forum, ForumItem> getController() {
return forumController;
}
@@ -82,7 +82,7 @@ public class ForumActivity extends
}
@Override
protected ThreadItemAdapter<ForumPostItem> createAdapter(
protected ThreadItemAdapter<ForumItem> createAdapter(
LinearLayoutManager layoutManager) {
return new ThreadItemAdapter<>(this, layoutManager);
}
@@ -156,7 +156,7 @@ public class ForumActivity extends
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -8,9 +8,9 @@ import org.briarproject.briar.api.forum.Forum;
import androidx.annotation.UiThread;
@NotNullByDefault
interface ForumController extends ThreadListController<Forum, ForumPostItem> {
interface ForumController extends ThreadListController<Forum, ForumItem> {
interface ForumListener extends ThreadListListener<ForumPostItem> {
interface ForumListener extends ThreadListListener<ForumItem> {
@UiThread
void onForumLeft(ContactId c);
}

View File

@@ -43,7 +43,7 @@ import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ForumControllerImpl extends
ThreadListControllerImpl<Forum, ForumPostItem, ForumPostHeader, ForumPost, ForumListener>
ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost, ForumListener>
implements ForumController {
private static final Logger LOG =
@@ -138,8 +138,8 @@ class ForumControllerImpl extends
@Override
public void createAndStoreMessage(String text,
@Nullable ForumPostItem parentItem,
ResultExceptionHandler<ForumPostItem, DbException> handler) {
@Nullable ForumItem parentItem,
ResultExceptionHandler<ForumItem, DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor author = identityManager.getLocalAuthor();
@@ -158,7 +158,7 @@ class ForumControllerImpl extends
private void createMessage(String text, long timestamp,
@Nullable MessageId parentId, LocalAuthor author,
ResultExceptionHandler<ForumPostItem, DbException> handler) {
ResultExceptionHandler<ForumItem, DbException> handler) {
cryptoExecutor.execute(() -> {
LOG.info("Creating forum post...");
ForumPost msg = forumManager.createLocalPost(getGroupId(), text,
@@ -178,8 +178,8 @@ class ForumControllerImpl extends
}
@Override
protected ForumPostItem buildItem(ForumPostHeader header, String text) {
return new ForumPostItem(header, text);
protected ForumItem buildItem(ForumPostHeader header, String text) {
return new ForumItem(header, text);
}
}

View File

@@ -10,15 +10,15 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
class ForumPostItem extends ThreadItem {
class ForumItem extends ThreadItem {
ForumPostItem(ForumPostHeader h, String text) {
ForumItem(ForumPostHeader h, String text) {
super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
h.getAuthorInfo(), h.isRead());
}
ForumPostItem(MessageId messageId, @Nullable MessageId parentId,
String text, long timestamp, Author author, AuthorInfo authorInfo) {
ForumItem(MessageId messageId, @Nullable MessageId parentId, String text,
long timestamp, Author author, AuthorInfo authorInfo) {
super(messageId, parentId, text, timestamp, author, authorInfo, true);
}

View File

@@ -1,47 +1,134 @@
package org.briarproject.briar.android.forum;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
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.DiffUtil.ItemCallback;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
@NotNullByDefault
class ForumListAdapter extends ListAdapter<ForumListItem, ForumViewHolder> {
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static androidx.recyclerview.widget.SortedList.INVALID_POSITION;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_NAME;
ForumListAdapter() {
super(new ForumListCallback());
class ForumListAdapter
extends BriarAdapter<ForumListItem, ForumListAdapter.ForumViewHolder> {
ForumListAdapter(Context ctx) {
super(ctx, ForumListItem.class);
}
@Override
public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(
View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_forum, parent, false);
return new ForumViewHolder(v);
}
@Override
public void onBindViewHolder(ForumViewHolder viewHolder, int position) {
viewHolder.bind(getItem(position));
}
public void onBindViewHolder(ForumViewHolder ui, int position) {
ForumListItem item = getItemAt(position);
if (item == null) return;
@NotNullByDefault
private static class ForumListCallback extends ItemCallback<ForumListItem> {
@Override
public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
return a.equals(b);
// Avatar
ui.avatar.setText(item.getForum().getName().substring(0, 1));
ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes());
ui.avatar.setUnreadCount(item.getUnreadCount());
// Forum Name
ui.name.setText(item.getForum().getName());
// Post Count
int postCount = item.getPostCount();
if (postCount > 0) {
ui.postCount.setText(ctx.getResources()
.getQuantityString(R.plurals.posts, postCount,
postCount));
} else {
ui.postCount.setText(ctx.getString(R.string.no_posts));
}
@Override
public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
return a.isEmpty() == b.isEmpty() &&
a.getTimestamp() == b.getTimestamp() &&
a.getUnreadCount() == b.getUnreadCount();
// Date
if (item.isEmpty()) {
ui.date.setVisibility(GONE);
} else {
long timestamp = item.getTimestamp();
ui.date.setText(UiUtils.formatDate(ctx, timestamp));
ui.date.setVisibility(VISIBLE);
}
// Open Forum on Click
ui.layout.setOnClickListener(v -> {
Intent i = new Intent(ctx, ForumActivity.class);
Forum f = item.getForum();
i.putExtra(GROUP_ID, f.getId().getBytes());
i.putExtra(GROUP_NAME, f.getName());
ctx.startActivity(i);
});
}
@Override
public int compare(ForumListItem a, ForumListItem b) {
if (a == b) return 0;
// The forum with the newest message comes first
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by forum name
String aName = a.getForum().getName();
String bName = b.getForum().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
@Override
public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
return a.isEmpty() == b.isEmpty() &&
a.getTimestamp() == b.getTimestamp() &&
a.getUnreadCount() == b.getUnreadCount();
}
@Override
public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
return a.getForum().equals(b.getForum());
}
int findItemPosition(GroupId g) {
int count = getItemCount();
for (int i = 0; i < count; i++) {
ForumListItem item = getItemAt(i);
if (item != null && item.getForum().getGroup().getId().equals(g))
return i;
}
return INVALID_POSITION; // Not found
}
static class ForumViewHolder extends RecyclerView.ViewHolder {
private final ViewGroup layout;
private final TextAvatarView avatar;
private final TextView name;
private final TextView postCount;
private final TextView date;
private ForumViewHolder(View v) {
super(v);
layout = (ViewGroup) v;
avatar = v.findViewById(R.id.avatarView);
name = v.findViewById(R.id.forumNameView);
postCount = v.findViewById(R.id.postCountView);
date = v.findViewById(R.id.dateView);
}
}
}

View File

@@ -12,48 +12,80 @@ import android.view.ViewGroup;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.fragment.BaseEventFragment;
import org.briarproject.briar.android.sharing.ForumInvitationActivity;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumManager;
import org.briarproject.briar.api.forum.ForumPostHeader;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.forum.event.ForumInvitationRequestReceivedEvent;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
import androidx.annotation.UiThread;
import androidx.recyclerview.widget.LinearLayoutManager;
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.forum.ForumManager.CLIENT_ID;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ForumListFragment extends BaseFragment implements
public class ForumListFragment extends BaseEventFragment implements
OnClickListener {
public final static String TAG = ForumListFragment.class.getName();
private final static Logger LOG = Logger.getLogger(TAG);
private ForumListViewModel viewModel;
private BriarRecyclerView list;
private ForumListAdapter adapter;
private Snackbar snackbar;
private final ForumListAdapter adapter = new ForumListAdapter();
@Inject
ViewModelProvider.Factory viewModelFactory;
AndroidNotificationManager notificationManager;
// Fields that are accessed from background threads must be volatile
@Inject
volatile ForumManager forumManager;
@Inject
volatile ForumSharingManager forumSharingManager;
public static ForumListFragment newInstance() {
return new ForumListFragment();
Bundle args = new Bundle();
ForumListFragment fragment = new ForumListFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(ForumListViewModel.class);
}
@Nullable
@@ -61,35 +93,24 @@ public class ForumListFragment extends BaseFragment implements
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireActivity().setTitle(R.string.forums_button);
View v = inflater.inflate(R.layout.fragment_forum_list, container,
false);
View contentView =
inflater.inflate(R.layout.fragment_forum_list, container,
false);
list = v.findViewById(R.id.forumList);
adapter = new ForumListAdapter(getActivity());
list = contentView.findViewById(R.id.forumList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
viewModel.getForumListItems().observe(getViewLifecycleOwner(), result ->
result.onError(this::handleException).onSuccess(items -> {
adapter.submitList(items);
if (requireNonNull(items).size() == 0) list.showData();
})
);
snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, this)
.make(list, "", LENGTH_INDEFINITE);
viewModel.getNumInvitations().observe(getViewLifecycleOwner(), num -> {
if (num == 0) {
snackbar.dismiss();
} else {
snackbar.setText(getResources().getQuantityString(
R.plurals.forums_shared, num, num));
if (!snackbar.isShownOrQueued()) snackbar.show();
}
});
return v;
return contentView;
}
@Override
@@ -100,23 +121,18 @@ public class ForumListFragment extends BaseFragment implements
@Override
public void onStart() {
super.onStart();
viewModel.blockAllForumPostNotifications();
viewModel.clearAllForumPostNotifications();
// The attributes and sorting of the forums may have changed while we
// were stopped and we have no way finding out about them, so re-load
// e.g. less unread posts in a forum after viewing it.
viewModel.loadForums();
// The number of invitations might have changed while we were stopped
// e.g. because of accepting an invitation which does not trigger event
viewModel.loadForumInvitations();
notificationManager.clearAllForumPostNotifications();
loadForums();
loadAvailableForums();
list.startPeriodicUpdate();
}
@Override
public void onStop() {
super.onStop();
adapter.clear();
list.showProgressBar();
list.stopPeriodicUpdate();
viewModel.unblockAllForumPostNotifications();
}
@Override
@@ -128,12 +144,123 @@ public class ForumListFragment extends BaseFragment implements
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle presses on the action bar items
if (item.getItemId() == R.id.action_create_forum) {
Intent intent = new Intent(getContext(), CreateForumActivity.class);
startActivity(intent);
return true;
switch (item.getItemId()) {
case R.id.action_create_forum:
Intent intent =
new Intent(getContext(), CreateForumActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
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

View File

@@ -4,16 +4,12 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumPostHeader;
import javax.annotation.concurrent.Immutable;
import androidx.annotation.Nullable;
@Immutable
class ForumListItem implements Comparable<ForumListItem> {
// This class is NOT thread-safe
class ForumListItem {
private final Forum forum;
private final int postCount, unread;
private final long timestamp;
private int postCount, unread;
private long timestamp;
ForumListItem(Forum forum, GroupCount count) {
this.forum = forum;
@@ -22,11 +18,10 @@ class ForumListItem implements Comparable<ForumListItem> {
this.timestamp = count.getLatestMsgTime();
}
ForumListItem(ForumListItem item, ForumPostHeader h) {
this.forum = item.forum;
this.postCount = item.postCount + 1;
this.unread = item.unread + (h.isRead() ? 0 : 1);
this.timestamp = Math.max(item.timestamp, h.getTimestamp());
void addHeader(ForumPostHeader h) {
postCount++;
if (!h.isRead()) unread++;
if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
}
Forum getForum() {
@@ -48,29 +43,4 @@ class ForumListItem implements Comparable<ForumListItem> {
int getUnreadCount() {
return unread;
}
@Override
public int hashCode() {
return forum.getId().hashCode();
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof ForumListItem && getForum().equals(
((ForumListItem) o).getForum());
}
@Override
public int compareTo(ForumListItem o) {
if (this == o) return 0;
// The forum with the newest message comes first
long aTime = getTimestamp(), bTime = o.getTimestamp();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by forum name
String aName = getForum().getName();
String bName = o.getForum().getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
}

View File

@@ -1,187 +0,0 @@
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;
}
}

View File

@@ -2,25 +2,13 @@ package org.briarproject.briar.android.forum;
import org.briarproject.briar.android.activity.ActivityScope;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
@Module
public class ForumModule {
@Module
public interface BindsModule {
@Binds
@IntoMap
@ViewModelKey(ForumListViewModel.class)
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
}
@ActivityScope
@Provides
ForumController provideForumController(BaseActivity activity,

View File

@@ -1,77 +0,0 @@
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);
});
}
}

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.os.Bundle;
import android.view.MenuItem;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.android.DestroyableContext;
@@ -76,7 +77,7 @@ public abstract class BaseFragment extends Fragment
void showNextFragment(BaseFragment f);
@UiThread
void handleException(Exception e);
void handleDbException(DbException e);
}
@CallSuper
@@ -99,8 +100,8 @@ public abstract class BaseFragment extends Fragment
}
@UiThread
protected void handleException(Exception e) {
listener.handleException(e);
protected void handleDbException(DbException e) {
listener.handleDbException(e);
}
}

View File

@@ -5,7 +5,6 @@ import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -29,10 +28,6 @@ import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import static android.os.Build.VERSION.SDK_INT;
import static android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION;
import static android.view.View.GONE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ScreenFilterDialogFragment extends DialogFragment {
@@ -42,7 +37,7 @@ public class ScreenFilterDialogFragment extends DialogFragment {
@Inject
ScreenFilterMonitor screenFilterMonitor;
private DismissListener dismissListener = null;
DismissListener dismissListener = null;
public static ScreenFilterDialogFragment newInstance(
Collection<AppDetails> apps) {
@@ -88,20 +83,10 @@ public class ScreenFilterDialogFragment extends DialogFragment {
View dialogView = inflater.inflate(R.layout.dialog_screen_filter, null);
builder.setView(dialogView);
TextView message = dialogView.findViewById(R.id.screen_filter_message);
message.setText(getString(R.string.screen_filter_body,
TextUtils.join("\n", appNames)));
CheckBox allow = dialogView.findViewById(R.id.screen_filter_checkbox);
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) -> {
builder.setNeutralButton(R.string.continue_button, (dialog, which) -> {
if (allow.isChecked()) screenFilterMonitor.allowApps(packageNames);
dialog.dismiss();
});

View File

@@ -10,11 +10,14 @@ import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.util.UiUtils;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
@@ -38,6 +41,9 @@ public class ContactExchangeErrorFragment extends BaseFragment {
return f;
}
@Inject
AndroidExecutor androidExecutor;
@Override
public String getUniqueTag() {
return TAG;
@@ -82,8 +88,8 @@ public class ContactExchangeErrorFragment extends BaseFragment {
}
private void triggerFeedback() {
UiUtils.triggerFeedback(requireContext());
finish();
UiUtils.triggerFeedback(androidExecutor);
}
}

View File

@@ -17,6 +17,7 @@ import android.widget.TextView;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@@ -364,7 +365,7 @@ public class NavDrawerActivity extends BriarActivity implements
}
@Override
public void handleException(Exception e) {
public void handleDbException(DbException e) {
// Do nothing for now
}

View File

@@ -4,13 +4,9 @@ import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -18,6 +14,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -31,7 +28,7 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
@NotNullByDefault
public class NavDrawerViewModel extends DbViewModel {
public class NavDrawerViewModel extends AndroidViewModel {
private static final Logger LOG =
getLogger(NavDrawerViewModel.class.getName());
@@ -40,6 +37,8 @@ public class NavDrawerViewModel extends DbViewModel {
private static final String SHOW_TRANSPORTS_ONBOARDING =
"showTransportsOnboarding";
@DatabaseExecutor
private final Executor dbExecutor;
private final SettingsManager settingsManager;
private final MutableLiveData<Boolean> showExpiryWarning =
@@ -50,13 +49,10 @@ public class NavDrawerViewModel extends DbViewModel {
new MutableLiveData<>();
@Inject
NavDrawerViewModel(Application app,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
NavDrawerViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
SettingsManager settingsManager) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
super(app);
this.dbExecutor = dbExecutor;
this.settingsManager = settingsManager;
}
@@ -66,7 +62,7 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void checkExpiryWarning() {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
@@ -101,7 +97,7 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void expiryWarningDismissed() {
showExpiryWarning.setValue(false);
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
Settings settings = new Settings();
int date = (int) (System.currentTimeMillis() / 1000L);
@@ -124,7 +120,7 @@ public class NavDrawerViewModel extends DbViewModel {
shouldAskForDozeWhitelisting.setValue(false);
return;
}
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
@@ -145,7 +141,7 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void checkTransportsOnboarding() {
if (showTransportsOnboarding.getValue() != null) return;
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
@@ -161,7 +157,7 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void transportsOnboardingShown() {
showTransportsOnboarding.setValue(false);
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
Settings settings = new Settings();
settings.putBoolean(SHOW_TRANSPORTS_ONBOARDING, false);

View File

@@ -9,11 +9,9 @@ import android.content.IntentFilter;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.network.NetworkManager;
import org.briarproject.bramble.api.network.NetworkStatus;
import org.briarproject.bramble.api.network.event.NetworkStatusEvent;
@@ -29,8 +27,6 @@ import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -38,6 +34,7 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -54,12 +51,13 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault
public class PluginViewModel extends DbViewModel implements EventListener {
public class PluginViewModel extends AndroidViewModel implements EventListener {
private static final Logger LOG =
getLogger(PluginViewModel.class.getName());
private final Application app;
private final Executor dbExecutor;
private final SettingsManager settingsManager;
private final PluginManager pluginManager;
private final EventBus eventBus;
@@ -87,12 +85,11 @@ public class PluginViewModel extends DbViewModel implements EventListener {
@Inject
PluginViewModel(Application app, @DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, TransactionManager db,
AndroidExecutor androidExecutor, SettingsManager settingsManager,
PluginManager pluginManager, EventBus eventBus,
NetworkManager networkManager) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
SettingsManager settingsManager, PluginManager pluginManager,
EventBus eventBus, NetworkManager networkManager) {
super(app);
this.app = app;
this.dbExecutor = dbExecutor;
this.settingsManager = settingsManager;
this.pluginManager = pluginManager;
this.eventBus = eventBus;
@@ -185,7 +182,7 @@ public class PluginViewModel extends DbViewModel implements EventListener {
}
private void loadSettings() {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
boolean tor = isPluginEnabled(TorConstants.ID,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
@@ -222,7 +219,7 @@ public class PluginViewModel extends DbViewModel implements EventListener {
}
private void mergeSettings(Settings s, String namespace) {
runOnDbThread(() -> {
dbExecutor.execute(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, namespace);
@@ -238,7 +235,8 @@ public class PluginViewModel extends DbViewModel implements EventListener {
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
bluetoothTurnedOn.postValue(state == STATE_ON);
if (state == STATE_ON) bluetoothTurnedOn.postValue(true);
else bluetoothTurnedOn.postValue(false);
}
}
}

View File

@@ -82,7 +82,6 @@ public class PanicPreferencesFragment extends PreferenceFragmentCompat
entries.add(0, getString(R.string.panic_app_setting_none));
entryValues.add(0, PACKAGE_NAME_NONE);
// only info.guardianproject.ripple is whitelisted in manifest
for (ResolveInfo resolveInfo : PanicResponder.resolveTriggerApps(pm)) {
if (resolveInfo.activityInfo == null)
continue;

View File

@@ -106,7 +106,7 @@ public class GroupActivity extends
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -125,7 +125,7 @@ public class GroupActivity extends
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -264,7 +264,7 @@ public class GroupActivity extends
// GroupRemovedEvent being fired
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -51,7 +51,7 @@ public class CreateGroupActivity extends BriarActivity
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -69,7 +69,7 @@ public class GroupInviteActivity extends ContactSelectorActivity
@Override
public void onExceptionUi(DbException exception) {
setResult(RESULT_CANCELED);
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -8,19 +8,15 @@ import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import javax.annotation.concurrent.Immutable;
import androidx.annotation.Nullable;
@Immutable
// This class is not thread-safe
@NotNullByDefault
class GroupItem implements Comparable<GroupItem> {
class GroupItem {
private final PrivateGroup privateGroup;
private final AuthorInfo authorInfo;
private final int messageCount, unreadCount;
private final long timestamp;
private final boolean dissolved;
private int messageCount, unreadCount;
private long timestamp;
private boolean dissolved;
GroupItem(PrivateGroup privateGroup, AuthorInfo authorInfo,
GroupCount count, boolean dissolved) {
@@ -32,22 +28,18 @@ class GroupItem implements Comparable<GroupItem> {
this.dissolved = dissolved;
}
GroupItem(GroupItem item, GroupMessageHeader header) {
this.privateGroup = item.privateGroup;
this.authorInfo = item.authorInfo;
this.messageCount = item.messageCount + 1;
this.unreadCount = item.unreadCount + (header.isRead() ? 0 : 1);
this.timestamp = Math.max(header.getTimestamp(), item.timestamp);
this.dissolved = item.dissolved;
void addMessageHeader(GroupMessageHeader header) {
messageCount++;
if (header.getTimestamp() > timestamp) {
timestamp = header.getTimestamp();
}
if (!header.isRead()) {
unreadCount++;
}
}
GroupItem(GroupItem item, boolean isDissolved) {
this.privateGroup = item.privateGroup;
this.authorInfo = item.authorInfo;
this.messageCount = item.messageCount;
this.unreadCount = item.unreadCount;
this.timestamp = item.timestamp;
this.dissolved = isDissolved;
PrivateGroup getPrivateGroup() {
return privateGroup;
}
GroupId getId() {
@@ -86,27 +78,8 @@ class GroupItem implements Comparable<GroupItem> {
return dissolved;
}
@Override
public int hashCode() {
return getId().hashCode();
void setDissolved() {
dissolved = true;
}
@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);
}
}

View File

@@ -1,52 +1,81 @@
package org.briarproject.briar.android.privategroup.list;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
import org.briarproject.briar.android.util.BriarAdapter;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import androidx.recyclerview.widget.ListAdapter;
import static androidx.recyclerview.widget.SortedList.INVALID_POSITION;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class GroupListAdapter extends ListAdapter<GroupItem, GroupViewHolder> {
class GroupListAdapter extends BriarAdapter<GroupItem, GroupViewHolder> {
private final OnGroupRemoveClickListener listener;
GroupListAdapter(OnGroupRemoveClickListener listener) {
super(new GroupItemCallback());
GroupListAdapter(Context ctx, OnGroupRemoveClickListener listener) {
super(ctx, GroupItem.class);
this.listener = listener;
}
@Override
public GroupViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_group, parent, false);
View v = LayoutInflater.from(ctx).inflate(
R.layout.list_item_group, parent, false);
return new GroupViewHolder(v);
}
@Override
public void onBindViewHolder(GroupViewHolder ui, int position) {
ui.bindView(getItem(position), listener);
ui.bindView(ctx, items.get(position), listener);
}
private static class GroupItemCallback extends ItemCallback<GroupItem> {
@Override
public boolean areItemsTheSame(GroupItem a, GroupItem b) {
return a.equals(b);
}
@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 int compare(GroupItem a, GroupItem b) {
if (a == b) return 0;
// The group with the latest message comes first
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
// Break ties by group name
String aName = a.getName();
String bName = b.getName();
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
}
@Override
public boolean areContentsTheSame(GroupItem a, GroupItem b) {
return a.getMessageCount() == b.getMessageCount() &&
a.getTimestamp() == b.getTimestamp() &&
a.getUnreadCount() == b.getUnreadCount() &&
a.isDissolved() == b.isDissolved();
}
@Override
public boolean areItemsTheSame(GroupItem a, GroupItem b) {
return a.getId().equals(b.getId());
}
int findItemPosition(GroupId g) {
for (int i = 0; i < items.size(); i++) {
GroupItem item = items.get(i);
if (item.getId().equals(g)) {
return i;
}
}
return INVALID_POSITION;
}
void removeItem(GroupId groupId) {
int pos = findItemPosition(groupId);
if (pos != INVALID_POSITION) items.removeItemAt(pos);
}
}

View File

@@ -0,0 +1,60 @@
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);
}
}

View File

@@ -1,12 +1,9 @@
package org.briarproject.briar.android.privategroup.list;
import android.app.Application;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.db.NoSuchGroupException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.event.EventListener;
@@ -19,12 +16,11 @@ import org.briarproject.bramble.api.sync.ClientId;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.android.controller.DbControllerImpl;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
@@ -34,7 +30,6 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -43,13 +38,10 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@@ -57,10 +49,11 @@ import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class GroupListViewModel extends DbViewModel implements EventListener {
class GroupListControllerImpl extends DbControllerImpl
implements GroupListController, EventListener {
private static final Logger LOG =
getLogger(GroupListViewModel.class.getName());
Logger.getLogger(GroupListControllerImpl.class.getName());
private final PrivateGroupManager groupManager;
private final GroupInvitationManager groupInvitationManager;
@@ -68,137 +61,120 @@ class GroupListViewModel extends DbViewModel implements EventListener {
private final AndroidNotificationManager notificationManager;
private final EventBus eventBus;
private final MutableLiveData<LiveResult<List<GroupItem>>> groupItems =
new MutableLiveData<>();
private final MutableLiveData<Integer> numInvitations =
new MutableLiveData<>();
// UI thread
@Nullable
private GroupListListener listener;
@Inject
GroupListViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
PrivateGroupManager groupManager,
GroupListControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, PrivateGroupManager groupManager,
GroupInvitationManager groupInvitationManager,
ContactManager contactManager,
AndroidNotificationManager notificationManager, EventBus eventBus) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
super(dbExecutor, lifecycleManager);
this.groupManager = groupManager;
this.groupInvitationManager = groupInvitationManager;
this.contactManager = contactManager;
this.notificationManager = notificationManager;
this.eventBus = eventBus;
this.eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
public void setGroupListListener(GroupListListener listener) {
this.listener = listener;
}
void clearAllGroupMessageNotifications() {
@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();
}
void blockAllGroupMessageNotifications() {
notificationManager.blockAllGroupMessageNotifications();
}
void unblockAllGroupMessageNotifications() {
notificationManager.unblockAllGroupMessageNotifications();
@Override
@CallSuper
public void onStop() {
eventBus.removeListener(this);
}
@Override
@CallSuper
public void eventOccurred(Event e) {
if (listener == null) throw new IllegalStateException();
if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
LOG.info("Private group message added");
onGroupMessageAdded(g.getHeader());
listener.onGroupMessageAdded(g.getHeader());
} else if (e instanceof GroupInvitationRequestReceivedEvent) {
LOG.info("Private group invitation received");
loadNumInvitations();
listener.onGroupInvitationReceived();
} else if (e instanceof GroupAddedEvent) {
GroupAddedEvent g = (GroupAddedEvent) e;
ClientId id = g.getGroup().getClientId();
if (id.equals(CLIENT_ID)) {
LOG.info("Private group added");
loadGroups();
listener.onGroupAdded(g.getGroup().getId());
}
} else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent g = (GroupRemovedEvent) e;
ClientId id = g.getGroup().getClientId();
if (id.equals(CLIENT_ID)) {
LOG.info("Private group removed");
onGroupRemoved(g.getGroup().getId());
listener.onGroupRemoved(g.getGroup().getId());
}
} else if (e instanceof GroupDissolvedEvent) {
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
LOG.info("Private group dissolved");
onGroupDissolved(g.getGroupId());
listener.onGroupDissolved(g.getGroupId());
}
}
void loadGroups() {
loadList(this::loadGroups, groupItems::setValue);
}
@DatabaseExecutor
private List<GroupItem> loadGroups(Transaction txn) throws DbException {
long start = now();
Collection<PrivateGroup> groups = groupManager.getPrivateGroups(txn);
List<GroupItem> items = new ArrayList<>(groups.size());
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
for (PrivateGroup g : groups) {
GroupId id = g.getId();
AuthorId authorId = g.getCreator().getId();
AuthorInfo authorInfo;
if (authorInfos.containsKey(authorId)) {
authorInfo = requireNonNull(authorInfos.get(authorId));
} else {
authorInfo = contactManager.getAuthorInfo(txn, authorId);
authorInfos.put(authorId, authorInfo);
@Override
public void loadGroups(
ResultExceptionHandler<Collection<GroupItem>, DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
Collection<PrivateGroup> groups =
groupManager.getPrivateGroups();
List<GroupItem> items = new ArrayList<>(groups.size());
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
for (PrivateGroup g : groups) {
try {
GroupId id = g.getId();
AuthorId authorId = g.getCreator().getId();
AuthorInfo authorInfo;
if (authorInfos.containsKey(authorId)) {
authorInfo = authorInfos.get(authorId);
} else {
authorInfo = contactManager.getAuthorInfo(authorId);
authorInfos.put(authorId, authorInfo);
}
GroupCount count = groupManager.getGroupCount(id);
boolean dissolved = groupManager.isDissolved(id);
items.add(
new GroupItem(g, authorInfo, count, dissolved));
} catch (NoSuchGroupException e) {
// Continue
}
}
logDuration(LOG, "Loading groups", start);
handler.onResult(items);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
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) {
@Override
public void removeGroup(GroupId g, ExceptionHandler<DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
@@ -206,26 +182,23 @@ class GroupListViewModel extends DbViewModel implements EventListener {
logDuration(LOG, "Removing group", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
void loadNumInvitations() {
@Override
public void loadAvailableGroups(
ResultExceptionHandler<Integer, DbException> handler) {
runOnDbThread(() -> {
try {
int i = groupInvitationManager.getInvitations().size();
numInvitations.postValue(i);
handler.onResult(
groupInvitationManager.getInvitations().size());
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
LiveData<LiveResult<List<GroupItem>>> getGroupItems() {
return groupItems;
}
LiveData<Integer> getNumInvitations() {
return numInvitations;
}
}

View File

@@ -12,50 +12,57 @@ import android.view.ViewGroup;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
import org.briarproject.briar.android.privategroup.invitation.GroupInvitationActivity;
import org.briarproject.briar.android.privategroup.list.GroupListController.GroupListListener;
import org.briarproject.briar.android.privategroup.list.GroupViewHolder.OnGroupRemoveClickListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import java.util.Collection;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class GroupListFragment extends BaseFragment implements
OnGroupRemoveClickListener, OnClickListener {
GroupListListener, OnGroupRemoveClickListener, OnClickListener {
public final static String TAG = GroupListFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG);
public static GroupListFragment newInstance() {
return new GroupListFragment();
}
@Inject
ViewModelProvider.Factory viewModelFactory;
GroupListController controller;
private GroupListViewModel viewModel;
private BriarRecyclerView list;
private GroupListAdapter adapter;
private Snackbar snackbar;
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(GroupListViewModel.class);
controller.setGroupListListener(this);
}
@Nullable
@@ -68,32 +75,17 @@ public class GroupListFragment extends BaseFragment implements
View v = inflater.inflate(R.layout.list, container, false);
adapter = new GroupListAdapter(this);
adapter = new GroupListAdapter(getActivity(), this);
list = v.findViewById(R.id.list);
list.setEmptyImage(R.drawable.ic_empty_state_group_list);
list.setEmptyText(R.string.groups_list_empty);
list.setEmptyAction(R.string.groups_list_empty_action);
list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setAdapter(adapter);
viewModel.getGroupItems().observe(getViewLifecycleOwner(), result ->
result.onError(this::handleException).onSuccess(items -> {
adapter.submitList(items);
if (requireNonNull(items).size() == 0) list.showData();
})
);
Snackbar snackbar = new BriarSnackbarBuilder()
snackbar = new BriarSnackbarBuilder()
.setAction(R.string.show, this)
.make(list, "", LENGTH_INDEFINITE);
viewModel.getNumInvitations().observe(getViewLifecycleOwner(), num -> {
if (num == 0) {
snackbar.dismiss();
} else {
snackbar.setText(getResources().getQuantityString(
R.plurals.groups_invitations_open, num, num));
if (!snackbar.isShownOrQueued()) snackbar.show();
}
});
return v;
}
@@ -101,23 +93,25 @@ public class GroupListFragment extends BaseFragment implements
@Override
public void onStart() {
super.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();
controller.onStart();
list.startPeriodicUpdate();
loadGroups();
loadAvailableGroups();
}
@Override
public void onStop() {
super.onStop();
controller.onStop();
list.stopPeriodicUpdate();
viewModel.unblockAllGroupMessageNotifications();
adapter.clear();
list.showProgressBar();
}
@Override
public void onDestroy() {
super.onDestroy();
controller.unsetGroupListListener(this);
}
@Override
@@ -128,18 +122,68 @@ public class GroupListFragment extends BaseFragment implements
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_add_group) {
Intent i = new Intent(getContext(), CreateGroupActivity.class);
startActivity(i);
return true;
switch (item.getItemId()) {
case R.id.action_add_group:
Intent i = new Intent(getContext(), CreateGroupActivity.class);
startActivity(i);
return true;
default:
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
@UiThread
@Override
public void onGroupRemoveClick(GroupItem item) {
viewModel.removeGroup(item.getId());
controller.removeGroup(item.getId(),
new UiExceptionHandler<DbException>(this) {
// result handled by GroupRemovedEvent and onGroupRemoved()
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
}
@UiThread
@Override
public void onGroupMessageAdded(GroupMessageHeader header) {
adapter.incrementRevision();
int position = adapter.findItemPosition(header.getGroupId());
GroupItem item = adapter.getItemAt(position);
if (item != null) {
item.addMessageHeader(header);
adapter.updateItemAt(position, item);
}
}
@Override
public void onGroupInvitationReceived() {
loadAvailableGroups();
}
@UiThread
@Override
public void onGroupAdded(GroupId groupId) {
loadGroups();
}
@UiThread
@Override
public void onGroupRemoved(GroupId groupId) {
adapter.incrementRevision();
adapter.removeItem(groupId);
}
@Override
public void onGroupDissolved(GroupId groupId) {
adapter.incrementRevision();
int position = adapter.findItemPosition(groupId);
GroupItem item = adapter.getItemAt(position);
if (item != null) {
item.setDissolved();
adapter.updateItemAt(position, item);
}
}
@Override
@@ -147,6 +191,52 @@ public class GroupListFragment extends BaseFragment implements
return TAG;
}
private void loadGroups() {
int revision = adapter.getRevision();
controller.loadGroups(
new UiResultExceptionHandler<Collection<GroupItem>, DbException>(
this) {
@Override
public void onResultUi(Collection<GroupItem> groups) {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (groups.isEmpty()) list.showData();
else adapter.replaceAll(groups);
} else {
LOG.info("Concurrent update, reloading");
loadGroups();
}
}
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
}
private void loadAvailableGroups() {
controller.loadAvailableGroups(
new UiResultExceptionHandler<Integer, DbException>(this) {
@Override
public void onResultUi(Integer num) {
if (num == 0) {
snackbar.dismiss();
} else {
snackbar.setText(getResources().getQuantityString(
R.plurals.groups_invitations_open, num,
num));
if (!snackbar.isShownOrQueued()) snackbar.show();
}
}
@Override
public void onExceptionUi(DbException exception) {
handleDbException(exception);
}
});
}
/**
* This method is handling the available groups snackbar action
*/

View File

@@ -1,18 +1,17 @@
package org.briarproject.briar.android.privategroup.list;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import org.briarproject.briar.android.activity.ActivityScope;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
import dagger.Provides;
@Module
public abstract class GroupListModule {
public class GroupListModule {
@Binds
@IntoMap
@ViewModelKey(GroupListViewModel.class)
abstract ViewModel bindGroupListViewModel(
GroupListViewModel groupListViewModel);
@ActivityScope
@Provides
GroupListController provideGroupListController(
GroupListControllerImpl groupListController) {
return groupListController;
}
}

View File

@@ -29,7 +29,6 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
private final static float ALPHA = 0.42f;
private final Context ctx;
private final ViewGroup layout;
private final TextAvatarView avatar;
private final TextView name;
@@ -41,7 +40,7 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
GroupViewHolder(View v) {
super(v);
ctx = v.getContext();
layout = (ViewGroup) v;
avatar = v.findViewById(R.id.avatarView);
name = v.findViewById(R.id.nameView);
@@ -52,7 +51,8 @@ class GroupViewHolder extends RecyclerView.ViewHolder {
remove = v.findViewById(R.id.removeButton);
}
void bindView(GroupItem group, OnGroupRemoveClickListener listener) {
void bindView(Context ctx, GroupItem group,
OnGroupRemoveClickListener listener) {
// Avatar
avatar.setText(group.getName().substring(0, 1));
avatar.setBackgroundBytes(group.getId().getBytes());

View File

@@ -124,7 +124,7 @@ public class GroupMemberListActivity extends BriarActivity
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -80,7 +80,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -120,7 +120,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -137,7 +137,7 @@ public class RevealContactsActivity extends ContactSelectorActivity
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
supportFinishAfterTransition();

View File

@@ -1,31 +0,0 @@
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);
}
}

View File

@@ -1,343 +0,0 @@
/*
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);
}
}

View File

@@ -0,0 +1,277 @@
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();
}
}
}

View File

@@ -0,0 +1,46 @@
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);
}
}
}

View File

@@ -0,0 +1,22 @@
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());
}
}

View File

@@ -8,36 +8,13 @@ import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.fragment.app.Fragment;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
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);
}
public class CrashFragment extends Fragment {
@Nullable
@Override
@@ -48,16 +25,15 @@ public class CrashFragment extends BaseFragment {
.inflate(R.layout.fragment_crash, container, false);
v.findViewById(R.id.acceptButton).setOnClickListener(view ->
viewModel.showReport());
getDevReportActivity().displayFragment(true));
v.findViewById(R.id.declineButton).setOnClickListener(view ->
viewModel.closeReport());
getDevReportActivity().closeReport());
return v;
}
@Override
public String getUniqueTag() {
return TAG;
private DevReportActivity getDevReportActivity() {
return (DevReportActivity) requireActivity();
}
}

View File

@@ -1,117 +0,0 @@
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();
}
}

View File

@@ -0,0 +1,188 @@
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();
}
}

View File

@@ -1,18 +0,0 @@
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);
}

View File

@@ -1,20 +0,0 @@
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);
}
}
}

View File

@@ -1,123 +0,0 @@
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);
}
}
}

View File

@@ -1,67 +0,0 @@
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());
}
}
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.reporting;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -9,58 +10,92 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.Toast;
import android.widget.LinearLayout;
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.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.json.JSONException;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
import androidx.fragment.app.Fragment;
import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.acra.ACRAConstants.EXTRA_REPORT_FILE;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PACKAGE_NAME;
import static org.acra.ReportField.REPORT_ID;
import static org.acra.ReportField.STACK_TRACE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ReportFormFragment extends BaseFragment {
public class ReportFormFragment extends Fragment
implements OnCheckedChangeListener {
public final static String TAG = ReportFormFragment.class.getName();
private static final Logger LOG =
getLogger(ReportFormFragment.class.getName());
private static final String IS_FEEDBACK = "isFeedback";
private static final Set<ReportField> requiredFields = new HashSet<>();
private static final Set<ReportField> excludedFields = new HashSet<>();
static {
requiredFields.add(REPORT_ID);
requiredFields.add(APP_VERSION_CODE);
requiredFields.add(APP_VERSION_NAME);
requiredFields.add(PACKAGE_NAME);
requiredFields.add(ANDROID_VERSION);
requiredFields.add(STACK_TRACE);
}
@Inject
ViewModelProvider.Factory viewModelFactory;
private ReportViewModel viewModel;
private boolean isFeedback;
private File reportFile;
private EditText userCommentView;
private EditText userEmailView;
private CheckBox includeDebugReport;
private Button chevron;
private RecyclerView list;
private LinearLayout report;
private View progress;
@Nullable
private MenuItem sendReport;
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
static ReportFormFragment newInstance(boolean isFeedback,
File reportFile) {
ReportFormFragment f = new ReportFormFragment();
Bundle args = new Bundle();
args.putBoolean(IS_FEEDBACK, isFeedback);
args.putSerializable(EXTRA_REPORT_FILE, reportFile);
f.setArguments(args);
return f;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(ReportViewModel.class);
}
@Nullable
@@ -75,10 +110,15 @@ public class ReportFormFragment extends BaseFragment {
userEmailView = v.findViewById(R.id.user_email);
includeDebugReport = v.findViewById(R.id.include_debug_report);
chevron = v.findViewById(R.id.chevron);
list = v.findViewById(R.id.list);
report = v.findViewById(R.id.report_content);
progress = v.findViewById(R.id.progress_wheel);
if (viewModel.isFeedback()) {
Bundle args = requireArguments();
isFeedback = args.getBoolean(IS_FEEDBACK);
reportFile =
(File) requireNonNull(args.getSerializable(EXTRA_REPORT_FILE));
if (isFeedback) {
includeDebugReport
.setText(getString(R.string.include_debug_report_feedback));
userCommentView.setHint(R.string.enter_feedback);
@@ -89,73 +129,163 @@ public class ReportFormFragment extends BaseFragment {
chevron.setOnClickListener(view -> {
boolean show = chevron.getText().equals(getString(R.string.show));
viewModel.showReportData(show);
});
viewModel.getShowReportData().observe(getViewLifecycleOwner(), show -> {
if (show) {
chevron.setText(R.string.hide);
list.setVisibility(VISIBLE);
if (list.getAdapter() == null) {
progress.setVisibility(VISIBLE);
} else {
progress.setVisibility(INVISIBLE);
}
refresh();
} else {
chevron.setText(R.string.show);
list.setVisibility(GONE);
progress.setVisibility(INVISIBLE);
report.setVisibility(GONE);
}
});
return v;
}
@Override
public void onStart() {
super.onStart();
if (chevron.isSelected()) refresh();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.dev_report_actions, menu);
sendReport = menu.findItem(R.id.action_send_report);
sendReport.setEnabled(false);
viewModel.getReportData().observe(getViewLifecycleOwner(), data -> {
list.setAdapter(new ReportDataAdapter(data.getItems()));
sendReport.setEnabled(true);
progress.setVisibility(INVISIBLE);
});
// calling setShowAsAction() shouldn't be needed, but for some reason is
sendReport.setShowAsAction(SHOW_AS_ACTION_ALWAYS);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_send_report) {
sendReport();
processReport();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public String getUniqueTag() {
return TAG;
}
private void sendReport() {
userCommentView.setEnabled(false);
userEmailView.setEnabled(false);
requireNonNull(sendReport).setEnabled(false);
list.setVisibility(GONE); // ensures that progress fits on screen
progress.setVisibility(VISIBLE);
// Retrieve user's comment and email address, if any
String comment = userCommentView.getText().toString();
String email = userEmailView.getText().toString();
boolean includeReport = includeDebugReport.isChecked();
// Send report (now or after next sign-in)
if (viewModel.sendReport(comment, email, includeReport)) {
// trying to send now
Toast.makeText(requireContext(), R.string.dev_report_sending,
LENGTH_SHORT).show();
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ReportField field = (ReportField) buttonView.getTag();
if (field != null) {
if (isChecked) excludedFields.remove(field);
else excludedFields.add(field);
}
}
private void refresh() {
report.setVisibility(INVISIBLE);
progress.setVisibility(VISIBLE);
report.removeAllViews();
new AsyncTask<Void, Void, CrashReportData>() {
@Override
protected CrashReportData doInBackground(Void... args) {
CrashReportPersister persister = new CrashReportPersister();
try {
return persister.load(reportFile);
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Could not load report file", e);
return null;
}
}
@Override
protected void onPostExecute(CrashReportData crashData) {
LayoutInflater inflater = getLayoutInflater();
if (crashData != null) {
for (Map.Entry<ReportField, Element> e : crashData
.entrySet()) {
ReportField field = e.getKey();
StringBuilder valueBuilder = new StringBuilder();
for (String pair : e.getValue().flatten()) {
valueBuilder.append(pair).append("\n");
}
String value = valueBuilder.toString();
boolean required = requiredFields.contains(field);
boolean excluded = excludedFields.contains(field);
View v = inflater.inflate(R.layout.list_item_crash,
report, false);
CheckBox cb = v.findViewById(R.id.include_in_report);
cb.setTag(field);
cb.setChecked(required || !excluded);
cb.setEnabled(!required);
cb.setOnCheckedChangeListener(ReportFormFragment.this);
cb.setText(field.toString());
TextView content = v.findViewById(R.id.content);
content.setText(value);
report.addView(v);
}
} else {
View v = inflater.inflate(
android.R.layout.simple_list_item_1, report, false);
TextView error = v.findViewById(android.R.id.text1);
error.setText(R.string.could_not_load_report_data);
report.addView(v);
}
report.setVisibility(VISIBLE);
progress.setVisibility(GONE);
}
}.execute();
}
private void processReport() {
userCommentView.setEnabled(false);
userEmailView.setEnabled(false);
requireNonNull(sendReport).setEnabled(false);
progress.setVisibility(VISIBLE);
boolean includeReport = !isFeedback || includeDebugReport.isChecked();
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... args) {
CrashReportPersister persister = new CrashReportPersister();
try {
CrashReportData data = persister.load(reportFile);
if (includeReport) {
for (ReportField field : excludedFields) {
LOG.info("Removing field " + field.name());
data.remove(field);
}
} else {
Iterator<Map.Entry<ReportField, Element>> iter =
data.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<ReportField, Element> e = iter.next();
if (!requiredFields.contains(e.getKey())) {
iter.remove();
}
}
}
persister.store(data, reportFile);
return true;
} catch (IOException | JSONException e) {
LOG.log(WARNING, "Error processing report file", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success) {
// Retrieve user's comment and email address, if any
String comment = "";
if (userCommentView != null)
comment = userCommentView.getText().toString();
String email = "";
if (userEmailView != null) {
email = userEmailView.getText().toString();
}
getDevReportActivity().sendCrashReport(comment, email);
}
if (getActivity() != null) getDevReportActivity().exit();
}
}.execute();
}
private DevReportActivity getDevReportActivity() {
return (DevReportActivity) requireActivity();
}
}

View File

@@ -1,213 +0,0 @@
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();
}
}
}

View File

@@ -26,6 +26,7 @@ import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.LocationUtils;
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.util.StringUtils;
@@ -71,7 +72,6 @@ import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE;
@@ -166,6 +166,9 @@ public class SettingsFragment extends PreferenceFragmentCompat
@Inject
CircumventionProvider circumventionProvider;
@Inject
AndroidExecutor androidExecutor;
@Override
public void onAttach(Context context) {
super.onAttach(context);
@@ -223,12 +226,11 @@ public class SettingsFragment extends PreferenceFragmentCompat
screenLock.setOnPreferenceChangeListener(this);
screenLockTimeout.setOnPreferenceChangeListener(this);
Preference prefFeedback =
requireNonNull(findPreference("pref_key_send_feedback"));
prefFeedback.setOnPreferenceClickListener(preference -> {
triggerFeedback(requireContext());
return true;
});
findPreference("pref_key_send_feedback").setOnPreferenceClickListener(
preference -> {
triggerFeedback(androidExecutor);
return true;
});
if (SDK_INT < 27) {
// remove System Default Theme option from preference entries
@@ -243,15 +245,17 @@ public class SettingsFragment extends PreferenceFragmentCompat
values.remove(getString(R.string.pref_theme_system_value));
theme.setEntryValues(values.toArray(new CharSequence[0]));
}
Preference explode = requireNonNull(findPreference("pref_key_explode"));
if (IS_DEBUG_BUILD) {
explode.setOnPreferenceClickListener(preference -> {
throw new RuntimeException("Boom!");
});
findPreference("pref_key_explode").setOnPreferenceClickListener(
preference -> {
throw new RuntimeException("Boom!");
}
);
} else {
explode.setVisible(false);
findPreference("pref_key_explode").setVisible(false);
findPreference("pref_key_test_data").setVisible(false);
PreferenceGroup testing = explode.getParent();
PreferenceGroup testing =
findPreference("pref_key_explode").getParent();
if (testing == null) throw new AssertionError();
testing.setVisible(false);
}

View File

@@ -98,7 +98,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -110,7 +110,7 @@ public abstract class InvitationActivity<I extends InvitationItem>
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -59,7 +59,7 @@ public class ShareBlogActivity extends ShareActivity {
Toast.makeText(ShareBlogActivity.this,
R.string.blogs_sharing_error, LENGTH_SHORT)
.show();
handleException(exception);
handleDbException(exception);
}
});

View File

@@ -59,7 +59,7 @@ public class ShareForumActivity extends ShareActivity {
Toast.makeText(ShareForumActivity.this,
R.string.forum_share_error, LENGTH_SHORT)
.show();
handleException(exception);
handleDbException(exception);
}
});
}

View File

@@ -152,7 +152,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -183,7 +183,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -214,7 +214,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
});
}
@@ -351,7 +351,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
handleDbException(exception);
}
};
getController().createAndStoreMessage(text, replyItem, handler);

View File

@@ -27,13 +27,14 @@ import android.widget.TextView;
import com.google.android.material.textfield.TextInputLayout;
import org.acra.ACRA;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.reporting.FeedbackActivity;
import org.briarproject.briar.android.view.ArticleMovementMethod;
import org.briarproject.briar.android.widget.LinkDialogFragment;
@@ -50,7 +51,6 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -92,8 +92,6 @@ import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.DAYS;
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_START_TIME;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -347,18 +345,10 @@ public class UiUtils {
return fm.hasEnrolledFingerprints() && fm.isHardwareDetected();
}
public static void triggerFeedback(Context ctx) {
startDevReportActivity(ctx, FeedbackActivity.class, null, null);
}
public static void startDevReportActivity(Context ctx,
Class<? extends FragmentActivity> activity, @Nullable Throwable t,
@Nullable Long appStartTime) {
final Intent dialogIntent = new Intent(ctx, activity);
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
dialogIntent.putExtra(EXTRA_THROWABLE, t);
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
ctx.startActivity(dialogIntent);
public static void triggerFeedback(AndroidExecutor androidExecutor) {
androidExecutor.runOnBackgroundThread(
() -> ACRA.getErrorReporter()
.handleException(new UserFeedback(), false));
}
public static boolean enterPressed(int actionId,

View File

@@ -0,0 +1,5 @@
package org.briarproject.briar.android.util;
public class UserFeedback extends Exception {
}

View File

@@ -1,195 +0,0 @@
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);
}
}

View File

@@ -3,15 +3,14 @@ package org.briarproject.briar.android.viewmodel;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
@NotNullByDefault
public class LiveResult<T> {
@Nullable
private final T result;
private T result;
@Nullable
private final Exception exception;
private Exception exception;
public LiveResult(T result) {
this.result = result;
@@ -37,20 +36,4 @@ public class LiveResult<T> {
return exception != null;
}
/**
* Runs the given function, if {@link #hasError()} is true.
*/
public LiveResult<T> onError(Consumer<Exception> fun) {
if (exception != null) fun.accept(exception);
return this;
}
/**
* Runs the given function, if {@link #hasError()} is false.
*/
public LiveResult<T> onSuccess(Consumer<T> fun) {
if (result != null) fun.accept(result);
return this;
}
}

View File

@@ -82,14 +82,6 @@ public interface AndroidNotificationManager {
void unblockNotification(GroupId g);
void blockAllForumPostNotifications();
void unblockAllForumPostNotifications();
void blockAllGroupMessageNotifications();
void unblockAllGroupMessageNotifications();
void blockAllBlogPostNotifications();
void unblockAllBlogPostNotifications();

View File

@@ -14,8 +14,6 @@ public interface ScreenFilterMonitor {
* SYSTEM_ALERT_WINDOW permission, excluding system apps, Google Play
* Services, and any apps that have been allowed by calling
* {@link #allowApps(Collection)}.
*
* Only works on SDK_INT 29 and below.
*/
@UiThread
Collection<AppDetails> getApps();
@@ -23,8 +21,6 @@ public interface ScreenFilterMonitor {
/**
* Allows the apps with the given package names to use overlay windows.
* They will not be returned by future calls to {@link #getApps()}.
*
* Only works on SDK_INT 29 and below.
*/
@UiThread
void allowApps(Collection<String> packageNames);

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@@ -79,20 +79,19 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_email_layout" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
<LinearLayout
android:id="@+id/report_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="@dimen/margin_large"
android:paddingTop="@dimen/margin_small"
android:paddingEnd="@dimen/margin_large"
android:paddingBottom="@dimen/margin_large"
android:paddingBottom="@dimen/listitem_height_one_line_avatar"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/include_debug_report"
tools:listitem="@layout/list_item_crash"
tools:visibility="visible" />
<ProgressBar
@@ -100,7 +99,8 @@
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -109,4 +109,4 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</ScrollView>

View File

@@ -572,12 +572,9 @@
<string name="optional_contact_email">بريدك الالكتروني (إختياري)</string>
<string name="include_debug_report_crash">تضمين بيانات مجهولة عن الإنهيار</string>
<string name="include_debug_report_feedback">تضمين بيانات مجهولة عن هذا الجهاز</string>
<string name="dev_report_basic_info">المعلومات الأساسية</string>
<string name="dev_report_storage">تخزين</string>
<string name="dev_report_connectivity">الاتصال</string>
<string name="could_not_load_report_data">لم يمكن تحميل بيانات التقرير.</string>
<string name="send_report">ارسال التقرير</string>
<string name="close">إغلاق</string>
<string name="dev_report_sending">ارسال التعليقات</string>
<string name="dev_report_saved">تم حفظ التقرير. سيتم إرساله عند تسجيل الدخول إلى Briar (براير) في المرة القادمة.</string>
<!--Sign Out-->
<string name="progress_title_logout">تسجيل الخروج من Briar (براير)...</string>

View File

@@ -451,6 +451,7 @@
<string name="optional_contact_email">Sizim elektron ünvanınız (istəyinizə bağlıdır)</string>
<string name="include_debug_report_crash">Cədvəl haqqında anonim məlumatları əlavə edin</string>
<string name="include_debug_report_feedback">Bu cihaz haqqında anonim məlumatları əlavə edin</string>
<string name="could_not_load_report_data">Hesabat yüklənə bilmədi.</string>
<string name="send_report">Hesabat göndər</string>
<string name="close">Bağla</string>
<string name="dev_report_saved">Hesabat saxlandı. Briar-a növbəti dəfə daxil olduqda göndəriləcək.</string>

View File

@@ -439,6 +439,7 @@
<string name="optional_contact_email">Vaša email adresa (opciono)</string>
<string name="include_debug_report_crash">Uključite anonimne podatke o rušenju</string>
<string name="include_debug_report_feedback">Uključite anonimne podatke o ovom uređaju</string>
<string name="could_not_load_report_data">Nije bilo moguće učitati podatke izvještaja</string>
<string name="send_report">Pošalji izvještaj</string>
<string name="close">Zatvori</string>
<string name="dev_report_saved">Izvještaj je sačuvan. Biće poslat slijedeći put kada se ulogujete u Briar.</string>

View File

@@ -532,8 +532,7 @@ Així que l\'actualitzi li veureu una icona diferent .</string>
<string name="optional_contact_email">La vostra adreça de correu (opcional)</string>
<string name="include_debug_report_crash">Inclou dades anònimes sobre la fallida</string>
<string name="include_debug_report_feedback">Inclou dades anònimes sobre el dispositiu</string>
<string name="dev_report_basic_info">Informació bàsica</string>
<string name="dev_report_storage">Emmagatzematge</string>
<string name="could_not_load_report_data">No s\'han pogut carregar les dades de l\'informe.</string>
<string name="send_report">Envia l\'informe</string>
<string name="close">Tanca</string>
<string name="dev_report_saved">S\'ha desat l\'informe. Se us enviarà la propera vegada que inicieu sessió a Briar.</string>

View File

@@ -531,22 +531,10 @@
<string name="optional_contact_email">Deine E-Mail-Adresse (optional)</string>
<string name="include_debug_report_crash">Anonymisierte Daten über den Absturz anhängen</string>
<string name="include_debug_report_feedback">Anonymisierte Daten über dieses Gerät anhängen</string>
<string name="dev_report_basic_info">Basisinformationen</string>
<string name="dev_report_device_info">Geräteinformationen</string>
<string name="dev_report_stacktrace">Stacktrace</string>
<string name="dev_report_time_info">Zeitangaben</string>
<string name="dev_report_memory">Speicher</string>
<string name="dev_report_storage">Speicher</string>
<string name="dev_report_connectivity">Konnektivität</string>
<string name="dev_report_build_config">Buildkonfiguration</string>
<string name="dev_report_logcat">App-Log</string>
<string name="dev_report_device_features">Geräteeigenschaften</string>
<string name="could_not_load_report_data">Konnte Daten des Berichts nicht laden</string>
<string name="send_report">Bericht senden</string>
<string name="close">Schließen</string>
<string name="dev_report_sending">Rückmeldung wird gesendet…</string>
<string name="dev_report_sent">Feedback senden</string>
<string name="dev_report_saved">Der Bericht wurde gespeichert. Er wird verschickt, wenn du dich das nächste Mal bei Briar anmeldest.</string>
<string name="dev_report_error">Fehler: Senden des Reports fehlgeschlagen</string>
<!--Sign Out-->
<string name="progress_title_logout">Von Briar abmelden...</string>
<!--Screen Filters & Tapjacking-->

View File

@@ -531,30 +531,16 @@
<string name="optional_contact_email">Tu correo electrónico (opcional)</string>
<string name="include_debug_report_crash">Incluir datos anónimos sobre la falla</string>
<string name="include_debug_report_feedback">Incluir datos anónimos sobre este dispositivo</string>
<string name="dev_report_basic_info">Información básica</string>
<string name="dev_report_device_info">Información del dispositivo</string>
<string name="dev_report_stacktrace">Traza de pila</string>
<string name="dev_report_time_info">Información temporal</string>
<string name="dev_report_memory">Memoria</string>
<string name="dev_report_storage">Almacenamiento</string>
<string name="dev_report_connectivity">Conectividad</string>
<string name="dev_report_build_config">Configuración de compilación</string>
<string name="dev_report_logcat">Registro de la aplicación</string>
<string name="dev_report_device_features">Características del dispositivo</string>
<string name="could_not_load_report_data">No se pudieron cargar los datos del informe.</string>
<string name="send_report">Enviar informe</string>
<string name="close">Cerrar</string>
<string name="dev_report_sending">Enviando sus comentarios...</string>
<string name="dev_report_sent">Comentarios enviados</string>
<string name="dev_report_saved">Informe guardado. Se enviará la próxima vez que inicies sesión en Briar.</string>
<string name="dev_report_error">Error: El envío del informe falló</string>
<!--Sign Out-->
<string name="progress_title_logout">Cerrando sesión de Briar…</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Superposición de pantalla detectada</string>
<string name="screen_filter_body">Otra aplicación se está mostrando por encima de Briar. Por seguridad, Briar no reaccionará a los toques mientras otras aplicaciones se muestren por encima.\n\nLas siguientes aplis pueden ser las causantes:\n\n%1$s</string>
<string name="screen_filter_body_api_30">Otra aplicación se está mostrando por encima de Briar. Para proteger tu seguridad, Briar no responderá a las pulsaciones cuando otra aplicación se muestre por encima.\n\nRevisa las aplicaciones aquí abajo para descubrir cuál puede ser la causante.</string>
<string name="screen_filter_allow">Permitir a estas aplicaciones a mostrarse por encima</string>
<string name="screen_filter_review_apps">Revisar aplicaciones</string>
<!--Permission Requests-->
<string name="permission_camera_title">Permiso de cámara</string>
<string name="permission_camera_request_body">Para escanear el código QR, Briar necesita acceso a la cámara.</string>

View File

@@ -497,6 +497,7 @@
<string name="optional_contact_email">Zure e-mail helbidea (aukerakoa)</string>
<string name="include_debug_report_crash">Gehitu kraskatzeari buruzko datu anonimoak</string>
<string name="include_debug_report_feedback">Gehitu gailu honi buruzko datu anonimoak</string>
<string name="could_not_load_report_data">Ezin izan dira txostenaren datuak kargatu.</string>
<string name="send_report">Bidali txostena</string>
<string name="close">Itxi</string>
<string name="dev_report_saved">Txostena gordeta. Briar-en saioa hasten duzun hurrengoan bidaliko da.</string>

View File

@@ -69,29 +69,19 @@
<string name="sign_out_button">خروج</string>
<!--Transports: Tor-->
<string name="transport_tor">اینترنت</string>
<string name="tor_device_status_offline">تلفن شما دارای دسترسی اینترنتی نیست</string>
<string name="tor_plugin_status_enabling">Briar در حال اتصال به اینترنت می باشد</string>
<string name="tor_plugin_status_active">Briar به اینترنت متصل شد</string>
<string name="tor_plugin_status_inactive">Briar نمی تواند به اینترنت متصل شود</string>
<string name="tor_plugin_status_disabled">Briar طوری پیکربندی شده تا از اینترنت استفاده نکند</string>
<string name="tor_plugin_status_disabled_mobile_data">Briar طوری پیکربندی شده تا از داده موبایل استفاده نکند</string>
<string name="tor_plugin_status_disabled_battery">Briar طوری پیکربندی شده تا از اینترنت در هنگام مصرف باتری استفاده نکند</string>
<string name="tor_plugin_status_disabled_country_blocked">Briar طوری پیکربندی شده تا از اینترنت در این کشور استفاده نکند</string>
<!--Transports: Wi-Fi-->
<string name="transport_lan">وای فای</string>
<string name="transport_lan_long">همان شبکه وای-فای</string>
<string name="lan_device_status_on">موبایل شما به وای-فای وصل می باشد</string>
<string name="lan_device_status_off">موبایل شما به وای-فای وصل نیست</string>
<string name="lan_plugin_status_enabling">Briar در حال اتصال به شبکه وای-فای می باشد</string>
<string name="lan_plugin_status_active">Briar به شبکه وای-فای متصل می باشد</string>
<string name="lan_plugin_status_inactive">Briar نمی‌تواند به شبکه وای-فای وصل شود</string>
<string name="lan_plugin_status_disabled">Briar طوری پیکربندی شده تا از شبکه وای-فای استفاده نکند</string>
<!--Transports: Bluetooth-->
<string name="transport_bt">بلوتوث</string>
<string name="bt_device_status_on">بلوتوث موبایل شما روشن می باشد</string>
<string name="bt_device_status_off">بلوتوث موبایل شما خاموش می باشد</string>
<string name="bt_plugin_status_enabling">Briar در حال اتصال به بلوتوث می باشد</string>
<string name="bt_plugin_status_active">Briar به بلوتوث وصل می باشد</string>
<string name="bt_plugin_status_inactive">Briar نمی تواند به بلوتوث وصل شود</string>
<string name="bt_plugin_status_disabled">Briar طوری پیکربندی شده که از بلوتوث استفاده نکند</string>
<!--Notifications-->
@@ -568,18 +558,9 @@
<string name="optional_contact_email">آدرس ایمیل شما (اختیاری)</string>
<string name="include_debug_report_crash">قرار دادن داده های ناشناس مربوط به خرابی</string>
<string name="include_debug_report_feedback">قرار دادن داده های ناشناس درباره این دستگاه</string>
<string name="dev_report_basic_info">اطلاعات پایه</string>
<string name="dev_report_device_info">اطلاعات دستگاه</string>
<string name="dev_report_time_info">اطلاعات زمانی</string>
<string name="dev_report_memory">حافظه</string>
<string name="dev_report_storage">حافظه</string>
<string name="dev_report_connectivity">اتصال</string>
<string name="dev_report_build_config">پیکربندی ساخت</string>
<string name="dev_report_device_features">ویژگی‌های دستگاه</string>
<string name="could_not_load_report_data">امکان بارگذاری داده های گزارش وجود ندارد.</string>
<string name="send_report">ارسال گزارش</string>
<string name="close">بستن</string>
<string name="dev_report_sending">در حال فرستادن نظر...</string>
<string name="dev_report_sent">بازخورد ارسال شد</string>
<string name="dev_report_saved">گزارش ذخیره شد. دفعه بعدی که وارد Briar (برایر) شدید فرستاده خواهد شد.</string>
<!--Sign Out-->
<string name="progress_title_logout">خروج از Briar (برایر)...</string>

View File

@@ -531,19 +531,10 @@
<string name="optional_contact_email">Votre adresse courriel (facultative)</string>
<string name="include_debug_report_crash">Inclure des données anonymes concernant le plantage</string>
<string name="include_debug_report_feedback">Inclure des données anonymes concernant cet appareil</string>
<string name="dev_report_basic_info">Informations de base</string>
<string name="dev_report_device_info">Informations sur l\'appareil</string>
<string name="dev_report_time_info">Informations temporelles</string>
<string name="dev_report_memory">Mémoire</string>
<string name="dev_report_storage">Stockage</string>
<string name="dev_report_connectivity">Connectivité</string>
<string name="dev_report_device_features">Fonctionnalités de l\'appareil</string>
<string name="could_not_load_report_data">Impossible de charger les données du rapport.</string>
<string name="send_report">Envoyer le rapport</string>
<string name="close">Fermer</string>
<string name="dev_report_sending">Envoi de la rétroaction…</string>
<string name="dev_report_sent">Rétroaction envoyée avec succès</string>
<string name="dev_report_saved">Le rapport a été enregistré. Il sera envoyé lors de votre prochaine connexion à Briar.</string>
<string name="dev_report_error">Erreur : l\'envoi du signalement a échoué</string>
<!--Sign Out-->
<string name="progress_title_logout">Déconnexion de Briar…</string>
<!--Screen Filters & Tapjacking-->

View File

@@ -531,30 +531,16 @@
<string name="optional_contact_email">O seu enderezo e-mail (optativo)</string>
<string name="include_debug_report_crash">Incluír datos anónimos sobre o fallo</string>
<string name="include_debug_report_feedback">Incluír datos anónimos sobre este dispositivo</string>
<string name="dev_report_basic_info">Información básica</string>
<string name="dev_report_device_info">Información do dispositivo</string>
<string name="dev_report_stacktrace">Trazas</string>
<string name="dev_report_time_info">Información da hora</string>
<string name="dev_report_memory">Memoria</string>
<string name="dev_report_storage">Almacenaxe</string>
<string name="dev_report_connectivity">Conectividade</string>
<string name="dev_report_build_config">Configuración da compilación</string>
<string name="dev_report_logcat">Rexistro da app</string>
<string name="dev_report_device_features">Características do dispositivo</string>
<string name="could_not_load_report_data">Non se puideron cargar os datos do informe.</string>
<string name="send_report">Enviar informe</string>
<string name="close">Pechar</string>
<string name="dev_report_sending">Enviando comentarios...</string>
<string name="dev_report_sent">Comentarios enviados</string>
<string name="dev_report_saved">Informe gardado. Enviarase a seguinte vez que se conecte con Briar.</string>
<string name="dev_report_error">Erro: fallou o envío do informe</string>
<!--Sign Out-->
<string name="progress_title_logout">Desconectando de Briar...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Detectouse unha sobreescrita da pantalla</string>
<string name="screen_filter_body">Outra aplicación estase a amosar enriba de Briar. Para protexer a súa seguridade, Briar non responderá a toques cando outra aplicación está debuxando enriba.\n\nAs seguintes aplicacións poderían estar debuxando enriba:\n\n%1$s</string>
<string name="screen_filter_body_api_30">Outra app ten acceso a ver a pantalla enriba de Briar. Para protexer a túa seguridade, Briar non vai responder aos toques cando outra app ten acceso a pantalla.\n\nRevisa as app de abaixo para atopara a resposable.</string>
<string name="screen_filter_allow">Permitir a estas aplicación amosarse enriba</string>
<string name="screen_filter_review_apps">Revisar apps</string>
<!--Permission Requests-->
<string name="permission_camera_title">Permiso da cámara</string>
<string name="permission_camera_request_body">Para escanear códigos QR, Briar precisa acceso a cámara.</string>

View File

@@ -555,29 +555,16 @@
<string name="optional_contact_email">כתובת הדוא״ל שלך (רשותי)</string>
<string name="include_debug_report_crash">כלול נתונים אלמוניים לגבי הקריסה</string>
<string name="include_debug_report_feedback">כלול נתונים אלמוניים לגבי מכשיר זה</string>
<string name="dev_report_basic_info">מידע בסיסי</string>
<string name="dev_report_device_info">מידע מכשיר</string>
<string name="dev_report_time_info">מידע זמן</string>
<string name="dev_report_memory">זיכרון</string>
<string name="dev_report_storage">אחסון</string>
<string name="dev_report_connectivity">קישוריות</string>
<string name="dev_report_build_config">תצורת בנייה</string>
<string name="dev_report_logcat">יומן יישום</string>
<string name="dev_report_device_features">מאפייני מכשיר</string>
<string name="could_not_load_report_data">לא היה ניתן לטעון נתוני דוח.</string>
<string name="send_report">שלח דוח</string>
<string name="close">סגור</string>
<string name="dev_report_sending">שולח משוב…</string>
<string name="dev_report_sent">משוב נשלח</string>
<string name="dev_report_saved">הדוח נשמר. הוא יישלח בפעם הבאה שתתחבר אל Briar.</string>
<string name="dev_report_error">שגיאה: שליחת דוח נכשלה</string>
<!--Sign Out-->
<string name="progress_title_logout">מתנתק מן Briar…</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">ציפוי מסך התגלה</string>
<string name="screen_filter_body">יישום אחר מציירת מעל Briar. כדי להגן על אבטחתך, Briar לא יגיב לנגיעות כאשר יישום אחר מצייר מעל.\n\nהיישומים הבאים יכולים לצייר מעל:\n\n%1$s</string>
<string name="screen_filter_body_api_30">יישום אחר מצייר מעל Briar. כדי להגן על אבטחתך, Briar לא יגיב לנגיעות כאשר יישום אחר מצייר מעל.\n\nסקור יישומים למטה כדי למצוא את היישום האחראי.</string>
<string name="screen_filter_allow">התר ליישומים אלו לצייר מעל</string>
<string name="screen_filter_review_apps">סקור יישומים</string>
<!--Permission Requests-->
<string name="permission_camera_title">הרשאת מצלמה</string>
<string name="permission_camera_request_body">כדי לסרוק את קוד ה־QR, היישום Briar צריך גישה אל המצלמה.</string>
@@ -586,7 +573,6 @@
<string name="permission_camera_location_title">מצלמה ומיקום</string>
<string name="permission_camera_location_request_body">כדי לסרוק את קוד ה־QR, היישום Briar צריך הרשאה אל המצלמה.\n\nכדי לגלות מכשירי שן־כחולה, Briar צריך הרשאה להשיג גישה אל מיקומך.\n\nBriar אינו מאחסן את מיקומך או משתף אותו עם אף אחד.</string>
<string name="permission_camera_denied_body">דחית גישה אל המצלמה, אבל הוספת אנשי קשר דורשת שימוש במצלמה.\n\nאנא שקול הענקת גישה.</string>
<string name="permission_location_denied_body">דחית גישה אל המיקום שלך, אבל Briar צריך הרשאה זו כדי לגלות מכשירי Bluetooth.\n\nאנא שקול להעניק גישה.</string>
<string name="qr_code">קוד QR</string>
<string name="show_qr_code_fullscreen">הראה קוד QR במסך מלא</string>
<!--App Locking-->

View File

@@ -479,6 +479,7 @@
<string name="optional_contact_email">आपका ईमेल पता (वैकल्पिक)</string>
<string name="include_debug_report_crash">दुर्घटना के बारे में अनाम डेटा शामिल करें</string>
<string name="include_debug_report_feedback">इस डिवाइस के बारे में अनाम डेटा शामिल करें</string>
<string name="could_not_load_report_data">रिपोर्ट डेटा लोड नहीं किया जा सका</string>
<string name="send_report">रिपोर्ट भेजो</string>
<string name="close">बंद करे</string>
<string name="dev_report_saved">रिपोर्ट सहेजी गई अगली बार जब आप Briar में प्रवेश करेंगे तो उसे भेजा जाएगा।</string>

View File

@@ -540,30 +540,16 @@ Vigyázat: Ez végleg törli az identitásait, kapcsolatait és üzeneteit</stri
<string name="optional_contact_email">Email címe (opcionális)</string>
<string name="include_debug_report_crash">Névtelen adat beágyazása az összeomlásról</string>
<string name="include_debug_report_feedback">Névtelen adat beágyazása az eszközről</string>
<string name="dev_report_basic_info">Alapinformáció</string>
<string name="dev_report_device_info">Eszköz információ</string>
<string name="dev_report_stacktrace">Stacktrace</string>
<string name="dev_report_time_info">Idő információ</string>
<string name="dev_report_memory">Memória</string>
<string name="dev_report_storage">Tárhely</string>
<string name="dev_report_connectivity">Csatlakozódás</string>
<string name="dev_report_build_config">Build konfiguráció</string>
<string name="dev_report_logcat">App log</string>
<string name="dev_report_device_features">Eszköz szolgáltatások</string>
<string name="could_not_load_report_data">Nem sikerült a jelentés adatot betölteni.</string>
<string name="send_report">Jelentés elküldése</string>
<string name="close">Bezár</string>
<string name="dev_report_sending">Visszajelzés küldése...</string>
<string name="dev_report_sent">Visszajelzés elküldve</string>
<string name="dev_report_saved">A jelentés mentve. Elküldésre kerül akkor, amikor legközelebb bejelentkezik a Briar-ba.</string>
<string name="dev_report_error">Hiba: Riport küldése sikertelen</string>
<!--Sign Out-->
<string name="progress_title_logout">Kilépés a Briar-ból...</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Képernyő felülírás észlelve </string>
<string name="screen_filter_body">Egy másik app a Briar tetejére rajzol. A biztonsága védelme érdekében a Briar nem válaszol az érintésre, ha egy másik app rajzol a tetejére.\n\nA következő app lehet, ami a tetejére rajzol:\n\n%1$s</string>
<string name="screen_filter_body_api_30">Egy másik app a Briar tetejére rajzol. A biztonsága védelme érdekében a Briar nem válaszol az érintésre, ha egy másik app rajzol a tetejére.\n\nTekintsd át az appokat, hogy megtaláld az ezért felelőst.</string>
<string name="screen_filter_allow">Az appok engedélyezése a felé rajzolásra</string>
<string name="screen_filter_review_apps">Appok áttekintése</string>
<!--Permission Requests-->
<string name="permission_camera_title">Kamera jogosultságok</string>
<string name="permission_camera_request_body">A QR kód olvasáshoz a Briar-nak szüksége van kamera hozzáférésre.</string>
@@ -572,7 +558,6 @@ Vigyázat: Ez végleg törli az identitásait, kapcsolatait és üzeneteit</stri
<string name="permission_camera_location_title">Kamera és lokáció</string>
<string name="permission_camera_location_request_body">A QR kód beszkenneléséhez a Briar-nak szüksége van a Kamerához hozzáférésre.\n\nA Bluetooth eszközök észleléséhez a Briar-nak szükségve van a lokációhoz hozzáférésre.\n\nA Briar nem tárolja lokációját vagy ossza meg bárkivel.</string>
<string name="permission_camera_denied_body">Megtiltotta hozzáférést a kamerához, de a kapcsolatok hozzáadásához szükséges a kamera.\n\nKérjük gondolja meg a jog megadását.</string>
<string name="permission_location_denied_body">Megtiltotta hozzáférést a helyhez, azonban a Briar-nak szüksége van erre, hogy detektálja a Bluetooth eszközöket.\n\nKérjük gondolja meg a jog megadását.</string>
<string name="qr_code">QR kód</string>
<string name="show_qr_code_fullscreen">A QR kód teljes képernyősen</string>
<!--App Locking-->
@@ -586,15 +571,15 @@ Vigyázat: Ez végleg törli az identitásait, kapcsolatait és üzeneteit</stri
<string name="transports_help_text">A Briar Interneten, Wi-Fi-n vagy Bluetooth-on keresztül csatlakozhat kapcsolataihoz.\n\nAz összes internetkapcsolat a Tor hálózaton megy keresztül megy az adatvédelem érdekében.\n\nHa egy kapcsolatot több módszerrel is el lehet érni, Briar párhuzamosan használja azokat.</string>
<!--Screenshots-->
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_alice">Alíz</string>
<string name="screenshot_alice">Alice</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_bob">Bob</string>
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
<string name="screenshot_carol">Carol</string>
<!--This is a message to be used in screenshots. Please use the same translation for Bob!-->
<string name="screenshot_message_1">Szia Bob!</string>
<string name="screenshot_message_1">Hi Bob!</string>
<!--This is a message to be used in screenshots. Please use the same translation for Alice!-->
<string name="screenshot_message_2">Szia Alíz! Köszönöm, hogy megemlítetted nekem a Briar-t!</string>
<string name="screenshot_message_2">Hi Alice! Köszönöm hogy említette nekem a Briar-t!</string>
<!--This is a message to be used in screenshots.-->
<string name="screenshot_message_3">Szívesen, remélem tetszeni fog 😀</string>
</resources>

View File

@@ -531,30 +531,16 @@
<string name="optional_contact_email">Tölvupóstfangið þitt (valfrjálst)</string>
<string name="include_debug_report_crash">Senda nafnlaus gögn um hrunið</string>
<string name="include_debug_report_feedback">Senda nafnlaus gögn um tækið</string>
<string name="dev_report_basic_info">Grunnupplýsingar</string>
<string name="dev_report_device_info">Upplýsingar um tæki</string>
<string name="dev_report_stacktrace">Stacktrace-rakning</string>
<string name="dev_report_time_info">Tímaupplýsingar</string>
<string name="dev_report_memory">Minni</string>
<string name="dev_report_storage">Geymslurými</string>
<string name="dev_report_connectivity">Tengingar</string>
<string name="dev_report_build_config">Byggingaruppsetning</string>
<string name="dev_report_logcat">Atvikaskrá forrits</string>
<string name="dev_report_device_features">Eiginleikar tækis</string>
<string name="could_not_load_report_data">Gat ekki hlaðið inn gögnum skýrslunnar.</string>
<string name="send_report">Senda skýrslu</string>
<string name="close">Loka</string>
<string name="dev_report_sending">Sendi umsögn…</string>
<string name="dev_report_sent">Umsögn send</string>
<string name="dev_report_saved">Skýrsla vistuð. Hún verður send næst þegar þú skráir þig inn í Briar.</string>
<string name="dev_report_error">Villa: Sending skýrslu mistókst</string>
<!--Sign Out-->
<string name="progress_title_logout">Skrái út úr Briar…</string>
<!--Screen Filters & Tapjacking-->
<string name="screen_filter_title">Skjáyfirlag fannst</string>
<string name="screen_filter_body">Annað forrit er að birta upplýsingar ofan á Briar. Í öryggisskyni mun Briar ekki bregðast við snertingum þegar annað forrit teiknar ofaná það.\n\nEftirfarandi forrit gætu verið að birta upplýsingar ofan á:\n\n%1$s</string>
<string name="screen_filter_body_api_30">Annað forrit er að birta upplýsingar ofan á Briar. Í öryggisskyni mun Briar ekki bregðast við snertingum þegar annað forrit teiknar ofaná það.\n\nSkoðaðu eftirfarandi forrit til að finna það þeirra sem gæti valdið þessu.</string>
<string name="screen_filter_allow">Leyfa þessum forritum að birta upplýsingar efst</string>
<string name="screen_filter_review_apps">Yfirfara forrit</string>
<!--Permission Requests-->
<string name="permission_camera_title">Heimildir á myndavél</string>
<string name="permission_camera_request_body">Til að skanna QR-kóðann þarf Briar heimild til að nota myndavélina.</string>

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