From edcad21e8b5500b56ab2305bb2861face7b09a3e Mon Sep 17 00:00:00 2001
From: Sergey Volkov <s.volkov@netris.ru>
Date: Tue, 1 Oct 2019 18:57:19 +0300
Subject: [PATCH] feat(android): semantic colors and night mode

- generate color resources from semantic.colors.json
- add Ti.UI.Android.nightModeStatus API
- add Ti.UI.Android.MODE_NIGHT_* constants
---
 android/cli/commands/_build.js                     | 91 ++++++++++++++++++++++
 .../modules/titanium/ui/android/AndroidModule.java | 19 +++++
 apidoc/Titanium/UI/Android/Android.yml             | 49 ++++++++++++
 3 files changed, 159 insertions(+)

diff --git a/android/cli/commands/_build.js b/android/cli/commands/_build.js
index 14d49a0..7439de3 100644
--- a/android/cli/commands/_build.js
+++ b/android/cli/commands/_build.js
@@ -1785,6 +1785,7 @@ AndroidBuilder.prototype.run = function run(logger, config, cli, finished) {
 		// overwritten by some module's strings.xml
 		'generateI18N',
 
+		'generateSemanticColors',
 		'generateTheme',
 		'generateAndroidManifest',
 		'packageApp',
@@ -3570,6 +3571,96 @@ AndroidBuilder.prototype.generateI18N = function generateI18N(next) {
 	next();
 };
 
+AndroidBuilder.prototype.generateSemanticColors = function generateSemanticColors(next) {
+	this.logger.info(__('Generating semantic colors resources'));
+	const _t = this;
+	const destLight = path.join(this.buildResDir, 'values', 'semantic.colors.xml');
+	const destNight = path.join(this.buildResDir, 'values-night', 'semantic.colors.xml');
+
+	let colorsFile = path.join(this.projectDir, 'Resources', 'android', 'semantic.colors.json');
+
+	if (!fs.existsSync(colorsFile)) {
+		// Fallback to root of Resources folder for Classic applications
+		colorsFile = path.join(this.projectDir, 'Resources', 'semantic.colors.json');
+	}
+
+	if (!fs.existsSync(colorsFile)) {
+		this.logger.debug(__('Skipping colorset generation as "semantic.colors.json" file does not exist'));
+		return next();
+	}
+
+	const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+
+	function hexToRgb(hex) {
+		let alphaHex = 'ff';
+		let color = hex;
+		if (hex.color) {
+			color = hex.color;
+			let alpha = Math.round(255 * parseFloat(hex.alpha) / 100);
+			if (alpha <= 255) {
+				alphaHex = alpha.toString(16);
+				if (alpha < 16) {
+					alphaHex = '0' + alphaHex;
+				}
+			}
+		}
+		// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
+		color = color.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
+
+		var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
+		if (alphaHex === 'ff') {
+			return `#${result[1]}${result[2]}${result[3]}`
+		} else {
+			return `#${alphaHex}${result[1]}${result[2]}${result[3]}`
+		}
+	}
+
+	function appendToXml(dom, root, color, colorValue) {
+		const appnameNode = dom.createElement('color');
+
+		appnameNode.setAttribute('name', `${color}`);
+		appnameNode.appendChild(dom.createTextNode(hexToRgb(colorValue)));
+		root.appendChild(dom.createTextNode('\n\t'));
+		root.appendChild(appnameNode);
+	}
+
+	function writeXml(dom, dest, mode) {
+		if (fs.existsSync(dest)) {
+			_t.logger.debug(__('Merging %s semantic colors => %s', mode.cyan, dest.cyan));
+		} else {
+			_t.logger.debug(__('Writing %s semantic colors => %s', mode.cyan, dest.cyan));
+		}
+		_t.writeXmlFile(dom.documentElement, dest);
+	}
+
+	const colors = fs.readJSONSync(colorsFile);
+	const domLight = new DOMParser().parseFromString('<resources/>', 'text/xml');
+	const domNight = new DOMParser().parseFromString('<resources/>', 'text/xml');
+
+	const rootLight = domLight.documentElement;
+	const rootNight = domNight.documentElement;
+
+	for (const [ color, colorValue ] of Object.entries(colors)) {
+		if (!colorValue.light) {
+			this.logger.warn(`Skipping ${color} as it does not include a light value`);
+			continue;
+		}
+
+		if (!colorValue.dark) {
+			this.logger.warn(`Skipping ${color} as it does not include a dark value`);
+			continue;
+		}
+
+		appendToXml(domLight, rootLight, color, colorValue.light);
+		appendToXml(domNight, rootNight, color, colorValue.dark);
+	}
+
+	writeXml(domLight, destLight, 'light');
+	writeXml(domNight, destNight, 'night');
+
+	next();
+};
+
 AndroidBuilder.prototype.generateTheme = function generateTheme(next) {
 	const themeFile = path.join(this.buildResDir, 'values', 'theme.xml');
 
diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java b/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java
index 8ff1f7e..c7e4ddd 100644
--- a/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java
+++ b/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java
@@ -18,6 +18,7 @@ import ti.modules.titanium.ui.widget.TiUIProgressIndicator;
 import ti.modules.titanium.ui.widget.webview.TiUIWebView;
 import android.app.Activity;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.graphics.PixelFormat;
 import android.text.util.Linkify;
 import android.view.Gravity;
@@ -228,6 +229,13 @@ public class AndroidModule extends KrollModule
 	@Kroll.constant
 	public static final int OVER_SCROLL_NEVER = 2; //android.view.View.OVER_SCROLL_NEVER;
 
+	@Kroll.constant
+	public static final int MODE_NIGHT_NO = Configuration.UI_MODE_NIGHT_NO;
+	@Kroll.constant
+	public static final int MODE_NIGHT_YES = Configuration.UI_MODE_NIGHT_YES;
+	@Kroll.constant
+	public static final int MODE_NIGHT_UNDEFINED = Configuration.UI_MODE_NIGHT_UNDEFINED;
+
 	public AndroidModule()
 	{
 		super();
@@ -270,6 +278,17 @@ public class AndroidModule extends KrollModule
 		});
 	}
 
