Make BlogController thread-safe. #555

This commit is contained in:
akwizgran
2016-08-08 17:58:27 +01:00
parent d34afa5f30
commit 98337a16ec
12 changed files with 381 additions and 292 deletions

View File

@@ -2,31 +2,36 @@ package org.briarproject.android.blogs;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Toast;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.android.ActivityComponent; import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity; import org.briarproject.android.BriarActivity;
import org.briarproject.android.blogs.BlogController.BlogPostListener; import org.briarproject.android.blogs.BlogController.BlogPostListener;
import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
import org.briarproject.android.controller.handler.UiResultHandler; import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener; import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
public class BlogActivity extends BriarActivity implements BlogPostListener, public class BlogActivity extends BriarActivity implements BlogPostListener,
OnBlogPostClickListener, BaseFragmentListener { OnBlogPostClickListener, BaseFragmentListener {
@@ -37,17 +42,17 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
static final String IS_MY_BLOG = "briar.IS_MY_BLOG"; static final String IS_MY_BLOG = "briar.IS_MY_BLOG";
static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG"; static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG";
private static final String BLOG_PAGER_ADAPTER = "briar.BLOG_PAGER_ADAPTER"; private static final String POST_ID = "briar.POST_ID";
private GroupId groupId;
private ProgressBar progressBar; private ProgressBar progressBar;
private ViewPager pager; private ViewPager pager;
private BlogPagerAdapter blogPagerAdapter; private BlogPagerAdapter blogPagerAdapter;
private BlogPostPagerAdapter postPagerAdapter; private BlogPostPagerAdapter postPagerAdapter;
private String blogName; private String blogName;
private boolean myBlog, isNew; private boolean myBlog, isNew;
private MessageId savedPostId;
// Fields that are accessed from background threads must be volatile
private volatile GroupId groupId = null;
@Inject @Inject
BlogController blogController; BlogController blogController;
@@ -58,8 +63,9 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
// GroupId from Intent // GroupId from Intent
Intent i = getIntent(); Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID); byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group in intent."); if (b == null) throw new IllegalStateException("No group ID in intent");
groupId = new GroupId(b); groupId = new GroupId(b);
blogController.setGroupId(groupId);
// Name of the Blog from Intent // Name of the Blog from Intent
blogName = i.getStringExtra(BLOG_NAME); blogName = i.getStringExtra(BLOG_NAME);
@@ -76,27 +82,42 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
hideLoadingScreen(); hideLoadingScreen();
blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager()); blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager());
if (state == null || state.getBoolean(BLOG_PAGER_ADAPTER, true)) { postPagerAdapter = new BlogPostPagerAdapter(
getSupportFragmentManager());
if (state == null || state.getByteArray(POST_ID) == null) {
pager.setAdapter(blogPagerAdapter); pager.setAdapter(blogPagerAdapter);
savedPostId = null;
} else { } else {
// this initializes and restores the postPagerAdapter // Adapter will be set in selectPostInPostPager()
loadBlogPosts(); savedPostId = new MessageId(state.getByteArray(POST_ID));
}
}
@Override
public void onResume() {
super.onResume();
if (savedPostId == null) {
MessageId selected = getSelectedPostInPostPager();
if (selected != null) loadBlogPosts(selected);
} else {
loadBlogPosts(savedPostId);
} }
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
MessageId selected = getSelectedPostInPostPager();
// remember which adapter we had active if (selected != null)
outState.putBoolean(BLOG_PAGER_ADAPTER, outState.putByteArray(POST_ID, selected.getBytes());
pager.getAdapter() == blogPagerAdapter);
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (pager.getAdapter() == postPagerAdapter) { if (pager.getAdapter() == postPagerAdapter) {
pager.setAdapter(blogPagerAdapter); pager.setAdapter(blogPagerAdapter);
savedPostId = null;
} else { } else {
super.onBackPressed(); super.onBackPressed();
} }
@@ -127,60 +148,80 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
} }
@Override @Override
public void onBlogPostClick(int position, BlogPostItem post) { public void onBlogPostClick(BlogPostItem post) {
loadBlogPosts(position, true); loadBlogPosts(post.getId());
} }
private void loadBlogPosts() { private void loadBlogPosts(final MessageId select) {
loadBlogPosts(0, false);
}
private void loadBlogPosts(final int position, final boolean setItem) {
showLoadingScreen(); showLoadingScreen();
blogController.loadBlog(groupId, false, blogController.loadBlogPosts(
new UiResultHandler<Boolean>(this) { new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
this) {
@Override @Override
public void onResultUi(Boolean result) { public void onResultUi(Collection<BlogPostItem> posts) {
if (result) { hideLoadingScreen();
Collection<BlogPostItem> posts = savedPostId = null;
blogController.getBlogPosts(); postPagerAdapter.setPosts(posts);
selectPostInPostPager(select);
}
if (postPagerAdapter == null) { @Override
postPagerAdapter = new BlogPostPagerAdapter( public void onExceptionUi(DbException exception) {
getSupportFragmentManager(), // TODO: Decide how to handle errors in the UI
posts.size()); finish();
} else {
postPagerAdapter.setSize(posts.size());
}
pager.setAdapter(postPagerAdapter);
if (setItem) pager.setCurrentItem(position);
} else {
Toast.makeText(BlogActivity.this,
R.string.blogs_blog_post_failed_to_load,
LENGTH_SHORT).show();
}
} }
}); });
} }
@Override @Override
public void onBlogPostAdded(final BlogPostItem post, final boolean local) { public void onBlogPostAdded(BlogPostHeader header, boolean local) {
runOnUiThread(new Runnable() { if (pager.getAdapter() == postPagerAdapter) {
@Override loadBlogPost(header);
public void run() { } else {
if (blogPagerAdapter != null) { BlogFragment f = blogPagerAdapter.getFragment();
BlogFragment f = blogPagerAdapter.getFragment(); if (f != null && f.isVisible()) f.onBlogPostAdded(header, local);
if (f != null && f.isVisible()) { }
f.onBlogPostAdded(post, local); }
}
}
if (postPagerAdapter != null) { private void loadBlogPost(BlogPostHeader header) {
postPagerAdapter.onBlogPostAdded(); blogController.loadBlogPost(header,
postPagerAdapter.notifyDataSetChanged(); new UiResultExceptionHandler<BlogPostItem, DbException>(this) {
} @Override
public void onResultUi(BlogPostItem post) {
addPostToPostPager(post);
}
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
}
});
}
@Nullable
private MessageId getSelectedPostInPostPager() {
if (pager.getAdapter() != postPagerAdapter) return null;
if (postPagerAdapter.getCount() == 0) return null;
int position = pager.getCurrentItem();
return postPagerAdapter.getPost(position).getId();
}
private void selectPostInPostPager(MessageId m) {
int count = postPagerAdapter.getCount();
for (int i = 0; i < count; i++) {
if (postPagerAdapter.getPost(i).getId().equals(m)) {
pager.setAdapter(postPagerAdapter);
pager.setCurrentItem(i);
return;
} }
}); }
}
private void addPostToPostPager(BlogPostItem post) {
MessageId selected = getSelectedPostInPostPager();
postPagerAdapter.addPost(post);
if (selected != null) selectPostInPostPager(selected);
} }
@Override @Override
@@ -191,18 +232,22 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
// The BlogPostAddedEvent arrives when the controller is not listening, // The BlogPostAddedEvent arrives when the controller is not listening,
// so we need to manually reload the blog posts :( // so we need to manually reload the blog posts :(
if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) { if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
BlogFragment f = blogPagerAdapter.getFragment(); if (pager.getAdapter() == postPagerAdapter) {
if (f != null && f.isVisible()) { MessageId selected = getSelectedPostInPostPager();
f.reload(); if (selected != null) loadBlogPosts(selected);
} else {
BlogFragment f = blogPagerAdapter.getFragment();
if (f != null && f.isVisible()) f.loadBlogPosts(true);
} }
} }
} }
@UiThread
private class BlogPagerAdapter extends FragmentStatePagerAdapter { private class BlogPagerAdapter extends FragmentStatePagerAdapter {
private BlogFragment fragment = null; private BlogFragment fragment = null;
BlogPagerAdapter(FragmentManager fm) { private BlogPagerAdapter(FragmentManager fm) {
super(fm); super(fm);
} }
@@ -224,36 +269,46 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
return fragment; return fragment;
} }
BlogFragment getFragment() { private BlogFragment getFragment() {
return fragment; return fragment;
} }
} }
private class BlogPostPagerAdapter extends FragmentStatePagerAdapter { @UiThread
private int size; private static class BlogPostPagerAdapter
extends FragmentStatePagerAdapter {
BlogPostPagerAdapter(FragmentManager fm, int size) { private final List<BlogPostItem> posts = new ArrayList<>();
private BlogPostPagerAdapter(FragmentManager fm) {
super(fm); super(fm);
this.size = size;
} }
@Override @Override
public int getCount() { public int getCount() {
return size; return posts.size();
} }
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
MessageId postIdOfPos = blogController.getBlogPostId(position); return BlogPostFragment.newInstance(posts.get(position).getId());
return BlogPostFragment.newInstance(groupId, postIdOfPos);
} }
void onBlogPostAdded() { private BlogPostItem getPost(int position) {
size++; return posts.get(position);
} }
void setSize(int size) { private void setPosts(Collection<BlogPostItem> posts) {
this.size = size; this.posts.clear();
this.posts.addAll(posts);
Collections.sort(this.posts);
notifyDataSetChanged();
}
private void addPost(BlogPostItem post) {
posts.add(post);
Collections.sort(posts);
notifyDataSetChanged();
} }
} }

