Add Appearance module

Summary:
Android implementation of the Appearance native module. Exposes the user's preferred color scheme: "dark" for Night theme ON, "light" for Night theme OFF.

Emits a `appearanceChanged` event when the current uiMode configuration changes.

To make your app handle Night mode changes, make sure to do the following:

* Declare your Activity can handle uiMode configuration changes (https://developer.android.com/preview/features/darktheme#java):
```
android:configChanges="uiMode"
```
* Make sure to pass the configuration changed activity lifecycle callback from your ReactActivity:
```
Override
protected void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged();

    if (mReactInstanceManager != null) {
        mReactInstanceManager.onConfigurationChanged(newConfig);
    }
}
```

### RNTester

Adds the AppearanceExample to RNTester on Android.

Changelog:

[Android][Added] - New Appearance module exposes the user's current Night theme preference

Reviewed By: makovkastar

Differential Revision: D16942161

fbshipit-source-id: d24a8ff800a1c5f70f4efdec6891396c2078067e
This commit is contained in:
Héctor Ramos
2019-08-31 11:22:44 -07:00
committed by Facebook Github Bot
parent 51681e80ab
commit 17862a78db
11 changed files with 285 additions and 48 deletions
@@ -33,6 +33,7 @@ rn_android_library(
react_native_target("java/com/facebook/react/module/annotations:annotations"),
react_native_target("java/com/facebook/react/module/model:model"),
react_native_target("java/com/facebook/react/modules/appregistry:appregistry"),
react_native_target("java/com/facebook/react/modules/appearance:appearance"),
react_native_target("java/com/facebook/react/modules/core:core"),
react_native_target("java/com/facebook/react/modules/debug:debug"),
react_native_target("java/com/facebook/react/modules/fabric:fabric"),
@@ -34,6 +34,7 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JS_VM_CALLS;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Process;
@@ -78,6 +79,7 @@ import com.facebook.react.devsupport.RedBoxHandler;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
import com.facebook.react.modules.appearance.AppearanceModule;
import com.facebook.react.modules.appregistry.AppRegistry;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -712,6 +714,17 @@ public class ReactInstanceManager {
}
}
/** Call this from {@link Activity#onConfigurationChanged()}. */
@ThreadConfined(UI)
public void onConfigurationChanged(Configuration newConfig) {
UiThreadUtil.assertOnUiThread();
ReactContext currentContext = getCurrentReactContext();
if (currentContext != null) {
currentContext.getNativeModule(AppearanceModule.class).onConfigurationChanged();
}
}
@ThreadConfined(UI)
public void showDevOptionsDialog() {
UiThreadUtil.assertOnUiThread();
@@ -0,0 +1,92 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.facebook.react.modules.appearance;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
/** Module that exposes the user's preferred color scheme. For API >= 29. */
@ReactModule(name = AppearanceModule.NAME)
public class AppearanceModule extends ReactContextBaseJavaModule {
public static final String NAME = "Appearance";
private static final String APPEARANCE_CHANGED_EVENT_NAME = "appearanceChanged";
private static final int ANDROID_TEN = 29;
private String mColorScheme = "light";
public AppearanceModule(ReactApplicationContext reactContext) {
super(reactContext);
mColorScheme = colorSchemeForCurrentConfiguration(reactContext);
}
private static String colorSchemeForCurrentConfiguration(Context context) {
// TODO: (hramos) T52929922: Switch to Build.VERSION_CODES.ANDROID_TEN or equivalent
if (Build.VERSION.SDK_INT >= ANDROID_TEN) {
int currentNightMode =
context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
return "light";
case Configuration.UI_MODE_NIGHT_YES:
return "dark";
}
}
return "light";
}
@Override
public String getName() {
return NAME;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String getColorScheme() {
mColorScheme = colorSchemeForCurrentConfiguration(getReactApplicationContext());
return mColorScheme;
}
/** Stub */
@ReactMethod
public void addListener(String eventName) {}
/** Stub */
@ReactMethod
public void removeListeners(double count) {}
/*
* Call this from your root activity whenever configuration changes. If the
* color scheme has changed, an event will emitted.
*/
public void onConfigurationChanged() {
String newColorScheme = colorSchemeForCurrentConfiguration(getReactApplicationContext());
if (!mColorScheme.equals(newColorScheme)) {
mColorScheme = newColorScheme;
emitAppearanceChanged(mColorScheme);
}
}
/** Sends an event to the JS instance that the preferred color scheme has changed. */
public void emitAppearanceChanged(String colorScheme) {
WritableMap appearancePreferences = Arguments.createMap();
appearancePreferences.putString("colorScheme", colorScheme);
getReactApplicationContext()
.getJSModule(RCTDeviceEventEmitter.class)
.emit(APPEARANCE_CHANGED_EVENT_NAME, appearancePreferences);
}
}
@@ -0,0 +1,19 @@
load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library")
rn_android_library(
name = "appearance",
srcs = glob(["**/*.java"]),
is_androidx = True,
visibility = [
"PUBLIC",
],
deps = [
react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_dep("third-party/android/support-annotations:android-support-annotations"),
react_native_dep("third-party/android/androidx:annotation"),
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/common:common"),
react_native_target("java/com/facebook/react/module/annotations:annotations"),
react_native_target("java/com/facebook/react/modules/core:core"),
],
)
@@ -26,6 +26,7 @@ rn_android_library(
react_native_target("java/com/facebook/react/devsupport:devsupport"),
react_native_target("java/com/facebook/react/module/model:model"),
react_native_target("java/com/facebook/react/modules/accessibilityinfo:accessibilityinfo"),
react_native_target("java/com/facebook/react/modules/appearance:appearance"),
react_native_target("java/com/facebook/react/modules/appstate:appstate"),
react_native_target("java/com/facebook/react/modules/blob:blob"),
react_native_target("java/com/facebook/react/modules/camera:camera"),
@@ -16,6 +16,7 @@ import com.facebook.react.module.annotations.ReactModuleList;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.accessibilityinfo.AccessibilityInfoModule;
import com.facebook.react.modules.appearance.AppearanceModule;
import com.facebook.react.modules.appstate.AppStateModule;
import com.facebook.react.modules.blob.BlobModule;
import com.facebook.react.modules.blob.FileReaderModule;
@@ -71,6 +72,7 @@ import java.util.Map;
@ReactModuleList(
nativeModules = {
AccessibilityInfoModule.class,
AppearanceModule.class,
AppStateModule.class,
BlobModule.class,
FileReaderModule.class,
@@ -112,6 +114,8 @@ public class MainReactPackage extends TurboReactPackage {
switch (name) {
case AccessibilityInfoModule.NAME:
return new AccessibilityInfoModule(context);
case AppearanceModule.NAME:
return new AppearanceModule(context);
case AppStateModule.NAME:
return new AppStateModule(context);
case BlobModule.NAME:
@@ -210,6 +214,7 @@ public class MainReactPackage extends TurboReactPackage {
Class<? extends NativeModule>[] moduleList =
new Class[] {
AccessibilityInfoModule.class,
AppearanceModule.class,
AppStateModule.class,
BlobModule.class,
FileReaderModule.class,