Added account to local contacts

This commit is contained in:
M66B 2019-03-17 08:26:17 +00:00
parent d63536b3fc
commit 5d98923ffc
10 changed files with 1749 additions and 73 deletions

File diff suppressed because it is too large Load Diff

View File

@ -509,14 +509,15 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
} }
jaccount.put("folders", jfolders); jaccount.put("folders", jfolders);
// Contacts
JSONArray jcontacts = new JSONArray();
for (EntityContact contact : db.contact().getContacts(account.id))
jcontacts.put(contact.toJSON());
jaccount.put("contacts", jcontacts);
jaccounts.put(jaccount); jaccounts.put(jaccount);
} }
// Contacts
JSONArray jcontacts = new JSONArray();
for (EntityContact contact : db.contact().getContacts())
jcontacts.put(contact.toJSON());
// Answers // Answers
JSONArray janswers = new JSONArray(); JSONArray janswers = new JSONArray();
for (EntityAnswer answer : db.answer().getAnswers()) for (EntityAnswer answer : db.answer().getAnswers())
@ -535,7 +536,6 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
JSONObject jexport = new JSONObject(); JSONObject jexport = new JSONObject();
jexport.put("accounts", jaccounts); jexport.put("accounts", jaccounts);
jexport.put("contacts", jcontacts);
jexport.put("answers", janswers); jexport.put("answers", janswers);
jexport.put("settings", jsettings); jexport.put("settings", jsettings);
@ -700,21 +700,22 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On
Log.i("Imported folder=" + folder.name); Log.i("Imported folder=" + folder.name);
} }
// Contacts
JSONArray jcontacts = jaccount.getJSONArray("contacts");
for (int c = 0; c < jcontacts.length(); c++) {
JSONObject jcontact = (JSONObject) jcontacts.get(c);
EntityContact contact = EntityContact.fromJSON(jcontact);
contact.account = account.id;
if (db.contact().getContact(contact.account, contact.type, contact.email) == null) {
contact.id = db.contact().insertContact(contact);
Log.i("Imported contact=" + contact);
}
}
// Update swipe left/right // Update swipe left/right
db.account().updateAccount(account); db.account().updateAccount(account);
} }
// Contacts
JSONArray jcontacts = jimport.getJSONArray("contacts");
for (int c = 0; c < jcontacts.length(); c++) {
JSONObject jcontact = (JSONObject) jcontacts.get(c);
EntityContact contact = EntityContact.fromJSON(jcontact);
if (db.contact().getContact(contact.type, contact.email) == null) {
contact.id = db.contact().insertContact(contact);
Log.i("Imported contact=" + contact);
}
}
// Answers // Answers
JSONArray janswers = jimport.getJSONArray("answers"); JSONArray janswers = jimport.getJSONArray("answers");
for (int a = 0; a < janswers.length(); a++) { for (int a = 0; a < janswers.length(); a++) {

View File

@ -51,8 +51,8 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
private int textColorSecondary; private int textColorSecondary;
private String search = null; private String search = null;
private List<EntityContact> all = new ArrayList<>(); private List<TupleContactEx> all = new ArrayList<>();
private List<EntityContact> selected = new ArrayList<>(); private List<TupleContactEx> selected = new ArrayList<>();
private static NumberFormat nf = NumberFormat.getNumberInstance(); private static NumberFormat nf = NumberFormat.getNumberInstance();
@ -89,7 +89,7 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
view.setOnLongClickListener(null); view.setOnLongClickListener(null);
} }
private void bindTo(EntityContact contact) { private void bindTo(TupleContactEx contact) {
view.setAlpha(contact.state == EntityContact.STATE_IGNORE ? Helper.LOW_LIGHT : 1.0f); view.setAlpha(contact.state == EntityContact.STATE_IGNORE ? Helper.LOW_LIGHT : 1.0f);
if (contact.type == EntityContact.TYPE_FROM) if (contact.type == EntityContact.TYPE_FROM)
@ -105,7 +105,7 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
ivAvatar.setImageURI(Uri.parse(contact.avatar + "/photo")); ivAvatar.setImageURI(Uri.parse(contact.avatar + "/photo"));
tvName.setText(contact.name == null ? contact.email : contact.name); tvName.setText(contact.name == null ? contact.email : contact.name);
tvEmail.setText(contact.email); tvEmail.setText(contact.accountName + "/" + contact.email);
tvTimes.setText(nf.format(contact.times_contacted)); tvTimes.setText(nf.format(contact.times_contacted));
tvLast.setText(contact.last_contacted == null ? null tvLast.setText(contact.last_contacted == null ? null
: DateUtils.getRelativeTimeSpanString(context, contact.last_contacted)); : DateUtils.getRelativeTimeSpanString(context, contact.last_contacted));
@ -124,7 +124,7 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
if (pos == RecyclerView.NO_POSITION) if (pos == RecyclerView.NO_POSITION)
return; return;
EntityContact contact = selected.get(pos); TupleContactEx contact = selected.get(pos);
if (contact.state == EntityContact.STATE_DEFAULT) if (contact.state == EntityContact.STATE_DEFAULT)
contact.state = EntityContact.STATE_FAVORITE; contact.state = EntityContact.STATE_FAVORITE;
else else
@ -166,7 +166,7 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
if (pos == RecyclerView.NO_POSITION) if (pos == RecyclerView.NO_POSITION)
return false; return false;
EntityContact contact = selected.get(pos); TupleContactEx contact = selected.get(pos);
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong("id", contact.id); args.putLong("id", contact.id);
@ -207,18 +207,18 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
setHasStableIds(true); setHasStableIds(true);
} }
public void set(@NonNull List<EntityContact> contacts) { public void set(@NonNull List<TupleContactEx> contacts) {
Log.i("Set contacts=" + contacts.size()); Log.i("Set contacts=" + contacts.size());
all = contacts; all = contacts;
List<EntityContact> items; List<TupleContactEx> items;
if (TextUtils.isEmpty(search)) if (TextUtils.isEmpty(search))
items = all; items = all;
else { else {
items = new ArrayList<>(); items = new ArrayList<>();
String query = search.toLowerCase().trim(); String query = search.toLowerCase().trim();
for (EntityContact contact : contacts) for (TupleContactEx contact : contacts)
if (contact.email.toLowerCase().contains(query) || if (contact.email.toLowerCase().contains(query) ||
(contact.name != null && contact.name.toLowerCase().contains(query))) (contact.name != null && contact.name.toLowerCase().contains(query)))
items.add(contact); items.add(contact);
@ -258,10 +258,10 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
} }
private class DiffCallback extends DiffUtil.Callback { private class DiffCallback extends DiffUtil.Callback {
private List<EntityContact> prev = new ArrayList<>(); private List<TupleContactEx> prev = new ArrayList<>();
private List<EntityContact> next = new ArrayList<>(); private List<TupleContactEx> next = new ArrayList<>();
DiffCallback(List<EntityContact> prev, List<EntityContact> next) { DiffCallback(List<TupleContactEx> prev, List<TupleContactEx> next) {
this.prev.addAll(prev); this.prev.addAll(prev);
this.next.addAll(next); this.next.addAll(next);
} }
@ -278,15 +278,15 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
@Override @Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
EntityContact c1 = prev.get(oldItemPosition); TupleContactEx c1 = prev.get(oldItemPosition);
EntityContact c2 = next.get(newItemPosition); TupleContactEx c2 = next.get(newItemPosition);
return c1.id.equals(c2.id); return c1.id.equals(c2.id);
} }
@Override @Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
EntityContact c1 = prev.get(oldItemPosition); TupleContactEx c1 = prev.get(oldItemPosition);
EntityContact c2 = next.get(newItemPosition); TupleContactEx c2 = next.get(newItemPosition);
return c1.equals(c2); return c1.equals(c2);
} }
} }
@ -310,7 +310,7 @@ public class AdapterContact extends RecyclerView.Adapter<AdapterContact.ViewHold
@Override @Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) { public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.unwire(); holder.unwire();
EntityContact contact = selected.get(position); TupleContactEx contact = selected.get(position);
holder.bindTo(contact); holder.bindTo(contact);
holder.wire(); holder.wire();
} }

