display spinning wheel + swipe to refresh for quicksy

Daniel Gultsch created

Change summary

src/conversations/java/eu/siacs/conversations/services/QuickConversationsService.java |  10 
src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java  |   4 
src/main/java/eu/siacs/conversations/services/AvatarService.java                      |  21 
src/main/java/eu/siacs/conversations/services/NotificationService.java                |   4 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java                |  42 
src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java          | 148 
src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java       |  21 
7 files changed, 221 insertions(+), 29 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/services/AvatarService.java 🔗

@@ -66,14 +66,14 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
 		if (avatar != null || cachedOnly) {
 			return avatar;
 		}
-		if (contact.getProfilePhoto() != null) {
-			avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
-		}
-		if (avatar == null && contact.getAvatarFilename() != null) {
+		if (contact.getAvatarFilename() != null) {
 			avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size);
 		}
+		if (avatar == null && contact.getProfilePhoto() != null) {
+			avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
+		}
 		if (avatar == null) {
-			avatar = get(contact.getDisplayName(), contact.getJid().asBareJid().toString(), size, cachedOnly);
+			avatar = get(contact.getDisplayName(), contact.getJid().asBareJid().toString(), size, false);
 		}
 		this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
 		return avatar;
@@ -147,10 +147,10 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
 		if (avatar == null) {
 			Contact contact = user.getContact();
 			if (contact != null) {
-				avatar = get(contact, size, cachedOnly);
+				avatar = get(contact, size, false);
 			} else {
 				String seed = user.getRealJid() != null ? user.getRealJid().asBareJid().toString() : null;
-				avatar = get(user.getName(), seed, size, cachedOnly);
+				avatar = get(user.getName(), seed, size, false);
 			}
 		}
 		this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
@@ -510,11 +510,10 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
 		Contact contact = user.getContact();
 		if (contact != null) {
 			Uri uri = null;
-			if (contact.getProfilePhoto() != null) {
+			if (contact.getAvatarFilename() != null) {
+				uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename());
+			} else if (contact.getProfilePhoto() != null) {
 				uri = Uri.parse(contact.getProfilePhoto());
-			} else if (contact.getAvatarFilename() != null) {
-				uri = mXmppConnectionService.getFileBackend().getAvatarUri(
-						contact.getAvatarFilename());
 			}
 			if (drawTile(canvas, uri, left, top, right, bottom)) {
 				return true;

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -849,9 +849,9 @@ public class NotificationService {
         return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
     }
 
-    public Notification createForegroundNotification() {
+    Notification createForegroundNotification() {
         final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
-        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service));
+        mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
         if (Compatibility.runsAndTargetsTwentySix(mXmppConnectionService) || Config.SHOW_CONNECTED_ACCOUNTS) {
             List<Account> accounts = mXmppConnectionService.getAccounts();
             int enabled = 0;

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -22,6 +22,7 @@ import android.support.v4.app.FragmentTransaction;
 import android.support.v4.app.ListFragment;
 import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
+import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.Toolbar;
@@ -72,13 +73,14 @@ import eu.siacs.conversations.ui.util.JidDialog;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
+import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import rocks.xmpp.addr.Jid;
 
-public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreateConferenceDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener {
+public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreateConferenceDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener {
 
 	public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
 
@@ -757,6 +759,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 			Log.d(Config.LOGTAG, "calling on backend connected on dialog");
 			((OnBackendConnected) fragment).onBackendConnected();
 		}
+		if (QuickConversationsService.isQuicksy()) {
+			setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
+		}
 	}
 
 	protected boolean processViewIntent(@NonNull Intent intent) {
@@ -877,16 +882,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 		mContactsAdapter.notifyDataSetChanged();
 	}
 
-	private static boolean isSingleAccountActive(final List<Account> accounts) {
-		int i = 0;
-		for(Account account : accounts) {
-			if (account.getStatus() != Account.State.DISABLED) {
-				++i;
-			}
-		}
-		return i == 1;
-	}
-
 	protected void filterConferences(String needle) {
 		this.conferences.clear();
 		for (Account account : xmppConnectionService.getAccounts()) {
@@ -924,6 +919,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 			filter(mSearchEditText.getText().toString());
 		}
 		configureHomeButton();
+		if (QuickConversationsService.isQuicksy()) {
+			setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
+		}
 	}
 
 	@Override
@@ -1006,7 +1004,23 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 		refreshUi();
 	}
 
-	public static class MyListFragment extends ListFragment {
+	@Override
+	public void onRefresh() {
+		Log.d(Config.LOGTAG,"user requested to refresh");
+		if (QuickConversationsService.isQuicksy() && xmppConnectionService != null) {
+			xmppConnectionService.getQuickConversationsService().considerSync(true);
+		}
+	}
+
+
+	private void setRefreshing(boolean refreshing) {
+		MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0);
+		if (fragment != null) {
+			fragment.setRefreshing(refreshing);
+		}
+	}
+
+	public static class MyListFragment extends SwipeRefreshListFragment {
 		private AdapterView.OnItemClickListener mOnItemClickListener;
 		private int mResContextMenu;
 
@@ -1164,10 +1178,12 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 					listFragment.setContextMenu(R.menu.conference_context);
 					listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p));
 				} else {
-
 					listFragment.setListAdapter(mContactsAdapter);
 					listFragment.setContextMenu(R.menu.contact_context);
 					listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p));
