diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java b/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java new file mode 100644 index 000000000..f971eb8cc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java @@ -0,0 +1,21 @@ +package fr.free.nrw.commons.concurrency; + +import android.support.annotation.NonNull; +import fr.free.nrw.commons.BuildConfig; + +public class BackgroundPoolExceptionHandler implements ExceptionHandler { + @Override + public void onException(@NonNull final Throwable t) { + //Crash for debug build + if (BuildConfig.DEBUG) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + throw new RuntimeException(t); + } + }); + thread.start(); + } + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.java new file mode 100644 index 000000000..8196b26e7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.java @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.concurrency; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; + + +class ExceptionAwareThreadPoolExecutor extends ScheduledThreadPoolExecutor { + + private final ExceptionHandler exceptionHandler; + + public ExceptionAwareThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, + ExceptionHandler exceptionHandler) { + super(corePoolSize, threadFactory); + this.exceptionHandler = exceptionHandler; + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null && r instanceof Future) { + try { + Future future = (Future) r; + if (future.isDone()) future.get(); + } catch (CancellationException | InterruptedException e) { + //ignore + } catch (ExecutionException e) { + t = e.getCause() != null ? e.getCause() : e; + } catch (Exception e) { + t = e; + } + } + + if (t != null) { + exceptionHandler.onException(t); + } + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.java new file mode 100644 index 000000000..cc5a4c008 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.java @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.concurrency; + +import android.support.annotation.NonNull; + +public interface ExceptionHandler { + void onException(@NonNull Throwable t); +} diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadFactoryMaker.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadFactoryMaker.java new file mode 100644 index 000000000..221ebef41 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadFactoryMaker.java @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.concurrency; + +import android.os.Process; +import android.support.annotation.NonNull; + +import java.util.concurrent.ThreadFactory; + +class ThreadFactoryMaker { + public static ThreadFactory get(@NonNull final String name, final int priority) { + return new ThreadFactory() { + private int count = 0; + + @Override + public Thread newThread(final Runnable runnable) { + count++; + Runnable wrapperRunnable = new Runnable() { + @Override + public void run() { + Process.setThreadPriority(priority); + runnable.run(); + } + }; + Thread t = new Thread(wrapperRunnable, String.format("%s-%s", name, count)); + return t; + } + }; + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolExecutorService.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolExecutorService.java new file mode 100644 index 000000000..1516d85d8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolExecutorService.java @@ -0,0 +1,101 @@ +package fr.free.nrw.commons.concurrency; + +import android.os.Process; +import android.support.annotation.NonNull; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolExecutorService implements Executor { + private final ScheduledThreadPoolExecutor backgroundPool; + + private ThreadPoolExecutorService(Builder b) { + backgroundPool = new ExceptionAwareThreadPoolExecutor(b.poolSize, + ThreadFactoryMaker.get(b.name, b.priority), b.exceptionHandler); + } + + public ScheduledFuture schedule(Callable callable, long time, TimeUnit timeUnit) { + return backgroundPool.schedule(callable, time, timeUnit); + } + + public ScheduledFuture schedule(Runnable runnable) { + return schedule(runnable, 0, TimeUnit.SECONDS); + } + + public ScheduledFuture schedule(Runnable runnable, long time, TimeUnit timeUnit) { + return backgroundPool.schedule(runnable, time, timeUnit); + } + + public ScheduledFuture scheduleAtFixedRate(final Runnable task, long initialDelay, + long period, final TimeUnit timeUnit) { + return backgroundPool.scheduleAtFixedRate(task, initialDelay, period, timeUnit); + } + + public void shutdown() { + backgroundPool.shutdown(); + } + + @Override + public void execute(Runnable command) { + backgroundPool.execute(command); + } + + /** + * Builder class for {@link ThreadPoolExecutorService} + */ + public static class Builder { + //Required + private final String name; + + //Optional + private int poolSize = 1; + private int priority = Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE; + private ExceptionHandler exceptionHandler = null; + + /** + * @param name the name of the threads in the service. if there are N threads, + * the thread names will be like name-1, name-2, name-3,...,name-N + */ + public Builder(@NonNull String name) { + this.name = name; + } + + /** + * @param poolSize the number of threads to keep in the pool + * @throws IllegalArgumentException if size of pool <=0 + */ + public Builder setPoolSize(int poolSize) throws IllegalArgumentException { + if (poolSize <= 0) { + throw new IllegalArgumentException("Pool size must be grater than 0"); + } + this.poolSize = poolSize; + return this; + } + + /** + * @param priority Priority of the threads in the service. You can supply a constant from + * {@link android.os.Process} + * By default, the priority is set to a value slightly higher than the normal + * background priority + */ + public Builder setPriority(int priority) { + this.priority = priority; + return this; + } + + /** + * @param handler The handler to use to handle exceptions in the service + */ + public Builder setExceptionHandler(ExceptionHandler handler) { + this.exceptionHandler = handler; + return this; + } + + public ThreadPoolExecutorService build() { + return new ThreadPoolExecutorService(this); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index 1586d8202..7c03c33c7 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -11,6 +11,7 @@ import android.database.DataSetObserver; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v4.app.FragmentManager; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; @@ -22,8 +23,11 @@ import android.view.View; import android.widget.Adapter; import android.widget.AdapterView; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + import java.util.ArrayList; -import java.util.Locale; import butterknife.ButterKnife; import fr.free.nrw.commons.CommonsApplication; @@ -35,6 +39,7 @@ import fr.free.nrw.commons.hamburger.HamburgerMenuContainer; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadService; +import fr.free.nrw.commons.utils.ExecutorUtils; import timber.log.Timber; public class ContributionsActivity @@ -234,18 +239,7 @@ public class ContributionsActivity ((CursorAdapter)contributionsList.getAdapter()).swapCursor(cursor); } - if (cursor.getCount() == 0 - && Locale.getDefault().getISO3Language().equals(Locale.ENGLISH.getISO3Language())) { - //cursor count is zero and language is english - - // we need to set the message for 0 case explicitly. - getSupportActionBar().setSubtitle(getResources() - .getString(R.string.contributions_subtitle_zero)); - } else { - getSupportActionBar().setSubtitle(getResources() - .getQuantityString(R.plurals.contributions_subtitle, - cursor.getCount(), - cursor.getCount())); - } + setUploadCount(); contributionsList.clearSyncMessage(); notifyAndMigrateDataSetObservers(); @@ -275,6 +269,27 @@ public class ContributionsActivity return contributionsList.getAdapter().getCount(); } + private void setUploadCount() { + UploadCountClient uploadCountClient = new UploadCountClient(); + CommonsApplication application = CommonsApplication.getInstance(); + ListenableFuture future = uploadCountClient + .getUploadCount(application.getCurrentAccount().name); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Integer uploadCount) { + getSupportActionBar().setSubtitle(getResources() + .getQuantityString(R.plurals.contributions_subtitle, + uploadCount, + uploadCount)); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Timber.e(t, "Fetching upload count failed"); + } + }, ExecutorUtils.uiExecutor()); + } + @Override public void notifyDatasetChanged() { // Do nothing for now diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/UploadCountClient.java b/app/src/main/java/fr/free/nrw/commons/contributions/UploadCountClient.java new file mode 100644 index 000000000..5aaf0fdbe --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/contributions/UploadCountClient.java @@ -0,0 +1,56 @@ +package fr.free.nrw.commons.contributions; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Locale; + +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler; +import fr.free.nrw.commons.concurrency.ThreadPoolExecutorService; +import timber.log.Timber; + +public class UploadCountClient { + private ThreadPoolExecutorService threadPoolExecutor; + + public UploadCountClient() { + threadPoolExecutor = new ThreadPoolExecutorService.Builder("bg-pool") + .setPoolSize(Runtime.getRuntime().availableProcessors()) + .setExceptionHandler(new BackgroundPoolExceptionHandler()) + .build(); + } + + private static final String UPLOAD_COUNT_URL_TEMPLATE = + "https://tools.wmflabs.org/urbanecmbot/uploadsbyuser/uploadsbyuser.py?user=%s"; + + public ListenableFuture getUploadCount(final String userName) { + final SettableFuture future = SettableFuture.create(); + threadPoolExecutor.schedule(new Runnable() { + @Override + public void run() { + URL url; + try { + url = new URL(String.format(Locale.ENGLISH, UPLOAD_COUNT_URL_TEMPLATE, userName)); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + try { + BufferedReader bufferedReader = new BufferedReader(new + InputStreamReader(urlConnection.getInputStream())); + String uploadCount = bufferedReader.readLine(); + bufferedReader.close(); + future.set(Integer.parseInt(uploadCount)); + } finally { + urlConnection.disconnect(); + } + } catch (Exception e) { + Timber.e("Error getting upload count Error", e); + future.setException(e); + } + } + }); + return future; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java new file mode 100644 index 000000000..2cb93a058 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java @@ -0,0 +1,23 @@ +package fr.free.nrw.commons.utils; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.Executor; + +public class ExecutorUtils { + + private static final Executor uiExecutor = new Executor() { + @Override + public void execute(Runnable command) { + if (Looper.myLooper() == Looper.getMainLooper()) { + command.run(); + } else { + new Handler(Looper.getMainLooper()).post(command); + } + } + }; + + public static Executor uiExecutor () { return uiExecutor;} + +}