Move a lot of utils into core.

This commit is contained in:
Greyson Parrelli
2026-05-21 23:32:12 -04:00
committed by Michelle Tang
parent 15a3a8efde
commit 0284da2d0f
227 changed files with 301 additions and 307 deletions
@@ -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);
}
}