package io.expo.appearance; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.modules.core.DeviceEventManagerModule; import java.util.HashMap; import java.util.Map; public class RNCAppearanceModule extends ReactContextBaseJavaModule implements LifecycleEventListener { public static final String REACT_CLASS = "RNCAppearance"; private BroadcastReceiver mBroadcastReceiver = null; public RNCAppearanceModule(@NonNull ReactApplicationContext reactContext) { super(reactContext); // Only Android 10+ supports dark mode if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { final ReactApplicationContext ctx = reactContext; mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Configuration newConfig = intent.getParcelableExtra("newConfig"); sendEvent(ctx, "appearanceChanged", getPreferences()); } }; ctx.addLifecycleEventListener(this); } } @NonNull @Override public String getName() { return REACT_CLASS; } // `protected` to allow overriding in Expo client for scoping purposes protected String getColorScheme(Configuration config) { String colorScheme = "no-preference"; // Only Android 10+ support dark mode if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; switch (currentNightMode) { case Configuration.UI_MODE_NIGHT_NO: case Configuration.UI_MODE_NIGHT_UNDEFINED: colorScheme = "light"; break; case Configuration.UI_MODE_NIGHT_YES: colorScheme = "dark"; break; } } return colorScheme; } private WritableMap getPreferences() { WritableMap preferences = Arguments.createMap(); // Attempt to use the Activity context first in order to get the most up to date // scheme. This covers the scenario when AppCompatDelegate.setDefaultNightMode() // is called directly (which can occur in Brownfield apps for example). Activity activity = getCurrentActivity(); Context context = activity != null ? activity : getReactApplicationContext(); String colorScheme = getColorScheme(context.getResources().getConfiguration()); preferences.putString("colorScheme", colorScheme); return preferences; } @Nullable @Override public Map<String, Object> getConstants() { HashMap<String, Object> constants = new HashMap(); constants.put("initialPreferences", getPreferences()); return constants; } private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { if (reactContext.hasActiveCatalystInstance()) { FLog.i("sendEvent", eventName + ": " + params.toString()); reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } } private void sendEvent(String eventName, @Nullable WritableMap params) { if (getReactApplicationContext().hasActiveCatalystInstance()) { FLog.i("sendEvent", eventName + ": " + params.toString()); getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } } // We don't do any validation on whether the appearance has actually changed since the last // event was sent. We ignore this on the JS side if unchanged. private void updateAndSendAppearancePreferences() { WritableMap preferences = getPreferences(); sendEvent("appearanceChanged", preferences); } @Override public void onHostResume() { final Activity activity = getCurrentActivity(); if (activity == null) { FLog.e(ReactConstants.TAG, "no activity to register receiver"); return; } activity.registerReceiver(mBroadcastReceiver, new IntentFilter("onConfigurationChanged")); // Send updated preferences to JS when the app is resumed, because we don't receive updates // when backgrounded updateAndSendAppearancePreferences(); } @Override public void onHostPause() { final Activity activity = getCurrentActivity(); if (activity == null) return; try { activity.unregisterReceiver(mBroadcastReceiver); } catch (java.lang.IllegalArgumentException e) { FLog.e(ReactConstants.TAG, "mBroadcastReceiver already unregistered", e); } } @Override public void onHostDestroy() { // No need to do anything as far as I know? } }