Merge branch 'master' into '2005-connect-via-bt-error'

# Conflicts:
#   briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java
This commit is contained in:
akwizgran
2021-05-06 13:03:46 +00:00
77 changed files with 1073 additions and 603 deletions

View File

@@ -32,6 +32,7 @@ public class DozeFragment extends SetupFragment
private DozeView dozeView;
private HuaweiProtectedAppsView huaweiProtectedAppsView;
private HuaweiAppLaunchView huaweiAppLaunchView;
private XiaomiView xiaomiView;
private Button next;
private boolean secondAttempt = false;
@@ -53,6 +54,8 @@ public class DozeFragment extends SetupFragment
huaweiProtectedAppsView.setOnCheckedChangedListener(this);
huaweiAppLaunchView = v.findViewById(R.id.huaweiAppLaunchView);
huaweiAppLaunchView.setOnCheckedChangedListener(this);
xiaomiView = v.findViewById(R.id.xiaomiView);
xiaomiView.setOnCheckedChangedListener(this);
next = v.findViewById(R.id.next);
ProgressBar progressBar = v.findViewById(R.id.progress);
@@ -98,7 +101,8 @@ public class DozeFragment extends SetupFragment
public void onCheckedChanged() {
next.setEnabled(dozeView.isChecked() &&
huaweiProtectedAppsView.isChecked() &&
huaweiAppLaunchView.isChecked());
huaweiAppLaunchView.isChecked() &&
xiaomiView.isChecked());
}
@SuppressLint("BatteryLife")

View File

@@ -10,6 +10,7 @@ class DozeHelperImpl implements DozeHelper {
Context appContext = context.getApplicationContext();
return needsDozeWhitelisting(appContext) ||
HuaweiProtectedAppsView.needsToBeShown(appContext) ||
HuaweiAppLaunchView.needsToBeShown(appContext);
HuaweiAppLaunchView.needsToBeShown(appContext) ||
XiaomiView.isXiaomiOrRedmiDevice();
}
}

View File