View File

@ -1259,14 +1259,16 @@ class Core {
String email = ((InternetAddress) recipient).getAddress(); String email = ((InternetAddress) recipient).getAddress();
String name = ((InternetAddress) recipient).getPersonal(); String name = ((InternetAddress) recipient).getPersonal();
Uri avatar = ContactInfo.getLookupUri(context, new Address[]{recipient}); Uri avatar = ContactInfo.getLookupUri(context, new Address[]{recipient});
EntityContact contact = db.contact().getContact(type, email); EntityContact contact = db.contact().getContact(folder.account, type, email);
if (contact == null) { if (contact == null) {
contact = new EntityContact(); contact = new EntityContact();
contact.account = folder.account;
contact.type = type; contact.type = type;
contact.email = email; contact.email = email;
contact.name = name; contact.name = name;
contact.avatar = (avatar == null ? null : avatar.toString()); contact.avatar = (avatar == null ? null : avatar.toString());
contact.times_contacted = 1; contact.times_contacted = 1;
contact.first_contacted = message.received;
contact.last_contacted = message.received; contact.last_contacted = message.received;
contact.id = db.contact().insertContact(contact); contact.id = db.contact().insertContact(contact);
Log.i("Inserted contact=" + contact + " type=" + type); Log.i("Inserted contact=" + contact + " type=" + type);
@ -1274,6 +1276,7 @@ class Core {
contact.name = name; contact.name = name;
contact.avatar = (avatar == null ? null : avatar.toString()); contact.avatar = (avatar == null ? null : avatar.toString());
contact.times_contacted++; contact.times_contacted++;
contact.first_contacted = Math.min(contact.first_contacted, message.received);
contact.last_contacted = message.received; contact.last_contacted = message.received;
db.contact().updateContact(contact); db.contact().updateContact(contact);
Log.i("Updated contact=" + contact + " type=" + type); Log.i("Updated contact=" + contact + " type=" + type);

View File

@ -50,7 +50,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html // https://developer.android.com/topic/libraries/architecture/room.html
@Database( @Database(
version = 57, version = 58,
entities = { entities = {
EntityIdentity.class, EntityIdentity.class,
EntityAccount.class, EntityAccount.class,
@ -624,6 +624,30 @@ public abstract class DB extends RoomDatabase {
db.execSQL("CREATE INDEX `index_contact_favorite` ON `contact` (`favorite`)"); db.execSQL("CREATE INDEX `index_contact_favorite` ON `contact` (`favorite`)");
} }
}) })
.addMigrations(new Migration(57, 58) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("DROP TABLE `contact`");
db.execSQL("CREATE TABLE IF NOT EXISTS `contact`" +
" (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
", `account` INTEGER NOT NULL" +
", `type` INTEGER NOT NULL" +
", `email` TEXT NOT NULL" +
", `name` TEXT, `avatar` TEXT" +
", `times_contacted` INTEGER NOT NULL" +
", `first_contacted` INTEGER NOT NULL" +
", `last_contacted` INTEGER NOT NULL" +
", `state` INTEGER NOT NULL" +
", FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
db.execSQL("CREATE UNIQUE INDEX `index_contact_account_type_email` ON `contact` (`account`, `type`, `email`)");
db.execSQL("CREATE INDEX `index_contact_email` ON `contact` (`email`)");
db.execSQL("CREATE INDEX `index_contact_name` ON `contact` (`name`)");
db.execSQL("CREATE INDEX `index_contact_times_contacted` ON `contact` (`times_contacted`)");
db.execSQL("CREATE INDEX `index_contact_last_contacted` ON `contact` (`last_contacted`)");
db.execSQL("CREATE INDEX `index_contact_state` ON `contact` (`state`)");
}
})
.build(); .build();
} }