View File

@@ -1,36 +1,36 @@
package org.briarproject.android.blogs; package org.briarproject.android.blogs;
import android.support.annotation.Nullable; import android.support.annotation.UiThread;
import org.briarproject.android.controller.ActivityLifecycleController; import org.briarproject.android.controller.ActivityLifecycleController;
import org.briarproject.android.controller.handler.ResultExceptionHandler; import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.android.controller.handler.ResultHandler; import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import java.util.SortedSet; import java.util.Collection;
public interface BlogController extends ActivityLifecycleController { public interface BlogController extends ActivityLifecycleController {
void loadBlog(GroupId groupId, boolean reload, void setGroupId(GroupId g);
ResultHandler<Boolean> resultHandler);
SortedSet<BlogPostItem> getBlogPosts(); void loadBlogPosts(
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
@Nullable void loadBlogPost(BlogPostHeader header,
BlogPostItem getBlogPost(MessageId postId); ResultExceptionHandler<BlogPostItem, DbException> handler);
@Nullable void loadBlogPost(MessageId m,
MessageId getBlogPostId(int position); ResultExceptionHandler<BlogPostItem, DbException> handler);
void canDeleteBlog(GroupId groupId, void canDeleteBlog(ResultExceptionHandler<Boolean, DbException> handler);
ResultExceptionHandler<Boolean, DbException> resultHandler);
void deleteBlog(ResultHandler<Boolean> resultHandler); void deleteBlog(ResultExceptionHandler<Void, DbException> handler);
interface BlogPostListener { interface BlogPostListener {
void onBlogPostAdded(BlogPostItem post, boolean local); @UiThread
void onBlogPostAdded(BlogPostHeader header, boolean local);
} }
} }

View File

@@ -1,11 +1,9 @@
package org.briarproject.android.blogs; package org.briarproject.android.blogs;
import android.app.Activity; import android.app.Activity;
import android.support.annotation.Nullable;
import org.briarproject.android.controller.DbControllerImpl; import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.ResultExceptionHandler; import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.android.controller.handler.ResultHandler;
import org.briarproject.api.blogs.Blog; import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogManager; import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.blogs.BlogPostHeader;
@@ -20,8 +18,9 @@ import org.briarproject.api.sync.MessageId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.SortedSet; import java.util.List;
import java.util.TreeSet; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -38,19 +37,25 @@ public class BlogControllerImpl extends DbControllerImpl
@Inject @Inject
protected Activity activity; protected Activity activity;
@Inject @Inject
protected volatile BlogManager blogManager; protected EventBus eventBus;
@Inject @Inject
protected volatile EventBus eventBus; protected volatile BlogManager blogManager;
private final Map<MessageId, byte[]> bodyCache = new ConcurrentHashMap<>();
private volatile BlogPostListener listener; private volatile BlogPostListener listener;
private volatile GroupId groupId = null; private volatile GroupId groupId = null;
// FIXME: This collection isn't thread-safe, isn't updated atomically
private volatile TreeSet<BlogPostItem> posts = null;
@Inject @Inject
BlogControllerImpl() { BlogControllerImpl() {
} }
@Override
public void setGroupId(GroupId g) {
groupId = g;
}
@Override @Override
public void onActivityCreate() { public void onActivityCreate() {
if (activity instanceof BlogPostListener) { if (activity instanceof BlogPostListener) {
@@ -78,26 +83,17 @@ public class BlogControllerImpl extends DbControllerImpl
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (groupId == null) throw new IllegalStateException();
if (e instanceof BlogPostAddedEvent) { if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent m = (BlogPostAddedEvent) e; final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
if (m.getGroupId().equals(groupId)) { if (m.getGroupId().equals(groupId)) {
LOG.info("New blog post added"); LOG.info("New blog post added");
if (posts == null) { activity.runOnUiThread(new Runnable() {
LOG.info("Posts have not loaded, yet"); @Override
// FIXME: Race condition, new post may not get loaded public void run() {
return; listener.onBlogPostAdded(m.getHeader(), m.isLocal());
} }
final BlogPostHeader header = m.getHeader(); });
// FIXME: Don't make blocking calls in event handlers
try {
byte[] body = blogManager.getPostBody(header.getId());
BlogPostItem post = new BlogPostItem(groupId, header, body);
posts.add(post);
listener.onBlogPostAdded(post, m.isLocal());
} catch (DbException ex) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, ex.toString(), ex);
}
} }
} else if (e instanceof GroupRemovedEvent) { } else if (e instanceof GroupRemovedEvent) {
GroupRemovedEvent s = (GroupRemovedEvent) e; GroupRemovedEvent s = (GroupRemovedEvent) e;
@@ -106,6 +102,7 @@ public class BlogControllerImpl extends DbControllerImpl
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
// TODO: Not the controller's job, add a listener method
activity.finish(); activity.finish();
} }
}); });
@@ -114,106 +111,127 @@ public class BlogControllerImpl extends DbControllerImpl
} }
@Override @Override
public void loadBlog(final GroupId g, final boolean reload, public void loadBlogPosts(
final ResultHandler<Boolean> resultHandler) { final ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
if (reload || posts == null) { long now = System.currentTimeMillis();
groupId = g; Collection<BlogPostHeader> headers =
posts = new TreeSet<>(); blogManager.getPostHeaders(groupId);
// load blog posts long duration = System.currentTimeMillis() - now;
long now = System.currentTimeMillis(); if (LOG.isLoggable(INFO))
Collection<BlogPostItem> newPosts = new ArrayList<>(); LOG.info("Loading headers took " + duration + " ms");
Collection<BlogPostHeader> header = List<BlogPostItem> items = new ArrayList<>(headers.size());
blogManager.getPostHeaders(g); now = System.currentTimeMillis();
for (BlogPostHeader h : header) { for (BlogPostHeader h : headers) {
byte[] body = blogManager.getPostBody(h.getId()); byte[] body = getPostBody(h.getId());
newPosts.add(new BlogPostItem(g, h, body)); items.add(new BlogPostItem(groupId, h, body));
}
posts.addAll(newPosts);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading blog took " + duration + " ms");
} }
resultHandler.onResult(true); duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading bodies took " + duration + " ms");
handler.onResult(items);
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false); handler.onException(e);
} }
} }
}); });
} }
@Override @Override
@Nullable public void loadBlogPost(final BlogPostHeader header,
public SortedSet<BlogPostItem> getBlogPosts() { final ResultExceptionHandler<BlogPostItem, DbException> handler) {
return posts; if (groupId == null) throw new IllegalStateException();
}
@Override
@Nullable
public BlogPostItem getBlogPost(MessageId id) {
if (posts == null) return null;
for (BlogPostItem item : posts) {
if (item.getId().equals(id)) return item;
}
return null;
}
@Override
@Nullable
public MessageId getBlogPostId(int position) {
if (posts == null) return null;
int i = 0;
for (BlogPostItem post : posts) {
if (i == position) return post.getId();
i++;
}
return null;
}
@Override
public void canDeleteBlog(final GroupId g,
final ResultExceptionHandler<Boolean, DbException> resultHandler) {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (groupId == null) {
resultHandler.onResult(false);
return;
}
try { try {
resultHandler.onResult(blogManager.canBeRemoved(groupId)); long now = System.currentTimeMillis();
byte[] body = getPostBody(header.getId());
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading body took " + duration + " ms");
handler.onResult(new BlogPostItem(groupId, header, body));
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
resultHandler.onException(e); handler.onException(e);
} }
} }
}); });
} }
@Override @Override
public void deleteBlog(final ResultHandler<Boolean> resultHandler) { public void loadBlogPost(final MessageId m,
final ResultExceptionHandler<BlogPostItem, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (groupId == null) { try {
resultHandler.onResult(false); long now = System.currentTimeMillis();
return; BlogPostHeader header = blogManager.getPostHeader(m);
byte[] body = getPostBody(m);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading post took " + duration + " ms");
handler.onResult(new BlogPostItem(groupId, header, body));
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
} }
}
});
}
private byte[] getPostBody(MessageId m) throws DbException {
byte[] body = bodyCache.get(m);
if (body == null) {
body = blogManager.getPostBody(m);
if (body != null) bodyCache.put(m, body);
}
return body;
}
@Override
public void canDeleteBlog(
final ResultExceptionHandler<Boolean, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
handler.onResult(blogManager.canBeRemoved(groupId));
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@Override
public void deleteBlog(
final ResultExceptionHandler<Void, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(new Runnable() {
@Override
public void run() {
try { try {
Blog b = blogManager.getBlog(groupId); Blog b = blogManager.getBlog(groupId);
blogManager.removeBlog(b); blogManager.removeBlog(b);
resultHandler.onResult(true); handler.onResult(null);
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
resultHandler.onResult(false); handler.onException(e);
} }
} }
}); });