@@ -118,7 +118,7 @@ public class SetPasswordFragment extends SetupFragment {
@Override
public void onClick(View view) {
IBinder token = passwordEntry.getWindowToken();
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
Object o = requireContext().getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
viewModel.setPassword(passwordEntry.getText().toString());
}

View File

@@ -26,6 +26,8 @@ import static org.briarproject.briar.android.account.SetupViewModel.State.CREATE
import static org.briarproject.briar.android.account.SetupViewModel.State.DOZE;
import static org.briarproject.briar.android.account.SetupViewModel.State.FAILED;
import static org.briarproject.briar.android.account.SetupViewModel.State.SET_PASSWORD;
import static org.briarproject.briar.android.util.UiUtils.setInputStateAlwaysVisible;
import static org.briarproject.briar.android.util.UiUtils.setInputStateHidden;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
@@ -55,10 +57,13 @@ public class SetupActivity extends BaseActivity
private void onStateChanged(SetupViewModel.State state) {
if (state == AUTHOR_NAME) {
setInputStateAlwaysVisible(this);
showInitialFragment(AuthorNameFragment.newInstance());
} else if (state == SET_PASSWORD) {
setInputStateAlwaysVisible(this);
showPasswordFragment();
} else if (state == DOZE) {
setInputStateHidden(this);
showDozeFragment();
} else if (state == CREATED || state == FAILED) {
// TODO: Show an error if failed

View File

@@ -0,0 +1,74 @@
package org.briarproject.briar.android.account;
import android.content.Context;
import android.util.AttributeSet;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import javax.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import static android.os.Build.BRAND;
import static org.briarproject.bramble.util.AndroidUtils.getSystemProperty;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
@UiThread
@NotNullByDefault
class XiaomiView extends PowerView {
public XiaomiView(Context context) {
this(context, null);
}
public XiaomiView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public XiaomiView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
setText(R.string.setup_xiaomi_text);
setButtonText(R.string.setup_xiaomi_button);
}
@Override
public boolean needsToBeShown() {
return isXiaomiOrRedmiDevice();
}
public static boolean isXiaomiOrRedmiDevice() {
return "Xiaomi".equalsIgnoreCase(BRAND) ||
"Redmi".equalsIgnoreCase(BRAND);
}
@Override
@StringRes
protected int getHelpText() {
return R.string.setup_xiaomi_help;
}
@Override
protected void onButtonClick() {
int bodyRes = isMiuiTenOrLater()
? R.string.setup_xiaomi_dialog_body_new
: R.string.setup_xiaomi_dialog_body_old;
showOnboardingDialog(getContext(), getContext().getString(bodyRes));
setChecked(true);
}
private boolean isMiuiTenOrLater() {
String version = getSystemProperty("ro.miui.ui.version.name");
if (isNullOrEmpty(version)) return false;
version = version.replaceAll("[^\\d]", "");
try {
return Integer.parseInt(version) >= 10;
} catch (NumberFormatException e) {
return false;
}
}
}

View File

@@ -13,8 +13,11 @@ import org.briarproject.briar.android.blog.BlogPostFragment;
import org.briarproject.briar.android.blog.FeedFragment;
import org.briarproject.briar.android.blog.ReblogActivity;
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.RssFeedActivity;
import org.briarproject.briar.android.blog.RssFeedDeleteFeedDialogFragment;
import org.briarproject.briar.android.blog.RssFeedImportFailedDialogFragment;
import org.briarproject.briar.android.blog.RssFeedImportFragment;
import org.briarproject.briar.android.blog.RssFeedManageFragment;
import org.briarproject.briar.android.blog.WriteBlogPostActivity;
import org.briarproject.briar.android.contact.ContactListFragment;
import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity;
@@ -161,9 +164,7 @@ public interface ActivityComponent {
void inject(IntroductionActivity activity);
void inject(RssFeedImportActivity activity);
void inject(RssFeedManageActivity activity);
void inject(RssFeedActivity activity);
void inject(StartupFailureActivity activity);
@@ -233,4 +234,12 @@ public interface ActivityComponent {
void inject(
BluetoothConnecterDialogFragment bluetoothConnecterDialogFragment);
void inject(RssFeedImportFragment fragment);
void inject(RssFeedManageFragment fragment);
void inject(RssFeedImportFailedDialogFragment fragment);
void inject(RssFeedDeleteFeedDialogFragment fragment);
}

View File

@@ -20,4 +20,8 @@ public interface BlogModule {
@ViewModelKey(BlogViewModel.class)
ViewModel bindBlogViewModel(BlogViewModel blogViewModel);
@Binds
@IntoMap
@ViewModelKey(RssFeedViewModel.class)
ViewModel bindRssFeedViewModel(RssFeedViewModel rssFeedViewModel);
}

View File

@@ -131,15 +131,8 @@ public class FeedFragment extends BaseFragment
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_import) {
Intent i = new Intent(getActivity(), RssFeedImportActivity.class);
startActivity(i);
return true;
} else if (itemId == R.id.action_rss_feeds_manage) {
Blog personalBlog = viewModel.getPersonalBlog().getValue();
if (personalBlog == null) return false;
Intent i = new Intent(getActivity(), RssFeedManageActivity.class);
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
} else if (itemId == R.id.action_rss_feeds) {
Intent i = new Intent(getActivity(), RssFeedActivity.class);
startActivity(i);
return true;
}

View File

@@ -0,0 +1,69 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.EXISTS;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedActivity extends BriarActivity
implements BaseFragmentListener {
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_container);
if (savedInstanceState == null) {
showInitialFragment(RssFeedManageFragment.newInstance());
}
viewModel.getImportResult().observeEvent(this, this::onImportResult);
}
private void onImportResult(RssFeedViewModel.ImportResult result) {
if (result == IMPORTED) {
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentByTag(RssFeedImportFragment.TAG) != null) {
onBackPressed();
}
} else if (result == FAILED) {
RssFeedImportFailedDialogFragment dialog =
RssFeedImportFailedDialogFragment.newInstance();
dialog.show(getSupportFragmentManager(),
RssFeedImportFailedDialogFragment.TAG);
} else if (result == EXISTS) {
Toast.makeText(this, R.string.blogs_rss_feeds_import_exists,
Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -7,91 +7,54 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.util.BriarAdapter;
import org.briarproject.briar.android.util.UiUtils;
import org.briarproject.briar.api.feed.Feed;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static org.briarproject.briar.android.util.UiUtils.formatDate;
class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
@NotNullByDefault
class RssFeedAdapter extends ListAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
private final RssFeedListener listener;
RssFeedAdapter(Context ctx, RssFeedListener listener) {
super(ctx, Feed.class);
RssFeedAdapter(RssFeedListener listener) {
super(new DiffUtil.ItemCallback<Feed>() {
@Override
public boolean areItemsTheSame(Feed a, Feed b) {
return a.getUrl().equals(b.getUrl()) &&
a.getBlogId().equals(b.getBlogId()) &&
a.getAdded() == b.getAdded();
}
@Override
public boolean areContentsTheSame(Feed a, Feed b) {
return a.getUpdated() == b.getUpdated();
}
});
this.listener = listener;
}
@Override
public FeedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(ctx).inflate(
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_item_rss_feed, parent, false);
return new FeedViewHolder(v);
}
@Override
public void onBindViewHolder(FeedViewHolder ui, int position) {
Feed item = getItemAt(position);
if (item == null) return;
// Feed Title
ui.title.setText(item.getTitle());
// Delete Button
ui.delete.setOnClickListener(v -> listener.onDeleteClick(item));
// Author
if (item.getRssAuthor() != null) {
ui.author.setText(item.getRssAuthor());
ui.author.setVisibility(VISIBLE);
ui.authorLabel.setVisibility(VISIBLE);
} else {
ui.author.setVisibility(GONE);
ui.authorLabel.setVisibility(GONE);
}
// Imported and Last Updated
ui.imported.setText(UiUtils.formatDate(ctx, item.getAdded()));
ui.updated.setText(UiUtils.formatDate(ctx, item.getUpdated()));
// Description
if (item.getDescription() != null) {
ui.description.setText(item.getDescription());
ui.description.setVisibility(VISIBLE);
} else {
ui.description.setVisibility(GONE);
}
// Open feed's blog when clicked
ui.layout.setOnClickListener(v -> listener.onFeedClick(item));
ui.bindItem(getItem(position));
}
@Override
public int compare(Feed a, Feed b) {
if (a == b) return 0;
long aTime = a.getAdded(), bTime = b.getAdded();
if (aTime > bTime) return -1;
if (aTime < bTime) return 1;
return 0;
}
@Override
public boolean areContentsTheSame(Feed a, Feed b) {
return a.getUpdated() == b.getUpdated();
}
@Override
public boolean areItemsTheSame(Feed a, Feed b) {
return a.getUrl().equals(b.getUrl()) &&
a.getBlogId().equals(b.getBlogId()) &&
a.getAdded() == b.getAdded();
}
static class FeedViewHolder extends RecyclerView.ViewHolder {
class FeedViewHolder extends RecyclerView.ViewHolder {
private final Context ctx;
private final View layout;
private final TextView title;
private final ImageButton delete;
@@ -104,6 +67,7 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
private FeedViewHolder(View v) {
super(v);
ctx = v.getContext();
layout = v;
title = v.findViewById(R.id.titleView);
delete = v.findViewById(R.id.deleteButton);
@@ -113,10 +77,44 @@ class RssFeedAdapter extends BriarAdapter<Feed, RssFeedAdapter.FeedViewHolder> {
authorLabel = v.findViewById(R.id.author);
description = v.findViewById(R.id.descriptionView);
}
private void bindItem(Feed item) {
// Feed Title
title.setText(item.getTitle());
// Delete Button
delete.setOnClickListener(v -> listener.onDeleteClick(item));
// Author
if (item.getRssAuthor() != null) {
author.setText(item.getRssAuthor());
author.setVisibility(VISIBLE);
authorLabel.setVisibility(VISIBLE);
} else {
author.setVisibility(GONE);
authorLabel.setVisibility(GONE);
}
// Imported and Last Updated
imported.setText(formatDate(ctx, item.getAdded()));
updated.setText(formatDate(ctx, item.getUpdated()));
// Description
if (item.getDescription() != null) {
description.setText(item.getDescription());
description.setVisibility(VISIBLE);
} else {
description.setVisibility(GONE);
}
// Open feed's blog when clicked
layout.setOnClickListener(v -> listener.onFeedClick(item));
}
}
interface RssFeedListener {
void onFeedClick(Feed feed);
void onDeleteClick(Feed feed);
}

View File

@@ -0,0 +1,64 @@
package org.briarproject.briar.android.blog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BaseActivity;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedDeleteFeedDialogFragment extends DialogFragment {
final static String TAG = RssFeedDeleteFeedDialogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
static RssFeedDeleteFeedDialogFragment newInstance(GroupId groupId) {
Bundle args = new Bundle();
args.putByteArray(GROUP_ID, groupId.getBytes());
RssFeedDeleteFeedDialogFragment f =
new RssFeedDeleteFeedDialogFragment();
f.setArguments(args);
return f;
}
@Override
public void onAttach(Context ctx) {
super.onAttach(ctx);
((BaseActivity) requireActivity()).getActivityComponent().inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
GroupId groupId = new GroupId(
requireNonNull(requireArguments().getByteArray(GROUP_ID)));
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(),
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_rss_remove_feed));
builder.setMessage(
getString(R.string.blogs_rss_remove_feed_dialog_message));
builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_rss_remove_feed_ok,
(dialog, which) -> viewModel.removeFeed(groupId));
return builder.create();
}
}

View File

@@ -1,170 +0,0 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.api.feed.FeedManager;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
public class RssFeedImportActivity extends BriarActivity {
private static final Logger LOG =
Logger.getLogger(RssFeedImportActivity.class.getName());
private EditText urlInput;
private Button importButton;
private ProgressBar progressBar;
@Inject
@IoExecutor
Executor ioExecutor;
@Inject
@SuppressWarnings("WeakerAccess")
volatile FeedManager feedManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rss_feed_import);
urlInput = findViewById(R.id.urlInput);
urlInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
@Override
public void afterTextChanged(Editable s) {
enableOrDisableImportButton();
}
});
urlInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
importButton.getVisibility() == VISIBLE) {
publish();
return true;
}
return false;
});
importButton = findViewById(R.id.importButton);
importButton.setOnClickListener(v -> publish());
progressBar = findViewById(R.id.progressBar);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
private void enableOrDisableImportButton() {
String url = urlInput.getText().toString();
importButton.setEnabled(validateAndNormaliseUrl(url) != null);
}
@Nullable
private String validateAndNormaliseUrl(String url) {
if (!Patterns.WEB_URL.matcher(url).matches()) return null;
try {
return new URL(url).toString();
} catch (MalformedURLException e) {
return null;
}
}
private void publish() {
// hide import button, show progress bar
importButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
hideSoftKeyboard(urlInput);
String url = validateAndNormaliseUrl(urlInput.getText().toString());
if (url == null) throw new AssertionError();
importFeed(url);
}
private void importFeed(String url) {
ioExecutor.execute(() -> {
try {
feedManager.addFeed(url);
feedImported();
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
importFailed();
}
});
}
private void feedImported() {
runOnUiThreadUnlessDestroyed(this::supportFinishAfterTransition);
}
private void importFailed() {
runOnUiThreadUnlessDestroyed(() -> {
// hide progress bar, show publish button
progressBar.setVisibility(GONE);
importButton.setVisibility(VISIBLE);
// show error dialog
AlertDialog.Builder builder =
new AlertDialog.Builder(RssFeedImportActivity.this,
R.style.BriarDialogTheme);
builder.setMessage(R.string.blogs_rss_feeds_import_error);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.try_again_button,
(dialog, which) -> publish());
AlertDialog dialog = builder.create();
dialog.show();
});
}
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.briar.android.blog;
import android.app.Dialog;
import android.content.Context;
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.BaseActivity;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedImportFailedDialogFragment extends DialogFragment {
final static String TAG = RssFeedImportFailedDialogFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
static RssFeedImportFailedDialogFragment newInstance() {
return new RssFeedImportFailedDialogFragment();
}
@Override
public void onAttach(Context ctx) {
super.onAttach(ctx);
((BaseActivity) requireActivity()).getActivityComponent().inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder =
new AlertDialog.Builder(requireActivity(),
R.style.BriarDialogTheme);
builder.setMessage(R.string.blogs_rss_feeds_import_error);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.try_again_button,
(dialog, which) -> viewModel.retryImportFeed());
return builder.create();
}
}

