diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index a826f8b1..0eb2b3ad 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -83,6 +83,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateRosterUi(); + mXmppConnectionService.getShortcutService().refresh(); } public String avatarData(final IqPacket packet) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 005975eb..e6d0e290 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -49,6 +49,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.entities.ServiceDiscoveryResult; +import eu.siacs.conversations.services.ShortcutService; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -1423,4 +1424,21 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("delete from " + START_TIMES_TABLE); } } + + public List getFrequentContacts(int days) { + SQLiteDatabase db = this.getReadableDatabase(); + final String SQL = "select "+Conversation.TABLENAME+"."+Conversation.ACCOUNT+","+Conversation.TABLENAME+"."+Conversation.CONTACTJID+" from "+Conversation.TABLENAME+" join "+Message.TABLENAME+" on conversations.uuid=messages.conversationUuid where messages.status!=0 and carbon==0 and conversations.mode=0 and messages.timeSent>=? group by conversations.uuid order by count(body) desc limit 4;"; + String[] whereArgs = new String[]{String.valueOf(System.currentTimeMillis() - (Config.MILLISECONDS_IN_DAY * days))}; + Cursor cursor = db.rawQuery(SQL,whereArgs); + ArrayList contacts = new ArrayList<>(); + while(cursor.moveToNext()) { + try { + contacts.add(new ShortcutService.FrequentContact(cursor.getString(0), Jid.fromString(cursor.getString(1)))); + } catch (Exception e) { + Log.d(Config.LOGTAG,e.getMessage()); + } + } + cursor.close(); + return contacts; + } } diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 4b4d1ed3..c7f97cd1 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -3,9 +3,12 @@ package eu.siacs.conversations.services; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Typeface; import android.net.Uri; +import android.util.DisplayMetrics; import android.util.Log; import java.util.ArrayList; @@ -65,6 +68,24 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded { return avatar; } + public Bitmap getRoundedShortcut(final Contact contact) { + DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics(); + int size = Math.round(metrics.density * 48); + Bitmap bitmap = get(contact,size); + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + return output; + } + public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) { Contact c = user.getContact(); if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null || user.getAvatar() == null)) { diff --git a/src/main/java/eu/siacs/conversations/services/ShortcutService.java b/src/main/java/eu/siacs/conversations/services/ShortcutService.java new file mode 100644 index 00000000..924db67c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/ShortcutService.java @@ -0,0 +1,133 @@ +package eu.siacs.conversations.services; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.ui.StartConversationActivity; +import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class ShortcutService { + + private final XmppConnectionService xmppConnectionService; + private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(false); + + public ShortcutService(XmppConnectionService xmppConnectionService) { + this.xmppConnectionService = xmppConnectionService; + } + + public void refresh() { + refresh(false); + } + + public void refresh(final boolean forceUpdate) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + final Runnable r = new Runnable() { + @Override + public void run() { + refreshImpl(forceUpdate); + } + }; + replacingSerialSingleThreadExecutor.execute(r); + } + } + + @TargetApi(25) + public void report(Contact contact) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); + shortcutManager.reportShortcutUsed(getShortcutId(contact)); + } + } + + @TargetApi(25) + private void refreshImpl(boolean forceUpdate) { + List frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30); + HashMap accounts = new HashMap<>(); + for(Account account : xmppConnectionService.getAccounts()) { + accounts.put(account.getUuid(),account); + } + List contacts = new ArrayList<>(); + for(FrequentContact frequentContact : frequentContacts) { + Account account = accounts.get(frequentContact.account); + if (account != null) { + contacts.add(account.getRoster().getContact(frequentContact.contact)); + } + } + ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); + boolean needsUpdate = forceUpdate || contactsChanged(contacts,shortcutManager.getDynamicShortcuts()); + if (!needsUpdate) { + Log.d(Config.LOGTAG,"skipping shortcut update"); + return; + } + List newDynamicShortCuts = new ArrayList<>(); + for (Contact contact : contacts) { + ShortcutInfo shortcut = new ShortcutInfo.Builder(xmppConnectionService, getShortcutId(contact)) + .setShortLabel(contact.getDisplayName()) + .setIntent(getShortcutIntent(contact)) + .setIcon(Icon.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(contact))) + .build(); + newDynamicShortCuts.add(shortcut); + } + if (shortcutManager.setDynamicShortcuts(newDynamicShortCuts)) { + Log.d(Config.LOGTAG,"updated dynamic shortcuts"); + } else { + Log.d(Config.LOGTAG, "unable to update dynamic shortcuts"); + } + } + + private static boolean contactsChanged(List needles, List haystack) { + for(Contact needle : needles) { + if(!contactExists(needle,haystack)) { + return true; + } + } + return needles.size() != haystack.size(); + } + + @TargetApi(25) + private static boolean contactExists(Contact needle, List haystack) { + for(ShortcutInfo shortcutInfo : haystack) { + if (getShortcutId(needle).equals(shortcutInfo.getId()) && needle.getDisplayName().equals(shortcutInfo.getShortLabel())) { + return true; + } + } + return false; + } + + private static String getShortcutId(Contact contact) { + return contact.getAccount().getJid().toBareJid().toPreppedString()+"#"+contact.getJid().toBareJid().toPreppedString(); + } + + private Intent getShortcutIntent(Contact contact) { + Intent intent = new Intent(xmppConnectionService, StartConversationActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse("xmpp:"+contact.getJid().toBareJid().toString())); + intent.putExtra("account",contact.getAccount().getJid().toBareJid().toString()); + return intent; + } + + public static class FrequentContact { + private final String account; + private final Jid contact; + + public FrequentContact(String account, Jid contact) { + this.account = account; + this.contact = contact; + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ff5adfdc..f4e6f456 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -65,6 +65,7 @@ import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import de.duenndns.ssl.MemorizingTrustManager; @@ -182,8 +183,9 @@ public class XmppConnectionService extends Service { }; private FileBackend fileBackend = new FileBackend(this); private MemorizingTrustManager mMemorizingTrustManager; - private NotificationService mNotificationService = new NotificationService( - this); + private NotificationService mNotificationService = new NotificationService(this); + private ShortcutService mShortcutService = new ShortcutService(this); + private AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); private OnMessagePacketReceived mMessageParser = new MessageParser(this); private OnPresencePacketReceived mPresenceParser = new PresenceParser(this); private IqParser mIqParser = new IqParser(this); @@ -1553,6 +1555,7 @@ public class XmppConnectionService extends Service { } } Log.d(Config.LOGTAG, "finished merging phone contacts"); + mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false,true)); updateAccountUi(); } }); @@ -3616,10 +3619,11 @@ public class XmppConnectionService extends Service { return this.mMessageArchiveService; } - public List findContacts(Jid jid) { + public List findContacts(Jid jid, String accountJid) { ArrayList contacts = new ArrayList<>(); for (Account account : getAccounts()) { - if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (!account.isOptionSet(Account.OPTION_DISABLED) + && (accountJid == null || accountJid.equals(account.getJid().toBareJid().toString()))) { Contact contact = account.getRoster().getContactFromRoster(jid); if (contact != null) { contacts.add(contact); @@ -3964,6 +3968,10 @@ public class XmppConnectionService extends Service { return getPreferences().getBoolean(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, true); } + public ShortcutService getShortcutService() { + return mShortcutService; + } + public interface OnMamPreferencesFetched { void onPreferencesFetched(Element prefs); void onPreferencesFetchFailed(); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 35d6201a..aa7adff0 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -832,7 +832,9 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU case Intent.ACTION_VIEW: Uri uri = intent.getData(); if (uri != null) { - return new Invite(intent.getData(),false).invite(); + Invite invite = new Invite(intent.getData(),false); + invite.account = intent.getStringExtra("account"); + return invite.invite(); } else { return false; } @@ -871,7 +873,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU finish(); return true; } - List contacts = xmppConnectionService.findContacts(invite.getJid()); + List contacts = xmppConnectionService.findContacts(invite.getJid(),invite.account); if (invite.isMuc()) { Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid()); if (muc != null) { @@ -894,6 +896,9 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show(); } } + if (invite.account != null) { + xmppConnectionService.getShortcutService().report(contact); + } switchToConversation(contact, invite.getBody()); } return true; @@ -1183,6 +1188,8 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU super(uri,safeSource); } + public String account; + boolean invite() { if (getJid() != null) { return handleJid(this);