+	// clang-format off
+	@Kroll.method
+	@Kroll.getProperty
+	public int getNightModeStatus()
+	// clang-format on
+	{
+		int status = TiApplication.getInstance().getApplicationContext().getResources().getConfiguration().uiMode
+					 & Configuration.UI_MODE_NIGHT_MASK;
+		return status;
+	}
+
 	@Override
 	public String getApiName()
 	{
diff --git a/apidoc/Titanium/UI/Android/Android.yml b/apidoc/Titanium/UI/Android/Android.yml
index 2596538..e087e1b 100644
--- a/apidoc/Titanium/UI/Android/Android.yml
+++ b/apidoc/Titanium/UI/Android/Android.yml
@@ -232,6 +232,43 @@ properties:
     type: Number
     permission: read-only
 
+  - name: MODE_NIGHT_NO
+    summary: |
+        Constant for <Titanium.UI.Android.nightModeStatus>
+    description: |
+        Value indicating that night mode type has been set.
+        Used in the `nightModeStatus` property.
+        See [UI_MODE_NIGHT_NO](https://developer.android.com/reference/android/content/res/Configuration.html#UI_MODE_NIGHT_NO)
+    type: Number
+    permission: read-only
+    platforms: [android]
+    since: '8.3.0'
+
+  - name: MODE_NIGHT_UNDEFINED
+    summary: |
+        Constant for <Titanium.UI.Android.nightModeStatus>
+    description: |
+        Value indicating that no mode type has been set.
+        Used in the `nightModeStatus` property.
+        See [UI_MODE_NIGHT_UNDEFINED](https://developer.android.com/reference/android/content/res/Configuration.html#UI_MODE_NIGHT_UNDEFINED)
+    type: Number
+    permission: read-only
+    platforms: [android]
+    since: '8.3.0'
+
+  - name: MODE_NIGHT_YES
+    summary: |
+        Constant for <Titanium.UI.Android.nightModeStatus>
+    description: |
+        Value indicating that night mode type has been set.
+        Used in the `nightModeStatus` property.
+        See [UI_MODE_NIGHT_YES](https://developer.android.com/reference/android/content/res/Configuration.html#UI_MODE_NIGHT_YES)
+    type: Number
+    permission: read-only
+    platforms: [android]
+    since: '8.3.0'
+
+
   - name: OVER_SCROLL_ALWAYS
     summary: Always allow a user to over-scroll this view, provided it is a view that can scroll.
     description: |
@@ -837,6 +874,18 @@ properties:
     platforms: [android]
     since: "8.0.0"
 
+  - name: nightModeStatus
+    summary: The status of night mode of user interface.
+    description: |
+      Use this property to determine whether your interface should be configured with a dark or light appearance.
+      The default value of this trait is set to the corresponding appearance setting on the user's device.
+      See [UI_MODE_NIGHT_MASK](https://developer.android.com/reference/android/content/res/Configuration.html#UI_MODE_NIGHT_MASK).
+      See <Titanium.App.iOS.userInterfaceStyle>
+    type: Number
+    permission: read-only
+    platforms: [android]
+    since: "8.2.0"
+
 examples:
   - title: Android Preferences Example
     example: |
-- 
2.7.4