View File

@@ -0,0 +1,124 @@
package org.briarproject.briar.android.blog;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.lifecycle.ViewModelProvider;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedImportFragment extends BaseFragment {
public static final String TAG = RssFeedImportFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
private EditText urlInput;
private Button importButton;
private ProgressBar progressBar;
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireActivity().setTitle(getString(R.string.blogs_rss_feeds_import));
View v = inflater.inflate(R.layout.fragment_rss_feed_import,
container, false);
urlInput = v.findViewById(R.id.urlInput);
urlInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
@Override
public void afterTextChanged(Editable s) {
enableOrDisableImportButton();
}
});
urlInput.setOnEditorActionListener((view, actionId, event) -> {
if (actionId == IME_ACTION_DONE && importButton.isEnabled() &&
importButton.getVisibility() == VISIBLE) {
publish();
return true;
}
return false;
});
importButton = v.findViewById(R.id.importButton);
importButton.setOnClickListener(view -> publish());
progressBar = v.findViewById(R.id.progressBar);
viewModel.getIsImporting().observe(getViewLifecycleOwner(),
this::onIsImporting);
return v;
}
@Override
public String getUniqueTag() {
return TAG;
}
private void enableOrDisableImportButton() {
String url = urlInput.getText().toString();
importButton.setEnabled(viewModel.validateAndNormaliseUrl(url) != null);
}
private void publish() {
String url = viewModel
.validateAndNormaliseUrl(urlInput.getText().toString());
if (url == null) throw new AssertionError();
viewModel.importFeed(url);
}
private void onIsImporting(Boolean importing) {
if (importing) {
// show progress bar, hide import button
importButton.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
hideSoftKeyboard(urlInput);
} else {
// show publish button, hide progress bar
importButton.setVisibility(VISIBLE);
progressBar.setVisibility(GONE);
}
}
}