View File

@@ -22,11 +22,11 @@ import org.briarproject.android.ActivityComponent;
import org.briarproject.android.blogs.BlogController.BlogPostListener; import org.briarproject.android.blogs.BlogController.BlogPostListener;
import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.controller.handler.UiResultHandler;
import org.briarproject.android.fragment.BaseFragment; import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.sharing.ShareBlogActivity; import org.briarproject.android.sharing.ShareBlogActivity;
import org.briarproject.android.sharing.SharingStatusBlogActivity; import org.briarproject.android.sharing.SharingStatusBlogActivity;
import org.briarproject.android.util.BriarRecyclerView; import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
@@ -59,7 +59,7 @@ public class BlogFragment extends BaseFragment implements BlogPostListener {
private boolean myBlog; private boolean myBlog;
private BlogPostAdapter adapter; private BlogPostAdapter adapter;
private BriarRecyclerView list; private BriarRecyclerView list;
private MenuItem deleteButton = null; private MenuItem deleteButton;
static BlogFragment newInstance(GroupId groupId, String name, static BlogFragment newInstance(GroupId groupId, String name,
boolean myBlog, boolean isNew) { boolean myBlog, boolean isNew) {
@@ -85,7 +85,7 @@ public class BlogFragment extends BaseFragment implements BlogPostListener {
Bundle args = getArguments(); Bundle args = getArguments();
byte[] b = args.getByteArray(GROUP_ID); byte[] b = args.getByteArray(GROUP_ID);
if (b == null) throw new IllegalStateException("No Group found."); if (b == null) throw new IllegalStateException("No group ID in args");
groupId = new GroupId(b); groupId = new GroupId(b);
blogName = args.getString(BLOG_NAME); blogName = args.getString(BLOG_NAME);
myBlog = args.getBoolean(IS_MY_BLOG); myBlog = args.getBoolean(IS_MY_BLOG);
@@ -126,19 +126,21 @@ public class BlogFragment extends BaseFragment implements BlogPostListener {
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
loadData(false);
if (!myBlog) checkIfBlogCanBeDeleted(); if (!myBlog) checkIfBlogCanBeDeleted();
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
loadBlogPosts(false);
list.startPeriodicUpdate(); list.startPeriodicUpdate();
} }
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
adapter.clear();
list.showProgressBar();
list.stopPeriodicUpdate(); list.stopPeriodicUpdate();
} }
@@ -208,41 +210,49 @@ public class BlogFragment extends BaseFragment implements BlogPostListener {
} }
@Override @Override
public void onBlogPostAdded(BlogPostItem post, boolean local) { public void onBlogPostAdded(BlogPostHeader header, final boolean local) {
adapter.add(post); blogController.loadBlogPost(header,
if (local) list.scrollToPosition(0); new UiResultExceptionHandler<BlogPostItem, DbException>(
getActivity()) {
@Override
public void onResultUi(BlogPostItem post) {
adapter.add(post);
if (local) list.scrollToPosition(0);
}
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
getActivity().finish();
}
}
);
} }
private void loadData(final boolean reload) { void loadBlogPosts(final boolean reload) {
blogController.loadBlog(groupId, reload, blogController.loadBlogPosts(
new UiResultHandler<Boolean>(getActivity()) { new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
getActivity()) {
@Override @Override
public void onResultUi(Boolean result) { public void onResultUi(Collection<BlogPostItem> posts) {
if (result) { if (posts.size() > 0) {
Collection<BlogPostItem> posts = adapter.addAll(posts);
blogController.getBlogPosts(); if (reload) list.scrollToPosition(0);
if (posts.size() > 0) {
adapter.addAll(posts);
if (reload) list.scrollToPosition(0);
} else {
list.showData();
}
} else { } else {
Toast.makeText(getActivity(), list.showData();
R.string.blogs_blog_failed_to_load,
LENGTH_SHORT).show();
getActivity().supportFinishAfterTransition();
} }
} }
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
getActivity().finish();
}
}); });
} }
void reload() {
loadData(true);
}
private void checkIfBlogCanBeDeleted() { private void checkIfBlogCanBeDeleted() {
blogController.canDeleteBlog(groupId, blogController.canDeleteBlog(
new UiResultExceptionHandler<Boolean, DbException>( new UiResultExceptionHandler<Boolean, DbException>(
getActivity()) { getActivity()) {
@Override @Override
@@ -251,9 +261,11 @@ public class BlogFragment extends BaseFragment implements BlogPostListener {
showDeleteButton(); showDeleteButton();
} }
} }
@Override @Override
public void onExceptionUi(DbException exception) { public void onExceptionUi(DbException exception) {
// nothing to do here, delete button is already hidden // TODO: Decide how to handle errors in the UI
getActivity().finish();
} }
}); });
} }
@@ -290,15 +302,20 @@ public class BlogFragment extends BaseFragment implements BlogPostListener {
private void deleteBlog() { private void deleteBlog() {
blogController.deleteBlog( blogController.deleteBlog(
new UiResultHandler<Boolean>(getActivity()) { new UiResultExceptionHandler<Void, DbException>(getActivity()) {
@Override @Override
public void onResultUi(Boolean result) { public void onResultUi(Void result) {
if (!result) return;
Toast.makeText(getActivity(), Toast.makeText(getActivity(),
R.string.blogs_blog_removed, LENGTH_SHORT) R.string.blogs_blog_removed, LENGTH_SHORT)
.show(); .show();
getActivity().supportFinishAfterTransition(); getActivity().supportFinishAfterTransition();
} }
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
getActivity().finish();
}
}); });
} }

