DB code rewrite
There's still a problem storing or retreiving messages
This commit is contained in:
parent
cb2040b0ce
commit
5e2d19df58
@ -28,10 +28,8 @@ dependencies {
|
||||
|
||||
compile 'ch.dissem.jabit:jabit-domain:0.2.1-SNAPSHOT'
|
||||
compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT'
|
||||
compile 'ch.dissem.jabit:jabit-repositories:0.2.1-SNAPSHOT'
|
||||
compile 'ch.dissem.jabit:jabit-security-spongy:0.2.1-SNAPSHOT'
|
||||
|
||||
compile 'org.sqldroid:sqldroid:1.0.3'
|
||||
compile 'org.slf4j:slf4j-android:1.7.12'
|
||||
|
||||
compile('com.mikepenz:materialdrawer:3.1.0@aar') {
|
||||
|
@ -1,5 +1,5 @@
|
||||
CREATE TABLE Message (
|
||||
id INTEGER PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
iv BINARY(32) UNIQUE,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
sender VARCHAR(40) NOT NULL,
|
||||
@ -14,7 +14,7 @@ CREATE TABLE Message (
|
||||
);
|
||||
|
||||
CREATE TABLE Label (
|
||||
id INTEGER PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(20),
|
||||
color INT NOT NULL DEFAULT 4278190080, -- FF000000
|
||||
@ -36,5 +36,6 @@ CREATE TABLE Message_Label (
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Inbox', 'INBOX', 4278190335, 0);
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Drafts', 'DRAFT', 4294940928, 10);
|
||||
INSERT INTO Label(label, type, color, ord) VALUES ('Sent', 'SENT', 4294967040, 20);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Broadcast', 'BROADCAST', 50);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Unread', 'UNREAD', 90);
|
||||
INSERT INTO Label(label, type, ord) VALUES ('Trash', 'TRASH', 100);
|
||||
|
@ -166,6 +166,8 @@ public class MessageListActivity extends AppCompatActivity
|
||||
public boolean onItemClick(AdapterView<?> adapterView, View view, int i, long l, IDrawerItem item) {
|
||||
if (item.getTag() instanceof Label) {
|
||||
selectedLabel = (Label) item.getTag();
|
||||
((MessageListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.message_list)).updateList(selectedLabel);
|
||||
} else if (item instanceof Nameable<?>) {
|
||||
Nameable<?> ni = (Nameable<?>) item;
|
||||
switch (ni.getNameRes()) {
|
||||
@ -203,9 +205,14 @@ public class MessageListActivity extends AppCompatActivity
|
||||
case R.id.sync_disabled:
|
||||
bmc.startup(new BitmessageContext.Listener() {
|
||||
@Override
|
||||
public void receive(Plaintext plaintext) {
|
||||
public void receive(final Plaintext plaintext) {
|
||||
// TODO
|
||||
Toast.makeText(MessageListActivity.this, plaintext.getSubject(), Toast.LENGTH_LONG).show();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(MessageListActivity.this, plaintext.getSubject(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
updateMenu();
|
||||
|
@ -13,6 +13,7 @@ import android.widget.ListView;
|
||||
import ch.dissem.apps.abit.service.Singleton;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.entity.Plaintext;
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label;
|
||||
|
||||
/**
|
||||
* A list fragment representing a list of Messages. This fragment
|
||||
@ -79,12 +80,15 @@ public class MessageListFragment extends ListFragment {
|
||||
|
||||
bmc = Singleton.getBitmessageContext(getActivity());
|
||||
|
||||
// TODO: replace with a real list adapter.
|
||||
updateList(((MessageListActivity) getActivity()).getSelectedLabel());
|
||||
}
|
||||
|
||||
public void updateList(Label label) {
|
||||
setListAdapter(new ArrayAdapter<>(
|
||||
getActivity(),
|
||||
android.R.layout.simple_list_item_activated_1,
|
||||
android.R.id.text1,
|
||||
bmc.messages().findMessages(((MessageListActivity) getActivity()).getSelectedLabel())));
|
||||
bmc.messages().findMessages(label)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repositories;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||
import ch.dissem.bitmessage.entity.payload.Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
|
||||
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
|
||||
import ch.dissem.bitmessage.factory.Factory;
|
||||
import ch.dissem.bitmessage.ports.AddressRepository;
|
||||
import ch.dissem.bitmessage.utils.Encode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AddressRepository} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidAddressRepository implements AddressRepository {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidAddressRepository.class);
|
||||
|
||||
private static final String TABLE_NAME = "Address";
|
||||
private static final String COLUMN_ADDRESS = "address";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
private static final String COLUMN_ALIAS = "alias";
|
||||
private static final String COLUMN_PUBLIC_KEY = "public_key";
|
||||
private static final String COLUMN_PRIVATE_KEY = "private_key";
|
||||
private static final String COLUMN_SUBSCRIBED = "subscribed";
|
||||
|
||||
private final SqlHelper sql;
|
||||
|
||||
public AndroidAddressRepository(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress findContact(byte[] ripeOrTag) {
|
||||
for (BitmessageAddress address : find("public_key is null")) {
|
||||
if (address.getVersion() > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress findIdentity(byte[] ripeOrTag) {
|
||||
for (BitmessageAddress address : find("private_key is not null")) {
|
||||
if (address.getVersion() > 3) {
|
||||
if (Arrays.equals(ripeOrTag, address.getTag())) return address;
|
||||
} else {
|
||||
if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getIdentities() {
|
||||
return find("private_key IS NOT NULL");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getSubscriptions() {
|
||||
return find("subscribed = '1'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getSubscriptions(long broadcastVersion) {
|
||||
if (broadcastVersion > 4) {
|
||||
return find("subscribed = '1' AND version > 3");
|
||||
} else {
|
||||
return find("subscribed = '1' AND version <= 3");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitmessageAddress> getContacts() {
|
||||
return find("private_key IS NULL");
|
||||
}
|
||||
|
||||
private List<BitmessageAddress> find(String where) {
|
||||
List<BitmessageAddress> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_ADDRESS,
|
||||
COLUMN_ALIAS,
|
||||
COLUMN_PUBLIC_KEY,
|
||||
COLUMN_PRIVATE_KEY,
|
||||
COLUMN_SUBSCRIBED
|
||||
};
|
||||
|
||||
try {
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null, null
|
||||
);
|
||||
c.moveToFirst();
|
||||
while (!c.isAfterLast()) {
|
||||
BitmessageAddress address;
|
||||
|
||||
byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY));
|
||||
if (privateKeyBytes != null) {
|
||||
PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream(privateKeyBytes));
|
||||
address = new BitmessageAddress(privateKey);
|
||||
} else {
|
||||
address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS)));
|
||||
byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY));
|
||||
if (publicKeyBytes != null) {
|
||||
Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(),
|
||||
new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, false);
|
||||
if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) {
|
||||
pubkey = new V4Pubkey((V3Pubkey) pubkey);
|
||||
}
|
||||
address.setPubkey(pubkey);
|
||||
}
|
||||
}
|
||||
address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS)));
|
||||
address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1);
|
||||
|
||||
result.add(address);
|
||||
c.moveToNext();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(BitmessageAddress address) {
|
||||
try {
|
||||
if (exists(address)) {
|
||||
update(address);
|
||||
} else {
|
||||
insert(address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean exists(BitmessageAddress address) {
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM Address WHERE address='" + address.getAddress() + "'", null);
|
||||
cursor.moveToFirst();
|
||||
return cursor.getInt(0) > 0;
|
||||
}
|
||||
|
||||
private void update(BitmessageAddress address) throws IOException {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ALIAS, address.getAlias());
|
||||
values.put(COLUMN_PUBLIC_KEY, Encode.bytes(address.getPubkey()));
|
||||
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
|
||||
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
|
||||
|
||||
int update = db.update(TABLE_NAME, values, "address = '" + address.getAddress() + "'", null);
|
||||
if (update < 0) {
|
||||
LOG.error("Could not update address " + address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(BitmessageAddress address) throws IOException {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_ADDRESS, address.getAddress());
|
||||
values.put(COLUMN_VERSION, address.getVersion());
|
||||
values.put(COLUMN_ALIAS, address.getAlias());
|
||||
if (address.getPubkey() != null) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
address.getPubkey().writeUnencrypted(out);
|
||||
values.put(COLUMN_PUBLIC_KEY, out.toByteArray());
|
||||
} else {
|
||||
values.put(COLUMN_PUBLIC_KEY, (byte[]) null);
|
||||
}
|
||||
values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey()));
|
||||
values.put(COLUMN_SUBSCRIBED, address.isSubscribed());
|
||||
|
||||
long insert = db.insert(TABLE_NAME, null, values);
|
||||
if (insert < 0) {
|
||||
LOG.error("Could not insert address " + address);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(BitmessageAddress address) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "address = " + address.getAddress(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmessageAddress getAddress(String address) {
|
||||
List<BitmessageAddress> result = find("address = '" + address + "'");
|
||||
if (result.size() > 0) return result.get(0);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,7 +1,185 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repositories;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage;
|
||||
import ch.dissem.bitmessage.entity.payload.ObjectType;
|
||||
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
|
||||
import ch.dissem.bitmessage.factory.Factory;
|
||||
import ch.dissem.bitmessage.ports.Inventory;
|
||||
import ch.dissem.bitmessage.utils.Encode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static ch.dissem.apps.abit.repositories.SqlHelper.join;
|
||||
import static ch.dissem.bitmessage.utils.UnixTime.now;
|
||||
|
||||
/**
|
||||
* Created by chris on 14.08.15.
|
||||
* {@link Inventory} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidInventory {
|
||||
public class AndroidInventory implements Inventory {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class);
|
||||
|
||||
private static final String TABLE_NAME = "Inventory";
|
||||
private static final String COLUMN_HASH = "hash";
|
||||
private static final String COLUMN_STREAM = "stream";
|
||||
private static final String COLUMN_EXPIRES = "expires";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_TYPE = "type";
|
||||
private static final String COLUMN_VERSION = "version";
|
||||
|
||||
private final SqlHelper sql;
|
||||
|
||||
public AndroidInventory(SqlHelper sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InventoryVector> getInventory(long... streams) {
|
||||
return getInventory(false, streams);
|
||||
}
|
||||
|
||||
public List<InventoryVector> getInventory(boolean includeExpired, long... streams) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_HASH
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
(includeExpired ? "" : "expires > " + now() + " AND ") + "stream IN (" + join(streams) + ")",
|
||||
null, null, null, null
|
||||
);
|
||||
c.moveToFirst();
|
||||
List<InventoryVector> result = new LinkedList<>();
|
||||
while (!c.isAfterLast()) {
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH));
|
||||
result.add(new InventoryVector(blob));
|
||||
c.moveToNext();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) {
|
||||
offer.removeAll(getInventory(true, streams));
|
||||
return offer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectMessage getObject(InventoryVector vector) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_VERSION,
|
||||
COLUMN_DATA
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
"hash = X'" + vector + "'",
|
||||
null, null, null, null
|
||||
);
|
||||
c.moveToFirst();
|
||||
if (c.isAfterLast()) {
|
||||
LOG.info("Object requested that we don't have. IV: " + vector);
|
||||
return null;
|
||||
}
|
||||
|
||||
int version = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) {
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_VERSION,
|
||||
COLUMN_DATA
|
||||
};
|
||||
StringBuilder where = new StringBuilder("1=1");
|
||||
if (stream > 0) {
|
||||
where.append(" AND stream = ").append(stream);
|
||||
}
|
||||
if (version > 0) {
|
||||
where.append(" AND version = ").append(version);
|
||||
}
|
||||
if (types.length > 0) {
|
||||
where.append(" AND type IN (").append(join(types)).append(")");
|
||||
}
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where.toString(),
|
||||
null, null, null, null
|
||||
);
|
||||
c.moveToFirst();
|
||||
List<ObjectMessage> result = new LinkedList<>();
|
||||
while (!c.isAfterLast()) {
|
||||
int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION));
|
||||
byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA));
|
||||
result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), blob.length));
|
||||
c.moveToNext();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeObject(ObjectMessage object) {
|
||||
InventoryVector iv = object.getInventoryVector();
|
||||
LOG.trace("Storing object " + iv);
|
||||
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
// Create a new map of values, where column names are the keys
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_HASH, object.getInventoryVector().getHash());
|
||||
values.put(COLUMN_STREAM, object.getStream());
|
||||
values.put(COLUMN_EXPIRES, object.getExpiresTime());
|
||||
values.put(COLUMN_DATA, Encode.bytes(object));
|
||||
values.put(COLUMN_TYPE, object.getType());
|
||||
values.put(COLUMN_VERSION, object.getVersion());
|
||||
|
||||
long insert = db.insert(TABLE_NAME, null, values);
|
||||
if (insert < 0) {
|
||||
LOG.trace("Error while inserting object. Most probably it was requested twice.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "expires < " + (now() - 300), null);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repositories;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import ch.dissem.apps.abit.R;
|
||||
import ch.dissem.bitmessage.InternalContext;
|
||||
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 ch.dissem.bitmessage.utils.Encode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static ch.dissem.apps.abit.repositories.SqlHelper.join;
|
||||
|
||||
/**
|
||||
* {@link MessageRepository} implementation using the Android SQL API.
|
||||
*/
|
||||
public class AndroidMessageRepository implements MessageRepository, InternalContext.ContextHolder {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class);
|
||||
|
||||
private static final String TABLE_NAME = "Message";
|
||||
private static final String COLUMN_ID = "id";
|
||||
private static final String COLUMN_IV = "iv";
|
||||
private static final String COLUMN_TYPE = "type";
|
||||
private static final String COLUMN_SENDER = "sender";
|
||||
private static final String COLUMN_RECIPIENT = "recipient";
|
||||
private static final String COLUMN_DATA = "data";
|
||||
private static final String COLUMN_SENT = "sent";
|
||||
private static final String COLUMN_RECEIVED = "received";
|
||||
private static final String COLUMN_STATUS = "status";
|
||||
|
||||
private static final String JOIN_TABLE_NAME = "Message_Label";
|
||||
private static final String JT_COLUMN_MESSAGE = "message_id";
|
||||
private static final String JT_COLUMN_LABEL = "label_id";
|
||||
|
||||
private static final String LBL_TABLE_NAME = "Label";
|
||||
private static final String LBL_COLUMN_ID = "id";
|
||||
private static final String LBL_COLUMN_LABEL = "label";
|
||||
private static final String LBL_COLUMN_TYPE = "type";
|
||||
private static final String LBL_COLUMN_COLOR = "color";
|
||||
private static final String LBL_COLUMN_ORDER = "ord";
|
||||
private final SqlHelper sql;
|
||||
private final Context ctx;
|
||||
private InternalContext bmc;
|
||||
|
||||
public AndroidMessageRepository(SqlHelper sql, Context ctx) {
|
||||
this.sql = sql;
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContext(InternalContext context) {
|
||||
bmc = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Label> getLabels() {
|
||||
return findLabels(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Label> getLabels(Label.Type... types) {
|
||||
return findLabels("type IN (" + join(types) + ")");
|
||||
}
|
||||
|
||||
public List<Label> findLabels(String where) {
|
||||
List<Label> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
LBL_COLUMN_ID,
|
||||
LBL_COLUMN_LABEL,
|
||||
LBL_COLUMN_TYPE,
|
||||
LBL_COLUMN_COLOR
|
||||
};
|
||||
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
Cursor c = db.query(
|
||||
LBL_TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null,
|
||||
LBL_COLUMN_ORDER
|
||||
);
|
||||
c.moveToFirst();
|
||||
while (!c.isAfterLast()) {
|
||||
result.add(getLabel(c));
|
||||
c.moveToNext();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
switch (type) {
|
||||
case INBOX:
|
||||
text = ctx.getString(R.string.inbox);
|
||||
break;
|
||||
case DRAFT:
|
||||
text = ctx.getString(R.string.draft);
|
||||
break;
|
||||
case SENT:
|
||||
text = ctx.getString(R.string.sent);
|
||||
break;
|
||||
case UNREAD:
|
||||
text = ctx.getString(R.string.unread);
|
||||
break;
|
||||
case TRASH:
|
||||
text = ctx.getString(R.string.trash);
|
||||
break;
|
||||
case BROADCAST:
|
||||
text = ctx.getString(R.string.broadcast);
|
||||
break;
|
||||
default:
|
||||
text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL));
|
||||
}
|
||||
Label label = new Label(
|
||||
text,
|
||||
type,
|
||||
c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR)));
|
||||
label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID)));
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plaintext> findMessages(Label label) {
|
||||
if (label != null) {
|
||||
return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")");
|
||||
} else {
|
||||
return find("id NOT IN (SELECT message_id FROM Message_Label)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) {
|
||||
return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Plaintext> findMessages(Plaintext.Status status) {
|
||||
return find("status='" + status.name() + "'");
|
||||
}
|
||||
|
||||
private List<Plaintext> find(String where) {
|
||||
List<Plaintext> result = new LinkedList<>();
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
String[] projection = {
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS
|
||||
};
|
||||
|
||||
try {
|
||||
SQLiteDatabase db = sql.getReadableDatabase();
|
||||
Cursor c = db.query(
|
||||
TABLE_NAME, projection,
|
||||
where,
|
||||
null, null, null, null
|
||||
);
|
||||
c.moveToFirst();
|
||||
while (!c.isAfterLast()) {
|
||||
byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV));
|
||||
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));
|
||||
long id = c.getLong(c.getColumnIndex(COLUMN_ID));
|
||||
builder.id(id);
|
||||
builder.IV(new InventoryVector(iv));
|
||||
builder.from(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_SENDER))));
|
||||
builder.to(bmc.getAddressRepo().getAddress(c.getString(c.getColumnIndex(COLUMN_RECIPIENT))));
|
||||
builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT)));
|
||||
builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED)));
|
||||
builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS))));
|
||||
builder.labels(findLabels(id));
|
||||
result.add(builder.build());
|
||||
c.moveToNext();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Collection<Label> findLabels(long id) {
|
||||
return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Plaintext message) {
|
||||
try {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
|
||||
// save from address if necessary
|
||||
if (message.getId() == null) {
|
||||
BitmessageAddress savedAddress = bmc.getAddressRepo().getAddress(message.getFrom().getAddress());
|
||||
if (savedAddress == null || savedAddress.getPrivateKey() == null) {
|
||||
if (savedAddress != null && savedAddress.getAlias() != null) {
|
||||
message.getFrom().setAlias(savedAddress.getAlias());
|
||||
}
|
||||
bmc.getAddressRepo().save(message.getFrom());
|
||||
}
|
||||
}
|
||||
|
||||
// save message
|
||||
if (message.getId() == null) {
|
||||
insert(db, message);
|
||||
} else {
|
||||
update(db, message);
|
||||
}
|
||||
|
||||
// remove existing labels
|
||||
db.delete(JOIN_TABLE_NAME, "message_id=" + message.getId(), null);
|
||||
|
||||
// save labels
|
||||
ContentValues values = new ContentValues();
|
||||
for (Label label : message.getLabels()) {
|
||||
values.put(JT_COLUMN_LABEL, (Long) label.getId());
|
||||
values.put(JT_COLUMN_MESSAGE, (Long) message.getId());
|
||||
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
|
||||
}
|
||||
db.endTransaction();
|
||||
} catch (IOException e) {
|
||||
LOG.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(SQLiteDatabase db, Plaintext message) throws IOException {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash());
|
||||
values.put(COLUMN_TYPE, message.getType().name());
|
||||
values.put(COLUMN_SENDER, message.getFrom().getAddress());
|
||||
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
|
||||
values.put(COLUMN_DATA, Encode.bytes(message));
|
||||
values.put(COLUMN_SENT, message.getSent());
|
||||
values.put(COLUMN_RECEIVED, message.getReceived());
|
||||
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
|
||||
long id = db.insertOrThrow(TABLE_NAME, null, values);
|
||||
message.setId(id);
|
||||
}
|
||||
|
||||
private void update(SQLiteDatabase db, Plaintext message) throws IOException {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash());
|
||||
values.put(COLUMN_TYPE, message.getType().name());
|
||||
values.put(COLUMN_SENDER, message.getFrom().getAddress());
|
||||
values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress());
|
||||
values.put(COLUMN_DATA, Encode.bytes(message));
|
||||
values.put(COLUMN_SENT, message.getSent());
|
||||
values.put(COLUMN_RECEIVED, message.getReceived());
|
||||
values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name());
|
||||
db.update(TABLE_NAME, values, "id = " + message.getId(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Plaintext message) {
|
||||
SQLiteDatabase db = sql.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, "id = " + message.getId(), null);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.repositories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import ch.dissem.apps.abit.utils.Assets;
|
||||
|
||||
/**
|
||||
* Handles database migration and provides access.
|
||||
*/
|
||||
public class SqlHelper extends SQLiteOpenHelper {
|
||||
// If you change the database schema, you must increment the database version.
|
||||
public static final int DATABASE_VERSION = 1;
|
||||
public static final String DATABASE_NAME = "jabit.db";
|
||||
|
||||
protected final Context ctx;
|
||||
|
||||
public SqlHelper(Context ctx) {
|
||||
super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
setWriteAheadLoggingEnabled(true);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
switch (oldVersion) {
|
||||
case 0:
|
||||
executeMigration(db, "V1.0__Create_table_inventory");
|
||||
executeMigration(db, "V1.1__Create_table_address");
|
||||
executeMigration(db, "V1.2__Create_table_message");
|
||||
default:
|
||||
// Nothing to do. Let's assume we won't upgrade from a version that's newer than DATABASE_VERSION.
|
||||
}
|
||||
}
|
||||
|
||||
protected void executeMigration(SQLiteDatabase db, String name) {
|
||||
for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) {
|
||||
db.execSQL(statement);
|
||||
}
|
||||
}
|
||||
|
||||
public static StringBuilder join(long... numbers) {
|
||||
StringBuilder streamList = new StringBuilder();
|
||||
for (int i = 0; i < numbers.length; i++) {
|
||||
if (i > 0) streamList.append(", ");
|
||||
streamList.append(numbers[i]);
|
||||
}
|
||||
return streamList;
|
||||
}
|
||||
|
||||
public static StringBuilder join(Enum<?>... types) {
|
||||
StringBuilder streamList = new StringBuilder();
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
if (i > 0) streamList.append(", ");
|
||||
streamList.append('\'').append(types[i].name()).append('\'');
|
||||
}
|
||||
return streamList;
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package ch.dissem.apps.abit.service;
|
||||
|
||||
import android.content.Context;
|
||||
import ch.dissem.apps.abit.SQLiteConfig;
|
||||
import ch.dissem.apps.abit.repositories.AndroidAddressRepository;
|
||||
import ch.dissem.apps.abit.repositories.AndroidInventory;
|
||||
import ch.dissem.apps.abit.repositories.AndroidMessageRepository;
|
||||
import ch.dissem.apps.abit.repositories.SqlHelper;
|
||||
import ch.dissem.bitmessage.BitmessageContext;
|
||||
import ch.dissem.bitmessage.networking.DefaultNetworkHandler;
|
||||
import ch.dissem.bitmessage.repository.*;
|
||||
import ch.dissem.bitmessage.ports.MemoryNodeRegistry;
|
||||
import ch.dissem.bitmessage.security.sc.SpongySecurity;
|
||||
|
||||
/**
|
||||
@ -17,13 +20,14 @@ public class Singleton {
|
||||
if (bitmessageContext == null) {
|
||||
synchronized (Singleton.class) {
|
||||
if (bitmessageContext == null) {
|
||||
JdbcConfig config = new SQLiteConfig(ctx);
|
||||
ctx = ctx.getApplicationContext();
|
||||
SqlHelper sqlHelper = new SqlHelper(ctx);
|
||||
bitmessageContext = new BitmessageContext.Builder()
|
||||
.security(new SpongySecurity())
|
||||
.nodeRegistry(new MemoryNodeRegistry())
|
||||
.inventory(new JdbcInventory(config))
|
||||
.addressRepo(new JdbcAddressRepository(config))
|
||||
.messageRepo(new JdbcMessageRepository(config))
|
||||
.inventory(new AndroidInventory(sqlHelper))
|
||||
.addressRepo(new AndroidAddressRepository(sqlHelper))
|
||||
.messageRepo(new AndroidMessageRepository(sqlHelper, ctx))
|
||||
.networkHandler(new DefaultNetworkHandler())
|
||||
.build();
|
||||
}
|
||||
|
56
app/src/main/java/ch/dissem/apps/abit/utils/Assets.java
Normal file
56
app/src/main/java/ch/dissem/apps/abit/utils/Assets.java
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2015 Christian Basler
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ch.dissem.apps.abit.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Helper class to work with Assets.
|
||||
*/
|
||||
public class Assets {
|
||||
public static String readToString(Context ctx, String name) {
|
||||
try {
|
||||
InputStream in = ctx.getAssets().open(name);
|
||||
return new Scanner(in, "UTF-8").useDelimiter("\\A").next();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> readSqlStatements(Context ctx, String name) {
|
||||
try {
|
||||
InputStream in = ctx.getAssets().open(name);
|
||||
Scanner scanner = new Scanner(in, "UTF-8").useDelimiter(";");
|
||||
List<String> result = new LinkedList<>();
|
||||
while (scanner.hasNext()) {
|
||||
String statement = scanner.next().trim();
|
||||
if (!"".equals(statement)) {
|
||||
result.add(statement);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
android:elevation="4dp" />
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
android:elevation="4dp" />
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
app:layout_scrollFlags="scroll|enterAlways"/>
|
||||
|
||||
|
25
app/src/main/res/values-de/labels.xml
Normal file
25
app/src/main/res/values-de/labels.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2015 Christian Basler
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="inbox">Posteingang</string>
|
||||
<string name="draft">Entwürfe</string>
|
||||
<string name="sent">Gesendet</string>
|
||||
<string name="unread">Ungelesen</string>
|
||||
<string name="trash">Papierkorb</string>
|
||||
<string name="broadcast">Broadcasts</string>
|
||||
</resources>
|
25
app/src/main/res/values/labels.xml
Normal file
25
app/src/main/res/values/labels.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2015 Christian Basler
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="inbox">Inbox</string>
|
||||
<string name="draft">Drafts</string>
|
||||
<string name="sent">Sent</string>
|
||||
<string name="unread">Unread</string>
|
||||
<string name="trash">Trash</string>
|
||||
<string name="broadcast">Broadcasts</string>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user