View File

@@ -1,178 +0,0 @@
package org.briarproject.briar.android.blog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.google.android.material.snackbar.Snackbar;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.RssFeedAdapter.RssFeedListener;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
public class RssFeedManageActivity extends BriarActivity
implements RssFeedListener {
private static final Logger LOG =
Logger.getLogger(RssFeedManageActivity.class.getName());
private BriarRecyclerView list;
private RssFeedAdapter adapter;
@Inject
@SuppressWarnings("WeakerAccess")
volatile FeedManager feedManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rss_feed_manage);
adapter = new RssFeedAdapter(this, this);
list = findViewById(R.id.feedList);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter);
}
@Override
public void onStart() {
super.onStart();
loadFeeds();
}
@Override
public void onStop() {
super.onStop();
adapter.clear();
list.showProgressBar();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.rss_feed_manage_actions, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
} else if (item.getItemId() == R.id.action_rss_feeds_import) {
Intent i = new Intent(this, RssFeedImportActivity.class);
startActivity(i);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
@Override
public void onFeedClick(Feed feed) {
Intent i = new Intent(this, BlogActivity.class);
i.putExtra(GROUP_ID, feed.getBlogId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
}
@Override
public void onDeleteClick(Feed feed) {
DialogInterface.OnClickListener okListener =
(dialog, which) -> deleteFeed(feed);
AlertDialog.Builder builder = new AlertDialog.Builder(this,
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.blogs_rss_remove_feed));
builder.setMessage(
getString(R.string.blogs_rss_remove_feed_dialog_message));
builder.setPositiveButton(R.string.cancel, null);
builder.setNegativeButton(R.string.blogs_rss_remove_feed_ok,
okListener);
builder.show();
}
private void loadFeeds() {
int revision = adapter.getRevision();
runOnDbThread(() -> {
try {
displayFeeds(revision, feedManager.getFeeds());
} catch (DbException e) {
logException(LOG, WARNING, e);
onLoadError();
}
});
}
private void displayFeeds(int revision, List<Feed> feeds) {
runOnUiThreadUnlessDestroyed(() -> {
if (revision == adapter.getRevision()) {
adapter.incrementRevision();
if (feeds.isEmpty()) list.showData();
else adapter.addAll(feeds);
} else {
LOG.info("Concurrent update, reloading");
loadFeeds();
}
});
}
private void deleteFeed(Feed feed) {
runOnDbThread(() -> {
try {
feedManager.removeFeed(feed);
onFeedDeleted(feed);
} catch (DbException e) {
logException(LOG, WARNING, e);
onDeleteError();
}
});
}
private void onLoadError() {
runOnUiThreadUnlessDestroyed(() -> {
list.setEmptyText(R.string.blogs_rss_feeds_manage_error);
list.showData();
});
}
private void onFeedDeleted(Feed feed) {
runOnUiThreadUnlessDestroyed(() -> {
adapter.incrementRevision();
adapter.remove(feed);
});
}
private void onDeleteError() {
runOnUiThreadUnlessDestroyed(() -> Snackbar.make(list,
R.string.blogs_rss_feeds_manage_delete_error,
LENGTH_LONG).show());
}
}