View File

@@ -3,11 +3,9 @@ package org.briarproject.android.blogs;
import android.content.Context; import android.content.Context;
import android.support.v7.util.SortedList; import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.R; import org.briarproject.R;
@@ -97,7 +95,7 @@ class BlogPostAdapter extends
ui.layout.setOnClickListener(new View.OnClickListener() { ui.layout.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
listener.onBlogPostClick(ui.getAdapterPosition(), post); listener.onBlogPostClick(post);
} }
}); });
} }
@@ -132,15 +130,12 @@ class BlogPostAdapter extends
} }
static class BlogPostHolder extends RecyclerView.ViewHolder { static class BlogPostHolder extends RecyclerView.ViewHolder {
private final ViewGroup layout; private final ViewGroup layout;
private final CircleImageView avatar; private final CircleImageView avatar;
private final TextView author; private final TextView author;
private final TrustIndicatorView trust; private final TrustIndicatorView trust;
private final TextView date; private final TextView date;
private final TextView unread;
private final ImageView chat;
private final ImageView comment;
private final TextView title;
private final TextView body; private final TextView body;
BlogPostHolder(View v) { BlogPostHolder(View v) {
@@ -151,16 +146,12 @@ class BlogPostAdapter extends
author = (TextView) v.findViewById(R.id.authorName); author = (TextView) v.findViewById(R.id.authorName);
trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator); trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);
date = (TextView) v.findViewById(R.id.dateView); date = (TextView) v.findViewById(R.id.dateView);
unread = (TextView) v.findViewById(R.id.newView);
chat = (ImageView) v.findViewById(R.id.chatView);
comment = (ImageView) v.findViewById(R.id.commentView);
title = (TextView) v.findViewById(R.id.titleView);
body = (TextView) v.findViewById(R.id.bodyView); body = (TextView) v.findViewById(R.id.bodyView);
} }
} }
interface OnBlogPostClickListener { interface OnBlogPostClickListener {
void onBlogPostClick(int position, BlogPostItem post); void onBlogPostClick(BlogPostItem post);
} }
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.android.blogs; package org.briarproject.android.blogs;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@@ -10,16 +9,15 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.android.ActivityComponent; import org.briarproject.android.ActivityComponent;
import org.briarproject.android.controller.handler.UiResultHandler; import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.fragment.BaseFragment; import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.AndroidUtils; import org.briarproject.android.util.AndroidUtils;
import org.briarproject.android.util.TrustIndicatorView; import org.briarproject.android.util.TrustIndicatorView;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
@@ -30,31 +28,27 @@ import javax.inject.Inject;
import im.delight.android.identicons.IdenticonDrawable; import im.delight.android.identicons.IdenticonDrawable;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.widget.Toast.LENGTH_SHORT;
import static org.briarproject.android.BriarActivity.GROUP_ID;
import static org.briarproject.android.util.AndroidUtils.MIN_RESOLUTION; import static org.briarproject.android.util.AndroidUtils.MIN_RESOLUTION;
public class BlogPostFragment extends BaseFragment { public class BlogPostFragment extends BaseFragment {
public final static String TAG = BlogPostFragment.class.getName(); public final static String TAG = BlogPostFragment.class.getName();
private static final Logger LOG = Logger.getLogger(TAG); private static final Logger LOG = Logger.getLogger(TAG);
private static final String BLOG_POST_ID = "briar.BLOG_POST_ID";
private final static String BLOG_POST_ID = "briar.BLOG_NAME";
private GroupId groupId;
private MessageId postId; private MessageId postId;
private BlogPostViewHolder ui; private BlogPostViewHolder ui;
private BlogPostItem post = null; private BlogPostItem post;
private Runnable refresher = null; private Runnable refresher;
@Inject @Inject
BlogController blogController; BlogController blogController;
static BlogPostFragment newInstance(GroupId groupId, MessageId postId) { static BlogPostFragment newInstance(MessageId postId) {
BlogPostFragment f = new BlogPostFragment(); BlogPostFragment f = new BlogPostFragment();
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putByteArray(GROUP_ID, groupId.getBytes());
bundle.putByteArray(BLOG_POST_ID, postId.getBytes()); bundle.putByteArray(BLOG_POST_ID, postId.getBytes());
f.setArguments(bundle); f.setArguments(bundle);
@@ -65,15 +59,11 @@ public class BlogPostFragment extends BaseFragment {
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
setHasOptionsMenu(true); setHasOptionsMenu(true);
byte[] b = getArguments().getByteArray(GROUP_ID); byte[] b = getArguments().getByteArray(BLOG_POST_ID);
if (b == null) throw new IllegalStateException("No Group found."); if (b == null) throw new IllegalStateException("No post ID in args");
groupId = new GroupId(b); postId = new MessageId(b);
byte[] p = getArguments().getByteArray(BLOG_POST_ID);
if (p == null) throw new IllegalStateException("No MessageId found.");
postId = new MessageId(p);
View v = inflater.inflate(R.layout.fragment_blog_post, container, View v = inflater.inflate(R.layout.fragment_blog_post, container,
false); false);
@@ -89,21 +79,20 @@ public class BlogPostFragment extends BaseFragment {
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
blogController.loadBlog(groupId, false, blogController.loadBlogPost(postId,
new UiResultHandler<Boolean>((Activity) listener) { new UiResultExceptionHandler<BlogPostItem, DbException>(
getActivity()) {
@Override @Override
public void onResultUi(Boolean result) { public void onResultUi(BlogPostItem post) {
listener.hideLoadingScreen(); listener.hideLoadingScreen();
if (result) { BlogPostFragment.this.post = post;
post = blogController.getBlogPost(postId); bind();
if (post != null) { }
bind();
} @Override
} else { public void onExceptionUi(DbException exception) {
Toast.makeText(getActivity(), // TODO: Decide how to handle errors in the UI
R.string.blogs_blog_post_failed_to_load, getActivity().finish();
LENGTH_SHORT).show();
}
} }
}); });
} }
@@ -157,14 +146,15 @@ public class BlogPostFragment extends BaseFragment {
} }
private static class BlogPostViewHolder { private static class BlogPostViewHolder {
private ImageView avatar;
private TextView authorName;
private TrustIndicatorView trust;
private TextView date;
private TextView title;
private TextView body;
BlogPostViewHolder(View v) { private final ImageView avatar;
private final TextView authorName;
private final TrustIndicatorView trust;
private final TextView date;
private final TextView title;
private final TextView body;
private BlogPostViewHolder(View v) {
avatar = (ImageView) v.findViewById(R.id.avatar); avatar = (ImageView) v.findViewById(R.id.avatar);
authorName = (TextView) v.findViewById(R.id.authorName); authorName = (TextView) v.findViewById(R.id.authorName);
trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator); trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);

View File

@@ -1,6 +1,7 @@
package org.briarproject.android.blogs; package org.briarproject.android.blogs;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
@@ -31,14 +32,11 @@ class BlogPostItem implements Comparable<BlogPostItem> {
return groupId; return groupId;
} }
@Nullable
public String getTitle() { public String getTitle() {
return header.getTitle(); return header.getTitle();
} }
public byte[] getBody() {
return body;
}
public long getTimestamp() { public long getTimestamp() {
return header.getTimestamp(); return header.getTimestamp();
} }
@@ -55,18 +53,22 @@ class BlogPostItem implements Comparable<BlogPostItem> {
return header.getAuthorStatus(); return header.getAuthorStatus();
} }
public void setRead(boolean read) { public byte[] getBody() {
this.read = read; return body;
} }
public boolean isRead() { public boolean isRead() {
return read; return read;
} }
public void setRead(boolean read) {
this.read = read;
}
@Override @Override
public int compareTo(@NonNull BlogPostItem other) { public int compareTo(@NonNull BlogPostItem other) {
if (this == other) return 0; if (this == other) return 0;
// The blog with the newest message comes first // The newest post comes first
long aTime = getTimeReceived(), bTime = other.getTimeReceived(); long aTime = getTimeReceived(), bTime = other.getTimeReceived();
if (aTime > bTime) return -1; if (aTime > bTime) return -1;
if (aTime < bTime) return 1; if (aTime < bTime) return 1;

View File

@@ -182,7 +182,7 @@ public class FeedFragment extends BaseFragment implements
} }
@Override @Override
public void onBlogPostClick(int position, BlogPostItem post) { public void onBlogPostClick(BlogPostItem post) {
byte[] groupId = post.getGroupId().getBytes(); byte[] groupId = post.getGroupId().getBytes();
String name = getString(R.string.blogs_personal_blog, String name = getString(R.string.blogs_personal_blog,
post.getAuthor().getName()); post.getAuthor().getName());

View File

@@ -1,6 +1,7 @@
package org.briarproject.android.controller.handler; package org.briarproject.android.controller.handler;
import android.app.Activity; import android.app.Activity;
import android.support.annotation.UiThread;
public abstract class UiResultExceptionHandler<R, E extends Exception> public abstract class UiResultExceptionHandler<R, E extends Exception>
implements ResultExceptionHandler<R, E> { implements ResultExceptionHandler<R, E> {
@@ -31,7 +32,9 @@ public abstract class UiResultExceptionHandler<R, E extends Exception>
}); });
} }
@UiThread
public abstract void onResultUi(R result); public abstract void onResultUi(R result);
@UiThread
public abstract void onExceptionUi(E exception); public abstract void onExceptionUi(E exception);
} }

