Compare commits

..

9 Commits

Author SHA1 Message Date
Daniel Lublin
bb8333660b Remember to return 2021-01-29 11:02:13 +01:00
Daniel Lublin
9925c799a1 WIP add old BT-diagram to BT-setup screen 2021-01-28 15:55:26 +01:00
Daniel Lublin
3ee2d2112c WIP 2021-01-28 11:08:34 +01:00
Daniel Lublin
da5b2c194a WIP 2021-01-28 11:08:33 +01:00
Daniel Lublin
244d03a718 WIP mock 2021-01-28 11:08:32 +01:00
Torsten Grote
5c1bcdeb9d Merge branch 'update-bridges' into 'master'
Update bridges

See merge request briar/briar!1352
2021-01-26 14:11:00 +00:00
akwizgran
6c1f5450cb Add run configuration for BridgeTest. 2021-01-26 13:57:33 +00:00
akwizgran
0d070cf422 Change dummy address for meek bridge.
See https://gitweb.torproject.org/builders/tor-browser-build.git/commit/projects/tor-browser/Bundle-Data/PTConfigs/bridge_prefs.js?id=8bd845464ae14bf56e0187dfa6f6e773a6593f55
2021-01-26 13:53:51 +00:00
akwizgran
d34d66c691 Update list of obfs4 bridges. 2021-01-26 13:51:41 +00:00
74 changed files with 3004 additions and 1603 deletions