+					if (QuickConversationsService.isQuicksy()) {
+						listFragment.setOnRefreshListener(StartConversationActivity.this);
+					}
 				}
 				fragments[position] = listFragment;
 			}

src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java 🔗

@@ -0,0 +1,148 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * 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 eu.siacs.conversations.ui.widget;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.util.StyledAttributes;
+
+/**
+ * Subclass of {@link android.support.v4.app.ListFragment} which provides automatic support for
+ * providing the 'swipe-to-refresh' UX gesture by wrapping the the content view in a
+ * {@link android.support.v4.widget.SwipeRefreshLayout}.
+ */
+public class SwipeRefreshListFragment extends ListFragment {
+
+    private boolean enabled = false;
+    private boolean refreshing = false;
+
+    private SwipeRefreshLayout.OnRefreshListener onRefreshListener;
+
+    private SwipeRefreshLayout mSwipeRefreshLayout;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+
+        // Create the list fragment's content view by calling the super method
+        final View listFragmentView = super.onCreateView(inflater, container, savedInstanceState);
+
+        // Now create a SwipeRefreshLayout to wrap the fragment's content view
+        mSwipeRefreshLayout = new ListFragmentSwipeRefreshLayout(container.getContext());
+        mSwipeRefreshLayout.setEnabled(enabled);
+        mSwipeRefreshLayout.setRefreshing(refreshing);
+
+        final Context context = getActivity();
+        if (context != null) {
+            mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, R.attr.colorAccent));
+        }
+
+        if (onRefreshListener != null) {
+            mSwipeRefreshLayout.setOnRefreshListener(onRefreshListener);
+        }
+
+        // Add the list fragment's content view to the SwipeRefreshLayout, making sure that it fills
+        // the SwipeRefreshLayout
+        mSwipeRefreshLayout.addView(listFragmentView,
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+
+        // Make sure that the SwipeRefreshLayout will fill the fragment
+        mSwipeRefreshLayout.setLayoutParams(
+                new ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+
+        // Now return the SwipeRefreshLayout as this fragment's content view
+        return mSwipeRefreshLayout;
+    }
+
+    /**
+     * Set the {@link android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener} to listen for
+     * initiated refreshes.
+     *
+     * @see android.support.v4.widget.SwipeRefreshLayout#setOnRefreshListener(android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener)
+     */
+    public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener) {
+        onRefreshListener = listener;
+        enabled = true;
+        if (mSwipeRefreshLayout != null) {
+            mSwipeRefreshLayout.setEnabled(true);
+            mSwipeRefreshLayout.setOnRefreshListener(listener);
+        }
+    }
+
+    /**
+     * Set whether the {@link android.support.v4.widget.SwipeRefreshLayout} should be displaying
+     * that it is refreshing or not.
+     *
+     * @see android.support.v4.widget.SwipeRefreshLayout#setRefreshing(boolean)
+     */
+    public void setRefreshing(boolean refreshing) {
+        this.refreshing = refreshing;
+        if (mSwipeRefreshLayout != null) {
+            mSwipeRefreshLayout.setRefreshing(refreshing);
+        }
+    }
+
+
+    /**
+     * Sub-class of {@link android.support.v4.widget.SwipeRefreshLayout} for use in this
+     * {@link android.support.v4.app.ListFragment}. The reason that this is needed is because
+     * {@link android.support.v4.widget.SwipeRefreshLayout} only supports a single child, which it
+     * expects to be the one which triggers refreshes. In our case the layout's child is the content
+     * view returned from
+     * {@link android.support.v4.app.ListFragment#onCreateView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle)}
+     * which is a {@link android.view.ViewGroup}.
+     *
+     * <p>To enable 'swipe-to-refresh' support via the {@link android.widget.ListView} we need to
+     * override the default behavior and properly signal when a gesture is possible. This is done by
+     * overriding {@link #canChildScrollUp()}.
+     */
+    private class ListFragmentSwipeRefreshLayout extends SwipeRefreshLayout {
+
+        public ListFragmentSwipeRefreshLayout(Context context) {
+            super(context);
+        }
+
+        /**
+         * As mentioned above, we need to override this method to properly signal when a
+         * 'swipe-to-refresh' is possible.
+         *
+         * @return true if the {@link android.widget.ListView} is visible and can scroll up.
+         */
+        @Override
+        public boolean canChildScrollUp() {
+            final ListView listView = getListView();
+            if (listView.getVisibility() == View.VISIBLE) {
+                return listView.canScrollVertically(-1);
+            } else {
+                return false;
+            }
+        }
+
+    }
+
+}