View File

@@ -1,6 +1,7 @@
package org.briarproject.android.controller.handler; package org.briarproject.android.controller.handler;
import android.app.Activity; import android.app.Activity;
import android.support.annotation.UiThread;
public abstract class UiResultHandler<R> implements ResultHandler<R> { public abstract class UiResultHandler<R> implements ResultHandler<R> {
@@ -20,5 +21,6 @@ public abstract class UiResultHandler<R> implements ResultHandler<R> {
}); });
} }
@UiThread
public abstract void onResultUi(R result); public abstract void onResultUi(R result);
} }

View File

@@ -7,7 +7,6 @@ import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.jetbrains.annotations.Nullable;
import java.util.Collection; import java.util.Collection;
@@ -44,8 +43,10 @@ public interface BlogManager {
/** Returns all blogs to which the user subscribes. */ /** Returns all blogs to which the user subscribes. */
Collection<Blog> getBlogs() throws DbException; Collection<Blog> getBlogs() throws DbException;
/** Returns the header of the blog post with the given ID. */
BlogPostHeader getPostHeader(MessageId m) throws DbException;
/** Returns the body of the blog post with the given ID. */ /** Returns the body of the blog post with the given ID. */
@Nullable
byte[] getPostBody(MessageId m) throws DbException; byte[] getPostBody(MessageId m) throws DbException;
/** Returns the headers of all posts in the given blog. */ /** Returns the headers of all posts in the given blog. */

View File

@@ -33,7 +33,6 @@ import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.clients.BdfIncomingMessageHook; import org.briarproject.clients.BdfIncomingMessageHook;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -337,10 +336,26 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
} }
@Override @Override
@Nullable public BlogPostHeader getPostHeader(MessageId m) throws DbException {
Transaction txn = db.startTransaction(true);
try {
BdfDictionary meta =
clientHelper.getMessageMetadataAsDictionary(txn, m);
BlogPostHeader h = getPostHeaderFromMetadata(txn, m, meta);
txn.setComplete();
return h;
} catch (FormatException e) {
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
}
@Override
public byte[] getPostBody(MessageId m) throws DbException { public byte[] getPostBody(MessageId m) throws DbException {
try { try {
BdfList message = clientHelper.getMessageAsList(m); BdfList message = clientHelper.getMessageAsList(m);
if (message == null) throw new DbException();
return getPostBody(message); return getPostBody(message);
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
@@ -358,24 +373,23 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
public Collection<BlogPostHeader> getPostHeaders(GroupId g) public Collection<BlogPostHeader> getPostHeaders(GroupId g)
throws DbException { throws DbException {
Map<MessageId, BdfDictionary> metadata; Transaction txn = db.startTransaction(true);
try { try {
metadata = clientHelper.getMessageMetadataAsDictionary(g); Map<MessageId, BdfDictionary> metadata =
clientHelper.getMessageMetadataAsDictionary(txn, g);
List<BlogPostHeader> headers = new ArrayList<BlogPostHeader>();
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
BlogPostHeader h = getPostHeaderFromMetadata(txn,
entry.getKey(), entry.getValue());
headers.add(h);
}
txn.setComplete();
return headers;
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} finally {
db.endTransaction(txn);
} }
Collection<BlogPostHeader> headers = new ArrayList<BlogPostHeader>();
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
try {
BdfDictionary meta = entry.getValue();
BlogPostHeader h =
getPostHeaderFromMetadata(null, entry.getKey(), meta);
headers.add(h);
} catch (FormatException e) {
throw new DbException(e);
}
}
return headers;
} }
@Override @Override
@@ -400,7 +414,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
return d.getString(KEY_DESCRIPTION, ""); return d.getString(KEY_DESCRIPTION, "");
} }
private BlogPostHeader getPostHeaderFromMetadata(@Nullable Transaction txn, private BlogPostHeader getPostHeaderFromMetadata(Transaction txn,
MessageId id, BdfDictionary meta) MessageId id, BdfDictionary meta)
throws DbException, FormatException { throws DbException, FormatException {
@@ -414,11 +428,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY); byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY);
Author author = new Author(authorId, name, publicKey); Author author = new Author(authorId, name, publicKey);
Status authorStatus; Status authorStatus;
if (txn == null) authorStatus = identityManager.getAuthorStatus(txn, authorId);
authorStatus = identityManager.getAuthorStatus(authorId);
else {
authorStatus = identityManager.getAuthorStatus(txn, authorId);
}
String contentType = meta.getString(KEY_CONTENT_TYPE); String contentType = meta.getString(KEY_CONTENT_TYPE);
boolean read = meta.getBoolean(KEY_READ); boolean read = meta.getBoolean(KEY_READ);