View File

@@ -0,0 +1,123 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.api.feed.Feed;
import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
import static org.briarproject.briar.android.blog.RssFeedAdapter.RssFeedListener;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class RssFeedManageFragment extends BaseFragment
implements RssFeedListener {
public static final String TAG = RssFeedManageFragment.class.getName();
@Inject
ViewModelProvider.Factory viewModelFactory;
private RssFeedViewModel viewModel;
private BriarRecyclerView list;
private final RssFeedAdapter adapter = new RssFeedAdapter(this);
public static RssFeedManageFragment newInstance() {
return new RssFeedManageFragment();
}
@Override
public void injectFragment(ActivityComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.get(RssFeedViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
requireActivity().setTitle(R.string.blogs_rss_feeds);
View v = inflater.inflate(R.layout.fragment_rss_feed_manage,
container, false);
list = v.findViewById(R.id.feedList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
viewModel.getFeeds().observe(getViewLifecycleOwner(), result -> result
.onError(e -> {
list.setEmptyText(R.string.blogs_rss_feeds_manage_error);
list.showData();
})
.onSuccess(feeds -> {
adapter.submitList(feeds);
if (requireNonNull(feeds).size() == 0) {
list.showData();
}
})
);
return v;
}
@Override
public String getUniqueTag() {
return TAG;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.rss_feed_manage_actions, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
requireActivity().onBackPressed();
return true;
} else if (item.getItemId() == R.id.action_rss_feeds_import) {
showNextFragment(new RssFeedImportFragment());
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onFeedClick(Feed feed) {
Intent i = new Intent(getActivity(), BlogActivity.class);
i.putExtra(GROUP_ID, feed.getBlogId().getBytes());
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
}
@Override
public void onDeleteClick(Feed feed) {
RssFeedDeleteFeedDialogFragment dialog =
RssFeedDeleteFeedDialogFragment.newInstance(feed.getBlogId());
dialog.show(getParentFragmentManager(),
RssFeedDeleteFeedDialogFragment.TAG);
}
}

View File

@@ -0,0 +1,180 @@
package org.briarproject.briar.android.blog;
import android.app.Application;
import android.util.Patterns;
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.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.LiveResult;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
import org.briarproject.briar.api.feed.Feed;
import org.briarproject.briar.api.feed.FeedManager;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.inject.Inject;
import androidx.annotation.Nullable;
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.android.blog.RssFeedViewModel.ImportResult.EXISTS;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED;
import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED;
@NotNullByDefault
class RssFeedViewModel extends DbViewModel {
enum ImportResult {IMPORTED, FAILED, EXISTS}
private static final Logger LOG =
getLogger(RssFeedViewModel.class.getName());
private final FeedManager feedManager;
private final Executor ioExecutor;
private final Executor dbExecutor;
private final MutableLiveData<LiveResult<List<Feed>>> feeds =
new MutableLiveData<>();
@Nullable
private volatile String urlFailedImport = null;
private final MutableLiveData<Boolean> isImporting =
new MutableLiveData<>(false);
private final MutableLiveEvent<ImportResult> importResult =
new MutableLiveEvent<>();
@Inject
RssFeedViewModel(Application app,
FeedManager feedManager,
@IoExecutor Executor ioExecutor,
@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager,
TransactionManager db,
AndroidExecutor androidExecutor) {
super(app, dbExecutor, lifecycleManager, db, androidExecutor);
this.feedManager = feedManager;
this.ioExecutor = ioExecutor;
this.dbExecutor = dbExecutor;
loadFeeds();
}
@Nullable
String validateAndNormaliseUrl(String url) {
if (!Patterns.WEB_URL.matcher(url).matches()) return null;
try {
return new URL(url).toString();
} catch (MalformedURLException e) {
return null;
}
}
LiveData<LiveResult<List<Feed>>> getFeeds() {
return feeds;
}
private void loadFeeds() {
loadFromDb(this::loadFeeds, feeds::setValue);
}
@DatabaseExecutor
private List<Feed> loadFeeds(Transaction txn) throws DbException {
long start = now();
List<Feed> feeds = feedManager.getFeeds(txn);
Collections.sort(feeds);
logDuration(LOG, "Loading feeds", start);
return feeds;
}
void removeFeed(GroupId groupId) {
dbExecutor.execute(() -> {
List<Feed> updated = removeListItems(getList(feeds), feed -> {
if (feed.getBlogId().equals(groupId)) {
try {
feedManager.removeFeed(feed);
return true;
} catch (DbException e) {
handleException(e);
}
}
return false;
});
if (updated != null) {
feeds.postValue(new LiveResult<>(updated));
}
});
}
LiveEvent<ImportResult> getImportResult() {
return importResult;
}
LiveData<Boolean> getIsImporting() {
return isImporting;
}
void importFeed(String url) {
isImporting.setValue(true);
urlFailedImport = null;
ioExecutor.execute(() -> {
try {
if (exists(url)) {
importResult.postEvent(EXISTS);
return;
}
Feed feed = feedManager.addFeed(url);
List<Feed> updated = addListItem(getList(feeds), feed);
if (updated != null) {
Collections.sort(updated);
feeds.postValue(new LiveResult<>(updated));
}
importResult.postEvent(IMPORTED);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
urlFailedImport = url;
importResult.postEvent(FAILED);
} finally {
isImporting.postValue(false);
}
});
}
void retryImportFeed() {
if (urlFailedImport == null) {
throw new AssertionError();
}
importFeed(urlFailedImport);
}
private boolean exists(String url) {
List<Feed> list = getList(feeds);
if (list != null) {
for (Feed feed : list) {
if (url.equals(feed.getUrl())) {
return true;
}
}
}
return false;
}
}

View File

@@ -176,6 +176,7 @@ class BluetoothConnecter implements EventListener {
connect();
}
@UiThread
@Override
public void eventOccurred(@NonNull Event e) {
if (e instanceof ConnectionOpenedEvent) {
@@ -192,40 +193,49 @@ class BluetoothConnecter implements EventListener {
}
private void connect() {
bluetoothPlugin.disablePolling();
pluginManager.setPluginEnabled(ID, true);
ioExecutor.execute(() -> {
if (!waitForBluetoothActive()) {
showToast(R.string.bt_plugin_status_inactive);
LOG.warning("Bluetooth plugin didn't become active");
return;
}
showToast(R.string.toast_connect_via_bluetooth_start);
eventBus.addListener(this);
try {
String uuid = null;
try {
uuid = transportPropertyManager
.getRemoteProperties(contactId, ID).get(PROP_UUID);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
if (isNullOrEmpty(uuid)) {
LOG.warning("PROP_UUID missing for contact");
if (!waitForBluetoothActive()) {
showToast(R.string.bt_plugin_status_inactive);
LOG.warning("Bluetooth plugin didn't become active");
return;
}
DuplexTransportConnection conn = bluetoothPlugin
.discoverAndConnectForSetup(uuid);
if (conn == null) {
waitAfterConnectionFailed();
} else {
LOG.info("Could connect, handling connection");
showToast(R.string.toast_connect_via_bluetooth_start);
eventBus.addListener(this);
try {
String uuid = null;
try {
uuid = transportPropertyManager
.getRemoteProperties(contactId, ID)
.get(PROP_UUID);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
if (isNullOrEmpty(uuid)) {
LOG.warning("PROP_UUID missing for contact");
return;
}
DuplexTransportConnection conn = bluetoothPlugin
.discoverAndConnectForSetup(uuid);
if (conn == null) {
waitAfterConnectionFailed();
} else {
LOG.info("Could connect, handling connection");
connectionManager
.manageOutgoingConnection(contactId, ID, conn);
showToast(R.string.toast_connect_via_bluetooth_success);
}
connectionManager
.manageOutgoingConnection(contactId, ID, conn);
showToast(R.string.toast_connect_via_bluetooth_success);
} finally {
eventBus.removeListener(this);
}
} finally {
eventBus.removeListener(this);
bluetoothPlugin.enablePolling();
}
});
}

View File

@@ -142,6 +142,7 @@ class ConversationAdapter
}
items.beginBatchedUpdates();
for (ConversationItem item : toRemove) items.remove(item);
updateTimersInBatch();
items.endBatchedUpdates();
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -90,6 +91,9 @@ import static android.text.format.DateUtils.WEEK_IN_MILLIS;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
import static android.view.inputmethod.EditorInfo.IME_NULL;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static android.widget.Toast.LENGTH_LONG;
@@ -537,4 +541,14 @@ public class UiUtils {
Toast.makeText(context, msg, LENGTH_LONG).show();
});
}
public static void setInputStateAlwaysVisible(Activity activity) {
activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE |
SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
public static void setInputStateHidden(Activity activity) {
activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE |
SOFT_INPUT_STATE_HIDDEN);
}
}