View File

@ -31,17 +31,20 @@ import androidx.room.Update;
@Dao @Dao
public interface DaoContact { public interface DaoContact {
@Query("SELECT * FROM contact")
List<EntityContact> getContacts();
@Query("SELECT * FROM contact" + @Query("SELECT * FROM contact" +
" WHERE account = :account")
List<EntityContact> getContacts(long account);
@Query("SELECT contact.*, account.name AS accountName" +
" FROM contact" +
" JOIN account ON account.id = contact.account" +
" ORDER BY times_contacted DESC, last_contacted DESC") " ORDER BY times_contacted DESC, last_contacted DESC")
LiveData<List<EntityContact>> liveContacts(); LiveData<List<TupleContactEx>> liveContacts();
@Query("SELECT * FROM contact" + @Query("SELECT * FROM contact" +
" WHERE favorite <> " + EntityContact.STATE_IGNORE + " WHERE state <> " + EntityContact.STATE_IGNORE +
" ORDER BY" + " ORDER BY" +
" CASE WHEN favorite = " + EntityContact.STATE_FAVORITE + " THEN 0 ELSE 1 END" + " CASE WHEN state = " + EntityContact.STATE_FAVORITE + " THEN 0 ELSE 1 END" +
", times_contacted DESC" + ", times_contacted DESC" +
", last_contacted DESC" + ", last_contacted DESC" +
" LIMIT :count") " LIMIT :count")
@ -49,9 +52,10 @@ public interface DaoContact {
@Query("SELECT *" + @Query("SELECT *" +
" FROM contact" + " FROM contact" +
" WHERE email = :email" + " WHERE account = :account" +
" AND (:type IS NULL OR type = :type)") " AND type = :type" +
EntityContact getContact(Integer type, String email); " AND email = :email")
EntityContact getContact(long account, int type, String email);
@Query("SELECT id AS _id, name, email" + @Query("SELECT id AS _id, name, email" +
", CASE type" + ", CASE type" +
@ -60,9 +64,10 @@ public interface DaoContact {
" ELSE '?'" + " ELSE '?'" +
" END AS type" + " END AS type" +
" FROM contact" + " FROM contact" +
" WHERE name LIKE :name" + " WHERE (:account IS NULL OR account = :account)" +
" AND (:type IS NULL OR type = :type)") " AND (:type IS NULL OR type = :type)" +
Cursor searchContacts(Integer type, String name); " AND (email LIKE :query COLLATE NOCASE OR name LIKE :query COLLATE NOCASE)")
Cursor searchContacts(Long account, Integer type, String query);
@Insert @Insert
long insertContact(EntityContact contact); long insertContact(EntityContact contact);
@ -70,13 +75,13 @@ public interface DaoContact {
@Update @Update
int updateContact(EntityContact contact); int updateContact(EntityContact contact);
@Query("UPDATE contact SET favorite = :state WHERE id = :id") @Query("UPDATE contact SET state = :state WHERE id = :id")
int setContactState(long id, int state); int setContactState(long id, int state);
@Query("DELETE FROM contact" + @Query("DELETE FROM contact" +
" WHERE last_contacted IS NOT NULL" + " WHERE last_contacted IS NOT NULL" +
" AND last_contacted < :before" + " AND last_contacted < :before" +
" AND favorite <> " + EntityContact.STATE_FAVORITE) " AND state <> " + EntityContact.STATE_FAVORITE)
int deleteContacts(long before); int deleteContacts(long before);
@Query("DELETE FROM contact") @Query("DELETE FROM contact")

View File

@ -27,23 +27,27 @@ import java.util.Objects;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index; import androidx.room.Index;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import static androidx.room.ForeignKey.CASCADE;
// https://developer.android.com/training/data-storage/room/defining-data // https://developer.android.com/training/data-storage/room/defining-data
@Entity( @Entity(
tableName = EntityContact.TABLE_NAME, tableName = EntityContact.TABLE_NAME,
foreignKeys = { foreignKeys = {
@ForeignKey(childColumns = "account", entity = EntityAccount.class, parentColumns = "id", onDelete = CASCADE)
}, },
indices = { indices = {
@Index(value = {"email", "type"}, unique = true), @Index(value = {"account", "type", "email"}, unique = true),
@Index(value = {"name", "type"}), @Index(value = {"email"}),
@Index(value = {"name"}),
@Index(value = {"times_contacted"}), @Index(value = {"times_contacted"}),
@Index(value = {"last_contacted"}), @Index(value = {"last_contacted"}),
@Index(value = {"favorite"}) @Index(value = {"state"})
} }
) )
public class EntityContact implements Serializable { public class EntityContact implements Serializable {
@ -59,6 +63,8 @@ public class EntityContact implements Serializable {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
public Long id; public Long id;
@NonNull @NonNull
public Long account;
@NonNull
public int type; public int type;
@NonNull @NonNull
public String email; public String email;
@ -67,9 +73,11 @@ public class EntityContact implements Serializable {
@NonNull @NonNull
public Integer times_contacted; public Integer times_contacted;
@NonNull
public Long first_contacted;
@NonNull
public Long last_contacted; public Long last_contacted;
@NonNull @NonNull
@ColumnInfo(name = "favorite")
public Integer state = STATE_DEFAULT; public Integer state = STATE_DEFAULT;
public JSONObject toJSON() throws JSONException { public JSONObject toJSON() throws JSONException {
@ -80,6 +88,7 @@ public class EntityContact implements Serializable {
json.put("name", name); json.put("name", name);
json.put("avatar", avatar); json.put("avatar", avatar);
json.put("times_contacted", times_contacted); json.put("times_contacted", times_contacted);
json.put("first_contacted", first_contacted);
json.put("last_contacted", last_contacted); json.put("last_contacted", last_contacted);
json.put("state", state); json.put("state", state);
return json; return json;
@ -97,18 +106,10 @@ public class EntityContact implements Serializable {
if (json.has("avatar") && !json.isNull("avatar")) if (json.has("avatar") && !json.isNull("avatar"))
contact.avatar = json.getString("avatar"); contact.avatar = json.getString("avatar");
if (json.has("times_contacted")) contact.times_contacted = json.getInt("times_contacted");
contact.times_contacted = json.getInt("times_contacted"); contact.first_contacted = json.getLong("first_contacted");
else contact.last_contacted = json.getLong("last_contacted");
contact.times_contacted = 1; contact.state = json.getInt("state");
if (json.has("last_contacted") && !json.isNull("last_contacted"))
contact.last_contacted = json.getLong("last_contacted");
if (json.has("favorite"))
contact.state = (json.getBoolean("favorite") ? 1 : 0);
if (json.has("state"))
contact.state = json.getInt("state");
return contact; return contact;
} }
@ -117,14 +118,15 @@ public class EntityContact implements Serializable {
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (obj instanceof EntityContact) { if (obj instanceof EntityContact) {
EntityContact other = (EntityContact) obj; EntityContact other = (EntityContact) obj;
return (this.type == other.type && return (this.account == other.account &&
this.type == other.type &&
this.email.equals(other.email) && this.email.equals(other.email) &&
Objects.equals(this.name, other.name) && Objects.equals(this.name, other.name) &&
Objects.equals(this.avatar, other.avatar) && Objects.equals(this.avatar, other.avatar) &&
this.times_contacted.equals(other.times_contacted) && this.times_contacted.equals(other.times_contacted) &&
Objects.equals(this.last_contacted, other.last_contacted) && this.first_contacted.equals(first_contacted) &&
this.last_contacted.equals(last_contacted) &&
this.state.equals(other.state)); this.state.equals(other.state));
} else } else
return false; return false;
} }

View File

@ -473,7 +473,7 @@ public class FragmentCompose extends FragmentBase {
@Override @Override
public Cursor runQuery(CharSequence typed) { public Cursor runQuery(CharSequence typed) {
Log.i("Searching local contact=" + typed); Log.i("Searching local contact=" + typed);
return db.contact().searchContacts(null, "%" + typed + "%"); return db.contact().searchContacts(null, null, "%" + typed + "%");
} }
}); });

View File

@ -92,9 +92,9 @@ public class FragmentContacts extends FragmentBase {
} }
DB db = DB.getInstance(getContext()); DB db = DB.getInstance(getContext());
db.contact().liveContacts().observe(getViewLifecycleOwner(), new Observer<List<EntityContact>>() { db.contact().liveContacts().observe(getViewLifecycleOwner(), new Observer<List<TupleContactEx>>() {
@Override @Override
public void onChanged(List<EntityContact> contacts) { public void onChanged(List<TupleContactEx> contacts) {
if (contacts == null) if (contacts == null)
contacts = new ArrayList<>(); contacts = new ArrayList<>();

View File

@ -0,0 +1,15 @@
package eu.faircode.email;
public class TupleContactEx extends EntityContact {
public String accountName;
@Override
public boolean equals(Object obj) {
if (obj instanceof TupleContactEx) {
TupleContactEx other = (TupleContactEx) obj;
return (super.equals(obj) &&
accountName.equals(other.accountName));
} else
return false;
}
}