Merge branch 'feature/conversations' into develop

This commit is contained in:
Christian Basler 2017-04-25 21:27:41 +02:00
commit 3ae572bcf2
31 changed files with 850 additions and 223 deletions

View File

@ -21,6 +21,7 @@ android {
versionCode 11
versionName "1.0-beta11"
jackOptions.enabled = false
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
@ -38,11 +39,14 @@ android {
//ext.jabitVersion = '2.0.4'
ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT'
ext.supportVersion = '25.3.1'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.1.0'
compile 'com.android.support:support-v4:25.1.0'
compile 'com.android.support:design:25.1.0'
compile "com.android.support:appcompat-v7:$supportVersion"
compile "com.android.support:support-v4:$supportVersion"
compile "com.android.support:design:$supportVersion"
compile "com.android.support:multidex:1.0.1"
compile "ch.dissem.jabit:jabit-core:$jabitVersion"
compile "ch.dissem.jabit:jabit-networking:$jabitVersion"
@ -50,30 +54,32 @@ dependencies {
compile "ch.dissem.jabit:jabit-extensions:$jabitVersion"
compile "ch.dissem.jabit:jabit-wif:$jabitVersion"
compile 'org.slf4j:slf4j-android:1.7.12'
compile 'org.slf4j:slf4j-android:1.7.25'
compile 'com.mikepenz:materialize:1.0.0@aar'
compile('com.mikepenz:materialdrawer:5.6.0@aar') {
compile 'com.mikepenz:materialize:1.0.1@aar'
compile('com.mikepenz:materialdrawer:5.9.0@aar') {
transitive = true
}
compile('com.mikepenz:aboutlibraries:5.8.1@aar') {
compile('com.mikepenz:aboutlibraries:5.9.5@aar') {
transitive = true
}
compile 'com.mikepenz:iconics:1.6.2@aar'
compile 'com.mikepenz:community-material-typeface:1.5.54.2@aar'
compile "com.mikepenz:iconics-core:2.8.3@aar"
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
compile 'com.mikepenz:community-material-typeface:1.9.32.1@aar'
compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar'
compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar'
compile 'com.google.zxing:core:3.3.0'
compile 'io.github.yavski:fab-speed-dial:1.0.6'
compile 'com.github.amlcurran.showcaseview:library:5.4.3'
compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar') {
compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') {
transitive = true
}
compile 'com.github.angads25:filepicker:1.0.6'
compile 'com.github.angads25:filepicker:1.1.0'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'
testCompile 'org.mockito:mockito-core:2.7.22'
}
idea.module {

View File

@ -19,6 +19,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name="android.support.multidex.MultiDexApplication"
tools:replace="android:allowBackup">
<activity
android:name=".MainActivity"

View File

@ -0,0 +1,11 @@
ALTER TABLE Message ADD COLUMN conversation BINARY[16];
CREATE TABLE Message_Parent (
parent BINARY(64) NOT NULL,
child BINARY(64) NOT NULL,
pos INT NOT NULL,
conversation BINARY[16] NOT NULL,
PRIMARY KEY (parent, child),
FOREIGN KEY (child) REFERENCES Message (iv)
);

View File

@ -20,7 +20,6 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
@ -38,24 +37,14 @@ import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.mikepenz.community_material_typeface_library.CommunityMaterial;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.apps.abit.util.Drawables;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.wif.WifExporter;
import static android.graphics.Color.BLACK;
import static android.graphics.Color.WHITE;
/**
* A fragment representing a single Message detail screen.
@ -64,7 +53,6 @@ import static android.graphics.Color.WHITE;
* on handsets.
*/
public class AddressDetailFragment extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(AddressDetailFragment.class);
/**
* The fragment argument representing the item ID that this fragment
* represents.
@ -72,8 +60,6 @@ public class AddressDetailFragment extends Fragment {
public static final String ARG_ITEM = "item";
public static final String EXPORT_POSTFIX = ".keys.dat";
private static final int QR_CODE_SIZE = 350;
/**
* The content this fragment is presenting.
*/
@ -257,40 +243,12 @@ public class AddressDetailFragment extends Fragment {
// QR code
ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code);
qrCode.setImageBitmap(qrCode(item));
qrCode.setImageBitmap(Drawables.qrCode(item));
}
return rootView;
}
Bitmap qrCode(BitmessageAddress address) {
StringBuilder link = new StringBuilder("bitmessage:");
link.append(address.getAddress());
if (address.getAlias() != null) {
link.append("?label=").append(address.getAlias());
}
BitMatrix result;
try {
result = new MultiFormatWriter().encode(link.toString(),
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null);
} catch (WriterException e) {
LOG.error(e.getMessage(), e);
return null;
}
int w = result.getWidth();
int h = result.getHeight();
int[] pixels = new int[w * h];
for (int y = 0; y < h; y++) {
int offset = y * w;
for (int x = 0; x < w; x++) {
pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h);
return bitmap;
}
@Override
public void onPause() {
if (item != null) {

View File

@ -87,8 +87,8 @@ public class ComposeMessageActivity extends AppCompatActivity {
// so features like threading can be supported
if (item.getEncoding() == EXTENDED) {
replyIntent.putExtra(EXTRA_ENCODING, EXTENDED);
replyIntent.putExtra(EXTRA_PARENT, item);
}
replyIntent.putExtra(EXTRA_PARENT, item);
String prefix;
if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3)
.equalsIgnoreCase("RE:")) {

View File

@ -16,6 +16,7 @@
package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@ -79,6 +80,11 @@ public class ComposeMessageFragment extends Fragment {
if (getArguments() != null) {
if (getArguments().containsKey(EXTRA_IDENTITY)) {
identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY);
if (getActivity() != null) {
if (identity == null || identity.getPrivateKey() == null) {
identity = Singleton.getIdentity(getActivity());
}
}
} else {
throw new RuntimeException("No identity set for ComposeMessageFragment");
}
@ -156,6 +162,14 @@ public class ComposeMessageFragment extends Fragment {
return rootView;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (identity == null || identity.getPrivateKey() == null) {
identity = Singleton.getIdentity(context);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.compose, menu);
@ -170,6 +184,9 @@ public class ComposeMessageFragment extends Fragment {
return true;
case R.id.select_encoding:
SelectEncodingDialogFragment encodingDialog = new SelectEncodingDialogFragment();
Bundle args = new Bundle();
args.putSerializable(EXTRA_ENCODING, encoding);
encodingDialog.setArguments(args);
encodingDialog.setTargetFragment(this, 0);
encodingDialog.show(getFragmentManager(), "select encoding dialog");
return true;

View File

@ -20,17 +20,32 @@ import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Base64;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Switch;
import android.widget.TextView;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V2Pubkey;
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import static android.util.Base64.URL_SAFE;
public class CreateAddressActivity extends AppCompatActivity {
private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$");
private byte[] pubkeyBytes;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -48,12 +63,21 @@ public class CreateAddressActivity extends AppCompatActivity {
String addressText = getAddress(uri);
String[] parameters = getParameters(uri);
for (String parameter : parameters) {
String name = parameter.substring(0, 6).toLowerCase();
if (name.startsWith("label")) {
label.setText(parameter.substring(parameter.indexOf('=') + 1).trim());
} else if (name.startsWith("action")) {
parameter = parameter.toLowerCase();
subscribe.setChecked(parameter.contains("subscribe"));
Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter);
if (matcher.find()) {
String key = matcher.group(1).toLowerCase();
String value = matcher.group(2);
switch (key) {
case "label":
label.setText(value.trim());
break;
case "action":
subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe"));
break;
case "pubkey":
pubkeyBytes = Base64.decode(value, URL_SAFE);
break;
}
}
}
@ -83,6 +107,30 @@ public class CreateAddressActivity extends AppCompatActivity {
if (subscribe.isChecked()) {
bmc.addSubscribtion(bmAddress);
}
if (pubkeyBytes != null) {
try {
final Pubkey pubkey;
InputStream pubkeyStream = new ByteArrayInputStream(pubkeyBytes);
long stream = bmAddress.getStream();
switch ((int) bmAddress.getVersion()) {
case 2:
pubkey = V2Pubkey.read(pubkeyStream, stream);
break;
case 3:
pubkey = V3Pubkey.read(pubkeyStream, stream);
break;
case 4:
pubkey = new V4Pubkey(V3Pubkey.read(pubkeyStream, stream));
break;
default:
pubkey = null;
}
if (pubkey != null) {
bmAddress.setPubkey(pubkey);
}
} catch (Exception ignore) {
}
}
setResult(Activity.RESULT_OK);
finish();

View File

@ -16,16 +16,23 @@
package ch.dissem.apps.abit;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Point;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Toast;
@ -48,9 +55,6 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
import com.mikepenz.materialdrawer.model.interfaces.Nameable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@ -64,6 +68,8 @@ import ch.dissem.apps.abit.repository.AndroidMessageRepository;
import ch.dissem.apps.abit.service.BitmessageService;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.apps.abit.synchronization.SyncAdapter;
import ch.dissem.apps.abit.util.Drawables;
import ch.dissem.apps.abit.util.Labels;
import ch.dissem.apps.abit.util.Preferences;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
@ -72,6 +78,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label;
import static android.widget.Toast.LENGTH_LONG;
import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo;
import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE;
import static ch.dissem.apps.abit.service.BitmessageService.isRunning;
@ -99,7 +106,6 @@ public class MainActivity extends AppCompatActivity
public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage";
public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox";
private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class);
private static final int ADD_IDENTITY = 1;
private static final int MANAGE_IDENTITY = 2;
@ -233,6 +239,50 @@ public class MainActivity extends AppCompatActivity
.withActivity(this)
.withHeaderBackground(R.drawable.header)
.withProfiles(profiles)
.withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() {
@Override
public boolean onProfileImageClick(View view, IProfile profile, boolean current) {
if (current) {
// Show QR code in modal dialog
final Dialog dialog = new Dialog(MainActivity.this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
ImageView imageView = new ImageView(MainActivity.this);
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(MainActivity.this)));
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
dialog.addContentView(imageView, new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
Window window = dialog.getWindow();
if (window != null) {
Display display = window.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int dim = size.x < size.y ? size.x : size.y;
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.copyFrom(window.getAttributes());
lp.width = dim;
lp.height = dim;
window.setAttributes(lp);
}
dialog.show();
return true;
}
return false;
}
@Override
public boolean onProfileImageLongClick(View view, IProfile iProfile, boolean b) {
return false;
}
})
.withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() {
@Override
public boolean onProfileChanged(View view, IProfile profile, boolean current) {
@ -272,7 +322,7 @@ public class MainActivity extends AppCompatActivity
final ArrayList<IDrawerItem> drawerItems = new ArrayList<>();
drawerItems.add(new PrimaryDrawerItem()
.withName(R.string.archive)
.withTag(AndroidMessageRepository.LABEL_ARCHIVE)
.withTag(LABEL_ARCHIVE)
.withIcon(CommunityMaterial.Icon.cmd_archive)
);
drawerItems.add(new DividerDrawerItem());
@ -311,7 +361,15 @@ public class MainActivity extends AppCompatActivity
public boolean onItemClick(View view, int position, IDrawerItem item) {
if (item.getTag() instanceof Label) {
selectedLabel = (Label) item.getTag();
showSelectedLabel();
if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof
MessageListFragment) {
((MessageListFragment) getSupportFragmentManager()
.findFragmentById(R.id.item_list)).updateList(selectedLabel);
} else {
MessageListFragment listFragment = new MessageListFragment();
changeList(listFragment);
listFragment.updateList(selectedLabel);
}
return false;
} else if (item instanceof Nameable<?>) {
Nameable<?> ni = (Nameable<?>) item;
@ -374,7 +432,10 @@ public class MainActivity extends AppCompatActivity
for (Label label : labels) {
addLabelEntry(label);
}
showSelectedLabel();
IDrawerItem selectedDrawerItem = drawer.getDrawerItem(selectedLabel);
if (selectedDrawerItem != null) {
drawer.setSelection(selectedDrawerItem);
}
}
}.execute();
}
@ -389,7 +450,11 @@ public class MainActivity extends AppCompatActivity
@SuppressWarnings("unchecked")
protected void onRestoreInstanceState(Bundle savedInstanceState) {
selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel");
showSelectedLabel();
IDrawerItem selectedItem = drawer.getDrawerItem(selectedLabel);
if (selectedItem != null) {
drawer.setSelection(selectedItem);
}
super.onRestoreInstanceState(savedInstanceState);
}
@ -426,37 +491,9 @@ public class MainActivity extends AppCompatActivity
public void addLabelEntry(Label label) {
PrimaryDrawerItem item = new PrimaryDrawerItem()
.withName(label.toString())
.withTag(label);
if (label.getType() == null) {
item.withIcon(CommunityMaterial.Icon.cmd_label)
.withIconColor(label.getColor());
} else {
switch (label.getType()) {
case INBOX:
item.withIcon(GoogleMaterial.Icon.gmd_inbox);
break;
case DRAFT:
item.withIcon(CommunityMaterial.Icon.cmd_file);
break;
case OUTBOX:
item.withIcon(CommunityMaterial.Icon.cmd_outbox);
break;
case SENT:
item.withIcon(CommunityMaterial.Icon.cmd_send);
break;
case BROADCAST:
item.withIcon(CommunityMaterial.Icon.cmd_rss);
break;
case UNREAD:
item.withIcon(GoogleMaterial.Icon.gmd_markunread_mailbox);
break;
case TRASH:
item.withIcon(GoogleMaterial.Icon.gmd_delete);
break;
default:
item.withIcon(CommunityMaterial.Icon.cmd_label);
}
}
.withTag(label)
.withIcon(Labels.getIcon(label))
.withIconColor(Labels.getColor(label));
drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3);
}
@ -497,13 +534,15 @@ public class MainActivity extends AppCompatActivity
for (IDrawerItem item : drawer.getDrawerItems()) {
if (item.getTag() instanceof Label) {
Label label = (Label) item.getTag();
int unread = bmc.messages().countUnread(label);
if (unread > 0) {
((PrimaryDrawerItem) item).withBadge(String.valueOf(unread));
} else {
((PrimaryDrawerItem) item).withBadge((String) null);
if (label != LABEL_ARCHIVE) {
int unread = bmc.messages().countUnread(label);
if (unread > 0) {
((PrimaryDrawerItem) item).withBadge(String.valueOf(unread));
} else {
((PrimaryDrawerItem) item).withBadge((String) null);
}
drawer.updateItem(item);
}
drawer.updateItem(item);
}
}
}
@ -521,18 +560,6 @@ public class MainActivity extends AppCompatActivity
}
}
private void showSelectedLabel() {
if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof
MessageListFragment) {
((MessageListFragment) getSupportFragmentManager()
.findFragmentById(R.id.item_list)).updateList(selectedLabel);
} else {
MessageListFragment listFragment = new MessageListFragment();
changeList(listFragment);
listFragment.updateList(selectedLabel);
}
}
/**
* Callback method from {@link ListSelectionListener}
* indicating that the item with the given ID was selected.

View File

@ -16,8 +16,14 @@
package ch.dissem.apps.abit;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.util.Linkify;
import android.view.LayoutInflater;
import android.view.Menu;
@ -29,22 +35,29 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.view.IconicsImageView;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import ch.dissem.apps.abit.listener.ActionBarListener;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.apps.abit.util.Assets;
import ch.dissem.apps.abit.util.Drawables;
import ch.dissem.apps.abit.util.Labels;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.MessageRepository;
import static android.text.util.Linkify.WEB_URLS;
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN;
import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA;
import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces;
/**
@ -106,6 +119,11 @@ public class MessageDetailFragment extends Fragment {
} else if (item.getType() == Plaintext.Type.BROADCAST) {
((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast);
}
RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels);
LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels());
labelView.setAdapter(labelAdapter);
labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2));
TextView messageBody = (TextView) rootView.findViewById(R.id.text);
messageBody.setText(item.getText());
@ -130,16 +148,33 @@ public class MessageDetailFragment extends Fragment {
removed = true;
}
}
MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext());
if (removed) {
if (getActivity() instanceof ActionBarListener) {
((ActionBarListener) getActivity()).updateUnread();
}
Singleton.getMessageRepository(inflater.getContext()).save(item);
messageRepo.save(item);
}
List<Plaintext> parents = new ArrayList<>(item.getParents().size());
for (InventoryVector parentIV : item.getParents()) {
Plaintext parent = messageRepo.getMessage(parentIV);
if (parent != null) {
parents.add(parent);
}
}
showRelatedMessages(rootView, R.id.parents, parents);
showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item));
}
return rootView;
}
private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) {
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(id);
RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.message, menu);
@ -197,4 +232,136 @@ public class MessageDetailFragment extends Fragment {
}
return false;
}
private static class RelatedMessageAdapter extends RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder> {
private final List<Plaintext> messages;
private final Context ctx;
private RelatedMessageAdapter(Context ctx, List<Plaintext> messages) {
this.messages = messages;
this.ctx = ctx;
}
@Override
public RelatedMessageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Inflate the custom layout
View contactView = inflater.inflate(R.layout.item_message_minimized, parent, false);
// Return a new holder instance
return new RelatedMessageAdapter.ViewHolder(contactView);
}
// Involves populating data into the item through holder
@Override
public void onBindViewHolder(RelatedMessageAdapter.ViewHolder viewHolder, int position) {
// Get the data model based on position
Plaintext message = messages.get(position);
viewHolder.avatar.setImageDrawable(new Identicon(message.getFrom()));
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus()));
viewHolder.sender.setText(message.getFrom().toString());
viewHolder.extract.setText(normalizeWhitespaces(message.getText()));
viewHolder.item = message;
}
// Returns the total count of items in the list
@Override
public int getItemCount() {
return messages.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
private final ImageView avatar;
private final ImageView status;
private final TextView sender;
private final TextView extract;
private Plaintext item;
ViewHolder(final View itemView) {
super(itemView);
avatar = (ImageView) itemView.findViewById(R.id.avatar);
status = (ImageView) itemView.findViewById(R.id.status);
sender = (TextView) itemView.findViewById(R.id.sender);
extract = (TextView) itemView.findViewById(R.id.text);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ctx instanceof MainActivity) {
((MainActivity) ctx).onItemSelected(item);
} else {
Intent detailIntent;
detailIntent = new Intent(ctx, MessageDetailActivity.class);
detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item);
ctx.startActivity(detailIntent);
}
}
});
}
}
}
private static class LabelAdapter extends
RecyclerView.Adapter<LabelAdapter.ViewHolder> {
private final List<Label> labels;
private final Context ctx;
private LabelAdapter(Context ctx, Set<Label> labels) {
this.labels = new ArrayList<>(labels);
this.ctx = ctx;
}
@Override
public LabelAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Inflate the custom layout
View contactView = inflater.inflate(R.layout.item_label, parent, false);
// Return a new holder instance
return new ViewHolder(contactView);
}
// Involves populating data into the item through holder
@Override
public void onBindViewHolder(LabelAdapter.ViewHolder viewHolder, int position) {
// Get the data model based on position
Label label = labels.get(position);
viewHolder.icon.setColor(Labels.getColor(label));
viewHolder.icon.setIcon(Labels.getIcon(label));
viewHolder.label.setText(Labels.getText(label, ctx));
}
// Returns the total count of items in the list
@Override
public int getItemCount() {
return labels.size();
}
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
static class ViewHolder extends RecyclerView.ViewHolder {
// Your holder should contain a member variable
// for any view that will be set as you render a row
public IconicsImageView icon;
public TextView label;
// We also create a constructor that accepts the entire item row
// and does the view lookups to find each subview
ViewHolder(View itemView) {
// Stores the itemView in a public final member variable that can be used
// to access the context from any ViewHolder instance.
super(itemView);
icon = (IconicsImageView) itemView.findViewById(R.id.icon);
label = (TextView) itemView.findViewById(R.id.label);
}
}
}
}

View File

@ -29,8 +29,10 @@ import android.widget.Toast;
import com.mikepenz.aboutlibraries.Libs;
import com.mikepenz.aboutlibraries.LibsBuilder;
import ch.dissem.apps.abit.repository.AndroidNodeRegistry;
import ch.dissem.apps.abit.service.Singleton;
import ch.dissem.apps.abit.synchronization.SyncAdapter;
import ch.dissem.bitmessage.BitmessageContext;
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW;
import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE;
@ -78,7 +80,9 @@ public class SettingsFragment
@Override
protected Void doInBackground(Void... voids) {
Singleton.getBitmessageContext(ctx).cleanup();
BitmessageContext bmc = Singleton.getBitmessageContext(ctx);
bmc.cleanup();
bmc.internals().getNodeRegistry().clear();
return null;
}

View File

@ -46,8 +46,7 @@ public class SelectEncodingDialogFragment extends AppCompatDialogFragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (getArguments() != null && getArguments().containsKey(EXTRA_ENCODING)) {
encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING);
}

View File

@ -31,8 +31,10 @@ import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import ch.dissem.apps.abit.R;
import ch.dissem.apps.abit.util.Labels;
import ch.dissem.apps.abit.util.UuidUtils;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label;
@ -40,6 +42,8 @@ import ch.dissem.bitmessage.ports.AbstractMessageRepository;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.utils.Encode;
import static ch.dissem.apps.abit.util.UuidUtils.asUuid;
import static ch.dissem.bitmessage.utils.Strings.hex;
import static java.lang.String.valueOf;
/**
@ -65,6 +69,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
private static final String COLUMN_RETRIES = "retries";
private static final String COLUMN_NEXT_TRY = "next_try";
private static final String COLUMN_INITIAL_HASH = "initial_hash";
private static final String COLUMN_CONVERSATION = "conversation";
private static final String PARENTS_TABLE_NAME = "Message_Parent";
private static final String JOIN_TABLE_NAME = "Message_Label";
private static final String JT_COLUMN_MESSAGE = "message_id";
@ -122,35 +129,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
private Label getLabel(Cursor c) {
String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE));
Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName);
String text;
if (type == null) {
String text = Labels.getText(type, null, context);
if (text == null) {
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
} else {
switch (type) {
case INBOX:
text = context.getString(R.string.inbox);
break;
case DRAFT:
text = context.getString(R.string.draft);
break;
case OUTBOX:
text = context.getString(R.string.outbox);
break;
case SENT:
text = context.getString(R.string.sent);
break;
case UNREAD:
text = context.getString(R.string.unread);
break;
case TRASH:
text = context.getString(R.string.trash);
break;
case BROADCAST:
text = context.getString(R.string.broadcasts);
break;
default:
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
}
}
Label label = new Label(
text,
@ -164,7 +145,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
public int countUnread(Label label) {
String[] args;
String where;
if (label == null){
if (label == null) {
return 0;
}
if (label == LABEL_ARCHIVE) {
@ -187,6 +168,72 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
);
}
@Override
public List<UUID> findConversations(Label label) {
String[] projection = {
COLUMN_CONVERSATION,
};
String where;
if (label == null) {
where = "id NOT IN (SELECT message_id FROM Message_Label)";
} else {
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")";
}
List<UUID> result = new LinkedList<>();
SQLiteDatabase db = sql.getReadableDatabase();
try (Cursor c = db.query(
TABLE_NAME, projection,
where,
null, null, null, null
)) {
while (c.moveToNext()) {
byte[] uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION));
result.add(asUuid(uuidBytes));
}
}
return result;
}
private void updateParents(SQLiteDatabase db, Plaintext message) {
if (message.getInventoryVector() == null || message.getParents().isEmpty()) {
// There are no parents to save yet (they are saved in the extended data, that's enough for now)
return;
}
byte[] childIV = message.getInventoryVector().getHash();
db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(childIV).toString()});
// save new parents
int order = 0;
for (InventoryVector parentIV : message.getParents()) {
Plaintext parent = getMessage(parentIV);
mergeConversations(db, parent.getConversationId(), message.getConversationId());
order++;
ContentValues values = new ContentValues();
values.put("parent", parentIV.getHash());
values.put("child", childIV);
values.put("pos", order);
values.put("conversation", UuidUtils.asBytes(message.getConversationId()));
db.insertOrThrow(PARENTS_TABLE_NAME, null, values);
}
}
/**
* Replaces every occurrence of the source conversation ID with the target ID
*
* @param db is used to keep everything within one transaction
* @param source ID of the conversation to be merged
* @param target ID of the merge target
*/
private void mergeConversations(SQLiteDatabase db, UUID source, UUID target) {
ContentValues values = new ContentValues();
values.put("conversation", UuidUtils.asBytes(target));
String[] whereArgs = {hex(UuidUtils.asBytes(source)).toString()};
db.update(TABLE_NAME, values, "conversation=?", whereArgs);
db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs);
}
protected List<Plaintext> find(String where) {
List<Plaintext> result = new LinkedList<>();
@ -205,7 +252,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
COLUMN_STATUS,
COLUMN_TTL,
COLUMN_RETRIES,
COLUMN_NEXT_TRY
COLUMN_NEXT_TRY,
COLUMN_CONVERSATION
};
SQLiteDatabase db = sql.getReadableDatabase();
@ -220,8 +268,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA));
Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex
(COLUMN_TYPE)));
Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new
ByteArrayInputStream(data));
Plaintext.Builder builder = Plaintext.readWithoutSignature(type,
new ByteArrayInputStream(data));
long id = c.getLong(c.getColumnIndex(COLUMN_ID));
builder.id(id);
builder.IV(new InventoryVector(iv));
@ -240,6 +288,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
if (!c.isNull(nextTryColumn)) {
builder.nextTry(c.getLong(nextTryColumn));
}
builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))));
builder.labels(findLabels(id));
result.add(builder.build());
}
@ -268,6 +317,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
update(db, message);
}
updateParents(db, message);
// remove existing labels
db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())});
@ -302,6 +353,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
values.put(COLUMN_TTL, message.getTTL());
values.put(COLUMN_RETRIES, message.getRetries());
values.put(COLUMN_NEXT_TRY, message.getNextTry());
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
long id = db.insertOrThrow(TABLE_NAME, null, values);
message.setId(id);
}
@ -322,6 +374,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository {
values.put(COLUMN_TTL, message.getTTL());
values.put(COLUMN_RETRIES, message.getRetries());
values.put(COLUMN_NEXT_TRY, message.getNextTry());
values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId()));
db.update(TABLE_NAME, values, "id = " + message.getId(), null);
}

