mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-01 11:35:46 +01:00
Move a lot of utils into core.
This commit is contained in:
committed by
Michelle Tang
parent
15a3a8efde
commit
0284da2d0f
@@ -0,0 +1,14 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
|
||||
public final class AccessibilityUtil {
|
||||
|
||||
private AccessibilityUtil() {
|
||||
}
|
||||
|
||||
public static boolean areAnimationsDisabled(Context context) {
|
||||
return Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1) == 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.signal.core.util
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* A wrapper around [ProcessLifecycleOwner] that allows for safely adding/removing observers
|
||||
* on multiple threads.
|
||||
*/
|
||||
object AppForegroundObserver {
|
||||
private val listeners: MutableSet<Listener> = CopyOnWriteArraySet()
|
||||
|
||||
@Volatile
|
||||
private var isInitialized: Boolean = false
|
||||
|
||||
@Volatile
|
||||
private var isForegrounded: Boolean = false
|
||||
|
||||
@MainThread
|
||||
@JvmStatic
|
||||
fun begin() {
|
||||
ThreadUtil.assertMainThread()
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
onForeground()
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
onBackground()
|
||||
}
|
||||
})
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to be notified of when the app moves between the background and the foreground.
|
||||
* To mimic the behavior of subscribing to [ProcessLifecycleOwner], this listener will be
|
||||
* immediately notified of the foreground state if we've experienced a foreground/background event
|
||||
* already.
|
||||
*/
|
||||
@AnyThread
|
||||
@JvmStatic
|
||||
fun addListener(listener: Listener) {
|
||||
listeners.add(listener)
|
||||
|
||||
if (isInitialized) {
|
||||
if (isForegrounded) {
|
||||
listener.onForeground()
|
||||
} else {
|
||||
listener.onBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@JvmStatic
|
||||
fun removeListener(listener: Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isForegrounded(): Boolean {
|
||||
return isInitialized && isForegrounded
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun onForeground() {
|
||||
isForegrounded = true
|
||||
|
||||
for (listener in listeners) {
|
||||
listener.onForeground()
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun onBackground() {
|
||||
isForegrounded = false
|
||||
|
||||
for (listener in listeners) {
|
||||
listener.onBackground()
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onForeground() {}
|
||||
fun onBackground() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class ConfigurationUtil {
|
||||
|
||||
private ConfigurationUtil() {}
|
||||
|
||||
public static int getNightModeConfiguration(@NonNull Context context) {
|
||||
return getNightModeConfiguration(context.getResources().getConfiguration());
|
||||
}
|
||||
|
||||
public static int getNightModeConfiguration(@NonNull Configuration configuration) {
|
||||
return configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
}
|
||||
|
||||
public static float getFontScale(@NonNull Configuration configuration) {
|
||||
return configuration.fontScale;
|
||||
}
|
||||
|
||||
public static boolean isUiModeChanged(@NonNull Configuration configuration, @NonNull Configuration newConfiguration) {
|
||||
int oldTheme = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
int newTheme = newConfiguration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
return oldTheme != newTheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A class that will throttle the number of runnables executed to be at most once every specified
|
||||
* interval. However, it could be longer if events are published consistently.
|
||||
*
|
||||
* Useful for performing actions in response to rapid user input, such as inputting text, where you
|
||||
* don't necessarily want to perform an action after <em>every</em> input.
|
||||
*
|
||||
* See http://rxmarbles.com/#debounce
|
||||
*/
|
||||
public class Debouncer {
|
||||
|
||||
private final Handler handler;
|
||||
private final long threshold;
|
||||
|
||||
public Debouncer(long threshold, TimeUnit timeUnit) {
|
||||
this(timeUnit.toMillis(threshold));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every
|
||||
* {@code threshold} milliseconds.
|
||||
*/
|
||||
public Debouncer(long threshold) {
|
||||
this.handler = new Handler(Looper.getMainLooper());
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public void publish(Runnable runnable) {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
handler.postDelayed(runnable, threshold);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class DisplayMetricsUtil {
|
||||
private DisplayMetricsUtil() {
|
||||
}
|
||||
|
||||
public static void forceAspectRatioToScreenByAdjustingHeight(@NonNull DisplayMetrics displayMetrics, @NonNull View view) {
|
||||
int screenHeight = displayMetrics.heightPixels;
|
||||
int screenWidth = displayMetrics.widthPixels;
|
||||
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
params.height = params.width * screenHeight / screenWidth;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
public final class DrawableUtil {
|
||||
|
||||
private DrawableUtil() {}
|
||||
|
||||
public static @NonNull Bitmap toBitmap(@NonNull Drawable drawable, int width, int height) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Drawable} that safely wraps and tints the provided drawable.
|
||||
*/
|
||||
public static @NonNull Drawable tint(@NonNull Drawable drawable, @ColorInt int tint) {
|
||||
Drawable tinted = DrawableCompat.wrap(drawable).mutate();
|
||||
DrawableCompat.setTint(tinted, tint);
|
||||
return tinted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.LabeledIntent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IntentUtils {
|
||||
|
||||
public static boolean isResolvable(@NonNull Context context, @NonNull Intent intent) {
|
||||
List<ResolveInfo> resolveInfoList = context.getPackageManager().queryIntentActivities(intent, 0);
|
||||
return resolveInfoList != null && resolveInfoList.size() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* From: <a href="https://stackoverflow.com/a/12328282">https://stackoverflow.com/a/12328282</a>
|
||||
*/
|
||||
public static @Nullable LabeledIntent getLabelintent(@NonNull Context context, @NonNull Intent origIntent, int name, int drawable) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
ComponentName launchName = origIntent.resolveActivity(pm);
|
||||
|
||||
if (launchName != null) {
|
||||
Intent resolved = new Intent();
|
||||
resolved.setComponent(launchName);
|
||||
resolved.setData(origIntent.getData());
|
||||
|
||||
return new LabeledIntent(resolved, context.getPackageName(), name, drawable);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
|
||||
public class JsonUtils {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
|
||||
objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
|
||||
com.fasterxml.jackson.module.kotlin.ExtensionsKt.registerKotlinModule(objectMapper);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(byte[] serialized, Class<T> clazz) throws IOException {
|
||||
return fromJson(new String(serialized), clazz);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(String serialized, Class<T> clazz) throws IOException {
|
||||
return objectMapper.readValue(serialized, clazz);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(InputStream serialized, Class<T> clazz) throws IOException {
|
||||
return objectMapper.readValue(serialized, clazz);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(Reader serialized, Class<T> clazz) throws IOException {
|
||||
return objectMapper.readValue(serialized, clazz);
|
||||
}
|
||||
|
||||
public static <T> List<T> fromJsonArray(String serialized, Class<T> clazz) throws IOException {
|
||||
TypeFactory typeFactory = objectMapper.getTypeFactory();
|
||||
return objectMapper.readValue(serialized, typeFactory.constructCollectionType(List.class, clazz));
|
||||
}
|
||||
|
||||
public static String toJson(Object object) throws IOException {
|
||||
return objectMapper.writeValueAsString(object);
|
||||
}
|
||||
|
||||
public static ObjectMapper getMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
public static class SaneJSONObject {
|
||||
|
||||
private final JSONObject delegate;
|
||||
|
||||
public SaneJSONObject(JSONObject delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public @Nullable String getString(String name) throws JSONException {
|
||||
if (delegate.isNull(name)) return null;
|
||||
else return delegate.getString(name);
|
||||
}
|
||||
|
||||
public long getLong(String name) throws JSONException {
|
||||
return delegate.getLong(name);
|
||||
}
|
||||
|
||||
public boolean getBoolean(String name) throws JSONException {
|
||||
return delegate.getBoolean(name);
|
||||
}
|
||||
|
||||
public boolean isNull(String name) {
|
||||
return delegate.isNull(name);
|
||||
}
|
||||
|
||||
public int getInt(String name) throws JSONException {
|
||||
return delegate.getInt(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class ParcelUtil {
|
||||
|
||||
public static byte[] serialize(Parcelable parceable) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parceable.writeToParcel(parcel, 0);
|
||||
byte[] bytes = parcel.marshall();
|
||||
parcel.recycle();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static Parcel deserialize(byte[] bytes) {
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parcel.unmarshall(bytes, 0, bytes.length);
|
||||
parcel.setDataPosition(0);
|
||||
return parcel;
|
||||
}
|
||||
|
||||
public static <T> T deserialize(byte[] bytes, Parcelable.Creator<T> creator) {
|
||||
Parcel parcel = deserialize(bytes);
|
||||
return creator.createFromParcel(parcel);
|
||||
}
|
||||
|
||||
public static void writeStringCollection(@NonNull Parcel dest, @NonNull Collection<String> collection) {
|
||||
dest.writeStringList(new ArrayList<>(collection));
|
||||
}
|
||||
|
||||
public static @NonNull Collection<String> readStringCollection(@NonNull Parcel in) {
|
||||
List<String> list = new ArrayList<>();
|
||||
in.readStringList(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
public static void writeParcelableCollection(@NonNull Parcel dest, @NonNull Collection<? extends Parcelable> collection) {
|
||||
Parcelable[] values = collection.toArray(new Parcelable[0]);
|
||||
dest.writeParcelableArray(values, 0);
|
||||
}
|
||||
|
||||
public static @NonNull <E> Collection<E> readParcelableCollection(@NonNull Parcel in, Class<E> clazz) {
|
||||
//noinspection unchecked
|
||||
return Arrays.asList((E[]) in.readParcelableArray(clazz.getClassLoader()));
|
||||
}
|
||||
|
||||
public static void writeBoolean(@NonNull Parcel dest, boolean value) {
|
||||
dest.writeByte(value ? (byte) 1 : 0);
|
||||
}
|
||||
|
||||
public static boolean readBoolean(@NonNull Parcel in) {
|
||||
return in.readByte() != 0;
|
||||
}
|
||||
|
||||
public static void writeByteArray(@NonNull Parcel dest, @Nullable byte[] data) {
|
||||
if (data == null) {
|
||||
dest.writeInt(-1);
|
||||
} else {
|
||||
dest.writeInt(data.length);
|
||||
dest.writeByteArray(data);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable byte[] readByteArray(@NonNull Parcel in) {
|
||||
int length = in.readInt();
|
||||
if (length == -1) {
|
||||
return null;
|
||||
}
|
||||
byte[] data = new byte[length];
|
||||
in.readByteArray(data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.DownloadManager;
|
||||
import android.app.KeyguardManager;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.job.JobScheduler;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.hardware.SensorManager;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.location.LocationManager;
|
||||
import android.media.AudioManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.PowerManager;
|
||||
import android.os.Vibrator;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class ServiceUtil {
|
||||
public static InputMethodManager getInputMethodManager(Context context) {
|
||||
return (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
}
|
||||
|
||||
public static WindowManager getWindowManager(Context context) {
|
||||
return (WindowManager) context.getSystemService(Activity.WINDOW_SERVICE);
|
||||
}
|
||||
|
||||
public static StorageManager getStorageManager(Context context) {
|
||||
return ContextCompat.getSystemService(context, StorageManager.class);
|
||||
}
|
||||
|
||||
public static ConnectivityManager getConnectivityManager(Context context) {
|
||||
return (ConnectivityManager) context.getSystemService(Activity.CONNECTIVITY_SERVICE);
|
||||
}
|
||||
|
||||
public static NotificationManager getNotificationManager(Context context) {
|
||||
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
public static TelephonyManager getTelephonyManager(Context context) {
|
||||
return (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
}
|
||||
|
||||
public static AudioManager getAudioManager(Context context) {
|
||||
return (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
public static SensorManager getSensorManager(Context context) {
|
||||
return (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
}
|
||||
|
||||
public static PowerManager getPowerManager(Context context) {
|
||||
return (PowerManager)context.getSystemService(Context.POWER_SERVICE);
|
||||
}
|
||||
|
||||
public static AlarmManager getAlarmManager(Context context) {
|
||||
return (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
public static Vibrator getVibrator(Context context) {
|
||||
return (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
}
|
||||
|
||||
public static DisplayManager getDisplayManager(@NonNull Context context) {
|
||||
return (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
|
||||
}
|
||||
|
||||
public static AccessibilityManager getAccessibilityManager(@NonNull Context context) {
|
||||
return (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
}
|
||||
|
||||
public static ClipboardManager getClipboardManager(@NonNull Context context) {
|
||||
return (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
public static JobScheduler getJobScheduler(Context context) {
|
||||
return (JobScheduler) context.getSystemService(JobScheduler.class);
|
||||
}
|
||||
|
||||
@RequiresApi(22)
|
||||
public static @Nullable SubscriptionManager getSubscriptionManager(@NonNull Context context) {
|
||||
return (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
|
||||
}
|
||||
|
||||
public static ActivityManager getActivityManager(@NonNull Context context) {
|
||||
return (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
}
|
||||
|
||||
public static LocationManager getLocationManager(@NonNull Context context) {
|
||||
return ContextCompat.getSystemService(context, LocationManager.class);
|
||||
}
|
||||
|
||||
public static KeyguardManager getKeyguardManager(@NonNull Context context) {
|
||||
return ContextCompat.getSystemService(context, KeyguardManager.class);
|
||||
}
|
||||
|
||||
public static BluetoothManager getBluetoothManager(@NonNull Context context) {
|
||||
return ContextCompat.getSystemService(context, BluetoothManager.class);
|
||||
}
|
||||
|
||||
public static DownloadManager getDownloadManager(@NonNull Context context) {
|
||||
return ContextCompat.getSystemService(context, DownloadManager.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Mixes the behavior of {@link Throttler} and {@link Debouncer}.
|
||||
*
|
||||
* Like a throttler, it will limit the number of runnables to be executed to be at most once every
|
||||
* specified interval, while allowing the first runnable to be run immediately.
|
||||
*
|
||||
* However, like a debouncer, instead of completely discarding runnables that are published in the
|
||||
* throttling period, the most recent one will be saved and run at the end of the throttling period.
|
||||
*
|
||||
* Useful for publishing a set of identical or near-identical tasks that you want to be responsive
|
||||
* and guaranteed, but limited in execution frequency.
|
||||
*/
|
||||
public class ThrottledDebouncer {
|
||||
|
||||
private static final int WHAT = 24601;
|
||||
|
||||
private final OverflowHandler handler;
|
||||
private final long threshold;
|
||||
|
||||
/**
|
||||
* @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every
|
||||
* {@code threshold} milliseconds.
|
||||
*/
|
||||
@MainThread
|
||||
public ThrottledDebouncer(long threshold) {
|
||||
this.handler = new OverflowHandler();
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void publish(Runnable runnable) {
|
||||
handler.setRunnable(runnable);
|
||||
|
||||
if (handler.hasMessages(WHAT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
long sinceLastRun = System.currentTimeMillis() - handler.lastRun;
|
||||
long delay = Math.max(0, threshold - sinceLastRun);
|
||||
|
||||
handler.sendMessageDelayed(handler.obtainMessage(WHAT), delay);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void clear() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
private static class OverflowHandler extends Handler {
|
||||
|
||||
public OverflowHandler() {
|
||||
super(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
private Runnable runnable;
|
||||
private long lastRun = 0;
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == WHAT && runnable != null) {
|
||||
lastRun = System.currentTimeMillis();
|
||||
runnable.run();
|
||||
runnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setRunnable(@NonNull Runnable runnable) {
|
||||
this.runnable = runnable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.signal.core.util;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
/**
|
||||
* A class that will throttle the number of runnables executed to be at most once every specified
|
||||
* interval.
|
||||
*
|
||||
* Useful for performing actions in response to rapid user input where you want to take action on
|
||||
* the initial input but prevent follow-up spam.
|
||||
*
|
||||
* This is different from {@link Debouncer} in that it will run the first runnable immediately
|
||||
* instead of waiting for input to die down.
|
||||
*
|
||||
* See http://rxmarbles.com/#throttle
|
||||
*/
|
||||
public class Throttler {
|
||||
|
||||
private static final int WHAT = 8675309;
|
||||
|
||||
private final Handler handler;
|
||||
private final long threshold;
|
||||
|
||||
/**
|
||||
* @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every
|
||||
* {@code threshold} milliseconds.
|
||||
*/
|
||||
public Throttler(long threshold) {
|
||||
this.handler = new Handler(Looper.getMainLooper());
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public void publish(Runnable runnable) {
|
||||
if (handler.hasMessages(WHAT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
runnable.run();
|
||||
handler.sendMessageDelayed(handler.obtainMessage(WHAT), threshold);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user