diff --git a/src/conversations/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/conversations/java/eu/siacs/conversations/services/QuickConversationsService.java index 8c1e9fe71..7632206fe 100644 --- a/src/conversations/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/conversations/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -15,4 +15,14 @@ public class QuickConversationsService extends AbstractQuickConversationsService public void signalAccountStateChange() { } + + @Override + public boolean isSynchronizing() { + return false; + } + + @Override + public void considerSync(boolean force) { + + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java index 709178f1c..b7550c47c 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java @@ -21,4 +21,8 @@ public abstract class AbstractQuickConversationsService { } public abstract void signalAccountStateChange(); + + public abstract boolean isSynchronizing(); + + public abstract void considerSync(boolean force); } diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index f084a3a11..02ab3f4cd 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/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; diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c0ddb5c02..9269fd7fa 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/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 accounts = mXmppConnectionService.getAccounts(); int enabled = 0; diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 598bfc3c6..98c7a582f 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/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 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; } diff --git a/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java b/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java new file mode 100644 index 000000000..5f04647c9 --- /dev/null +++ b/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}. + * + *

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; + } + } + + } + +} diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index f931f0aef..f777d768b 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/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 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 contacts) { + private boolean considerSync(Account account, final Map 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 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(); });