src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java 🔗

@@ -27,6 +27,7 @@ import java.util.WeakHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.net.ssl.SSLHandshakeException;
 
@@ -68,6 +69,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
 
     private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
     private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
+    private final AtomicInteger mRunningSyncJobs = new AtomicInteger(0);
     private CountDownLatch awaitingAccountStateChange;
 
     private Attempt mLastSyncAttempt = Attempt.NULL;
@@ -282,12 +284,23 @@ public class QuickConversationsService extends AbstractQuickConversationsService
         return mVerificationRequestInProgress.get();
     }
 
+
+    @Override
+    public boolean isSynchronizing() {
+        return mRunningSyncJobs.get() > 0;
+    }
+
     @Override
     public void considerSync() {
+        considerSync(false);
+    }
+
+    @Override
+    public void considerSync(boolean forced) {
         Map<String, PhoneNumberContact> contacts = PhoneNumberContact.load(service);
         for (Account account : service.getAccounts()) {
             refresh(account, contacts.values());
-            if (!considerSync(account, contacts)) {
+            if (!considerSync(account, contacts, forced)) {
                 service.syncRoster(account);
             }
         }
@@ -313,13 +326,14 @@ public class QuickConversationsService extends AbstractQuickConversationsService
         }
     }
 
-    private boolean considerSync(Account account, final Map<String, PhoneNumberContact> contacts) {
+    private boolean considerSync(Account account, final Map<String, PhoneNumberContact> contacts, final boolean forced) {
         final int hash = contacts.keySet().hashCode();
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash);
-        if (!mLastSyncAttempt.retry(hash)) {
+        if (!mLastSyncAttempt.retry(hash) && !forced) {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt sync");
             return false;
         }
+        mRunningSyncJobs.incrementAndGet();
         final Jid syncServer = Jid.of(API_DOMAIN);
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending phone list to " + syncServer);
         List<Element> entries = new ArrayList<>();
@@ -366,6 +380,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
             } else {
                 Log.d(Config.LOGTAG,account.getJid().asBareJid()+": failed to sync contact list with api server");
             }
+            mRunningSyncJobs.decrementAndGet();
             service.syncRoster(account);
             service.updateRosterUi();
         });