add the 4 most frequently contacted contacts as app shortcuts

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/parser/IqParser.java                |   1 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    |  18 
src/main/java/eu/siacs/conversations/services/AvatarService.java         |  21 
src/main/java/eu/siacs/conversations/services/ShortcutService.java       | 133 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  16 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java   |  11 
6 files changed, 194 insertions(+), 6 deletions(-)

Detailed changes

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) {

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<ShortcutService.FrequentContact> 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<ShortcutService.FrequentContact> 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;
+	}
 }

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)) {

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<FrequentContact> frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30);
+        HashMap<String,Account> accounts = new HashMap<>();
+        for(Account account : xmppConnectionService.getAccounts()) {
+            accounts.put(account.getUuid(),account);
+        }
+        List<Contact> 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<ShortcutInfo> 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<Contact> needles, List<ShortcutInfo> 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<ShortcutInfo> 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;
+        }
+    }
+
+}

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<Contact> findContacts(Jid jid) {
+	public List<Contact> findContacts(Jid jid, String accountJid) {
 		ArrayList<Contact> 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();

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<Contact> contacts = xmppConnectionService.findContacts(invite.getJid());
+        List<Contact> 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);