24
.idea/runConfigurations/BridgeTest.xml generated Normal file
View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BridgeTest" type="AndroidJUnit" factoryName="Android JUnit" nameIsGenerated="true">
<module name="briar.bramble-java" />
<useClassPathOnly />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="org.briarproject.bramble.plugin.tor.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="org.briarproject.bramble.plugin.tor" />
<option name="MAIN_CLASS_NAME" value="org.briarproject.bramble.plugin.tor.BridgeTest" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="class" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
<envs>
<env name="OPTIONAL_TESTS" value="org.briarproject.bramble.plugin.tor.BridgeTest" />
</envs>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -1,6 +0,0 @@
package org.briarproject.bramble.api;
public interface ThrowingRunnable<T extends Throwable> {
void run() throws T;
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.ThrowingRunnable;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseComponent;

View File

@@ -1,6 +1,5 @@
package org.briarproject.bramble.sync;
import org.briarproject.bramble.api.ThrowingRunnable;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.db.DatabaseComponent;

View File

@@ -0,0 +1,6 @@
package org.briarproject.bramble.sync;
interface ThrowingRunnable<T extends Throwable> {
void run() throws T;
}

View File

@@ -1,5 +1,14 @@
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
Bridge obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
Bridge obfs4 52.15.78.72:9443 02069A3C5362476936B62BA6F5ACC41ABD573A9B cert=ijYG/OKc7kqu2YzKNFfeXN7/BG2BOgfEP2KyYEiGDQthnHbsOiTWHeIG0WJVW+BckzDgKw iat-mode=0
Bridge obfs4 13.58.29.242:9443 0C58939A77DA6B6B29D4B5236A75865659607AE0 cert=OylWIEHb/ezpq1zWxW0sgKRn+9ARH2eOcQOZ8/Gew+4l+oKOhQ2jUX/Y+FSl61JorXZUWA iat-mode=0
Bridge obfs4 45.33.37.112:9443 60A609BB4ABE8D46E634AE81ED29ADAB7776B399 cert=t5v19WmNv5Sc2YPNr8RQids365W7MY8zJwQVkOxBjUMFomMWARDzsbYpcWLLcw0J9Gm+BQ iat-mode=0
Bridge meek_lite 0.0.2.0:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com

View File

@@ -448,6 +448,12 @@
android:label="@string/pending_contact_requests"
android:theme="@style/BriarTheme" />
<activity
android:name=".android.bluetoothsetup.BluetoothSetupActivity"
android:label="Bluetooth Setup"
android:theme="@style/BriarTheme"
android:windowSoftInputMode="adjustResize|stateHidden" />
</application>
<queries>

View File

@@ -30,16 +30,15 @@ import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.DozeHelperModule;
import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.account.SetupModule;
import org.briarproject.briar.android.bluetoothsetup.BluetoothSetupModule;
import org.briarproject.briar.android.contact.ContactListModule;
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.conversation.GroupConversationModule;
import org.briarproject.briar.android.settings.SettingsModule;
import org.briarproject.briar.android.privategroup.list.GroupListModule;
import org.briarproject.briar.android.reporting.DevReportModule;
import org.briarproject.briar.android.sharing.SharingModule;
import org.briarproject.briar.android.settings.SettingsModule;
import org.briarproject.briar.android.test.TestAvatarCreatorImpl;
import org.briarproject.briar.android.viewmodel.ViewModelModule;
import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -80,11 +79,10 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
SettingsModule.class,
DevReportModule.class,
ContactListModule.class,
BluetoothSetupModule.class,
// below need to be within same scope as ViewModelProvider.Factory
ForumModule.class,
ForumModule.BindsModule.class,
GroupListModule.class,
GroupConversationModule.class,
SharingModule.class,
})
public class AppModule {

View File

@@ -20,6 +20,10 @@ import org.briarproject.briar.android.blog.ReblogFragment;
import org.briarproject.briar.android.blog.RssFeedImportActivity;
import org.briarproject.briar.android.blog.RssFeedManageActivity;
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
import org.briarproject.briar.android.bluetoothsetup.BluetoothSetupActivity;
import org.briarproject.briar.android.bluetoothsetup.BluetoothSetupChooseFragment;
import org.briarproject.briar.android.bluetoothsetup.BluetoothSetupPendingFragment;
import org.briarproject.briar.android.bluetoothsetup.BluetoothSetupStartFragment;
import org.briarproject.briar.android.contact.ContactListFragment;
import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
import org.briarproject.briar.android.contact.add.remote.LinkExchangeFragment;
@@ -32,6 +36,7 @@ import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity;
import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.forum.ForumListFragment;
import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.introduction.ContactChooserFragment;
import org.briarproject.briar.android.introduction.IntroductionActivity;
@@ -49,6 +54,7 @@ import org.briarproject.briar.android.navdrawer.TransportsActivity;
import org.briarproject.briar.android.panic.PanicPreferencesActivity;
import org.briarproject.briar.android.panic.PanicResponderActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupModule;
@@ -87,10 +93,12 @@ import dagger.Component;
ActivityModule.class,
BlogModule.class,
CreateGroupModule.class,
ForumModule.class,
GroupInvitationModule.class,
GroupConversationModule.class,
GroupMemberModule.class,
GroupRevealModule.class,
SharingModule.SharingLegacyModule.class
SharingModule.class
}, dependencies = AndroidComponent.class)
public interface ActivityComponent {
@@ -182,6 +190,8 @@ public interface ActivityComponent {
void inject(CrashReportActivity crashReportActivity);
void inject(BluetoothSetupActivity activity);
// Fragments
void inject(AuthorNameFragment fragment);
@@ -238,4 +248,10 @@ public interface ActivityComponent {
void inject(ConfirmAvatarDialogFragment fragment);
void inject(BluetoothSetupStartFragment fragment);
void inject(BluetoothSetupChooseFragment fragment);
void inject(BluetoothSetupPendingFragment fragment);
}

View File

@@ -14,6 +14,7 @@ import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.Localizer;
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.util.UiUtils;
@@ -86,6 +87,7 @@ public abstract class BaseActivity extends AppCompatActivity
activityComponent = DaggerActivityComponent.builder()
.androidComponent(applicationComponent)
.activityModule(getActivityModule())
.forumModule(getForumModule())
.build();
injectActivity(activityComponent);
super.onCreate(state);
@@ -120,6 +122,11 @@ 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();
}
@Override
protected void onStart() {
super.onStart();

View File

@@ -160,6 +160,7 @@ public abstract class BriarActivity extends BaseActivity {
* @param ownLayout true if the custom toolbar brings its own layout
* @return the Toolbar object or null if content view did not contain one
*/
@Nullable
protected Toolbar setUpCustomToolbar(boolean ownLayout) {
// Custom Toolbar
Toolbar toolbar = findViewById(R.id.toolbar);

View File

@@ -0,0 +1,60 @@
package org.briarproject.briar.android.bluetoothsetup;
import android.os.Bundle;
import android.view.MenuItem;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.ViewModelProvider;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BluetoothSetupActivity extends BriarActivity implements
BaseFragmentListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
private BluetoothSetupViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_fragment_container);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayHomeAsUpEnabled(true);
}
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(BluetoothSetupViewModel.class);
if (state == null) {
showInitialFragment(new BluetoothSetupStartFragment());
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,72 @@
package org.briarproject.briar.android.bluetoothsetup;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
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.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BluetoothSetupChooseFragment extends BaseFragment {
private static final String TAG =
BluetoothSetupChooseFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private BluetoothSetupViewModel viewModel;
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (getActivity() == null || getContext() == null) return null;
viewModel = new ViewModelProvider(requireActivity())
.get(BluetoothSetupViewModel.class);
View v =
inflater.inflate(R.layout.fragment_bluetooth_setup_choose,
container, false);
// TODO to enable when user picks a device from list
Button continueButton = v.findViewById(R.id.continueButton);
continueButton.setOnClickListener(view -> {
showNextFragment(new BluetoothSetupPendingFragment());
});
continueButton.setEnabled(true);
// RecyclerView devices = v.findViewById(R.id.devices);
// devices.setHasFixedSize(true);
// final LinearLayoutManager layoutManager =
// new LinearLayoutManager(getActivity(),
// LinearLayoutManager.VERTICAL, false);
// devices.setLayoutManager(layoutManager);
return v;
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.briar.android.bluetoothsetup;
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 BluetoothSetupModule {
@Binds
@IntoMap
@ViewModelKey(BluetoothSetupViewModel.class)
abstract ViewModel bindBluetoothSetupViewModel(
BluetoothSetupViewModel bluetoothSetupViewModel);
}

View File

@@ -0,0 +1,59 @@
package org.briarproject.briar.android.bluetoothsetup;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BluetoothSetupPendingFragment extends BaseFragment {
private static final String TAG =
BluetoothSetupPendingFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private BluetoothSetupViewModel viewModel;
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (getActivity() == null || getContext() == null) return null;
viewModel = new ViewModelProvider(requireActivity())
.get(BluetoothSetupViewModel.class);
View v = inflater.inflate(R.layout.fragment_bluetooth_setup_pending,
container, false);
return v;
}
private void onContinueButtonClicked() {
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.briar.android.bluetoothsetup;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
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.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class BluetoothSetupStartFragment extends BaseFragment {
private static final String TAG =
BluetoothSetupStartFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private BluetoothSetupViewModel viewModel;
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (getActivity() == null || getContext() == null) return null;
viewModel = new ViewModelProvider(requireActivity())
.get(BluetoothSetupViewModel.class);
View v = inflater.inflate(R.layout.fragment_bluetooth_setup_start,
container, false);
// TODO device-BT and BT-plugin needs to be enabled at this point
Button startButton = v.findViewById(R.id.startButton);
startButton.setOnClickListener(view -> {
showNextFragment(new BluetoothSetupChooseFragment());
});
startButton.setEnabled(true);
return v;
}
}

View File

@@ -0,0 +1,33 @@
package org.briarproject.briar.android.bluetoothsetup;
import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor;
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 java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Logger.getLogger;
@NotNullByDefault
public class BluetoothSetupViewModel extends DbViewModel {
private final static Logger LOG =
getLogger(BluetoothSetupViewModel.class.getName());
@Inject
BluetoothSetupViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
}
}

View File

@@ -15,6 +15,7 @@ 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.bluetoothsetup.BluetoothSetupActivity;
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
@@ -134,6 +135,11 @@ public class ContactListFragment extends BaseFragment
case R.id.action_add_contact_remotely:
startActivity(
new Intent(getContext(), AddContactActivity.class));
return;
case R.id.action_bluetooth_setup:
startActivity(
new Intent(getContext(),
BluetoothSetupActivity.class));
}
}

View File

@@ -45,8 +45,10 @@ import androidx.arch.core.util.Function;
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;
@NotNullByDefault
@@ -171,9 +173,14 @@ class ContactListViewModel extends DbViewModel implements EventListener {
}
void checkForPendingContacts() {
runOnDbThreadOrLogException(() -> {
boolean hasPending = !contactManager.getPendingContacts().isEmpty();
hasPendingContacts.postValue(hasPending);
runOnDbThread(() -> {
try {
boolean hasPending =
!contactManager.getPendingContacts().isEmpty();
hasPendingContacts.postValue(hasPending);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}

View File

@@ -2,9 +2,12 @@ package org.briarproject.briar.android.contact.add.remote;
import android.app.Application;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UnsupportedVersionException;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchPendingContactException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
@@ -15,6 +18,7 @@ import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import java.security.GeneralSecurityException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
@@ -62,10 +66,15 @@ public class AddContactViewModel extends DbViewModel {
}
private void loadHandshakeLink() {
// If an exception is thrown the UI should stay disabled,
// leaving the user unable to proceed
runOnDbThreadOrLogException(() ->
handshakeLink.postValue(contactManager.getHandshakeLink()));
runOnDbThread(() -> {
try {
handshakeLink.postValue(contactManager.getHandshakeLink());
} catch (DbException e) {
logException(LOG, WARNING, e);
// the UI should stay disabled in this case,
// leaving the user unable to proceed
}
});
}
LiveData<String> getHandshakeLink() {
@@ -97,11 +106,17 @@ public class AddContactViewModel extends DbViewModel {
void addContact(String nickname) {
if (remoteHandshakeLink == null) throw new IllegalStateException();
runOnDbThread(() -> {
contactManager.addPendingContact(remoteHandshakeLink, nickname);
addContactResult.postValue(new LiveResult<>(true));
}, e -> {
logException(LOG, WARNING, e);
addContactResult.postValue(new LiveResult<>(e));
try {
contactManager.addPendingContact(remoteHandshakeLink, nickname);
addContactResult.postValue(new LiveResult<>(true));
} catch (UnsupportedVersionException e) {
logException(LOG, WARNING, e);
addContactResult.postValue(new LiveResult<>(e));
} catch (DbException | FormatException
| GeneralSecurityException e) {
logException(LOG, WARNING, e);
addContactResult.postValue(new LiveResult<>(e));
}
});
}
@@ -111,13 +126,13 @@ public class AddContactViewModel extends DbViewModel {
public void updatePendingContact(String name, PendingContact p) {
runOnDbThread(() -> {
contactManager.removePendingContact(p.getId());
addContact(name);
}, e -> {
if (e instanceof NoSuchPendingContactException) {
try {
contactManager.removePendingContact(p.getId());
addContact(name);
} catch (NoSuchPendingContactException e) {
logException(LOG, WARNING, e);
// no error in UI as pending contact was converted into contact
} else {
} catch (DbException e) {
logException(LOG, WARNING, e);
addContactResult.postValue(new LiveResult<>(e));
}

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.contact.PendingContactState;
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;
@@ -32,8 +33,10 @@ import javax.inject.Inject;
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.api.contact.PendingContactState.OFFLINE;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
public class PendingContactListViewModel extends DbViewModel
@@ -87,20 +90,24 @@ public class PendingContactListViewModel extends DbViewModel
}
private void loadPendingContacts() {
runOnDbThreadOrLogException(() -> {
Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = pairs.isEmpty();
for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond();
long lastPoll = rendezvousPoller.getLastPollTime(p.getId());
items.add(new PendingContactItem(p, state, lastPoll));
online = online || state != OFFLINE;
runOnDbThread(() -> {
try {
Collection<Pair<PendingContact, PendingContactState>> pairs =
contactManager.getPendingContacts();
List<PendingContactItem> items = new ArrayList<>(pairs.size());
boolean online = pairs.isEmpty();
for (Pair<PendingContact, PendingContactState> pair : pairs) {
PendingContact p = pair.getFirst();
PendingContactState state = pair.getSecond();
long lastPoll = rendezvousPoller.getLastPollTime(p.getId());
items.add(new PendingContactItem(p, state, lastPoll));
online = online || state != OFFLINE;
}
pendingContacts.postValue(items);
hasInternetConnection.postValue(online);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
pendingContacts.postValue(items);
hasInternetConnection.postValue(online);
});
}
@@ -109,8 +116,13 @@ public class PendingContactListViewModel extends DbViewModel
}
void removePendingContact(PendingContactId id) {
runOnDbThreadOrLogException(() ->
contactManager.removePendingContact(id));
runOnDbThread(() -> {
try {
contactManager.removePendingContact(id);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
LiveData<Boolean> getHasInternetConnection() {

View File

@@ -7,7 +7,6 @@ import java.util.Collection;
import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault
public interface SharingController {

View File

@@ -18,7 +18,6 @@ import javax.inject.Inject;
import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault
public class SharingControllerImpl implements SharingController, EventListener {

View File

@@ -149,7 +149,7 @@ public class ConversationViewModel extends DbViewModel
AttachmentReceivedEvent a = (AttachmentReceivedEvent) e;
if (a.getContactId().equals(contactId)) {
LOG.info("Attachment received");
runOnDbThreadOrLogException(() -> attachmentRetriever
runOnDbThread(() -> attachmentRetriever
.loadAttachmentItem(a.getMessageId()));
}
} else if (e instanceof AvatarUpdatedEvent) {
@@ -194,36 +194,44 @@ public class ConversationViewModel extends DbViewModel
private void loadContact(ContactId contactId) {
runOnDbThread(() -> {
long start = now();
Contact c = contactManager.getContact(contactId);
AuthorInfo authorInfo = authorManager.getAuthorInfo(c);
contactItem.postValue(new ContactItem(c, authorInfo));
logDuration(LOG, "Loading contact", start);
start = now();
checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start);
}, e -> {
if (e instanceof NoSuchContactException) {
try {
long start = now();
Contact c = contactManager.getContact(contactId);
AuthorInfo authorInfo = authorManager.getAuthorInfo(c);
contactItem.postValue(new ContactItem(c, authorInfo));
logDuration(LOG, "Loading contact", start);
start = now();
checkFeaturesAndOnboarding(contactId);
logDuration(LOG, "Checking for image support", start);
} catch (NoSuchContactException e) {
contactDeleted.postValue(true);
} else {
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
void markMessageRead(GroupId g, MessageId m) {
runOnDbThreadOrLogException(() -> {
long start = now();
messagingManager.setReadFlag(g, m, true);
logDuration(LOG, "Marking read", start);
runOnDbThread(() -> {
try {
long start = now();
messagingManager.setReadFlag(g, m, true);
logDuration(LOG, "Marking read", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
void setContactAlias(String alias) {
runOnDbThreadOrLogException(() -> {
contactManager.setContactAlias(requireNonNull(contactId),
alias.isEmpty() ? null : alias);
loadContact(contactId);
runOnDbThread(() -> {
try {
contactManager.setContactAlias(requireNonNull(contactId),
alias.isEmpty() ? null : alias);
loadContact(contactId);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@@ -319,17 +327,21 @@ public class ConversationViewModel extends DbViewModel
@UiThread
private void storeMessage(PrivateMessage m) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId());
runOnDbThreadOrLogException(() -> {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
m.hasText(), m.getAttachmentHeaders());
// TODO add text to cache when available here
addedHeader.postEvent(h);
runOnDbThread(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
m.hasText(), m.getAttachmentHeaders());
// TODO add text to cache when available here
addedHeader.postEvent(h);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@@ -371,7 +383,12 @@ public class ConversationViewModel extends DbViewModel
@UiThread
void recheckFeaturesAndOnboarding(ContactId contactId) {
runOnDbThreadOrLogException(() ->
checkFeaturesAndOnboarding(contactId));
runOnDbThread(() -> {
try {
checkFeaturesAndOnboarding(contactId);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
}

View File

@@ -6,6 +6,7 @@ import android.net.Uri;
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;
@@ -197,12 +198,14 @@ public class ImageViewModel extends DbViewModel implements EventListener {
private void saveImage(AttachmentItem attachment, OutputStreamProvider osp,
@Nullable Runnable afterCopy) {
runOnDbThread(() -> {
Attachment a =
attachmentReader.getAttachment(attachment.getHeader());
copyImageFromDb(a, osp, afterCopy);
}, e -> {
logException(LOG, WARNING, e);
saveState.postEvent(true);
try {
Attachment a =
attachmentReader.getAttachment(attachment.getHeader());
copyImageFromDb(a, osp, afterCopy);
} catch (DbException e) {
logException(LOG, WARNING, e);
saveState.postEvent(true);
}
});
}

View File

@@ -6,54 +6,52 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import org.briarproject.bramble.api.contact.ContactId;
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;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.forum.ForumController.ForumListener;
import org.briarproject.briar.android.sharing.ForumSharingStatusActivity;
import org.briarproject.briar.android.sharing.ShareForumActivity;
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadListActivity;
import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.forum.Forum;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM;
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ForumActivity extends
ThreadListActivity<ForumPostItem, ThreadItemAdapter<ForumPostItem>> {
ThreadListActivity<Forum, ForumPostItem, ThreadItemAdapter<ForumPostItem>>
implements ForumListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
private ForumViewModel viewModel;
ForumController forumController;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(ForumViewModel.class);
}
@Override
protected ThreadListViewModel<ForumPostItem> getViewModel() {
return viewModel;
}
@Override
protected ThreadItemAdapter<ForumPostItem> createAdapter() {
return new ThreadItemAdapter<>(this);
protected ThreadListController<Forum, ForumPostItem> getController() {
return forumController;
}
@Override
@@ -61,33 +59,36 @@ public class ForumActivity extends
super.onCreate(state);
Toolbar toolbar = setUpCustomToolbar(false);
// Open member list on Toolbar click
toolbar.setOnClickListener(v -> {
Intent i = new Intent(ForumActivity.this,
ForumSharingStatusActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
});
String groupName = getIntent().getStringExtra(GROUP_NAME);
if (groupName != null) {
setTitle(groupName);
} else {
observeOnce(viewModel.loadForum(), this, forum ->
setTitle(forum.getName())
);
Intent i = getIntent();
String groupName = i.getStringExtra(GROUP_NAME);
if (groupName != null) setTitle(groupName);
else loadNamedGroup();
// Open member list on Toolbar click
if (toolbar != null) {
toolbar.setOnClickListener(v -> {
Intent i1 = new Intent(ForumActivity.this,
ForumSharingStatusActivity.class);
i1.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i1);
});
}
}
@Override
public void onStart() {
super.onStart();
viewModel.clearForumPostNotification();
protected void onNamedGroupLoaded(Forum forum) {
setTitle(forum.getName());
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
protected ThreadItemAdapter<ForumPostItem> createAdapter(
LinearLayoutManager layoutManager) {
return new ThreadItemAdapter<>(this, layoutManager);
}
@Override
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data);
if (request == REQUEST_SHARE_FORUM && result == RESULT_OK) {
@@ -100,31 +101,32 @@ public class ForumActivity extends
// Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.forum_actions, menu);
super.onCreateOptionsMenu(menu);
return true;
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle presses on the action bar items
int itemId = item.getItemId();
if (itemId == R.id.action_forum_share) {
Intent i2 = new Intent(this, ShareForumActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_FORUM);
return true;
} else if (itemId == R.id.action_forum_sharing_status) {
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
return true;
} else if (itemId == R.id.action_forum_delete) {
showUnsubscribeDialog();
return true;
switch (item.getItemId()) {
case R.id.action_forum_share:
Intent i2 = new Intent(this, ShareForumActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_FORUM);
return true;
case R.id.action_forum_sharing_status:
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3);
return true;
case R.id.action_forum_delete:
showUnsubscribeDialog();
return true;
default:
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
@Override
@@ -133,7 +135,7 @@ public class ForumActivity extends
}
private void showUnsubscribeDialog() {
OnClickListener okListener = (dialog, which) -> viewModel.deleteForum();
OnClickListener okListener = (dialog, which) -> deleteForum();
AlertDialog.Builder builder = new AlertDialog.Builder(this,
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_leave_forum));
@@ -143,4 +145,27 @@ public class ForumActivity extends
builder.show();
}
private void deleteForum() {
forumController.deleteNamedGroup(
new UiResultExceptionHandler<Void, DbException>(this) {
@Override
public void onResultUi(Void v) {
Toast.makeText(ForumActivity.this,
R.string.forum_left_toast, LENGTH_SHORT).show();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onForumLeft(ContactId c) {
sharingController.remove(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.briar.android.forum;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.forum.Forum;
import androidx.annotation.UiThread;
@NotNullByDefault
interface ForumController extends ThreadListController<Forum, ForumPostItem> {
interface ForumListener extends ThreadListListener<ForumPostItem> {
@UiThread
void onForumLeft(ContactId c);
}
}

View File

@@ -0,0 +1,185 @@
package org.briarproject.briar.android.forum;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
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.Clock;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.forum.ForumController.ForumListener;
import org.briarproject.briar.android.threaded.ThreadListControllerImpl;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.forum.ForumManager;
import org.briarproject.briar.api.forum.ForumPost;
import org.briarproject.briar.api.forum.ForumPostHeader;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.lang.Math.max;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ForumControllerImpl extends
ThreadListControllerImpl<Forum, ForumPostItem, ForumPostHeader, ForumPost, ForumListener>
implements ForumController {
private static final Logger LOG =
Logger.getLogger(ForumControllerImpl.class.getName());
private final ForumManager forumManager;
private final ForumSharingManager forumSharingManager;
@Inject
ForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, IdentityManager identityManager,
@CryptoExecutor Executor cryptoExecutor,
ForumManager forumManager, ForumSharingManager forumSharingManager,
EventBus eventBus, Clock clock, MessageTracker messageTracker,
AndroidNotificationManager notificationManager) {
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
eventBus, clock, notificationManager, messageTracker);
this.forumManager = forumManager;
this.forumSharingManager = forumSharingManager;
}
@Override
public void onActivityStart() {
super.onActivityStart();
notificationManager.clearForumPostNotification(getGroupId());
}
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof ForumPostReceivedEvent) {
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
if (f.getGroupId().equals(getGroupId())) {
LOG.info("Forum post received, adding...");
listener.onItemReceived(buildItem(f.getHeader(), f.getText()));
}
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
ForumInvitationResponseReceivedEvent f =
(ForumInvitationResponseReceivedEvent) e;
ForumInvitationResponse r = f.getMessageHeader();
if (r.getShareableId().equals(getGroupId()) && r.wasAccepted()) {
LOG.info("Forum invitation was accepted");
listener.onInvitationAccepted(f.getContactId());
}
} else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent c = (ContactLeftShareableEvent) e;
if (c.getGroupId().equals(getGroupId())) {
LOG.info("Forum left by contact");
listener.onForumLeft(c.getContactId());
}
}
}
@Override
protected Forum loadNamedGroup() throws DbException {
return forumManager.getForum(getGroupId());
}
@Override
protected Collection<ForumPostHeader> loadHeaders() throws DbException {
return forumManager.getPostHeaders(getGroupId());
}
@Override
protected String loadMessageText(ForumPostHeader h) throws DbException {
return forumManager.getPostText(h.getId());
}
@Override
protected void markRead(MessageId id) throws DbException {
forumManager.setReadFlag(getGroupId(), id, true);
}
@Override
public void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
runOnDbThread(() -> {
try {
Collection<Contact> contacts =
forumSharingManager.getSharedWith(getGroupId());
Collection<ContactId> contactIds =
new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
handler.onResult(contactIds);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void createAndStoreMessage(String text,
@Nullable ForumPostItem parentItem,
ResultExceptionHandler<ForumPostItem, DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor author = identityManager.getLocalAuthor();
GroupCount count = forumManager.getGroupCount(getGroupId());
long timestamp = max(count.getLatestMsgTime() + 1,
clock.currentTimeMillis());
MessageId parentId = parentItem != null ?
parentItem.getId() : null;
createMessage(text, timestamp, parentId, author, handler);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
private void createMessage(String text, long timestamp,
@Nullable MessageId parentId, LocalAuthor author,
ResultExceptionHandler<ForumPostItem, DbException> handler) {
cryptoExecutor.execute(() -> {
LOG.info("Creating forum post...");
ForumPost msg = forumManager.createLocalPost(getGroupId(), text,
timestamp, parentId, author);
storePost(msg, text, handler);
});
}
@Override
protected ForumPostHeader addLocalMessage(ForumPost p) throws DbException {
return forumManager.addLocalPost(p);
}
@Override
protected void deleteNamedGroup(Forum forum) throws DbException {
forumManager.removeForum(forum);
}
@Override
protected ForumPostItem buildItem(ForumPostHeader header, String text) {
return new ForumPostItem(header, text);
}
}

View File

@@ -40,8 +40,10 @@ 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;
@@ -162,11 +164,15 @@ class ForumListViewModel extends DbViewModel implements EventListener {
}
void loadForumInvitations() {
runOnDbThreadOrLogException(() -> {
long start = now();
int available = forumSharingManager.getInvitations().size();
logDuration(LOG, "Loading available", start);
numInvitations.postValue(available);
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);
}
});
}

View File

@@ -1,23 +1,32 @@
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 interface ForumModule {
public class ForumModule {
@Binds
@IntoMap
@ViewModelKey(ForumListViewModel.class)
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
@Module
public interface BindsModule {
@Binds
@IntoMap
@ViewModelKey(ForumListViewModel.class)
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
}
@Binds
@IntoMap
@ViewModelKey(ForumViewModel.class)
ViewModel bindForumViewModel(ForumViewModel forumViewModel);
@ActivityScope
@Provides
ForumController provideForumController(BaseActivity activity,
ForumControllerImpl forumController) {
activity.addLifecycleController(forumController);
return forumController;
}
}

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.threaded.ThreadItem;
import org.briarproject.briar.api.forum.ForumPostHeader;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
@@ -16,4 +17,9 @@ class ForumPostItem extends ThreadItem {
h.getAuthorInfo(), h.isRead());
}
ForumPostItem(MessageId messageId, @Nullable MessageId parentId,
String text, long timestamp, Author author, AuthorInfo authorInfo) {
super(messageId, parentId, text, timestamp, author, authorInfo, true);
}
}

View File

@@ -1,203 +0,0 @@
package org.briarproject.briar.android.forum;
import android.app.Application;
import android.widget.Toast;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
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.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
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.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.R;
import org.briarproject.briar.android.sharing.SharingController;
import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.client.PostHeader;
import org.briarproject.briar.api.forum.Forum;
import org.briarproject.briar.api.forum.ForumInvitationResponse;
import org.briarproject.briar.api.forum.ForumManager;
import org.briarproject.briar.api.forum.ForumPost;
import org.briarproject.briar.api.forum.ForumPostHeader;
import org.briarproject.briar.api.forum.ForumSharingManager;
import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent;
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static android.widget.Toast.LENGTH_SHORT;
import static java.lang.Math.max;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
private static final Logger LOG = getLogger(ForumViewModel.class.getName());
private final ForumManager forumManager;
private final ForumSharingManager forumSharingManager;
@Inject
ForumViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
SharingController sharingController,
@CryptoExecutor Executor cryptoExecutor,
Clock clock,
MessageTracker messageTracker,
EventBus eventBus,
ForumManager forumManager,
ForumSharingManager forumSharingManager) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
identityManager, notificationManager, sharingController,
cryptoExecutor, clock, messageTracker, eventBus);
this.forumManager = forumManager;
this.forumSharingManager = forumSharingManager;
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ForumPostReceivedEvent) {
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
if (f.getGroupId().equals(groupId)) {
LOG.info("Forum post received, adding...");
ForumPostItem item = buildItem(f.getHeader(), f.getText());
addItem(item);
}
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
ForumInvitationResponseReceivedEvent f =
(ForumInvitationResponseReceivedEvent) e;
ForumInvitationResponse r = f.getMessageHeader();
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
LOG.info("Forum invitation was accepted");
sharingController.add(f.getContactId());
}
} else if (e instanceof ContactLeftShareableEvent) {
ContactLeftShareableEvent c = (ContactLeftShareableEvent) e;
if (c.getGroupId().equals(groupId)) {
LOG.info("Forum left by contact");
sharingController.remove(c.getContactId());
}
} else {
super.eventOccurred(e);
}
}
void clearForumPostNotification() {
notificationManager.clearForumPostNotification(groupId);
}
LiveData<Forum> loadForum() {
MutableLiveData<Forum> forum = new MutableLiveData<>();
runOnDbThreadOrLogException(() ->
forum.postValue(forumManager.getForum(groupId)));
return forum;
}
@Override
public void loadItems() {
loadList(txn -> {
long start = now();
List<ForumPostHeader> headers =
forumManager.getPostHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
return createItems(txn, headers, this::buildItem);
}, this::setItems);
}
@Override
public void createAndStoreMessage(String text,
@Nullable MessageId parentId) {
runOnDbThreadOrLogException(() -> {
LocalAuthor author = identityManager.getLocalAuthor();
GroupCount count = forumManager.getGroupCount(groupId);
long timestamp = max(count.getLatestMsgTime() + 1,
clock.currentTimeMillis());
createMessage(text, timestamp, parentId, author);
});
}
private void createMessage(String text, long timestamp,
@Nullable MessageId parentId, LocalAuthor author) {
cryptoExecutor.execute(() -> {
LOG.info("Creating forum post...");
ForumPost msg = forumManager.createLocalPost(groupId, text,
timestamp, parentId, author);
storePost(msg, text);
});
}
private void storePost(ForumPost msg, String text) {
runOnDbThreadOrLogException(() -> {
long start = now();
ForumPostHeader header = forumManager.addLocalPost(msg);
addItemAsync(buildItem(header, text));
logDuration(LOG, "Storing forum post", start);
});
}
private ForumPostItem buildItem(ForumPostHeader header, String text) {
return new ForumPostItem(header, text);
}
@Override
protected String loadMessageText(Transaction txn, PostHeader header)
throws DbException {
return forumManager.getPostText(txn, header.getId());
}
@Override
protected void markItemRead(ForumPostItem item) {
runOnDbThreadOrLogException(() ->
forumManager.setReadFlag(groupId, item.getId(), true));
}
public void loadSharingContacts() {
runOnDbThreadOrLogException(true, txn -> {
Collection<Contact> contacts =
forumSharingManager.getSharedWith(txn, groupId);
Collection<ContactId> contactIds = new ArrayList<>(contacts.size());
for (Contact c : contacts) contactIds.add(c.getId());
txn.attach(() -> sharingController.addAll(contactIds));
});
}
void deleteForum() {
runOnDbThreadOrLogException(() ->
forumManager.removeForum(forumManager.getForum(groupId)));
Toast.makeText(getApplication(), R.string.forum_left_toast,
LENGTH_SHORT).show();
}
}

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.navdrawer;
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;
@@ -65,27 +66,34 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void checkExpiryWarning() {
runOnDbThreadOrLogException(() -> {
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
int warningInt = settings.getInt(EXPIRY_DATE_WARNING, 0);
runOnDbThread(() -> {
try {
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
int warningInt = settings.getInt(EXPIRY_DATE_WARNING, 0);
if (warningInt == 0) {
// we have not warned before
showExpiryWarning.postValue(true);
} else {
long warningLong = warningInt * 1000L;
long now = System.currentTimeMillis();
long daysSinceLastWarning =
(now - warningLong) / DAYS.toMillis(1);
long daysBeforeExpiry = (EXPIRY_DATE - now) / DAYS.toMillis(1);
if (daysSinceLastWarning >= 30) {
showExpiryWarning.postValue(true);
} else if (daysBeforeExpiry <= 3 && daysSinceLastWarning > 0) {
if (warningInt == 0) {
// we have not warned before
showExpiryWarning.postValue(true);
} else {
showExpiryWarning.postValue(false);
long warningLong = warningInt * 1000L;
long now = System.currentTimeMillis();
long daysSinceLastWarning =
(now - warningLong) / DAYS.toMillis(1);
long daysBeforeExpiry =
(EXPIRY_DATE - now) / DAYS.toMillis(1);
if (daysSinceLastWarning >= 30) {
showExpiryWarning.postValue(true);
} else if (daysBeforeExpiry <= 3 &&
daysSinceLastWarning > 0) {
showExpiryWarning.postValue(true);
} else {
showExpiryWarning.postValue(false);
}
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@@ -93,11 +101,15 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void expiryWarningDismissed() {
showExpiryWarning.setValue(false);
runOnDbThreadOrLogException(() -> {
Settings settings = new Settings();
int date = (int) (System.currentTimeMillis() / 1000L);
settings.putInt(EXPIRY_DATE_WARNING, date);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
runOnDbThread(() -> {
try {
Settings settings = new Settings();
int date = (int) (System.currentTimeMillis() / 1000L);
settings.putInt(EXPIRY_DATE_WARNING, date);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@@ -113,12 +125,15 @@ public class NavDrawerViewModel extends DbViewModel {
return;
}
runOnDbThread(() -> {
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
boolean ask = settings.getBoolean(DOZE_ASK_AGAIN, true);
shouldAskForDozeWhitelisting.postValue(ask);
}, e -> {
logException(LOG, WARNING, e);
shouldAskForDozeWhitelisting.postValue(true);
try {
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
boolean ask = settings.getBoolean(DOZE_ASK_AGAIN, true);
shouldAskForDozeWhitelisting.postValue(ask);
} catch (DbException e) {
logException(LOG, WARNING, e);
shouldAskForDozeWhitelisting.postValue(true);
}
});
}
@@ -130,21 +145,30 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void checkTransportsOnboarding() {
if (showTransportsOnboarding.getValue() != null) return;
runOnDbThreadOrLogException(() -> {
Settings settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
boolean show =
settings.getBoolean(SHOW_TRANSPORTS_ONBOARDING, true);
showTransportsOnboarding.postValue(show);
runOnDbThread(() -> {
try {
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
boolean show =
settings.getBoolean(SHOW_TRANSPORTS_ONBOARDING, true);
showTransportsOnboarding.postValue(show);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@UiThread
void transportsOnboardingShown() {
showTransportsOnboarding.setValue(false);
runOnDbThreadOrLogException(() -> {
Settings settings = new Settings();
settings.putBoolean(SHOW_TRANSPORTS_ONBOARDING, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
runOnDbThread(() -> {
try {
Settings settings = new Settings();
settings.putBoolean(SHOW_TRANSPORTS_ONBOARDING, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
}

View File

@@ -45,10 +45,12 @@ import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
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.plugin.Plugin.PREF_PLUGIN_ENABLE;
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
@NotNullByDefault
@@ -183,16 +185,20 @@ public class PluginViewModel extends DbViewModel implements EventListener {
}
private void loadSettings() {
runOnDbThreadOrLogException(() -> {
boolean tor = isPluginEnabled(TorConstants.ID,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
torEnabledSetting.postValue(tor);
boolean wifi = isPluginEnabled(LanTcpConstants.ID,
LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE);
wifiEnabledSetting.postValue(wifi);
boolean bt = isPluginEnabled(BluetoothConstants.ID,
BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE);
btEnabledSetting.postValue(bt);
runOnDbThread(() -> {
try {
boolean tor = isPluginEnabled(TorConstants.ID,
TorConstants.DEFAULT_PREF_PLUGIN_ENABLE);
torEnabledSetting.postValue(tor);
boolean wifi = isPluginEnabled(LanTcpConstants.ID,
LanTcpConstants.DEFAULT_PREF_PLUGIN_ENABLE);
wifiEnabledSetting.postValue(wifi);
boolean bt = isPluginEnabled(BluetoothConstants.ID,
BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE);
btEnabledSetting.postValue(bt);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@@ -216,10 +222,14 @@ public class PluginViewModel extends DbViewModel implements EventListener {
}
private void mergeSettings(Settings s, String namespace) {
runOnDbThreadOrLogException(() -> {
long start = now();
settingsManager.mergeSettings(s, namespace);
logDuration(LOG, "Merging settings", start);
runOnDbThread(() -> {
try {
long start = now();
settingsManager.mergeSettings(s, namespace);
logDuration(LOG, "Merging settings", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}

View File

@@ -1,59 +1,67 @@
package org.briarproject.briar.android.privategroup.conversation;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor;
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.controller.handler.UiExceptionHandler;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.privategroup.conversation.GroupController.GroupListener;
import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity;
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.threaded.ThreadListActivity;
import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.Visibility;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_GROUP_INVITE;
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class GroupActivity extends
ThreadListActivity<GroupMessageItem, GroupMessageAdapter> {
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageAdapter>
implements GroupListener, OnClickListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
GroupController controller;
private GroupViewModel viewModel;
@Nullable
private Boolean isCreator = null;
private boolean isDissolved = false;
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
dissolveMenuItem;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(GroupViewModel.class);
}
@Override
protected ThreadListViewModel<GroupMessageItem> getViewModel() {
return viewModel;
}
@Override
protected GroupMessageAdapter createAdapter() {
return new GroupMessageAdapter(this);
protected ThreadListController<PrivateGroup, GroupMessageItem> getController() {
return controller;
}
@Override
@@ -61,33 +69,65 @@ public class GroupActivity extends
super.onCreate(state);
Toolbar toolbar = setUpCustomToolbar(false);
// Open member list on Toolbar click
toolbar.setOnClickListener(v -> {
Intent i = new Intent(GroupActivity.this,
GroupMemberListActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
});
String groupName = getIntent().getStringExtra(GROUP_NAME);
Intent i = getIntent();
String groupName = i.getStringExtra(GROUP_NAME);
if (groupName != null) setTitle(groupName);
observeOnce(viewModel.getPrivateGroup(), this, privateGroup ->
setTitle(privateGroup.getName())
);
observeOnce(viewModel.isCreator(), this, adapter::setIsCreator);
loadNamedGroup();
// Open member list on Toolbar click
if (toolbar != null) {
toolbar.setOnClickListener(v -> {
Intent i1 = new Intent(GroupActivity.this,
GroupMemberListActivity.class);
i1.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i1);
});
}
// start with group disabled and enable when not dissolved
setGroupEnabled(false);
viewModel.isDissolved().observe(this, dissolved -> {
setGroupEnabled(!dissolved);
if (dissolved) onGroupDissolved();
});
}
@Override
public void onStart() {
super.onStart();
viewModel.clearGroupMessageNotifications();
protected GroupMessageAdapter createAdapter(
LinearLayoutManager layoutManager) {
return new GroupMessageAdapter(this, layoutManager);
}
@Override
protected void loadItems() {
controller.isDissolved(
new UiResultExceptionHandler<Boolean, DbException>(this) {
@Override
public void onResultUi(Boolean isDissolved) {
setGroupEnabled(!isDissolved);
GroupActivity.super.loadItems();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
protected void onNamedGroupLoaded(PrivateGroup group) {
setTitle(group.getName());
controller.loadLocalAuthor(
new UiResultExceptionHandler<LocalAuthor, DbException>(this) {
@Override
public void onResultUi(LocalAuthor author) {
isCreator = group.getCreator().equals(author);
adapter.setPerspective(isCreator);
showMenuItems();
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
@@ -96,61 +136,74 @@ public class GroupActivity extends
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.group_actions, menu);
// show items based on role (which will not change, so observe once)
observeOnce(viewModel.isCreator(), this, isCreator -> {
menu.findItem(R.id.action_group_reveal).setVisible(!isCreator);
menu.findItem(R.id.action_group_invite).setVisible(isCreator);
menu.findItem(R.id.action_group_leave).setVisible(!isCreator);
menu.findItem(R.id.action_group_dissolve).setVisible(isCreator);
});
super.onCreateOptionsMenu(menu);
return true;
revealMenuItem = menu.findItem(R.id.action_group_reveal);
inviteMenuItem = menu.findItem(R.id.action_group_invite);
leaveMenuItem = menu.findItem(R.id.action_group_leave);
dissolveMenuItem = menu.findItem(R.id.action_group_dissolve);
// all role-dependent items are invisible until we know our role
revealMenuItem.setVisible(false);
inviteMenuItem.setVisible(false);
leaveMenuItem.setVisible(false);
dissolveMenuItem.setVisible(false);
// show items based on role
showMenuItems();
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_group_member_list) {
Intent i = new Intent(this, GroupMemberListActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_group_reveal) {
if (viewModel.isCreator().getValue())
throw new IllegalStateException();
Intent i = new Intent(this, RevealContactsActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_group_invite) {
if (!viewModel.isCreator().getValue())
throw new IllegalStateException();
Intent i = new Intent(this, GroupInviteActivity.class);
i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i, REQUEST_GROUP_INVITE);
return true;
} else if (itemId == R.id.action_group_leave) {
if (viewModel.isCreator().getValue())
throw new IllegalStateException();
showLeaveGroupDialog();
return true;
} else if (itemId == R.id.action_group_dissolve) {
if (!viewModel.isCreator().getValue())
throw new IllegalStateException();
showDissolveGroupDialog();
return true;
switch (item.getItemId()) {
case R.id.action_group_member_list:
Intent i1 = new Intent(this, GroupMemberListActivity.class);
i1.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i1);
return true;
case R.id.action_group_reveal:
if (isCreator == null || isCreator)
throw new IllegalStateException();
Intent i2 = new Intent(this, RevealContactsActivity.class);
i2.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i2);
return true;
case R.id.action_group_invite:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
Intent i3 = new Intent(this, GroupInviteActivity.class);
i3.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i3, REQUEST_GROUP_INVITE);
return true;
case R.id.action_group_leave:
if (isCreator == null || isCreator)
throw new IllegalStateException();
showLeaveGroupDialog();
return true;
case R.id.action_group_dissolve:
if (isCreator == null || !isCreator)
throw new IllegalStateException();
showDissolveGroupDialog();
default:
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int request, int result,
@Nullable Intent data) {
protected void onActivityResult(int request, int result, Intent data) {
if (request == REQUEST_GROUP_INVITE && result == RESULT_OK) {
displaySnackbar(R.string.groups_invitation_sent);
} else super.onActivityResult(request, result, data);
}
@Override
public void onItemReceived(GroupMessageItem item) {
super.onItemReceived(item);
if (item instanceof JoinMessageItem) {
if (((JoinMessageItem) item).isInitial()) loadSharingContacts();
}
}
@Override
protected int getMaxTextLength() {
return MAX_GROUP_POST_TEXT_LENGTH;
@@ -158,10 +211,11 @@ public class GroupActivity extends
@Override
public void onReplyClick(GroupMessageItem item) {
if (!viewModel.isDissolved().getValue()) super.onReplyClick(item);
if (!isDissolved) super.onReplyClick(item);
}
private void setGroupEnabled(boolean enabled) {
isDissolved = !enabled;
sendController.setReady(enabled);
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
@@ -173,13 +227,21 @@ public class GroupActivity extends
}
}
private void showMenuItems() {
// we need to have the menu items and know if we are the creator
if (leaveMenuItem == null || isCreator == null) return;
revealMenuItem.setVisible(!isCreator);
inviteMenuItem.setVisible(isCreator);
leaveMenuItem.setVisible(!isCreator);
dissolveMenuItem.setVisible(isCreator);
}
private void showLeaveGroupDialog() {
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_leave_dialog_title));
builder.setMessage(getString(R.string.groups_leave_dialog_message));
builder.setNegativeButton(R.string.dialog_button_leave,
(d, w) -> deleteGroup());
builder.setNegativeButton(R.string.dialog_button_leave, this);
builder.setPositiveButton(R.string.cancel, null);
builder.show();
}
@@ -189,19 +251,37 @@ public class GroupActivity extends
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_dissolve_dialog_title));
builder.setMessage(getString(R.string.groups_dissolve_dialog_message));
builder.setNegativeButton(R.string.groups_dissolve_button,
(d, w) -> deleteGroup());
builder.setNegativeButton(R.string.groups_dissolve_button, this);
builder.setPositiveButton(R.string.cancel, null);
builder.show();
}
private void deleteGroup() {
// The activity is going to be destroyed by the
// GroupRemovedEvent being fired
viewModel.deletePrivateGroup();
@Override
public void onClick(DialogInterface dialog, int which) {
controller.deleteNamedGroup(
new UiExceptionHandler<DbException>(this) {
// The activity is going to be destroyed by the
// GroupRemovedEvent being fired
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@Override
public void onContactRelationshipRevealed(AuthorId memberId, ContactId c,
Visibility v) {
adapter.updateVisibility(memberId, v);
sharingController.add(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
@Override
public void onGroupDissolved() {
setGroupEnabled(false);
AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_dissolved_dialog_title));

View File

@@ -0,0 +1,33 @@
package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadListController;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.Visibility;
import androidx.annotation.UiThread;
public interface GroupController
extends ThreadListController<PrivateGroup, GroupMessageItem> {
void loadLocalAuthor(
ResultExceptionHandler<LocalAuthor, DbException> handler);
void isDissolved(
ResultExceptionHandler<Boolean, DbException> handler);
interface GroupListener extends ThreadListListener<GroupMessageItem> {
@UiThread
void onContactRelationshipRevealed(AuthorId memberId,
ContactId contactId, Visibility v);
@UiThread
void onGroupDissolved();
}
}

View File

@@ -0,0 +1,242 @@
package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
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.MessageId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.android.privategroup.conversation.GroupController.GroupListener;
import org.briarproject.briar.android.threaded.ThreadListControllerImpl;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.privategroup.GroupMember;
import org.briarproject.briar.api.privategroup.GroupMessage;
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.JoinMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.event.ContactRelationshipRevealedEvent;
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent;
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.lang.Math.max;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class GroupControllerImpl extends
ThreadListControllerImpl<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage, GroupListener>
implements GroupController {
private static final Logger LOG =
Logger.getLogger(GroupControllerImpl.class.getName());
private final PrivateGroupManager privateGroupManager;
private final GroupMessageFactory groupMessageFactory;
@Inject
GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, IdentityManager identityManager,
@CryptoExecutor Executor cryptoExecutor,
PrivateGroupManager privateGroupManager,
GroupMessageFactory groupMessageFactory, EventBus eventBus,
MessageTracker messageTracker, Clock clock,
AndroidNotificationManager notificationManager) {
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
eventBus, clock, notificationManager, messageTracker);
this.privateGroupManager = privateGroupManager;
this.groupMessageFactory = groupMessageFactory;
}
@Override
public void onActivityStart() {
super.onActivityStart();
notificationManager.clearGroupMessageNotification(getGroupId());
}
@Override
public void eventOccurred(Event e) {
super.eventOccurred(e);
if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
if (!g.isLocal() && g.getGroupId().equals(getGroupId())) {
LOG.info("Group message received, adding...");
listener.onItemReceived(buildItem(g.getHeader(), g.getText()));
}
} else if (e instanceof ContactRelationshipRevealedEvent) {
ContactRelationshipRevealedEvent c =
(ContactRelationshipRevealedEvent) e;
if (getGroupId().equals(c.getGroupId())) {
listener.onContactRelationshipRevealed(c.getMemberId(),
c.getContactId(), c.getVisibility());
}
} else if (e instanceof GroupInvitationResponseReceivedEvent) {
GroupInvitationResponseReceivedEvent g =
(GroupInvitationResponseReceivedEvent) e;
GroupInvitationResponse r = g.getMessageHeader();
if (getGroupId().equals(r.getShareableId()) && r.wasAccepted()) {
listener.onInvitationAccepted(g.getContactId());
}
} else if (e instanceof GroupDissolvedEvent) {
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
if (getGroupId().equals(g.getGroupId())) {
listener.onGroupDissolved();
}
}
}
@Override
protected PrivateGroup loadNamedGroup() throws DbException {
return privateGroupManager.getPrivateGroup(getGroupId());
}
@Override
protected Collection<GroupMessageHeader> loadHeaders() throws DbException {
return privateGroupManager.getHeaders(getGroupId());
}
@Override
protected String loadMessageText(GroupMessageHeader header)
throws DbException {
if (header instanceof JoinMessageHeader) {
// will be looked up later
return "";
}
return privateGroupManager.getMessageText(header.getId());
}
@Override
protected void markRead(MessageId id) throws DbException {
privateGroupManager.setReadFlag(getGroupId(), id, true);
}
@Override
public void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
runOnDbThread(() -> {
try {
Collection<GroupMember> members =
privateGroupManager.getMembers(getGroupId());
Collection<ContactId> contactIds = new ArrayList<>();
for (GroupMember m : members) {
if (m.getContactId() != null)
contactIds.add(m.getContactId());
}
handler.onResult(contactIds);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void createAndStoreMessage(String text,
@Nullable GroupMessageItem parentItem,
ResultExceptionHandler<GroupMessageItem, DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor author = identityManager.getLocalAuthor();
MessageId parentId = null;
MessageId previousMsgId =
privateGroupManager.getPreviousMsgId(getGroupId());
GroupCount count =
privateGroupManager.getGroupCount(getGroupId());
long timestamp = count.getLatestMsgTime();
if (parentItem != null) parentId = parentItem.getId();
timestamp = max(clock.currentTimeMillis(), timestamp + 1);
createMessage(text, timestamp, parentId, author, previousMsgId,
handler);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
private void createMessage(String text, long timestamp,
@Nullable MessageId parentId, LocalAuthor author,
MessageId previousMsgId,
ResultExceptionHandler<GroupMessageItem, DbException> handler) {
cryptoExecutor.execute(() -> {
LOG.info("Creating group message...");
GroupMessage msg = groupMessageFactory
.createGroupMessage(getGroupId(), timestamp,
parentId, author, text, previousMsgId);
storePost(msg, text, handler);
});
}
@Override
protected GroupMessageHeader addLocalMessage(GroupMessage message)
throws DbException {
return privateGroupManager.addLocalMessage(message);
}
@Override
protected void deleteNamedGroup(PrivateGroup group) throws DbException {
privateGroupManager.removePrivateGroup(group.getId());
}
@Override
protected GroupMessageItem buildItem(GroupMessageHeader header,
String text) {
if (header instanceof JoinMessageHeader) {
return new JoinMessageItem((JoinMessageHeader) header, text);
}
return new GroupMessageItem(header, text);
}
@Override
public void loadLocalAuthor(
ResultExceptionHandler<LocalAuthor, DbException> handler) {
runOnDbThread(() -> {
try {
LocalAuthor author = identityManager.getLocalAuthor();
handler.onResult(author);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@Override
public void isDissolved(
ResultExceptionHandler<Boolean, DbException> handler) {
runOnDbThread(() -> {
try {
boolean isDissolved =
privateGroupManager.isDissolved(getGroupId());
handler.onResult(isDissolved);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
}

View File

@@ -1,18 +1,19 @@
package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import org.briarproject.briar.android.activity.ActivityScope;
import org.briarproject.briar.android.activity.BaseActivity;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
import dagger.Provides;
@Module
public interface GroupConversationModule {
@Binds
@IntoMap
@ViewModelKey(GroupViewModel.class)
ViewModel bindGroupViewModel(GroupViewModel groupViewModel);
public class GroupConversationModule {
@ActivityScope
@Provides
GroupController provideGroupController(BaseActivity activity,
GroupControllerImpl groupController) {
activity.addLifecycleController(groupController);
return groupController;
}
}

View File

@@ -4,14 +4,19 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder;
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadPostViewHolder;
import org.briarproject.briar.api.privategroup.Visibility;
import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread;
import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@UiThread
@NotNullByDefault
@@ -19,14 +24,15 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
private boolean isCreator = false;
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
super(listener);
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener,
LinearLayoutManager layoutManager) {
super(listener, layoutManager);
}
@LayoutRes
@Override
public int getItemViewType(int position) {
GroupMessageItem item = getItem(position);
GroupMessageItem item = items.get(position);
return item.getLayout();
}
@@ -41,9 +47,30 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
return new ThreadPostViewHolder<>(v);
}
void setIsCreator(boolean isCreator) {
void setPerspective(boolean isCreator) {
this.isCreator = isCreator;
notifyDataSetChanged();
}
void updateVisibility(AuthorId memberId, Visibility v) {
int position = findItemPosition(memberId);
if (position != NO_POSITION) {
GroupMessageItem item = items.get(position);
if (item instanceof JoinMessageItem) {
((JoinMessageItem) item).setVisibility(v);
notifyItemChanged(findItemPosition(item), item);
}
}
}
private int findItemPosition(AuthorId a) {
int count = items.size();
for (int i = 0; i < count; i++) {
GroupMessageItem item = items.get(i);
if (item.getAuthor().getId().equals(a))
return i;
}
return NO_POSITION; // Not found
}
}

View File

@@ -1,254 +0,0 @@
package org.briarproject.briar.android.privategroup.conversation;
import android.app.Application;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
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.identity.Author;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
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.MessageId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.sharing.SharingController;
import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.client.PostHeader;
import org.briarproject.briar.api.privategroup.GroupMember;
import org.briarproject.briar.api.privategroup.GroupMessage;
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
import org.briarproject.briar.api.privategroup.JoinMessageHeader;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
import org.briarproject.briar.api.privategroup.event.ContactRelationshipRevealedEvent;
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent;
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.lang.Math.max;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
private static final Logger LOG = getLogger(GroupViewModel.class.getName());
private final PrivateGroupManager privateGroupManager;
private final GroupMessageFactory groupMessageFactory;
private final MutableLiveData<PrivateGroup> privateGroup =
new MutableLiveData<>();
private final MutableLiveData<Boolean> isCreator = new MutableLiveData<>();
private final MutableLiveData<Boolean> isDissolved =
new MutableLiveData<>();
@Inject
GroupViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
EventBus eventBus,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
SharingController sharingController,
@CryptoExecutor Executor cryptoExecutor,
Clock clock,
MessageTracker messageTracker,
PrivateGroupManager privateGroupManager,
GroupMessageFactory groupMessageFactory) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
identityManager, notificationManager, sharingController,
cryptoExecutor, clock, messageTracker, eventBus);
this.privateGroupManager = privateGroupManager;
this.groupMessageFactory = groupMessageFactory;
}
@Override
public void eventOccurred(Event e) {
if (e instanceof GroupMessageAddedEvent) {
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
// only act on non-local messages in this group
if (!g.isLocal() && g.getGroupId().equals(groupId)) {
LOG.info("Group message received, adding...");
GroupMessageItem item = buildItem(g.getHeader(), g.getText());
addItem(item);
if (item instanceof JoinMessageItem &&
(((JoinMessageItem) item).isInitial())) {
loadSharingContacts();
}
}
} else if (e instanceof GroupInvitationResponseReceivedEvent) {
GroupInvitationResponseReceivedEvent g =
(GroupInvitationResponseReceivedEvent) e;
GroupInvitationResponse r = g.getMessageHeader();
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
sharingController.add(g.getContactId());
}
} else if (e instanceof ContactRelationshipRevealedEvent) {
ContactRelationshipRevealedEvent c =
(ContactRelationshipRevealedEvent) e;
if (c.getGroupId().equals(groupId)) {
sharingController.add(c.getContactId());
}
} else if (e instanceof GroupDissolvedEvent) {
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
if (g.getGroupId().equals(groupId)) {
isDissolved.setValue(true);
}
} else {
super.eventOccurred(e);
}
}
@Override
public void setGroupId(GroupId groupId) {
super.setGroupId(groupId);
loadPrivateGroup(groupId);
}
public void clearGroupMessageNotifications() {
notificationManager.clearGroupMessageNotification(groupId);
}
private void loadPrivateGroup(GroupId groupId) {
runOnDbThreadOrLogException(() -> {
PrivateGroup g = privateGroupManager.getPrivateGroup(groupId);
privateGroup.postValue(g);
Author author = identityManager.getLocalAuthor();
isCreator.postValue(g.getCreator().equals(author));
});
}
@Override
public void loadItems() {
loadList(txn -> {
// check first if group is dissolved
isDissolved
.postValue(privateGroupManager.isDissolved(txn, groupId));
// now continue to load the items
long start = now();
List<GroupMessageHeader> headers =
privateGroupManager.getHeaders(txn, groupId);
logDuration(LOG, "Loading headers", start);
return createItems(txn, headers, this::buildItem);
}, this::setItems);
}
private GroupMessageItem buildItem(GroupMessageHeader header, String text) {
if (header instanceof JoinMessageHeader) {
return new JoinMessageItem((JoinMessageHeader) header, text);
}
return new GroupMessageItem(header, text);
}
@Override
protected String loadMessageText(
Transaction txn, PostHeader header) throws DbException {
if (header instanceof JoinMessageHeader) {
// will be looked up later
return "";
}
return privateGroupManager.getMessageText(txn, header.getId());
}
@Override
public void createAndStoreMessage(String text,
@Nullable MessageId parentId) {
runOnDbThreadOrLogException(() -> {
LocalAuthor author = identityManager.getLocalAuthor();
MessageId previousMsgId =
privateGroupManager.getPreviousMsgId(groupId);
GroupCount count = privateGroupManager.getGroupCount(groupId);
long timestamp = count.getLatestMsgTime();
timestamp = max(clock.currentTimeMillis(), timestamp + 1);
createMessage(text, timestamp, parentId, author, previousMsgId);
});
}
private void createMessage(String text, long timestamp,
@Nullable MessageId parentId, LocalAuthor author,
MessageId previousMsgId) {
cryptoExecutor.execute(() -> {
LOG.info("Creating group message...");
GroupMessage msg = groupMessageFactory.createGroupMessage(groupId,
timestamp, parentId, author, text, previousMsgId);
storePost(msg, text);
});
}
private void storePost(GroupMessage msg, String text) {
runOnDbThreadOrLogException(() -> {
long start = now();
GroupMessageHeader header =
privateGroupManager.addLocalMessage(msg);
addItemAsync(buildItem(header, text));
logDuration(LOG, "Storing group message", start);
});
}
@Override
protected void markItemRead(GroupMessageItem item) {
runOnDbThreadOrLogException(() ->
privateGroupManager.setReadFlag(groupId, item.getId(), true));
}
public void loadSharingContacts() {
runOnDbThreadOrLogException(true, txn -> {
Collection<GroupMember> members =
privateGroupManager.getMembers(txn, groupId);
Collection<ContactId> contactIds = new ArrayList<>();
for (GroupMember m : members) {
if (m.getContactId() != null) contactIds.add(m.getContactId());
}
txn.attach(() -> sharingController.addAll(contactIds));
});
}
void deletePrivateGroup() {
runOnDbThreadOrLogException(() ->
privateGroupManager.removePrivateGroup(groupId));
}
LiveData<PrivateGroup> getPrivateGroup() {
return privateGroup;
}
LiveData<Boolean> isCreator() {
return isCreator;
}
LiveData<Boolean> isDissolved() {
return isDissolved;
}
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.briar.R;
import org.briarproject.briar.api.privategroup.JoinMessageHeader;
import org.briarproject.briar.api.privategroup.Visibility;
import javax.annotation.concurrent.NotThreadSafe;
@@ -12,10 +13,12 @@ import androidx.annotation.UiThread;
@NotThreadSafe
class JoinMessageItem extends GroupMessageItem {
private Visibility visibility;
private final boolean isInitial;
JoinMessageItem(JoinMessageHeader h, String text) {
super(h, text);
this.visibility = h.getVisibility();
this.isInitial = h.isInitial();
}
@@ -30,6 +33,14 @@ class JoinMessageItem extends GroupMessageItem {
return R.layout.list_item_group_join_notice;
}
Visibility getVisibility() {
return visibility;
}
void setVisibility(Visibility visibility) {
this.visibility = visibility;
}
boolean isInitial() {
return isInitial;
}

View File

@@ -2,6 +2,7 @@ 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;
@@ -48,8 +49,10 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.privategroup.PrivateGroupManager.CLIENT_ID;
@@ -197,17 +200,25 @@ class GroupListViewModel extends DbViewModel implements EventListener {
}
void removeGroup(GroupId g) {
runOnDbThreadOrLogException(() -> {
long start = now();
groupManager.removePrivateGroup(g);
logDuration(LOG, "Removing group", start);
runOnDbThread(() -> {
try {
long start = now();
groupManager.removePrivateGroup(g);
logDuration(LOG, "Removing group", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
void loadNumInvitations() {
runOnDbThreadOrLogException(() -> {
int i = groupInvitationManager.getInvitations().size();
numInvitations.postValue(i);
runOnDbThread(() -> {
try {
int i = groupInvitationManager.getInvitations().size();
numInvitations.postValue(i);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}

View File

@@ -1,54 +0,0 @@
package org.briarproject.briar.android.sharing;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
@NotNullByDefault
public interface SharingController {
/**
* Call this when the owning ViewModel gets cleared,
* so the {@link EventBus} can get unregistered.
*/
void onCleared();
/**
* Adds one contact to be tracked.
*/
@UiThread
void add(ContactId c);
/**
* Adds a collection of contacts to be tracked.
*/
@UiThread
void addAll(Collection<ContactId> contacts);
/**
* Call this when the contact identified by c is no longer sharing
* the given group identified by GroupId g.
*/
@UiThread
void remove(ContactId c);
/**
* Returns the total number of contacts that have been added.
*/
LiveData<SharingInfo> getSharingInfo();
class SharingInfo {
public final int total, online;
SharingInfo(int total, int online) {
this.total = total;
this.online = online;
}
}
}

View File

@@ -1,102 +0,0 @@
package org.briarproject.briar.android.sharing;
import org.briarproject.bramble.api.connection.ConnectionRegistry;
import org.briarproject.bramble.api.contact.ContactId;
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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@NotNullByDefault
public class SharingControllerImpl implements SharingController, EventListener {
private final EventBus eventBus;
private final ConnectionRegistry connectionRegistry;
// UI thread
private final Set<ContactId> contacts = new HashSet<>();
private final MutableLiveData<SharingInfo> sharingInfo =
new MutableLiveData<>();
@Inject
SharingControllerImpl(EventBus eventBus,
ConnectionRegistry connectionRegistry) {
this.eventBus = eventBus;
this.connectionRegistry = connectionRegistry;
eventBus.addListener(this);
}
@Override
public void onCleared() {
eventBus.removeListener(this);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactConnectedEvent) {
setConnected(((ContactConnectedEvent) e).getContactId());
} else if (e instanceof ContactDisconnectedEvent) {
setConnected(((ContactDisconnectedEvent) e).getContactId());
}
}
@UiThread
private void setConnected(ContactId c) {
if (contacts.contains(c)) {
updateLiveData();
}
}
@UiThread
private void updateLiveData() {
int online = getOnlineCount();
sharingInfo.setValue(new SharingInfo(contacts.size(), online));
}
private int getOnlineCount() {
int online = 0;
for (ContactId c : contacts) {
if (connectionRegistry.isConnected(c)) online++;
}
return online;
}
@UiThread
@Override
public void addAll(Collection<ContactId> c) {
contacts.addAll(c);
updateLiveData();
}
@UiThread
@Override
public void add(ContactId c) {
contacts.add(c);
updateLiveData();
}
@UiThread
@Override
public void remove(ContactId c) {
contacts.remove(c);
updateLiveData();
}
@Override
public LiveData<SharingInfo> getSharingInfo() {
return sharingInfo;
}
}

View File

@@ -9,47 +9,36 @@ import dagger.Provides;
@Module
public class SharingModule {
@Module
@Deprecated
public static class SharingLegacyModule {
@ActivityScope
@Provides
ShareForumController provideShareForumController(
ShareForumControllerImpl shareForumController) {
return shareForumController;
}
@ActivityScope
@Provides
BlogInvitationController provideInvitationBlogController(
BaseActivity activity,
BlogInvitationControllerImpl blogInvitationController) {
activity.addLifecycleController(blogInvitationController);
return blogInvitationController;
}
@ActivityScope
@Provides
ForumInvitationController provideInvitationForumController(
BaseActivity activity,
ForumInvitationControllerImpl forumInvitationController) {
activity.addLifecycleController(forumInvitationController);
return forumInvitationController;
}
@ActivityScope
@Provides
ShareBlogController provideShareBlogController(
ShareBlogControllerImpl shareBlogController) {
return shareBlogController;
}
@ActivityScope
@Provides
ShareForumController provideShareForumController(
ShareForumControllerImpl shareForumController) {
return shareForumController;
}
@ActivityScope
@Provides
SharingController provideSharingController(
SharingControllerImpl sharingController) {
return sharingController;
BlogInvitationController provideInvitationBlogController(
BaseActivity activity,
BlogInvitationControllerImpl blogInvitationController) {
activity.addLifecycleController(blogInvitationController);
return blogInvitationController;
}
@ActivityScope
@Provides
ForumInvitationController provideInvitationForumController(
BaseActivity activity,
ForumInvitationControllerImpl forumInvitationController) {
activity.addLifecycleController(forumInvitationController);
return forumInvitationController;
}
@ActivityScope
@Provides
ShareBlogController provideShareBlogController(
ShareBlogControllerImpl shareBlogController) {
return shareBlogController;
}
}

View File

@@ -0,0 +1,54 @@
package org.briarproject.briar.android.threaded;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.client.MessageTree;
import org.briarproject.briar.api.client.MessageTree.MessageNode;
import org.briarproject.briar.client.MessageTreeImpl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import androidx.annotation.UiThread;
@UiThread
@NotNullByDefault
public class NestedTreeList<T extends MessageNode> implements Iterable<T> {
private final MessageTree<T> tree = new MessageTreeImpl<>();
private List<T> depthFirstCollection = new ArrayList<>();
public void addAll(Collection<T> collection) {
tree.add(collection);
depthFirstCollection = new ArrayList<>(tree.depthFirstOrder());
}
public void add(T elem) {
tree.add(elem);
depthFirstCollection = new ArrayList<>(tree.depthFirstOrder());
}
public void clear() {
tree.clear();
depthFirstCollection.clear();
}
public T get(int index) {
return depthFirstCollection.get(index);
}
public int size() {
return depthFirstCollection.size();
}
public boolean contains(MessageId m) {
return tree.contains(m);
}
@Override
public Iterator<T> iterator() {
return depthFirstCollection.iterator();
}
}

View File

@@ -99,14 +99,4 @@ public abstract class ThreadItem implements MessageNode {
return highlighted;
}
@Override
public int hashCode() {
return messageId.hashCode();
}
@Override
public boolean equals(@Nullable Object o) {
return o instanceof ThreadItem &&
messageId.equals(((ThreadItem) o).messageId);
}
}

View File

@@ -4,47 +4,39 @@ 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.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.ItemReturningAdapter;
import org.briarproject.briar.android.util.VersionedAdapter;
import java.util.Collection;
import javax.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@UiThread
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ThreadItemAdapter<I extends ThreadItem>
extends ListAdapter<I, BaseThreadItemViewHolder<I>>
implements ItemReturningAdapter<I> {
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
implements VersionedAdapter, ItemReturningAdapter<I> {
static final int UNDEFINED = -1;
protected final NestedTreeList<I> items = new NestedTreeList<>();
private final ThreadItemListener<I> listener;
private final LinearLayoutManager layoutManager;
public ThreadItemAdapter(ThreadItemListener<I> listener) {
super(new DiffUtil.ItemCallback<I>() {
@Override
public boolean areItemsTheSame(I a, I b) {
return a.equals(b);
}
private volatile int revision = 0;
@Override
public boolean areContentsTheSame(I a, I b) {
return a.isHighlighted() == b.isHighlighted() &&
a.isRead() && b.isRead();
}
});
public ThreadItemAdapter(ThreadItemListener<I> listener,
LinearLayoutManager layoutManager) {
this.listener = listener;
this.layoutManager = layoutManager;
}
@NonNull
@@ -59,27 +51,76 @@ public class ThreadItemAdapter<I extends ThreadItem>
@Override
public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui,
int position) {
I item = getItem(position);
I item = items.get(position);
ui.bind(item, listener);
}
public int findItemPosition(MessageId id) {
for (int i = 0; i < getItemCount(); i++) {
if (id.equals(getItem(i).getId())) return i;
@Override
public int getItemCount() {
return items.size();
}
@Override
public int getRevision() {
return revision;
}
@Override
public void incrementRevision() {
revision++;
}
void setItemWithIdVisible(MessageId messageId) {
int pos = 0;
for (I item : items) {
if (item.getId().equals(messageId)) {
layoutManager.scrollToPosition(pos);
break;
}
pos++;
}
}
public void setItems(Collection<I> items) {
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
}
public void add(I item) {
items.add(item);
notifyItemInserted(findItemPosition(item));
}
@Nullable
public I getItemAt(int position) {
if (position == NO_POSITION || position >= items.size()) {
return null;
}
return items.get(position);
}
protected int findItemPosition(@Nullable I item) {
for (int i = 0; i < items.size(); i++) {
if (items.get(i).equals(item)) return i;
}
return NO_POSITION; // Not found
}
boolean contains(MessageId m) {
return items.contains(m);
}
/**
* Highlights the item with the given {@link MessageId}
* and disables the highlight for a previously highlighted item, if any.
* <p>
*
* Only one item can be highlighted at a time.
*/
void setHighlightedItem(@Nullable MessageId id) {
for (int i = 0; i < getItemCount(); i++) {
I item = getItem(i);
if (item.getId().equals(id)) {
for (int i = 0; i < items.size(); i++) {
I item = items.get(i);
if (id != null && item.getId().equals(id)) {
item.setHighlighted(true);
notifyItemChanged(i, item);
} else if (item.isHighlighted()) {
@@ -91,28 +132,20 @@ public class ThreadItemAdapter<I extends ThreadItem>
@Nullable
I getHighlightedItem() {
for (int i = 0; i < getItemCount(); i++) {
I item = getItem(i);
if (item.isHighlighted()) return item;
for (I i : items) {
if (i.isHighlighted()) return i;
}
return null;
}
@Nullable
MessageId getFirstVisibleMessageId(LinearLayoutManager layoutManager) {
int position = layoutManager.findFirstVisibleItemPosition();
if (position == NO_POSITION) return null;
return getItemAt(position).getId();
}
/**
* Returns the position of the first unread item below the current viewport
*/
int getVisibleUnreadPosBottom(LinearLayoutManager layoutManager) {
int getVisibleUnreadPosBottom() {
int positionBottom = layoutManager.findLastVisibleItemPosition();
if (positionBottom == NO_POSITION) return NO_POSITION;
for (int i = positionBottom + 1; i < getItemCount(); i++) {
if (!getItem(i).isRead()) return i;
for (int i = positionBottom + 1; i < items.size(); i++) {
if (!items.get(i).isRead()) return i;
}
return NO_POSITION;
}
@@ -120,11 +153,11 @@ public class ThreadItemAdapter<I extends ThreadItem>
/**
* Returns the position of the first unread item above the current viewport
*/
int getVisibleUnreadPosTop(LinearLayoutManager layoutManager) {
int getVisibleUnreadPosTop() {
int positionTop = layoutManager.findFirstVisibleItemPosition();
int position = NO_POSITION;
for (int i = 0; i < getItemCount(); i++) {
if (i < positionTop && !getItem(i).isRead()) {
for (int i = 0; i < items.size(); i++) {
if (i < positionTop && !items.get(i).isRead()) {
position = i;
} else if (i >= positionTop) {
return position;
@@ -133,11 +166,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
return NO_POSITION;
}
@Override
public I getItemAt(int position) {
return getItem(position);
}
public interface ThreadItemListener<I> {
void onReplyClick(I item);
}

View File

@@ -0,0 +1,15 @@
package org.briarproject.briar.android.threaded;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.List;
import javax.annotation.Nullable;
public interface ThreadItemList<I extends ThreadItem> extends List<I> {
@Nullable
MessageId getFirstVisibleItemId();
void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId);
}

View File

@@ -0,0 +1,23 @@
package org.briarproject.briar.android.threaded;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.ArrayList;
import javax.annotation.Nullable;
public class ThreadItemListImpl<I extends ThreadItem> extends ArrayList<I>
implements ThreadItemList<I> {
private MessageId bottomVisibleItemId;
@Override
public MessageId getFirstVisibleItemId() {
return bottomVisibleItemId;
}
@Override
public void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId) {
this.bottomVisibleItemId = bottomVisibleItemId;
}
}

View File

@@ -2,18 +2,25 @@ package org.briarproject.briar.android.threaded;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.MenuItem;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.contact.ContactId;
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.bramble.api.sync.MessageId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
import org.briarproject.briar.android.controller.SharingController;
import org.briarproject.briar.android.controller.SharingController.SharingListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDataSource;
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView;
@@ -21,13 +28,18 @@ import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.attachment.AttachmentHeader;
import org.briarproject.briar.api.client.NamedGroup;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.CallSuper;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -36,19 +48,32 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>>
extends BriarActivity implements SendListener, ThreadItemListener<I> {
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>>
extends BriarActivity
implements ThreadListListener<I>, SendListener, SharingListener,
ThreadItemListener<I>, ThreadListDataSource {
protected final A adapter = createAdapter();
protected abstract ThreadListViewModel<I> getViewModel();
protected abstract A createAdapter();
protected static final String KEY_REPLY_ID = "replyId";
private static final Logger LOG =
Logger.getLogger(ThreadListActivity.class.getName());
protected A adapter;
private ThreadScrollListener<I> scrollListener;
protected BriarRecyclerView list;
private LinearLayoutManager layoutManager;
protected TextInputView textInput;
protected TextSendController sendController;
protected GroupId groupId;
@Nullable
private Parcelable layoutManagerState;
@Nullable
private MessageId replyId;
private LinearLayoutManager layoutManager;
private ThreadScrollListener<I> scrollListener;
protected abstract ThreadListController<G, I> getController();
@Inject
protected SharingController sharingController;
@CallSuper
@Override
@@ -61,8 +86,7 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No GroupId in intent.");
groupId = new GroupId(b);
ThreadListViewModel<I> viewModel = getViewModel();
viewModel.setGroupId(groupId);
getController().setGroupId(groupId);
textInput = findViewById(R.id.text_input_container);
sendController = new TextSendController(textInput, this, false);
@@ -76,41 +100,131 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
list = findViewById(R.id.list);
layoutManager = new LinearLayoutManager(this);
list.setLayoutManager(layoutManager);
adapter = createAdapter(layoutManager);
list.setAdapter(adapter);
scrollListener = new ThreadScrollListener<>(adapter, viewModel,
scrollListener = new ThreadScrollListener<>(adapter, getController(),
upButton, downButton);
list.getRecyclerView().addOnScrollListener(scrollListener);
upButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosTop(layoutManager);
int position = adapter.getVisibleUnreadPosTop();
if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(position);
}
});
downButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosBottom(layoutManager);
int position = adapter.getVisibleUnreadPosBottom();
if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(position);
}
});
viewModel.getItems().observe(this, result -> result
.onError(this::handleException)
.onSuccess(this::displayItems)
);
if (state != null) {
byte[] replyIdBytes = state.getByteArray(KEY_REPLY_ID);
if (replyIdBytes != null) replyId = new MessageId(replyIdBytes);
}
viewModel.getSharingInfo().observe(this, this::setToolbarSubTitle);
sharingController.setSharingListener(this);
loadSharingContacts();
}
viewModel.getGroupRemoved().observe(this, removed -> {
if (removed) supportFinishAfterTransition();
});
@Override
@Nullable
public MessageId getFirstVisibleMessageId() {
if (layoutManager != null && adapter != null) {
int position =
layoutManager.findFirstVisibleItemPosition();
I i = adapter.getItemAt(position);
return i == null ? null : i.getId();
}
return null;
}
protected abstract A createAdapter(LinearLayoutManager layoutManager);
protected void loadNamedGroup() {
getController().loadNamedGroup(
new UiResultExceptionHandler<G, DbException>(this) {
@Override
public void onResultUi(G groupItem) {
onNamedGroupLoaded(groupItem);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@UiThread
protected abstract void onNamedGroupLoaded(G groupItem);
protected void loadItems() {
int revision = adapter.getRevision();
getController().loadItems(
new UiResultExceptionHandler<ThreadItemList<I>, DbException>(
this) {
@Override
public void onResultUi(ThreadItemList<I> items) {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (items.isEmpty()) {
list.showData();
} else {
displayItems(items);
updateTextInput();
}
} else {
LOG.info("Concurrent update, reloading");
loadItems();
}
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
private void displayItems(ThreadItemList<I> items) {
adapter.setItems(items);
MessageId messageId = items.getFirstVisibleItemId();
if (messageId != null)
adapter.setItemWithIdVisible(messageId);
list.showData();
if (layoutManagerState == null) {
list.scrollToPosition(0); // Scroll to the top
} else {
layoutManager.onRestoreInstanceState(layoutManagerState);
}
}
protected void loadSharingContacts() {
getController().loadSharingContacts(
new UiResultExceptionHandler<Collection<ContactId>, DbException>(
this) {
@Override
public void onResultUi(Collection<ContactId> contacts) {
sharingController.addAll(contacts);
int online = sharingController.getOnlineCount();
setToolbarSubTitle(contacts.size(), online);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
}
@CallSuper
@Override
public void onStart() {
super.onStart();
getViewModel().blockNotifications();
sharingController.onStart();
loadItems();
list.startPeriodicUpdate();
}
@@ -118,98 +232,91 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
@Override
public void onStop() {
super.onStop();
getViewModel().unblockNotifications();
sharingController.onStop();
list.stopPeriodicUpdate();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
supportFinishAfterTransition();
return true;
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (layoutManager != null) {
layoutManagerState = layoutManager.onSaveInstanceState();
outState.putParcelable("layoutManager", layoutManagerState);
}
if (replyId != null) {
outState.putByteArray(KEY_REPLY_ID, replyId.getBytes());
}
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
layoutManagerState = savedInstanceState.getParcelable("layoutManager");
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
supportFinishAfterTransition();
return true;
default:
return super.onOptionsItemSelected(item);
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
if (adapter.getHighlightedItem() != null) {
textInput.clearText();
getViewModel().setReplyId(null);
replyId = null;
updateTextInput();
} else {
super.onBackPressed();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// store list position, so we can restore it when coming back here
if (layoutManager != null && adapter != null) {
MessageId id = adapter.getFirstVisibleMessageId(layoutManager);
getViewModel().storeMessageId(id);
}
}
protected void displayItems(List<I> items) {
if (items.isEmpty()) {
list.showData();
} else {
adapter.submitList(items, () -> {
// do stuff *after* list had been updated
scrollAfterListCommitted();
updateTextInput();
});
}
}
/**
* Scrolls to the first visible item last time the activity was open,
* if one exists and this is the first time, the list gets displayed.
* Or scrolls to a locally added item that has just been added to the list.
*/
private void scrollAfterListCommitted() {
MessageId restoredFirstVisibleItemId =
getViewModel().getAndResetRestoredMessageId();
MessageId scrollToItem =
getViewModel().getAndResetScrollToItem();
if (restoredFirstVisibleItemId != null) {
scrollToItemAtTop(restoredFirstVisibleItemId);
} else if (scrollToItem != null) {
scrollToItemAtTop(scrollToItem);
}
scrollListener.updateUnreadButtons(layoutManager);
}
@Override
public void onReplyClick(I item) {
getViewModel().setReplyId(item.getId());
replyId = item.getId();
updateTextInput();
// FIXME This does not work for a hardware keyboard
if (textInput.isKeyboardOpen()) {
scrollToItemAtTop(item.getId());
scrollToItemAtTop(item);
} else {
// wait with scrolling until keyboard opened
textInput.setOnKeyboardShownListener(() -> {
scrollToItemAtTop(item.getId());
scrollToItemAtTop(item);
textInput.setOnKeyboardShownListener(null);
});
}
}
protected void setToolbarSubTitle(SharingInfo sharingInfo) {
@Override
public void onSharingInfoUpdated(int total, int online) {
setToolbarSubTitle(total, online);
}
@Override
public void onInvitationAccepted(ContactId c) {
sharingController.add(c);
setToolbarSubTitle(sharingController.getTotalCount(),
sharingController.getOnlineCount());
}
protected void setToolbarSubTitle(int total, int online) {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setSubtitle(getString(R.string.shared_with,
sharingInfo.total, sharingInfo.online));
actionBar.setSubtitle(
getString(R.string.shared_with, total, online));
}
}
private void scrollToItemAtTop(MessageId messageId) {
int position = adapter.findItemPosition(messageId);
private void scrollToItemAtTop(I item) {
int position = adapter.findItemPosition(item);
if (position != NO_POSITION) {
layoutManager.scrollToPositionWithOffset(position, 0);
layoutManager
.scrollToPositionWithOffset(position, 0);
}
}
@@ -220,7 +327,6 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
}
private void updateTextInput() {
MessageId replyId = getViewModel().getReplyId();
if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint);
textInput.showSoftKeyboard();
@@ -235,14 +341,54 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
List<AttachmentHeader> headers) {
if (isNullOrEmpty(text)) throw new AssertionError();
MessageId replyId = getViewModel().getReplyId();
getViewModel().createAndStoreMessage(text, replyId);
I replyItem = adapter.getHighlightedItem();
UiResultExceptionHandler<I, DbException> handler =
new UiResultExceptionHandler<I, DbException>(this) {
@Override
public void onResultUi(I result) {
addItem(result, true);
}
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
};
getController().createAndStoreMessage(text, replyItem, handler);
textInput.hideSoftKeyboard();
textInput.clearText();
getViewModel().setReplyId(null);
replyId = null;
updateTextInput();
}
protected abstract int getMaxTextLength();
@Override
public void onItemReceived(I item) {
addItem(item, false);
}
@Override
public void onGroupRemoved() {
supportFinishAfterTransition();
}
private void addItem(I item, boolean isLocal) {
adapter.incrementRevision();
MessageId parent = item.getParentId();
if (parent != null && !adapter.contains(parent)) {
// We've incremented the adapter's revision, so the item will be
// loaded when its parent has been loaded
LOG.info("Ignoring item with missing parent");
return;
}
adapter.add(item);
if (isLocal) {
scrollToItemAtTop(item);
} else {
scrollListener.updateUnreadButtons(layoutManager);
}
}
}

View File

@@ -0,0 +1,60 @@
package org.briarproject.briar.android.threaded;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.ActivityLifecycleController;
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
import org.briarproject.briar.api.client.NamedGroup;
import java.util.Collection;
import javax.annotation.Nullable;
import androidx.annotation.UiThread;
@NotNullByDefault
public interface ThreadListController<G extends NamedGroup, I extends ThreadItem>
extends ActivityLifecycleController {
void setGroupId(GroupId groupId);
void loadNamedGroup(ResultExceptionHandler<G, DbException> handler);
void loadSharingContacts(
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
void loadItems(
ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
void markItemRead(I item);
void markItemsRead(Collection<I> items);
void createAndStoreMessage(String text, @Nullable I parentItem,
ResultExceptionHandler<I, DbException> handler);
void deleteNamedGroup(ExceptionHandler<DbException> handler);
interface ThreadListListener<I> extends ThreadListDataSource {
@UiThread
void onItemReceived(I item);
@UiThread
void onGroupRemoved();
@UiThread
void onInvitationAccepted(ContactId c);
}
interface ThreadListDataSource {
@UiThread @Nullable
MessageId getFirstVisibleMessageId();
}
}

View File

@@ -0,0 +1,273 @@
package org.briarproject.briar.android.threaded;
import android.app.Activity;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
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.IdentityManager;
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.MessageId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.Clock;
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.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.NamedGroup;
import org.briarproject.briar.api.client.PostHeader;
import org.briarproject.briar.api.client.ThreadedMessage;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import androidx.annotation.CallSuper;
import static java.util.logging.Level.INFO;
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;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<I>>
extends DbControllerImpl
implements ThreadListController<G, I>, EventListener {
private static final Logger LOG =
Logger.getLogger(ThreadListControllerImpl.class.getName());
private final EventBus eventBus;
private final MessageTracker messageTracker;
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
private volatile GroupId groupId;
protected final IdentityManager identityManager;
protected final AndroidNotificationManager notificationManager;
protected final Executor cryptoExecutor;
protected final Clock clock;
// UI thread
protected L listener;
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, IdentityManager identityManager,
@CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
Clock clock, AndroidNotificationManager notificationManager,
MessageTracker messageTracker) {
super(dbExecutor, lifecycleManager);
this.identityManager = identityManager;
this.cryptoExecutor = cryptoExecutor;
this.notificationManager = notificationManager;
this.clock = clock;
this.eventBus = eventBus;
this.messageTracker = messageTracker;
}
@Override
public void setGroupId(GroupId groupId) {
this.groupId = groupId;
}
@CallSuper
@SuppressWarnings("unchecked")
@Override
public void onActivityCreate(Activity activity) {
listener = (L) activity;
}
@CallSuper
@Override
public void onActivityStart() {
notificationManager.blockNotification(getGroupId());
eventBus.addListener(this);
}
@CallSuper
@Override
public void onActivityStop() {
notificationManager.unblockNotification(getGroupId());
eventBus.removeListener(this);
}
@Override
public void onActivityDestroy() {
MessageId messageId = listener.getFirstVisibleMessageId();
if (messageId != null) {
dbExecutor.execute(() -> {
try {
messageTracker.storeMessageId(groupId, messageId);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
}
@CallSuper
@Override
public void eventOccurred(Event e) {
if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent s = (GroupRemovedEvent) e;
if (s.getGroup().getId().equals(getGroupId())) {
LOG.info("Group removed");
listener.onGroupRemoved();
}
}
}
@Override
public void loadNamedGroup(
ResultExceptionHandler<G, DbException> handler) {
checkGroupId();
runOnDbThread(() -> {
try {
long start = now();
G groupItem = loadNamedGroup();
logDuration(LOG, "Loading group", start);
handler.onResult(groupItem);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@DatabaseExecutor
protected abstract G loadNamedGroup() throws DbException;
@Override
public void loadItems(
ResultExceptionHandler<ThreadItemList<I>, DbException> handler) {
checkGroupId();
runOnDbThread(() -> {
try {
// Load headers
long start = now();
Collection<H> headers = loadHeaders();
logDuration(LOG, "Loading headers", start);
// Load bodies into cache
start = now();
for (H header : headers) {
if (!textCache.containsKey(header.getId())) {
textCache.put(header.getId(),
loadMessageText(header));
}
}
logDuration(LOG, "Loading bodies", start);
// Build and hand over items
handler.onResult(buildItems(headers));
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@DatabaseExecutor
protected abstract Collection<H> loadHeaders() throws DbException;
@DatabaseExecutor
protected abstract String loadMessageText(H header) throws DbException;
@Override
public void markItemRead(I item) {
markItemsRead(Collections.singletonList(item));
}
@Override
public void markItemsRead(Collection<I> items) {
runOnDbThread(() -> {
try {
long start = now();
for (I i : items) {
markRead(i.getId());
}
logDuration(LOG, "Marking read", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@DatabaseExecutor
protected abstract void markRead(MessageId id) throws DbException;
protected void storePost(M msg, String text,
ResultExceptionHandler<I, DbException> resultHandler) {
runOnDbThread(() -> {
try {
long start = now();
H header = addLocalMessage(msg);
textCache.put(msg.getMessage().getId(), text);
logDuration(LOG, "Storing message", start);
resultHandler.onResult(buildItem(header, text));
} catch (DbException e) {
logException(LOG, WARNING, e);
resultHandler.onException(e);
}
});
}
@DatabaseExecutor
protected abstract H addLocalMessage(M message) throws DbException;
@Override
public void deleteNamedGroup(ExceptionHandler<DbException> handler) {
runOnDbThread(() -> {
try {
long start = now();
G groupItem = loadNamedGroup();
deleteNamedGroup(groupItem);
logDuration(LOG, "Removing group", start);
} catch (DbException e) {
logException(LOG, WARNING, e);
handler.onException(e);
}
});
}
@DatabaseExecutor
protected abstract void deleteNamedGroup(G groupItem) throws DbException;
private ThreadItemList<I> buildItems(Collection<H> headers)
throws DbException {
ThreadItemList<I> items = new ThreadItemListImpl<>();
for (H h : headers) {
items.add(buildItem(h, textCache.get(h.getId())));
}
MessageId msgId = messageTracker.loadStoredMessageId(groupId);
if (LOG.isLoggable(INFO))
LOG.info("Loaded last top visible message id " + msgId);
items.setFirstVisibleId(msgId);
return items;
}
protected abstract I buildItem(H header, String text);
protected GroupId getGroupId() {
checkGroupId();
return groupId;
}
private void checkGroupId() {
if (groupId == null) throw new IllegalStateException();
}
}

View File

@@ -1,259 +0,0 @@
package org.briarproject.briar.android.threaded;
import android.app.Application;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
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.identity.IdentityManager;
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.MessageId;
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.briar.android.sharing.SharingController;
import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
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;
import org.briarproject.briar.api.client.MessageTree;
import org.briarproject.briar.api.client.PostHeader;
import org.briarproject.briar.client.MessageTreeImpl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.now;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class ThreadListViewModel<I extends ThreadItem>
extends DbViewModel
implements EventListener {
private static final Logger LOG =
getLogger(ThreadListViewModel.class.getName());
protected final IdentityManager identityManager;
protected final AndroidNotificationManager notificationManager;
protected final SharingController sharingController;
protected final Executor cryptoExecutor;
protected final Clock clock;
private final MessageTracker messageTracker;
private final EventBus eventBus;
@DatabaseExecutor
private final MessageTree<I> messageTree = new MessageTreeImpl<>();
private final MutableLiveData<LiveResult<List<I>>> items =
new MutableLiveData<>();
private final MutableLiveData<Boolean> groupRemoved =
new MutableLiveData<>();
private final AtomicReference<MessageId> scrollToItem =
new AtomicReference<>();
protected volatile GroupId groupId;
@Nullable
private MessageId replyId;
private final AtomicReference<MessageId> storedMessageId =
new AtomicReference<>();
public ThreadListViewModel(Application application,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor,
IdentityManager identityManager,
AndroidNotificationManager notificationManager,
SharingController sharingController,
@CryptoExecutor Executor cryptoExecutor,
Clock clock,
MessageTracker messageTracker,
EventBus eventBus) {
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
this.identityManager = identityManager;
this.notificationManager = notificationManager;
this.cryptoExecutor = cryptoExecutor;
this.clock = clock;
this.sharingController = sharingController;
this.messageTracker = messageTracker;
this.eventBus = eventBus;
this.eventBus.addListener(this);
}
@Override
protected void onCleared() {
super.onCleared();
eventBus.removeListener(this);
sharingController.onCleared();
}
/**
* Needs to be called right after initialization,
* before calling any other methods.
*/
@CallSuper
public void setGroupId(GroupId groupId) {
this.groupId = groupId;
loadStoredMessageId();
loadItems();
loadSharingContacts();
}
public void blockNotifications() {
notificationManager.blockNotification(groupId);
}
public void unblockNotifications() {
notificationManager.unblockNotification(groupId);
}
@Override
@CallSuper
public void eventOccurred(Event e) {
if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent s = (GroupRemovedEvent) e;
if (s.getGroup().getId().equals(groupId)) {
LOG.info("Group removed");
groupRemoved.setValue(true);
}
}
}
private void loadStoredMessageId() {
runOnDbThreadOrLogException(() -> {
storedMessageId.set(messageTracker.loadStoredMessageId(groupId));
if (LOG.isLoggable(INFO)) {
LOG.info("Loaded last top visible message id " +
storedMessageId);
}
});
}
public abstract void loadItems();
public abstract void createAndStoreMessage(String text,
@Nullable MessageId parentMessageId);
/**
* Loads the ContactIds of all contacts the group is shared with
* and adds them to {@link SharingController}.
*/
public abstract void loadSharingContacts();
@UiThread
protected void setItems(LiveResult<List<I>> items) {
this.items.setValue(items);
}
@DatabaseExecutor
protected <H extends PostHeader> List<I> createItems(
Transaction txn, Collection<H> headers, ItemGetter<H, I> itemGetter)
throws DbException {
long start = now();
List<I> items = new ArrayList<>();
for (H header : headers) {
String text = loadMessageText(txn, header);
items.add(itemGetter.getItem(header, text));
}
logDuration(LOG, "Loading bodies and creating items", start);
messageTree.clear();
messageTree.add(items);
return messageTree.depthFirstOrder();
}
/**
* Add a remote item on the UI thread.
* The list will not scroll, but show an unread indicator.
*/
@UiThread
protected void addItem(I item) {
messageTree.add(item);
items.setValue(new LiveResult<>(messageTree.depthFirstOrder()));
}
/**
* Add a local item from the DB thread.
* The list will scroll to the new item.
*/
@DatabaseExecutor
protected void addItemAsync(I item) {
messageTree.add(item);
scrollToItem.set(item.getId());
items.postValue(new LiveResult<>(messageTree.depthFirstOrder()));
}
@DatabaseExecutor
protected abstract String loadMessageText(Transaction txn,
PostHeader header) throws DbException;
@UiThread
public void setReplyId(@Nullable MessageId id) {
replyId = id;
}
@UiThread
@Nullable
public MessageId getReplyId() {
return replyId;
}
void storeMessageId(@Nullable MessageId messageId) {
if (messageId != null) {
runOnDbThreadOrLogException(() ->
messageTracker.storeMessageId(groupId, messageId));
}
}
protected abstract void markItemRead(I item);
@Nullable
MessageId getAndResetRestoredMessageId() {
return storedMessageId.getAndSet(null);
}
LiveData<LiveResult<List<I>>> getItems() {
return items;
}
LiveData<SharingInfo> getSharingInfo() {
return sharingController.getSharingInfo();
}
LiveData<Boolean> getGroupRemoved() {
return groupRemoved;
}
@Nullable
MessageId getAndResetScrollToItem() {
return scrollToItem.getAndSet(null);
}
public interface ItemGetter<H extends PostHeader, I> {
I getItem(H header, String text);
}
}

View File

@@ -20,15 +20,15 @@ class ThreadScrollListener<I extends ThreadItem>
private static final Logger LOG =
getLogger(ThreadScrollListener.class.getName());
private final ThreadListViewModel<I> viewModel;
private final ThreadListController<?, I> controller;
private final UnreadMessageButton upButton, downButton;
ThreadScrollListener(ThreadItemAdapter<I> adapter,
ThreadListViewModel<I> viewModel,
ThreadListController<?, I> controller,
UnreadMessageButton upButton,
UnreadMessageButton downButton) {
super(adapter);
this.viewModel = viewModel;
this.controller = controller;
this.upButton = upButton;
this.downButton = downButton;
}
@@ -44,7 +44,7 @@ class ThreadScrollListener<I extends ThreadItem>
protected void onItemVisible(I item) {
if (!item.isRead()) {
item.setRead(true);
viewModel.markItemRead(item);
controller.markItemRead(item);
}
}

View File

@@ -2,11 +2,9 @@ package org.briarproject.briar.android.viewmodel;
import android.app.Application;
import org.briarproject.bramble.api.ThrowingRunnable;
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.DbRunnable;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
@@ -25,7 +23,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.arch.core.util.Function;
import androidx.core.util.Consumer;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.RecyclerView;
@@ -60,15 +57,14 @@ public abstract class DbViewModel extends AndroidViewModel {
}
/**
* Waits for the DB to open and runs the given task on the
* {@link DatabaseExecutor}.
* 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(ThrowingRunnable<Exception> task,
Consumer<Exception> err) {
protected void runOnDbThread(Runnable task) {
dbExecutor.execute(() -> {
try {
lifecycleManager.waitForDatabase();
@@ -76,63 +72,10 @@ public abstract class DbViewModel extends AndroidViewModel {
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for database");
Thread.currentThread().interrupt();
} catch (Exception e) {
err.accept(e);
}
});
}
/**
* Waits for the DB to open and runs the given task on the
* {@link DatabaseExecutor}.
* <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(boolean readOnly,
DbRunnable<Exception> task, Consumer<Exception> err) {
dbExecutor.execute(() -> {
try {
lifecycleManager.waitForDatabase();
db.transaction(readOnly, task);
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for database");
Thread.currentThread().interrupt();
} catch (Exception e) {
err.accept(e);
}
});
}
/**
* Waits for the DB to open and runs the given task on the
* {@link DatabaseExecutor}. If the task throws a {@link DbException}
* it's caught and logged.
* <p>
* If you need a list of items to be displayed in a
* {@link RecyclerView.Adapter},
* use {@link #loadList(DbCallable, UiConsumer)} instead.
*/
protected void runOnDbThreadOrLogException(boolean readOnly,
DbRunnable<Exception> task) {
runOnDbThread(readOnly, task, e -> logException(LOG, WARNING, e));
}
/**
* Waits for the DB to open and runs the given task on the
* {@link DatabaseExecutor}. If the task throws a {@link DbException}
* it's caught and logged.
* <p>
* If you need a list of items to be displayed in a
* {@link RecyclerView.Adapter},
* use {@link #loadList(DbCallable, UiConsumer)} instead.
*/
protected void runOnDbThreadOrLogException(
ThrowingRunnable<Exception> task) {
runOnDbThread(task, e -> logException(LOG, WARNING, e));
}
/**
* Loads a list of items on the {@link DatabaseExecutor} within a single
* {@link Transaction} and publishes it as a {@link LiveResult}

View File

@@ -0,0 +1,151 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="499.24373dp"
android:height="175.49413dp"
android:viewportWidth="499.24373"
android:viewportHeight="175.49413">
<path
android:fillColor="#000000"
android:pathData="M459.809,171.16 L455.482,166.826 L448.412,164.399
C444.524,163.064,439.689,161.403,437.668,160.708 L433.994,159.445
L437.518,159.272 C445.76,158.868,450.506,155.265,451.854,148.388
C452.65,144.327,452.983,131.09,452.684,115.347
C452.531,107.263,452.518,99.9306,452.656,99.0522
C452.794,98.1738,453.191,96.6321,453.539,95.6262
C454.761,92.0938,453.879,83.7179,451.681,77.9947
C451.333,77.0872,449.747,73.7797,448.157,70.6447
C443.999,62.4444,443.652,61.7028,443.264,60.1606
C442.805,58.3392,443.052,54.7253,443.726,53.3947
C444.392,52.0797,445.883,50.4785,446.972,49.9081
C448.271,49.2279,449.588,50.0301,451.658,52.7632
C455.08,57.2792,465.659,72.2041,467.39,74.9562
C471.136,80.9126,473.346,86.1212,475.362,93.7447
C476.017,96.2197,477.247,100.742,478.097,103.795
C478.946,106.847,480.811,113.953,482.241,119.585 L484.841,129.825
L489.491,135.018 C492.048,137.874,495.288,141.482,496.691,143.035
C498.093,144.588,499.241,146.01,499.241,146.196
C499.241,146.531,464.741,175.495,464.342,175.495
C464.227,175.495,462.186,173.545,459.807,171.161 Z M365.944,154.772
C364.687,154.144,363.739,153.393,363.03,152.465
C360.941,149.729,361.08,154.834,361.166,83.9825 L361.244,19.9437
L361.905,18.7088 C362.92,16.8111,363.897,15.7979,365.645,14.8299
L367.245,13.9437 L405.029,13.8641
C447.485,13.7747,444.431,13.6158,447.145,16.056
C448.02,16.8431,448.902,18.0155,449.329,18.9598
C450.042,20.5368,450.045,20.5982,450.135,33.1639 L450.225,45.7841
L448.427,45.654 C447.001,45.5509,446.346,45.661,445.261,46.1861
C443.248,47.1606,441.321,49.0848,440.146,51.2936 L439.109,53.2436
L439.102,40.4186 L439.095,27.5936 L405.495,27.5936 L371.895,27.5936
L371.895,78.8936 L371.895,130.194 L405.494,130.194 L439.093,130.194
L439.169,95.769 C439.242,62.8088,439.267,61.4078,439.76,62.844
C440.043,63.669,441.566,66.8415,443.143,69.894
C448.636,80.5202,448.548,79.7541,448.539,117.097
C448.532,144.718,448.415,147.048,446.877,150.206
C445.806,152.405,444.166,153.916,441.819,154.866 L440.145,155.543
L403.977,155.623 L367.809,155.703 L365.945,154.772 Z M408.344,149.734
C411.215,148.425,412.802,146.056,412.774,143.117
C412.735,139.001,409.665,135.894,405.638,135.894
C403.518,135.894,402.071,136.503,400.474,138.066
C398.229,140.264,397.72,143.505,399.173,146.353
C399.886,147.751,401.694,149.362,403.135,149.883
C404.632,150.425,406.975,150.358,408.344,149.734 Z" />
<path
android:fillColor="#000000"
android:pathData="M39.4343,171.16 L43.7609,166.826 L50.8305,164.399
C54.7188,163.064,59.5534,161.403,61.574,160.708 L65.248,159.445 L61.724,159.272
C53.4822,158.868,48.7362,155.265,47.3877,148.388
C46.5914,144.327,46.2588,131.09,46.5575,115.347
C46.7109,107.263,46.7237,99.9306,46.5859,99.0522
C46.4482,98.1738,46.0506,96.6321,45.7025,95.6262
C44.48,92.0938,45.3625,83.7179,47.5602,77.9947
C47.9087,77.0872,49.4945,73.7797,51.0844,70.6447
C55.2429,62.4444,55.589,61.7028,55.9773,60.1606
C56.4359,58.3392,56.1889,54.7253,55.5148,53.3947
C54.8486,52.0797,53.3578,50.4785,52.2686,49.9081
C50.9697,49.2279,49.6529,50.0301,47.5822,52.7632
C44.1607,57.2792,33.5808,72.2041,31.8503,74.9562
C28.1048,80.9126,25.8947,86.1212,23.8781,93.7447
C23.2234,96.2197,21.9929,100.742,21.1435,103.795
C20.2942,106.847,18.4295,113.953,16.9996,119.585 L14.4,129.824 L9.75,135.017
C7.1925,137.873,3.9525,141.481,2.55,143.034 C1.1475,144.587,0,146.009,0,146.195
C0,146.53,34.4996,175.494,34.8991,175.494
C35.0138,175.494,37.0547,173.544,39.4343,171.16 Z M133.299,154.772
C134.557,154.144,135.505,153.393,136.213,152.465
C138.302,149.729,138.163,154.834,138.077,83.9825 L137.999,19.9437
L137.338,18.7088 C136.323,16.8111,135.346,15.7979,133.598,14.8299
L131.998,13.9437 L94.2141,13.8641
C51.7582,13.7747,54.8117,13.6158,52.0976,16.056
C51.2222,16.8431,50.3404,18.0155,49.9137,18.9598
C49.2011,20.5368,49.1976,20.5982,49.1081,33.1639 L49.0182,45.7841
L50.8164,45.654 C52.2422,45.5509,52.8978,45.661,53.9824,46.1861
C55.9953,47.1606,57.9223,49.0848,59.0972,51.2936 L60.1344,53.2436
L60.1414,40.4186 L60.1484,27.5936 L93.7484,27.5936 L127.348,27.5936
L127.348,78.8936 L127.348,130.194 L93.7493,130.194 L60.1506,130.194
L60.0743,95.769 C60.0013,62.8088,59.9761,61.4078,59.4833,62.844
C59.2002,63.669,57.6777,66.8415,56.0999,69.894
C50.6074,80.5202,50.695,79.7541,50.7039,117.097
C50.7109,144.718,50.8275,147.048,52.3659,150.206
C53.4373,152.405,55.0773,153.916,57.4241,154.866 L59.0979,155.543
L95.2656,155.623 L131.433,155.703 L133.297,154.772 Z M90.8996,149.734
C88.0284,148.425,86.4419,146.056,86.4696,143.117
C86.5084,139.001,89.5784,135.894,93.606,135.894
C95.7256,135.894,97.1733,136.503,98.7696,138.066
C101.015,140.264,101.524,143.505,100.071,146.353
C99.3579,147.751,97.5496,149.362,96.1089,149.883
C94.6118,150.425,92.2689,150.358,90.9,149.734 Z" />
<path
android:fillColor="#0a3d91"
android:pathData="M247.254,75.9792 L251.99,75.9792
C265.155,75.9792,275.753,86.5777,275.753,99.7427 L275.753,131.917
C275.753,145.082,265.154,155.68,251.99,155.68 L247.254,155.68
C234.089,155.68,223.491,145.081,223.491,131.917 L223.491,99.7424
C223.491,86.5774,234.09,75.9789,247.254,75.9789 Z" />
<path
android:strokeColor="#ffffff"
android:strokeWidth="4.32805729"
android:pathData="M236.311,102.927 L261.218,127.997 L249.214,142.533 L249.214,90.5963
L261.218,104.479 L236.311,128.896" />
<path
android:fillColor="#0a3d91"
android:strokeWidth="7.55000019"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M143.679,27.5571 C142.089,30.1809,143.861,33.1318,146.464,34.1998
C149.159,35.7722,152.635,39.187,155.612,36.432
C157.845,34.3925,156.213,30.7041,153.571,29.7617
C150.507,28.085,147.019,24.2469,143.68,27.5572 Z M351.705,27.1294
C348.937,29.09,344.636,29.8244,343.327,33.1721
C342.685,35.9031,345.263,38.1647,347.911,37.5986
C351.113,35.9607,354.968,34.7562,357.089,31.6509
C357.658,29.0465,355.596,26.3002,352.821,26.9052
C352.405,26.7975,352.061,26.9402,351.704,27.1294 Z M324.943,40.3012
C322.009,42.0092,317.623,42.224,316.012,45.4542
C315.104,48.1085,317.444,50.6134,320.136,50.3152
C323.492,49.0109,327.456,48.2025,329.873,45.3129
C330.687,42.7752,328.896,39.8433,326.077,40.184
C325.673,40.0383,325.317,40.1458,324.944,40.3011 Z M169.707,42.7641
C168.488,45.476,170.464,48.2246,173.151,48.9095
C176.009,49.9903,179.82,53.0273,182.463,50.1434
C184.778,48.1069,182.802,44.308,180.053,43.7252
C176.725,42.5494,172.668,39.0116,169.707,42.7641 Z M296.98,50.3813
C293.857,51.6905,289.488,51.352,287.488,54.3514
C286.256,56.8701,288.261,59.6497,290.968,59.6911
C294.455,58.7838,298.535,58.5783,301.271,55.952
C302.385,53.5298,300.959,50.4059,298.119,50.401
C297.736,50.2053,297.369,50.274,296.98,50.3813 Z M199.49,51.9262
C197.261,53.388,196.81,56.7921,199.214,58.3031
C201.801,59.7737,205.129,60.258,208.054,60.7984
C210.839,61.1306,212.8,58.3192,211.945,55.7497
C209.902,52.47,205.473,53.0161,202.228,51.6605
C201.315,51.749,200.402,51.8376,199.49,51.9261 Z M268.004,56.434
C264.728,57.2229,260.543,56.2461,258.068,58.816
C256.418,61.0841,257.911,64.1697,260.572,64.6779
C264.184,64.3855,268.29,64.7686,271.412,62.5839
C272.874,60.3563,271.935,57.0522,269.127,56.6245
C268.778,56.3753,268.405,56.3852,268.004,56.434 Z M228.555,57.852
C226.622,59.6879,226.79,63.1186,229.426,64.1735
C232.235,65.1623,235.597,65.0202,238.57,65.0349
C241.369,64.8621,242.796,61.7451,241.494,59.3702
C238.89,56.5186,234.647,57.8284,231.201,57.1003
C230.319,57.3509,229.437,57.6015,228.555,57.8521 Z" />
</vector>

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/stepOne"
style="@style/StepBubble"
android:text="@string/step_1"
app:layout_constraintBottom_toTopOf="@+id/stepOneText"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/stepOneText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Pick contact"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stepOne" />
<View
android:id="@+id/stepConnector"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_margin="16dp"
android:alpha="0.5"
android:background="@color/briar_accent"
app:layout_constraintBottom_toBottomOf="@+id/stepOne"
app:layout_constraintEnd_toStartOf="@+id/stepTwo"
app:layout_constraintStart_toEndOf="@+id/stepOne"
app:layout_constraintTop_toTopOf="@+id/stepOne" />
<TextView
android:id="@+id/stepTwo"
style="@style/StepBubble.Upcoming"
android:text="@string/step_2"
app:layout_constraintBottom_toTopOf="@+id/stepTwoText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/stepOne"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/stepTwoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:alpha="0.5"
android:text="Wait for setup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/stepTwo" />
<TextView
android:id="@+id/discoveryText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="32dp"
android:text="Pick the nearby contact to perform the setup with."
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/contacts"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/stepOneText" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginVertical="32dp"
app:layout_constraintBottom_toTopOf="@id/continueButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/discoveryText" />
<Button
android:id="@+id/continueButton"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="@string/continue_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:enabled="true" />
<TextView
android:id="@+id/commentTextView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="148dp"
android:layout_marginLeft="148dp"
android:layout_marginTop="124dp"
android:text="list of all [contact-name-and-avatar]"
android:textColor="@color/briar_red_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/stepOne"
style="@style/StepBubble.Completed"
app:layout_constraintBottom_toTopOf="@+id/stepOneText"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/stepOneText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Pick contact"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stepOne" />
<View
android:id="@+id/stepConnector"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="@color/briar_accent"
app:layout_constraintBottom_toBottomOf="@+id/stepOne"
app:layout_constraintEnd_toStartOf="@+id/stepTwo"
app:layout_constraintStart_toEndOf="@+id/stepOne"
app:layout_constraintTop_toTopOf="@+id/stepOne" />
<TextView
android:id="@+id/stepTwo"
style="@style/StepBubble"
android:text="@string/step_2"
app:layout_constraintBottom_toTopOf="@+id/stepTwoText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/stepOne"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/stepTwoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Wait for setup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/stepTwo" />
<TextView
android:id="@+id/pendingText"
android:layout_marginTop="32dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:textSize="16sp"
android:text="Waiting for a connection to be established with:\n\n[chosen contact-name-and-avatar]"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/stepOneText" />
<Button
android:id="@+id/doneButton"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Done"
android:enabled="false"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_large">
<ImageView
android:id="@+id/diagram"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:paddingBottom="@dimen/margin_large"
android:scaleType="fitCenter"
android:src="@drawable/bluetooth"
app:layout_constraintBottom_toTopOf="@id/explanationText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/explanationText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="left|start"
android:text="To be able to communicate over Bluetooth with contacts that were previously added at a distance, Briar needs to learn the Bluetooth address of your device. This only needs to be done once.\n\nTo accomplish this, you need to have one of your contacts nearby to perform the Bluetooth Setup.\n\nBefore you start the setup, your contact also needs bring up the Bluetooth Setup screen by navigating [here-and-here].\n\nNote: Bluetooth Pairing is not a required part of the setup."
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/startButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/diagram" />
<Button
android:id="@+id/startButton"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Start"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1.0"
tools:enabled="true" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
@@ -8,13 +7,20 @@
android:icon="@drawable/ic_nearby"
android:orderInCategory="3"
android:title="@string/add_contact_nearby_title"
app:showAsAction="never"/>
app:showAsAction="never" />
<item
android:id="@+id/action_add_contact_remotely"
android:icon="@drawable/ic_link_menu"
android:orderInCategory="2"
android:title="@string/add_contact_remotely_title"
app:showAsAction="never"/>
app:showAsAction="never" />
<item
android:id="@+id/action_bluetooth_setup"
android:icon="@drawable/ic_link_menu"
android:orderInCategory="4"
android:title="Bluetooth setup"
app:showAsAction="never" />
</menu>

View File

@@ -1,44 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<menu 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">
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_group_invite"
android:icon="@drawable/social_share_white"
android:title="@string/groups_invite_members"
android:visible="false"
app:showAsAction="ifRoom"
tools:visible="true" />
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_group_member_list"
android:icon="@drawable/ic_group_white"
android:title="@string/groups_member_list"
app:showAsAction="never" />
app:showAsAction="never"/>
<item
android:id="@+id/action_group_reveal"
android:icon="@drawable/ic_visibility_white"
android:title="@string/groups_reveal_contacts"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
app:showAsAction="never"/>
<item
android:id="@+id/action_group_leave"
android:icon="@drawable/action_delete_white"
android:title="@string/groups_leave"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
app:showAsAction="never"/>
<item
android:id="@+id/action_group_dissolve"
android:icon="@drawable/action_delete_white"
android:title="@string/groups_dissolve"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
app:showAsAction="never"/>
</menu>

View File

@@ -0,0 +1,120 @@
package org.briarproject.briar.android.forum;
import android.content.Intent;
import junit.framework.Assert;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.briar.api.identity.AuthorInfo;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadItemList;
import org.briarproject.briar.android.threaded.ThreadItemListImpl;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import static junit.framework.Assert.assertEquals;
import static org.briarproject.briar.api.identity.AuthorInfo.Status.UNKNOWN;
import static org.briarproject.bramble.test.TestUtils.getAuthor;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 21)
public class ForumActivityTest {
private final static MessageId[] MESSAGE_IDS = new MessageId[6];
static {
for (int i = 0; i < MESSAGE_IDS.length; i++)
MESSAGE_IDS[i] = new MessageId(getRandomId());
}
private final static MessageId[] PARENT_IDS = {
null,
MESSAGE_IDS[0],
MESSAGE_IDS[1],
MESSAGE_IDS[2],
MESSAGE_IDS[0],
null
};
/*
1
-> 2
-> 3
-> 4
5
6
*/
private final static int[] LEVELS = {
0, 1, 2, 3, 1, 0
};
private TestForumActivity forumActivity;
@Captor
private ArgumentCaptor<UiResultExceptionHandler<ThreadItemList<ForumPostItem>, DbException>>
rc;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Intent intent = new Intent();
intent.putExtra("briar.GROUP_ID", getRandomId());
forumActivity = Robolectric.buildActivity(TestForumActivity.class,
intent).create().start().resume().get();
}
private ThreadItemList<ForumPostItem> getDummyData() {
ForumPostItem[] forumPostItems = new ForumPostItem[6];
for (int i = 0; i < forumPostItems.length; i++) {
Author author = getAuthor();
String text = getRandomString(MAX_FORUM_POST_TEXT_LENGTH);
forumPostItems[i] = new ForumPostItem(MESSAGE_IDS[i], PARENT_IDS[i],
text, System.currentTimeMillis(), author,
new AuthorInfo(UNKNOWN));
forumPostItems[i].setLevel(LEVELS[i]);
}
ThreadItemList<ForumPostItem> list = new ThreadItemListImpl<>();
list.addAll(Arrays.asList(forumPostItems));
return list;
}
@Test
public void testNestedEntries() {
ForumController mc = forumActivity.getController();
ThreadItemList<ForumPostItem> dummyData = getDummyData();
verify(mc, times(1)).loadItems(rc.capture());
rc.getValue().onResult(dummyData);
ThreadItemAdapter<ForumPostItem> adapter = forumActivity.getAdapter();
Assert.assertNotNull(adapter);
assertEquals(6, adapter.getItemCount());
assertEquals(dummyData.get(0).getText(),
adapter.getItemAt(0).getText());
assertEquals(dummyData.get(1).getText(),
adapter.getItemAt(1).getText());
assertEquals(dummyData.get(2).getText(),
adapter.getItemAt(2).getText());
assertEquals(dummyData.get(3).getText(),
adapter.getItemAt(3).getText());
assertEquals(dummyData.get(4).getText(),
adapter.getItemAt(4).getText());
assertEquals(dummyData.get(5).getText(),
adapter.getItemAt(5).getText());
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.briar.android.forum;
import android.os.Bundle;
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.ActivityModule;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.BriarControllerImpl;
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.mockito.Mockito;
import javax.annotation.Nullable;
/**
* This class exposes the ForumController and offers the possibility to
* override it.
*/
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class TestForumActivity extends ForumActivity {
@Override
public ForumController getController() {
return forumController;
}
public ThreadItemAdapter<ForumPostItem> getAdapter() {
return adapter;
}
@Override
public void onCreate(@Nullable Bundle state) {
setTheme(R.style.BriarTheme_NoActionBar);
super.onCreate(state);
}
@Override
protected ActivityModule getActivityModule() {
return new ActivityModule(this) {
@Override
protected BriarController provideBriarController(
BriarControllerImpl briarController) {
BriarController c = Mockito.mock(BriarController.class);
Mockito.when(c.accountSignedIn()).thenReturn(true);
return c;
}
};
}
@Override
protected ForumModule getForumModule() {
return new ForumModule() {
@Override
ForumController provideForumController(BaseActivity activity,
ForumControllerImpl forumController) {
return Mockito.mock(ForumController.class);
}
};
}
}

View File

@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import java.util.Collection;
import java.util.List;
import java.util.Comparator;
import javax.annotation.Nullable;
@@ -15,9 +15,11 @@ public interface MessageTree<T extends MessageTree.MessageNode> {
void add(T node);
void setComparator(Comparator<T> comparator);
void clear();
List<T> depthFirstOrder();
Collection<T> depthFirstOrder();
boolean contains(MessageId m);

View File

@@ -11,7 +11,6 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;
@@ -85,22 +84,11 @@ public interface ForumManager {
*/
String getPostText(MessageId m) throws DbException;
/**
* Returns the text of the forum post with the given ID.
*/
String getPostText(Transaction txn, MessageId m) throws DbException;
/**
* Returns the headers of all posts in the given forum.
*/
Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException;
/**
* Returns the headers of all posts in the given forum.
*/
List<ForumPostHeader> getPostHeaders(Transaction txn, GroupId g)
throws DbException;
/**
* Registers a hook to be called whenever a forum is removed.
*/
@@ -109,6 +97,7 @@ public interface ForumManager {
/**
* Returns the group count for the given forum.
*/
@Deprecated
GroupCount getGroupCount(GroupId g) throws DbException;
/**

View File

@@ -8,14 +8,21 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault
public class JoinMessageHeader extends GroupMessageHeader {
private final Visibility visibility;
private final boolean isInitial;
public JoinMessageHeader(GroupMessageHeader h, boolean isInitial) {
public JoinMessageHeader(GroupMessageHeader h, Visibility visibility,
boolean isInitial) {
super(h.getGroupId(), h.getId(), h.getParentId(), h.getTimestamp(),
h.getAuthor(), h.getAuthorInfo(), h.isRead());
this.visibility = visibility;
this.isInitial = isInitial;
}
public Visibility getVisibility() {
return visibility;
}
public boolean isInitial() {
return isInitial;
}

View File

@@ -12,7 +12,6 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import java.util.Collection;
import java.util.List;
@NotNullByDefault
public interface PrivateGroupManager {
@@ -108,33 +107,16 @@ public interface PrivateGroupManager {
*/
String getMessageText(MessageId m) throws DbException;
/**
* Returns the text of the private group message with the given ID.
*/
String getMessageText(Transaction txn, MessageId m) throws DbException;
/**
* Returns the headers of all messages in the given private group.
*/
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
/**
* Returns the headers of all messages in the given private group.
*/
List<GroupMessageHeader> getHeaders(Transaction txn, GroupId g)
throws DbException;
/**
* Returns all members of the given private group.
*/
Collection<GroupMember> getMembers(GroupId g) throws DbException;
/**
* Returns all members of the given private group.
*/
Collection<GroupMember> getMembers(Transaction txn, GroupId g)
throws DbException;
/**
* Returns true if the given author is a member of the given private group.
*/

View File

@@ -3,7 +3,6 @@ package org.briarproject.briar.api.sharing;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.api.client.SessionId;
@@ -46,12 +45,6 @@ public interface SharingManager<S extends Shareable>
*/
Collection<Contact> getSharedWith(GroupId g) throws DbException;
/**
* Returns all contacts with whom the given group is shared.
*/
Collection<Contact> getSharedWith(Transaction txn, GroupId g)
throws DbException;
/**
* Returns true if the group not already shared and no invitation is open
*/

View File

@@ -30,7 +30,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
private final List<List<T>> unsortedLists = new ArrayList<>();
@SuppressWarnings("UseCompareMethod")
private final Comparator<T> comparator = (o1, o2) ->
private Comparator<T> comparator = (o1, o2) ->
Long.valueOf(o1.getTimestamp()).compareTo(o2.getTimestamp());
@Override
@@ -79,7 +79,6 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
@GuardedBy("this")
private void sortUnsorted() {
for (List<T> list : unsortedLists) {
//noinspection Java8ListSort
Collections.sort(list, comparator);
}
unsortedLists.clear();
@@ -96,7 +95,17 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
}
@Override
public synchronized List<T> depthFirstOrder() {
public synchronized void setComparator(Comparator<T> comparator) {
this.comparator = comparator;
// Sort all lists with the new comparator
Collections.sort(roots, comparator);
for (Map.Entry<MessageId, List<T>> entry : nodeMap.entrySet()) {
Collections.sort(entry.getValue(), comparator);
}
}
@Override
public synchronized Collection<T> depthFirstOrder() {
List<T> orderedList = new ArrayList<>();
for (T root : roots) {
traverse(orderedList, root, 0);

View File

@@ -192,15 +192,6 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
}
}
@Override
public String getPostText(Transaction txn, MessageId m) throws DbException {
try {
return getPostText(clientHelper.getMessageAsList(txn, m));
} catch (FormatException e) {
throw new DbException(e);
}
}
private String getPostText(BdfList body) throws FormatException {
// Parent ID, author, text, signature
return body.getString(2);
@@ -209,35 +200,33 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
@Override
public Collection<ForumPostHeader> getPostHeaders(GroupId g)
throws DbException {
return db.transactionWithResult(true, txn -> getPostHeaders(txn, g));
}
@Override
public List<ForumPostHeader> getPostHeaders(Transaction txn, GroupId g)
throws DbException {
try {
List<ForumPostHeader> headers = new ArrayList<>();
Map<MessageId, BdfDictionary> metadata =
clientHelper.getMessageMetadataAsDictionary(txn, g);
// get all authors we need to get the info for
Set<AuthorId> authors = new HashSet<>();
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
BdfList authorList = entry.getValue().getList(KEY_AUTHOR);
Author a = clientHelper.parseAndValidateAuthor(authorList);
authors.add(a.getId());
}
// get information for all authors
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
for (AuthorId id : authors) {
authorInfos.put(id, authorManager.getAuthorInfo(txn, id));
}
// Parse the metadata
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
BdfDictionary meta = entry.getValue();
headers.add(getForumPostHeader(txn, entry.getKey(), meta,
authorInfos));
}
return headers;
return db.transactionWithResult(true, txn -> {
Collection<ForumPostHeader> headers = new ArrayList<>();
Map<MessageId, BdfDictionary> metadata =
clientHelper.getMessageMetadataAsDictionary(txn, g);
// get all authors we need to get the info for
Set<AuthorId> authors = new HashSet<>();
for (Entry<MessageId, BdfDictionary> entry :
metadata.entrySet()) {
BdfList authorList = entry.getValue().getList(KEY_AUTHOR);
Author a = clientHelper.parseAndValidateAuthor(authorList);
authors.add(a.getId());
}
// get information for all authors
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
for (AuthorId id : authors) {
authorInfos.put(id, authorManager.getAuthorInfo(txn, id));
}
// Parse the metadata
for (Entry<MessageId, BdfDictionary> entry :
metadata.entrySet()) {
BdfDictionary meta = entry.getValue();
headers.add(getForumPostHeader(txn, entry.getKey(), meta,
authorInfos));
}
return headers;
});
} catch (FormatException e) {
throw new DbException(e);
}

View File

@@ -145,7 +145,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
addMember(txn, m.getMessage().getGroupId(), m.getMember(), VISIBLE);
setPreviousMsgId(txn, m.getMessage().getGroupId(),
m.getMessage().getId());
attachJoinMessageAddedEvent(txn, m.getMessage(), meta, true);
attachJoinMessageAddedEvent(txn, m.getMessage(), meta, true, VISIBLE);
}
@Override
@@ -315,16 +315,6 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
}
}
@Override
public String getMessageText(Transaction txn, MessageId m)
throws DbException {
try {
return getMessageText(clientHelper.getMessageAsList(txn, m));
} catch (FormatException e) {
throw new DbException(e);
}
}
private String getMessageText(BdfList body) throws FormatException {
// Message type (0), member (1), parent ID (2), previous message ID (3),
// text (4), signature (5)
@@ -334,13 +324,8 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
@Override
public Collection<GroupMessageHeader> getHeaders(GroupId g)
throws DbException {
return db.transactionWithResult(true, txn -> getHeaders(txn, g));
}
@Override
public List<GroupMessageHeader> getHeaders(Transaction txn, GroupId g)
throws DbException {
List<GroupMessageHeader> headers = new ArrayList<>();
Collection<GroupMessageHeader> headers = new ArrayList<>();
Transaction txn = db.startTransaction(true);
try {
Map<MessageId, BdfDictionary> metadata =
clientHelper.getMessageMetadataAsDictionary(txn, g);
@@ -354,20 +339,27 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
for (AuthorId id : authors) {
authorInfos.put(id, authorManager.getAuthorInfo(txn, id));
}
// get current visibilities for join messages
Map<Author, Visibility> visibilities = getMembers(txn, g);
// parse the metadata
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
BdfDictionary meta = entry.getValue();
if (meta.getLong(KEY_TYPE) == JOIN.getInt()) {
Author member = getAuthor(meta);
Visibility v = visibilities.get(member);
headers.add(getJoinMessageHeader(txn, g, entry.getKey(),
meta, authorInfos));
meta, authorInfos, v));
} else {
headers.add(getGroupMessageHeader(txn, g, entry.getKey(),
meta, authorInfos));
}
}
db.commitTransaction(txn);
return headers;
} catch (FormatException e) {
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
}
@@ -397,46 +389,46 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
private JoinMessageHeader getJoinMessageHeader(Transaction txn, GroupId g,
MessageId id, BdfDictionary meta,
Map<AuthorId, AuthorInfo> authorInfos)
Map<AuthorId, AuthorInfo> authorInfos, Visibility v)
throws DbException, FormatException {
GroupMessageHeader header =
getGroupMessageHeader(txn, g, id, meta, authorInfos);
boolean creator = meta.getBoolean(KEY_INITIAL_JOIN_MSG);
return new JoinMessageHeader(header, creator);
return new JoinMessageHeader(header, v, creator);
}
@Override
public Collection<GroupMember> getMembers(GroupId g)
throws DbException {
return db.transactionWithResult(true, txn -> getMembers(txn, g));
}
@Override
public Collection<GroupMember> getMembers(Transaction txn, GroupId g)
throws DbException {
Collection<GroupMember> members = new ArrayList<>();
Map<Author, Visibility> authors = getMemberAuthors(txn, g);
LocalAuthor la = identityManager.getLocalAuthor(txn);
PrivateGroup privateGroup = getPrivateGroup(txn, g);
for (Entry<Author, Visibility> m : authors.entrySet()) {
Author a = m.getKey();
AuthorInfo authorInfo = authorManager.getAuthorInfo(txn, a.getId());
Status status = authorInfo.getStatus();
Visibility v = m.getValue();
ContactId c = null;
if (v != INVISIBLE &&
(status == VERIFIED || status == UNVERIFIED)) {
c = contactManager.getContact(txn, a.getId(), la.getId())
.getId();
public Collection<GroupMember> getMembers(GroupId g) throws DbException {
Transaction txn = db.startTransaction(true);
try {
Collection<GroupMember> members = new ArrayList<>();
Map<Author, Visibility> authors = getMembers(txn, g);
LocalAuthor la = identityManager.getLocalAuthor(txn);
PrivateGroup privateGroup = getPrivateGroup(txn, g);
for (Entry<Author, Visibility> m : authors.entrySet()) {
Author a = m.getKey();
AuthorInfo authorInfo =
authorManager.getAuthorInfo(txn, a.getId());
Status status = authorInfo.getStatus();
Visibility v = m.getValue();
ContactId c = null;
if (v != INVISIBLE &&
(status == VERIFIED || status == UNVERIFIED)) {
c = contactManager.getContact(txn, a.getId(), la.getId())
.getId();
}
boolean isCreator = privateGroup.getCreator().equals(a);
members.add(new GroupMember(a, authorInfo, isCreator, c, v));
}
boolean isCreator = privateGroup.getCreator().equals(a);
members.add(new GroupMember(a, authorInfo, isCreator, c, v));
db.commitTransaction(txn);
return members;
} finally {
db.endTransaction(txn);
}
return members;
}
private Map<Author, Visibility> getMemberAuthors(Transaction txn, GroupId g)
private Map<Author, Visibility> getMembers(Transaction txn, GroupId g)
throws DbException {
try {
BdfDictionary meta =
@@ -458,7 +450,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
@Override
public boolean isMember(Transaction txn, GroupId g, Author a)
throws DbException {
for (Author member : getMemberAuthors(txn, g).keySet()) {
for (Author member : getMembers(txn, g).keySet()) {
if (member.equals(a)) return true;
}
return false;
@@ -552,7 +544,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
addMember(txn, m.getGroupId(), member, v);
// track message and broadcast event
messageTracker.trackIncomingMessage(txn, m);
attachJoinMessageAddedEvent(txn, m, meta, false);
attachJoinMessageAddedEvent(txn, m, meta, false, v);
}
private void handleGroupMessage(Transaction txn, Message m,
@@ -602,10 +594,10 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
}
private void attachJoinMessageAddedEvent(Transaction txn, Message m,
BdfDictionary meta, boolean local)
BdfDictionary meta, boolean local, Visibility v)
throws DbException, FormatException {
JoinMessageHeader header = getJoinMessageHeader(txn, m.getGroupId(),
m.getId(), meta, Collections.emptyMap());
m.getId(), meta, Collections.emptyMap(), v);
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, "",
local));
}

View File

@@ -426,17 +426,17 @@ abstract class SharingManagerImpl<S extends Shareable>
@Override
public Collection<Contact> getSharedWith(GroupId g) throws DbException {
return db.transactionWithResult(true, txn -> getSharedWith(txn, g));
}
@Override
public Collection<Contact> getSharedWith(Transaction txn, GroupId g)
throws DbException {
// TODO report also pending invitations
Collection<Contact> contacts = new ArrayList<>();
for (Contact c : db.getContacts(txn)) {
if (db.getGroupVisibility(txn, c.getId(), g) == SHARED)
contacts.add(c);
Transaction txn = db.startTransaction(true);
try {
for (Contact c : db.getContacts(txn)) {
if (db.getGroupVisibility(txn, c.getId(), g) == SHARED)
contacts.add(c);
}
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
}
return contacts;
}

View File

@@ -20,10 +20,10 @@ import org.junit.Test;
import java.util.Collection;
import static org.briarproject.briar.api.identity.AuthorInfo.Status.VERIFIED;
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.briar.api.identity.AuthorInfo.Status.VERIFIED;
import static org.briarproject.briar.api.privategroup.Visibility.INVISIBLE;
import static org.briarproject.briar.api.privategroup.Visibility.REVEALED_BY_CONTACT;
import static org.briarproject.briar.api.privategroup.Visibility.REVEALED_BY_US;
@@ -124,8 +124,10 @@ public class PrivateGroupManagerIntegrationTest
addGroup();
// create and add test message with no previousMsgId
GroupMessage msg = groupMessageFactory.createGroupMessage(groupId0,
clock.currentTimeMillis(), null, author0, "test", null);
@SuppressWarnings("ConstantConditions")
GroupMessage msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
author0, "test", null);
groupManager0.addLocalMessage(msg);
// sync test message
@@ -340,19 +342,23 @@ public class PrivateGroupManagerIntegrationTest
Collection<GroupMember> members0 = groupManager0.getMembers(groupId0);
assertEquals(2, members0.size());
for (GroupMember m : members0) {
if (!m.getAuthor().equals(author0)) {
if (m.getAuthor().equals(author0)) {
assertEquals(VISIBLE, m.getVisibility());
} else {
assertEquals(author1, m.getAuthor());
assertEquals(VISIBLE, m.getVisibility());
}
assertEquals(VISIBLE, m.getVisibility());
}
Collection<GroupMember> members1 = groupManager1.getMembers(groupId0);
assertEquals(2, members1.size());
for (GroupMember m : members1) {
if (!m.getAuthor().equals(author1)) {
if (m.getAuthor().equals(author1)) {
assertEquals(VISIBLE, m.getVisibility());
} else {
assertEquals(author0, m.getAuthor());
assertEquals(VISIBLE, m.getVisibility());
}
assertEquals(VISIBLE, m.getVisibility());
}
}
@@ -362,11 +368,27 @@ public class PrivateGroupManagerIntegrationTest
Collection<GroupMessageHeader> headers0 =
groupManager0.getHeaders(groupId0);
assertEquals(2, headers0.size());
for (GroupMessageHeader h : headers0) {
if (h instanceof JoinMessageHeader) {
JoinMessageHeader j = (JoinMessageHeader) h;
// all relationships of the creator are visible
assertEquals(VISIBLE, j.getVisibility());
}
}
Collection<GroupMessageHeader> headers1 =
groupManager1.getHeaders(groupId0);
assertEquals(2, headers1.size());
for (GroupMessageHeader h : headers1) {
if (h instanceof JoinMessageHeader) {
JoinMessageHeader j = (JoinMessageHeader) h;
if (h.getAuthor().equals(author1))
// we are visible to ourselves
assertEquals(VISIBLE, j.getVisibility());
else
// our relationship to the creator is visible
assertEquals(VISIBLE, j.getVisibility());
}
}
}
@Test
@@ -441,6 +463,34 @@ public class PrivateGroupManagerIntegrationTest
assertEquals(REVEALED_BY_CONTACT, m.getVisibility());
}
}
// assert that join messages reflect revealed relationship
Collection<GroupMessageHeader> headers1 =
groupManager1.getHeaders(groupId0);
for (GroupMessageHeader h : headers1) {
if (h instanceof JoinMessageHeader) {
JoinMessageHeader j = (JoinMessageHeader) h;
if (h.getAuthor().equals(author2))
// 1 revealed the relationship to 2
assertEquals(REVEALED_BY_US, j.getVisibility());
else
// 1's other relationship (to 1 and creator) are visible
assertEquals(VISIBLE, j.getVisibility());
}
}
Collection<GroupMessageHeader> headers2 =
groupManager2.getHeaders(groupId0);
for (GroupMessageHeader h : headers2) {
if (h instanceof JoinMessageHeader) {
JoinMessageHeader j = (JoinMessageHeader) h;
if (h.getAuthor().equals(author1))
// 2's relationship was revealed by 1
assertEquals(REVEALED_BY_CONTACT, j.getVisibility());
else
// 2's other relationship (to 2 and creator) are visible
assertEquals(VISIBLE, j.getVisibility());
}
}
}
@Test