View File

@ -10,6 +10,7 @@ import android.database.sqlite.SQLiteStatement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -56,6 +57,12 @@ public class AndroidNodeRegistry implements NodeRegistry {
db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))});
}
@Override
public void clear() {
SQLiteDatabase db = sql.getWritableDatabase();
db.delete(TABLE_NAME, null, null);
}
private Long loadExistingTime(NetworkAddress node) {
SQLiteStatement statement = loadExistingStatement.get();
if (statement == null) {

View File

@ -27,7 +27,7 @@ import ch.dissem.apps.abit.util.Assets;
*/
public class SqlHelper extends SQLiteOpenHelper {
// If you change the database schema, you must increment the database version.
private static final int DATABASE_VERSION = 6;
private static final int DATABASE_VERSION = 7;
private static final String DATABASE_NAME = "jabit.db";
private final Context ctx;
@ -61,6 +61,8 @@ public class SqlHelper extends SQLiteOpenHelper {
executeMigration(db, "V3.3__Create_table_node");
case 5:
executeMigration(db, "V3.4__Add_label_outbox");
case 6:
executeMigration(db, "V4.0__Create_table_message_parent");
default:
// Nothing to do. Let's assume we won't upgrade from a version that's newer than
// DATABASE_VERSION.
@ -73,7 +75,7 @@ public class SqlHelper extends SQLiteOpenHelper {
}
}
public static StringBuilder join(long... numbers) {
static StringBuilder join(long... numbers) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < numbers.length; i++) {
if (i > 0) streamList.append(", ");
@ -82,7 +84,7 @@ public class SqlHelper extends SQLiteOpenHelper {
return streamList;
}
public static StringBuilder join(Enum<?>... types) {
static StringBuilder join(Enum<?>... types) {
StringBuilder streamList = new StringBuilder();
for (int i = 0; i < types.length; i++) {
if (i > 0) streamList.append(", ");

View File

@ -18,6 +18,7 @@ package ch.dissem.apps.abit.service;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.support.annotation.Nullable;
@ -38,6 +39,17 @@ public class BitmessageService extends Service {
private NetworkNotification notification = null;
private final Handler cleanupHandler = new Handler();
private final Runnable cleanupTask = new Runnable() {
@Override
public void run() {
bmc.cleanup();
if (isRunning()) {
cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L);
}
}
};
public static boolean isRunning() {
return running && bmc.isRunning();
}
@ -61,6 +73,7 @@ public class BitmessageService extends Service {
bmc.startup();
}
notification.show();
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L);
}
return Service.START_STICKY;
}
@ -72,6 +85,8 @@ public class BitmessageService extends Service {
}
running = false;
notification.showShutdown();
cleanupHandler.removeCallbacks(cleanupTask);
bmc.cleanup();
stopSelf();
}

View File

@ -42,6 +42,7 @@ import ch.dissem.bitmessage.networking.nio.NioNetworkHandler;
import ch.dissem.bitmessage.ports.AddressRepository;
import ch.dissem.bitmessage.ports.MessageRepository;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
import ch.dissem.bitmessage.utils.ConversationService;
import ch.dissem.bitmessage.utils.TTL;
import static ch.dissem.bitmessage.utils.UnixTime.DAY;
@ -51,6 +52,7 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY;
*/
public class Singleton {
private static BitmessageContext bitmessageContext;
private static ConversationService conversationService;
private static MessageListener messageListener;
private static BitmessageAddress identity;
private static AndroidProofOfWorkRepository powRepo;
@ -160,4 +162,16 @@ public class Singleton {
throw new IllegalArgumentException("Identity expected, but no private key available");
Singleton.identity = identity;
}
public static ConversationService getConversationService(Context ctx) {
if (conversationService == null) {
final BitmessageContext bmc = getBitmessageContext(ctx);
synchronized (Singleton.class) {
if (conversationService == null) {
conversationService = new ConversationService(bmc.messages());
}
}
}
return conversationService;
}
}

View File

@ -19,19 +19,41 @@ package ch.dissem.apps.abit.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.Base64;
import android.view.Menu;
import android.view.MenuItem;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.typeface.IIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import ch.dissem.apps.abit.Identicon;
import ch.dissem.apps.abit.R;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.exception.ApplicationException;
import static android.graphics.Color.BLACK;
import static android.graphics.Color.WHITE;
import static android.util.Base64.NO_WRAP;
import static android.util.Base64.URL_SAFE;
/**
* Some helper methods to work with drawables.
*/
public class Drawables {
private static final Logger LOG = LoggerFactory.getLogger(Drawables.class);
private static final int QR_CODE_SIZE = 350;
public static MenuItem addIcon(Context ctx, Menu menu, int menuItem, IIcon icon) {
MenuItem item = menu.findItem(menuItem);
item.setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar());
@ -49,4 +71,42 @@ public class Drawables {
identicon.draw(canvas);
return bitmap;
}
public static Bitmap qrCode(BitmessageAddress address) {
StringBuilder link = new StringBuilder("bitmessage:");
link.append(address.getAddress());
if (address.getAlias() != null) {
link.append("?label=").append(address.getAlias());
}
if (address.getPubkey() != null) {
link.append(address.getAlias() == null ? '?' : '&');
ByteArrayOutputStream pubkey = new ByteArrayOutputStream();
try {
address.getPubkey().writeUnencrypted(pubkey);
} catch (IOException e) {
throw new ApplicationException(e);
}
link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE | NO_WRAP));
}
BitMatrix result;
try {
result = new MultiFormatWriter().encode(link.toString(),
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null);
} catch (WriterException e) {
LOG.error(e.getMessage(), e);
return null;
}
int w = result.getWidth();
int h = result.getHeight();
int[] pixels = new int[w * h];
for (int y = 0; y < h; y++) {
int offset = y * w;
for (int x = 0; x < w; x++) {
pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h);
return bitmap;
}
}

View File

@ -0,0 +1,77 @@
package ch.dissem.apps.abit.util;
import android.content.Context;
import android.support.annotation.ColorInt;
import com.mikepenz.community_material_typeface_library.CommunityMaterial;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.typeface.IIcon;
import ch.dissem.apps.abit.R;
import ch.dissem.bitmessage.entity.valueobject.Label;
/**
* Helper class to help with translating the default labels, getting label colors and so on.
*/
public class Labels {
public static String getText(Label label, Context ctx) {
return getText(label.getType(), label.toString(), ctx);
}
public static String getText(Label.Type type, String alternative, Context ctx) {
if (type == null) {
return alternative;
} else {
switch (type) {
case INBOX:
return ctx.getString(R.string.inbox);
case DRAFT:
return ctx.getString(R.string.draft);
case OUTBOX:
return ctx.getString(R.string.outbox);
case SENT:
return ctx.getString(R.string.sent);
case UNREAD:
return ctx.getString(R.string.unread);
case TRASH:
return ctx.getString(R.string.trash);
case BROADCAST:
return ctx.getString(R.string.broadcasts);
default:
return alternative;
}
}
}
public static IIcon getIcon(Label label) {
if (label.getType() == null) {
return CommunityMaterial.Icon.cmd_label;
}
switch (label.getType()) {
case INBOX:
return GoogleMaterial.Icon.gmd_inbox;
case DRAFT:
return CommunityMaterial.Icon.cmd_file;
case OUTBOX:
return CommunityMaterial.Icon.cmd_inbox_arrow_up;
case SENT:
return CommunityMaterial.Icon.cmd_send;
case BROADCAST:
return CommunityMaterial.Icon.cmd_rss;
case UNREAD:
return GoogleMaterial.Icon.gmd_markunread_mailbox;
case TRASH:
return GoogleMaterial.Icon.gmd_delete;
default:
return CommunityMaterial.Icon.cmd_label;
}
}
@ColorInt
public static int getColor(Label label) {
if (label.getType() == null) {
return label.getColor();
}
return 0xFF000000;
}
}

View File

@ -0,0 +1,37 @@
package ch.dissem.apps.abit.util;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* SQLite has no UUID data type, and UUIDs are therefore best saved as BINARY[16]. This class
* takes care of conversion between byte[16] and UUID.
* <p>
* Thanks to Brice Roncace on
* <a href="http://stackoverflow.com/questions/17893609/convert-uuid-to-byte-that-works-when-using-uuid-nameuuidfrombytesb">
* Stack Overflow
* </a>
* for providing the UUID <-> byte[] conversions.
* </p>
*/
public class UuidUtils {
public static UUID asUuid(byte[] bytes) {
if (bytes == null) {
return null;
}
ByteBuffer bb = ByteBuffer.wrap(bytes);
long firstLong = bb.getLong();
long secondLong = bb.getLong();
return new UUID(firstLong, secondLong);
}
public static byte[] asBytes(UUID uuid) {
if (uuid == null) {
return null;
}
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uuid.getMostSignificantBits());
bb.putLong(uuid.getLeastSignificantBits());
return bb.array();
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:top="1dp" android:bottom="1dp">
<shape
android:shape="rectangle">
<stroke android:width="1dp" android:color="#FFDDDDDD" />
<solid android:color="#00000000" />
</shape>
</item>
</layer-list>

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<android.support.constraint.ConstraintLayout
<RelativeLayout
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"
@ -31,10 +31,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/select_encoding_warning"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_constraintLeft_creator="1"
tools:layout_constraintTop_creator="1"/>
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"/>
<RadioGroup
@ -43,9 +42,9 @@
android:layout_height="wrap_content"
android:paddingBottom="24dp"
android:paddingTop="24dp"
app:layout_constraintLeft_toLeftOf="@+id/description"
app:layout_constraintRight_toRightOf="@+id/description"
app:layout_constraintTop_toBottomOf="@+id/description">
android:layout_alignStart="@+id/description"
android:layout_alignEnd="@+id/description"
android:layout_below="@+id/description">
<RadioButton
android:id="@+id/simple"
@ -72,10 +71,8 @@
android:text="@string/ok"
android:textColor="@color/colorAccent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintRight_toRightOf="@+id/radioGroup"
app:layout_constraintTop_toBottomOf="@+id/radioGroup"
tools:layout_constraintRight_creator="1"
tools:layout_constraintTop_creator="1"/>
android:layout_alignRight="@+id/radioGroup"
android:layout_below="@+id/radioGroup"/>
<Button
android:id="@+id/dismiss"
@ -84,7 +81,7 @@
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textColor="@color/colorAccent"
app:layout_constraintRight_toLeftOf="@+id/ok"
app:layout_constraintTop_toBottomOf="@+id/radioGroup"/>
android:layout_toLeftOf="@+id/ok"
android:layout_below="@+id/radioGroup"/>
</android.support.constraint.ConstraintLayout>
</RelativeLayout>

View File

@ -1,14 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:focusableInTouchMode="true"
android:paddingBottom="64dp"
android:orientation="vertical">
<TextView
@ -24,7 +25,7 @@
android:padding="16dp"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:ignore="UnusedAttribute"
tools:text="Subject"/>
tools:text="Subject" />
<ImageView
android:id="@+id/status"
@ -32,17 +33,17 @@
android:layout_height="60dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:tint="@color/colorAccent"
tools:src="@drawable/ic_notification_proof_of_work"
android:padding="16dp"
tools:ignore="ContentDescription"/>
android:tint="@color/colorAccent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_notification_proof_of_work" />
<View
android:id="@+id/divider"
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_below="@id/subject"
android:background="@color/divider"/>
android:background="@color/divider" />
<ImageView
android:id="@+id/avatar"
@ -52,7 +53,7 @@
android:layout_below="@+id/divider"
android:layout_margin="16dp"
android:src="@color/colorAccent"
tools:ignore="ContentDescription"/>
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/sender"
@ -64,7 +65,7 @@
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textStyle="bold"
tools:text="Sender"/>
tools:text="Sender" />
<TextView
android:id="@+id/recipient"
@ -75,19 +76,44 @@
android:gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
tools:text="Recipient"/>
tools:text="Recipient" />
<android.support.v7.widget.RecyclerView
android:id="@+id/parents"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/avatar"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/avatar"
android:layout_below="@+id/parents"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="32dp"
android:paddingBottom="64dp"
android:text="New Text"
android:textIsSelectable="true"/>
android:paddingBottom="16dp"
tools:text="Message Body"
android:textIsSelectable="true" />
<android.support.v7.widget.RecyclerView
android:id="@+id/labels"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/text"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/responses"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/labels"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" />
</RelativeLayout>
</ScrollView>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
app:iiv_color="@android:color/black"
app:iiv_icon="cmd-label" />
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="24dp"
tools:text="Label"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/icon" />
</RelativeLayout>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/border_bottom">
<ImageView
android:id="@+id/avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginTop="5dp"
android:src="@color/colorPrimaryDark"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/sender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="end"
android:lines="1"
android:paddingBottom="0dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textStyle="bold"
tools:text="Sender" />
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@+id/sender"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="end"
android:gravity="center_vertical"
android:lines="1"
android:paddingBottom="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Text" />
<ImageView
android:id="@+id/status"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/avatar"
android:layout_alignEnd="@+id/avatar"
android:layout_marginBottom="-8dp"
android:layout_marginEnd="-8dp"
android:tint="@color/colorAccent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_notification_proof_of_work" />
</RelativeLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2015 Christian Basler
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@ -15,8 +14,7 @@
~ limitations under the License.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -31,11 +29,10 @@
android:foreground="?attr/selectableItemBackground"
tools:ignore="UselessParent">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/avatar"
@ -45,7 +42,7 @@
android:layout_alignParentTop="true"
android:layout_margin="16dp"
android:src="@color/colorPrimaryDark"
tools:ignore="ContentDescription"/>
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/sender"
@ -63,8 +60,7 @@
android:paddingTop="0dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
tools:text="Sender"
/>
tools:text="Sender" />
<TextView
android:id="@+id/subject"
@ -78,7 +74,7 @@
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Subject"/>
tools:text="Subject" />
<TextView
android:id="@+id/text"
@ -94,7 +90,7 @@
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Text"/>
tools:text="Text" />
<ImageView
android:id="@+id/status"
@ -106,7 +102,7 @@
android:layout_marginEnd="-8dp"
android:tint="@color/colorAccent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_notification_proof_of_work"/>
tools:src="@drawable/ic_notification_proof_of_work" />
</RelativeLayout>

View File

@ -19,7 +19,7 @@
<item name="android:textColor">@color/colorAccent</item>
</style>
<style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog">
<style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog.MinWidth">
<item name="windowNoTitle">false</item>
</style>
</resources>

View File

@ -9,7 +9,8 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.14.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -17,6 +18,8 @@ buildscript {
}
allprojects {
apply plugin: 'com.github.ben-manes.versions'
repositories {
jcenter()
mavenCentral()

Binary file not shown.

View File

@ -1,6 +1,6 @@
#Fri Aug 12 22:10:25 CEST 2016
#Tue Apr 04 00:02:32 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip

22
gradlew vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
##############################################################################
##
@ -154,11 +154,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

6
gradlew.bat vendored
View File

@ -49,7 +49,6 @@ goto fail
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@ -60,11 +59,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line