From 17862a78db59d60fe316961f9111efc330ba2abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Sat, 31 Aug 2019 11:21:03 -0700 Subject: [PATCH] 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 --- RNTester/android/app/build.gradle | 4 +- .../android/app/src/main/AndroidManifest.xml | 2 +- .../react/uiapp/RNTesterActivity.java | 12 ++ RNTester/js/RNTesterApp.android.js | 180 +++++++++++++----- RNTester/js/utils/RNTesterList.android.js | 4 + .../src/main/java/com/facebook/react/BUCK | 1 + .../facebook/react/ReactInstanceManager.java | 13 ++ .../modules/appearance/AppearanceModule.java | 92 +++++++++ .../facebook/react/modules/appearance/BUCK | 19 ++ .../main/java/com/facebook/react/shell/BUCK | 1 + .../react/shell/MainReactPackage.java | 5 + 11 files changed, 285 insertions(+), 48 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK diff --git a/RNTester/android/app/build.gradle b/RNTester/android/app/build.gradle index b805922c1fa0f3..6e40ad021b6634 100644 --- a/RNTester/android/app/build.gradle +++ b/RNTester/android/app/build.gradle @@ -101,7 +101,7 @@ def enableProguardInReleaseBuilds = true def useIntlJsc = false android { - compileSdkVersion 28 + compileSdkVersion 29 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -121,7 +121,7 @@ android { defaultConfig { applicationId "com.facebook.react.uiapp" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 1 versionName "1.0" } diff --git a/RNTester/android/app/src/main/AndroidManifest.xml b/RNTester/android/app/src/main/AndroidManifest.xml index 2c4ee4baa78321..d439651c88a5bd 100644 --- a/RNTester/android/app/src/main/AndroidManifest.xml +++ b/RNTester/android/app/src/main/AndroidManifest.xml @@ -28,7 +28,7 @@ android:name=".RNTesterActivity" android:label="@string/app_name" android:screenOrientation="fullSensor" - android:configChanges="orientation|screenSize" > + android:configChanges="orientation|screenSize|uiMode" > diff --git a/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java b/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java index bfa143ea8f2d1b..4facf807dbc55b 100644 --- a/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java +++ b/RNTester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterActivity.java @@ -6,10 +6,12 @@ */ package com.facebook.react.uiapp; +import android.content.res.Configuration; import android.os.Bundle; import androidx.annotation.Nullable; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; +import com.facebook.react.ReactInstanceManager; public class RNTesterActivity extends ReactActivity { public static class RNTesterActivityDelegate extends ReactActivityDelegate { @@ -53,4 +55,14 @@ protected ReactActivityDelegate createReactActivityDelegate() { protected String getMainComponentName() { return "RNTesterApp"; } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ReactInstanceManager instanceManager = getReactInstanceManager(); + + if (instanceManager != null) { + instanceManager.onConfigurationChanged(newConfig); + } + } } diff --git a/RNTester/js/RNTesterApp.android.js b/RNTester/js/RNTesterApp.android.js index 047522f8e3941c..90dcae9f674189 100644 --- a/RNTester/js/RNTesterApp.android.js +++ b/RNTester/js/RNTesterApp.android.js @@ -33,10 +33,13 @@ const { Text, TouchableWithoutFeedback, UIManager, + useColorScheme, View, } = require('react-native'); +import type {RNTesterExample} from './types/RNTesterTypes'; import type {RNTesterNavigationState} from './utils/RNTesterNavigationReducer'; +import {RNTesterThemeContext, themes} from './components/RNTesterTheme'; UIManager.setLayoutAnimationEnabledExperimental(true); @@ -54,18 +57,119 @@ const HEADER_NAV_ICON = nativeImageSource({ height: 48, }); -const Header = ({title, onPressDrawer}) => { +const Header = ({ + onPressDrawer, + title, +}: { + onPressDrawer?: () => mixed, + title: string, +}) => ( + + {theme => { + return ( + + + + {title} + + + + + + + + + ); + }} + +); + +const RNTesterExampleContainerViaHook = ({ + onPressDrawer, + title, + module, + exampleRef, +}: { + onPressDrawer?: () => mixed, + title: string, + module: RNTesterExample, + exampleRef: () => void, +}) => { + const colorScheme = useColorScheme(); + const theme = colorScheme === 'dark' ? themes.dark : themes.light; return ( - - - {title} + + +
=0.78.0 site=react_native_android_fb) This issue + * was found when making Flow check .android.js files. */ + onPressDrawer={onPressDrawer} + /> + - - - - + + ); +}; + +const RNTesterDrawerContentViaHook = ({ + onNavigate, + list, +}: { + onNavigate?: () => mixed, + list: { + ComponentExamples: Array, + APIExamples: Array, + }, +}) => { + const colorScheme = useColorScheme(); + const theme = colorScheme === 'dark' ? themes.dark : themes.light; + return ( + + + + + + ); +}; + +const RNTesterExampleListViaHook = ({ + title, + onPressDrawer, + onNavigate, + list, +}: { + title: string, + onPressDrawer?: () => mixed, + onNavigate?: () => mixed, + list: { + ComponentExamples: Array, + APIExamples: Array, + }, +}) => { + const colorScheme = useColorScheme(); + const theme = colorScheme === 'dark' ? themes.dark : themes.light; + return ( + + +
=0.78.0 site=react_native_android_fb) This issue + * was found when making Flow check .android.js files. */ + onPressDrawer={onPressDrawer} + /> + - + ); }; @@ -133,14 +237,10 @@ class RNTesterApp extends React.Component { _renderDrawerContent = () => { return ( - - - + ); }; @@ -164,39 +264,31 @@ class RNTesterApp extends React.Component { ); } else if (ExampleModule) { return ( - -
=0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ + onPressDrawer={() => this.drawer.openDrawer()} + title={ExampleModule.title} + module={ExampleModule} + exampleRef={example => { /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue * was found when making Flow check .android.js files. */ - onPressDrawer={() => this.drawer.openDrawer()} - /> - { - /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue - * was found when making Flow check .android.js files. */ - this._exampleRef = example; - }} - /> - + this._exampleRef = example; + }} + /> ); } } return ( - -
=0.78.0 site=react_native_android_fb) This issue - * was found when making Flow check .android.js files. */ - onPressDrawer={() => this.drawer.openDrawer()} - /> - - + =0.78.0 site=react_native_android_fb) This issue was found + * when making Flow check .android.js files. */ + onPressDrawer={() => this.drawer.openDrawer()} + onNavigate={this._handleAction} + list={RNTesterList} + /> ); } @@ -246,7 +338,6 @@ const styles = StyleSheet.create({ flex: 1, }, toolbar: { - backgroundColor: '#E9EAED', height: 56, }, toolbarLeft: { @@ -268,7 +359,6 @@ const styles = StyleSheet.create({ drawerContentWrapper: { flex: 1, paddingTop: StatusBar.currentHeight, - backgroundColor: 'white', }, }); diff --git a/RNTester/js/utils/RNTesterList.android.js b/RNTester/js/utils/RNTesterList.android.js index 238fca68defba2..2bc401723092d0 100644 --- a/RNTester/js/utils/RNTesterList.android.js +++ b/RNTester/js/utils/RNTesterList.android.js @@ -128,6 +128,10 @@ const APIExamples: Array = [ key: 'AnimatedExample', module: require('../examples/Animated/AnimatedExample'), }, + { + key: 'AppearanceExample', + module: require('../examples/Appearance/AppearanceExample'), + }, { key: 'AppStateExample', module: require('../examples/AppState/AppStateExample'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/BUCK b/ReactAndroid/src/main/java/com/facebook/react/BUCK index 58b58dcb46eef3..476cc659145a8c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/BUCK @@ -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"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 48dcbc93471fd3..6f2338c7978ff9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -34,6 +34,7 @@ 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.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 void onWindowFocusChange(boolean hasFocus) { } } + /** 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(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java new file mode 100644 index 00000000000000..4de3edd76448ce --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + *

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); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK new file mode 100644 index 00000000000000..e7dc34b1060c3b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK @@ -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"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index 1fa885dbd3bc24..5acd40190d59ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -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"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 4e0c7b243f1eea..66ff5d2d8b6ab4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -16,6 +16,7 @@ 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 @@ @ReactModuleList( nativeModules = { AccessibilityInfoModule.class, + AppearanceModule.class, AppStateModule.class, BlobModule.class, FileReaderModule.class, @@ -112,6 +114,8 @@ public MainReactPackage(MainPackageConfig config) { 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 ReactModuleInfoProvider getReactModuleInfoProvider() { Class[] moduleList = new Class[] { AccessibilityInfoModule.class, + AppearanceModule.class, AppStateModule.class, BlobModule.class, FileReaderModule.class,