Skip to content

Commit

Permalink
feat: add Ti.UI.overrideUserInterfaceStyle property
Browse files Browse the repository at this point in the history
Fixes TIMOB-28369
  • Loading branch information
jquick-axway authored and ewanharris committed Jun 10, 2021
1 parent 387d6be commit 2a32030
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 10 deletions.
59 changes: 55 additions & 4 deletions android/modules/ui/src/java/ti/modules/titanium/ui/UIModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.appcelerator.kroll.annotations.Kroll;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.TiBaseActivity;
import org.appcelerator.titanium.TiC;
import org.appcelerator.titanium.TiDimension;
import org.appcelerator.titanium.TiRootActivity;
Expand All @@ -21,6 +22,7 @@
import org.appcelerator.titanium.util.TiDeviceOrientation;
import org.appcelerator.titanium.util.TiUIHelper;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
Expand All @@ -34,6 +36,7 @@
import android.view.View;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatDelegate;

@Kroll.module
public class UIModule extends KrollModule
Expand Down Expand Up @@ -428,12 +431,14 @@ public class UIModule extends KrollModule

protected static final int MSG_LAST_ID = KrollProxy.MSG_LAST_ID + 101;

private UIModule.Receiver broadcastReceiver;

public UIModule()
{
super();

// Register the module's broadcast receiver.
final UIModule.Receiver broadcastReceiver = new UIModule.Receiver(this);
this.broadcastReceiver = new UIModule.Receiver(this);
TiApplication.getInstance().registerReceiver(broadcastReceiver,
new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));

Expand Down Expand Up @@ -520,11 +525,57 @@ public double convertUnits(String convertFromValue, String convertToUnits)
return result;
}

@Kroll.getProperty
public int getOverrideUserInterfaceStyle()
{
switch (AppCompatDelegate.getDefaultNightMode()) {
case AppCompatDelegate.MODE_NIGHT_NO:
return Configuration.UI_MODE_NIGHT_NO;
case AppCompatDelegate.MODE_NIGHT_YES:
return Configuration.UI_MODE_NIGHT_YES;
}
return Configuration.UI_MODE_NIGHT_UNDEFINED;
}

@Kroll.setProperty
public void setOverrideUserInterfaceStyle(int styleId)
{
// Convert given "UI_MODE_*" constant to a "MODE_NIGHT_*" constant.
int nightModeId = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
if (styleId == Configuration.UI_MODE_NIGHT_NO) {
nightModeId = AppCompatDelegate.MODE_NIGHT_NO;
} else if (styleId == Configuration.UI_MODE_NIGHT_YES) {
nightModeId = AppCompatDelegate.MODE_NIGHT_YES;
}

// Do not continue if the mode isn't changing.
if (nightModeId == AppCompatDelegate.getDefaultNightMode()) {
return;
}

// Change the night mode.
AppCompatDelegate.setDefaultNightMode(nightModeId);

// Fire a "userinterfacestyle" change event via our broadcast receiver.
this.broadcastReceiver.onReceive(TiApplication.getInstance(), null);

// Force our top-most activity apply the assigned night mode.
// Note: Works-around a Google bug where it doesn't always call the activity's onNightModeChanged() method.
Activity activity = TiApplication.getAppCurrentActivity();
if (activity instanceof TiBaseActivity) {
((TiBaseActivity) activity).applyNightMode();
}
}

@Kroll.getProperty
public int getUserInterfaceStyle()
{
return TiApplication.getInstance().getApplicationContext().getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK;
int styleId = getOverrideUserInterfaceStyle();
if (styleId == Configuration.UI_MODE_NIGHT_UNDEFINED) {
Configuration config = TiApplication.getInstance().getResources().getConfiguration();
styleId = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
}
return styleId;
}

@Override
Expand All @@ -533,7 +584,7 @@ public String getApiName()
return "Ti.UI";
}

private class Receiver extends BroadcastReceiver
private static class Receiver extends BroadcastReceiver
{
private UIModule module;
private int lastEmittedStyle;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@
import android.os.PowerManager;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;

import android.view.KeyEvent;
import android.view.Menu;
Expand Down Expand Up @@ -93,6 +95,7 @@ public abstract class TiBaseActivity extends AppCompatActivity implements TiActi
private final TiWeakList<OnPrepareOptionsMenuEvent> onPrepareOptionsMenuListeners = new TiWeakList<>();
private boolean sustainMode = false;
private int lastUIModeFlags = 0;
private int lastNightMode = AppCompatDelegate.MODE_NIGHT_UNSPECIFIED;
private Intent launchIntent = null;
private TiActionBarStyleHandler actionBarStyleHandler;
private TiActivitySafeAreaMonitor safeAreaMonitor;
Expand Down Expand Up @@ -731,6 +734,10 @@ protected void onCreate(Bundle savedInstanceState)
this.requestWindowFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
super.onCreate(savedInstanceState);

// Fetch app's current night mode setting used to override dark/light theme handling.
// In JavaScript, this can be changed via "Ti.UI.overrideUserInterfaceStyle" property.
this.lastNightMode = AppCompatDelegate.getDefaultNightMode();

// If activity is using Google's default ActionBar, then the below will return an ActionBar style handler
// intended to be called by onConfigurationChanged() which will resize its title bar and font.
// Note: We need to do this since we override "configChanges" in the "AndroidManifest.xml".
Expand Down Expand Up @@ -1203,11 +1210,28 @@ public void onConfigurationChanged(@NonNull Configuration newConfig)
// Recreate this activity if the OS has switched between light/dark theme.
final int NIGHT_MASK = Configuration.UI_MODE_NIGHT_MASK;
if ((newConfig.uiMode & NIGHT_MASK) != (this.lastUIModeFlags & NIGHT_MASK)) {
this.recreate();
this.lastNightMode = AppCompatDelegate.getDefaultNightMode();
ActivityCompat.recreate(this);
}
this.lastUIModeFlags = newConfig.uiMode;
}

@Override
protected void onNightModeChanged(int mode)
{
super.onNightModeChanged(mode);
applyNightMode();
}

public void applyNightMode()
{
int mode = AppCompatDelegate.getDefaultNightMode();
if (this.inForeground && (mode != this.lastNightMode)) {
this.lastNightMode = mode;
ActivityCompat.recreate(this);
}
}

@Override
protected void onNewIntent(Intent intent)
{
Expand Down Expand Up @@ -1438,6 +1462,8 @@ protected void onStart()
}
}
}

applyNightMode();
}

@Override
Expand Down Expand Up @@ -1478,6 +1504,8 @@ protected void onRestart()
super.onRestart();

Log.d(TAG, "Activity " + this + " onRestart", Log.DEBUG_MODE);

applyNightMode();
}

@Override
Expand Down
18 changes: 18 additions & 0 deletions apidoc/Titanium/UI/UI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2915,6 +2915,24 @@ properties:
windows.
type: String

- name: overrideUserInterfaceStyle
summary: Forces the app to used assigned theme instead of the system theme.
description: |
When set to [USER_INTERFACE_STYLE_DARK](Titanium.UI.USER_INTERFACE_STYLE_DARK) or
[USER_INTERFACE_STYLE_LIGHT](Titanium.UI.USER_INTERFACE_STYLE_LIGHT), the app will ignore
the system's current theme and use the theme assigned to this property instead.
When set to [USER_INTERFACE_STYLE_UNSPECIFIED](Titanium.UI.USER_INTERFACE_STYLE_UNSPECIFIED),
the app will use the system's current theme. To determine what the system's current theme is,
you must read the [userInterfaceStyle](Titanium.UI.userInterfaceStyle) property.
See [UI_MODE_NIGHT_MASK](https://developer.android.com/reference/android/content/res/Configuration.html#UI_MODE_NIGHT_MASK).
type: Number
default: Titanium.UI.USER_INTERFACE_STYLE_UNSPECIFIED
constants: Titanium.UI.USER_INTERFACE_STYLE_*
osver: {ios: {min: "13.0"}}
since: "10.0.0"

- name: tintColor
summary: |
Sets the global tint color of the application. It is inherited by the child views and can be
Expand Down
23 changes: 23 additions & 0 deletions iphone/Classes/UIModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,29 @@ - (NSString *)TEXT_STYLE_LARGE_TITLE
return UIFontTextStyleLargeTitle;
}

- (void)setOverrideUserInterfaceStyle:(id)args
{
ENSURE_SINGLE_ARG(args, NSNumber)
[self replaceValue:args
forKey:@"overrideUserInterfaceStyle"
notification:NO];
if ([TiUtils isIOSVersionOrGreater:@"13.0"] || [TiUtils isMacOS]) {
int style = [TiUtils intValue:args def:UIUserInterfaceStyleUnspecified];
TiApp.controller.overrideUserInterfaceStyle = style;
}
}

- (NSNumber *)overrideUserInterfaceStyle
{
NSNumber *style = nil;
if ([TiUtils isIOSVersionOrGreater:@"13.0"] || [TiUtils isMacOS]) {
style = @(TiApp.controller.overrideUserInterfaceStyle);
} else {
style = [self valueForKey:@"overrideUserInterfaceStyle"];
}
return (style != nil) ? style : self.USER_INTERFACE_STYLE_UNSPECIFIED;
}

- (NSNumber *)userInterfaceStyle
{
return @(TiApp.controller.traitCollection.userInterfaceStyle);
Expand Down
2 changes: 2 additions & 0 deletions tests/Resources/ti.app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ describe('Titanium.App', () => {
it.iosBroken('can be assigned a Boolean value', () => { // iOS does it async? I don't know
Ti.App.proximityDetection = true;
should(Ti.App.proximityDetection).be.true();
Ti.App.proximityDetection = false;
should(Ti.App.proximityDetection).be.false();
});

it('has no accessors', () => {
Expand Down
43 changes: 38 additions & 5 deletions tests/Resources/ti.ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,15 @@ describe('Titanium.UI', function () {
should(Ti.UI).have.a.constant('SEMANTIC_COLOR_TYPE_LIGHT').which.is.a.String();
});

it('.semanticColorType defaults to SEMANTIC_COLOR_TYPE_LIGHT', () => {
should(Ti.UI.semanticColorType).eql(Ti.UI.SEMANTIC_COLOR_TYPE_LIGHT);
it('.semanticColorType', () => {
if (OS_IOS && (OS_VERSION_MAJOR < 13)) {
should(Ti.UI.semanticColorType).eql(Ti.UI.SEMANTIC_COLOR_TYPE_LIGHT);
} else {
should(Ti.UI.semanticColorType).equalOneOf([
Ti.UI.SEMANTIC_COLOR_TYPE_DARK,
Ti.UI.SEMANTIC_COLOR_TYPE_LIGHT
]);
}
});

it('.USER_INTERFACE_STYLE_LIGHT', () => {
Expand All @@ -215,9 +222,35 @@ describe('Titanium.UI', function () {
should(Ti.UI).have.a.constant('USER_INTERFACE_STYLE_UNSPECIFIED').which.is.a.Number();
});

it('.userInterfaceStyle defaults to USER_INTERFACE_STYLE_LIGHT', () => {
// FIXME: we can't gurantee the emulator theme didn't get changed. Just specify it has to be one of the constants?
should(Ti.UI.userInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_LIGHT);
it('.userInterfaceStyle', () => {
if (OS_IOS && (OS_VERSION_MAJOR < 13)) {
should(Ti.UI.userInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_LIGHT);
} else {
should(Ti.UI.userInterfaceStyle).equalOneOf([
Ti.UI.USER_INTERFACE_STYLE_DARK,
Ti.UI.USER_INTERFACE_STYLE_LIGHT,
Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED
]);
}
});

it('.overrideUserInterfaceStyle', () => {
should(Ti.UI.overrideUserInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED);
if (OS_IOS) {
const originalStyle = Ti.UI.userInterfaceStyle;

Ti.UI.overrideUserInterfaceStyle = Ti.UI.USER_INTERFACE_STYLE_DARK;
should(Ti.UI.overrideUserInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_DARK);
should(Ti.UI.userInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_DARK);

Ti.UI.overrideUserInterfaceStyle = Ti.UI.USER_INTERFACE_STYLE_LIGHT;
should(Ti.UI.overrideUserInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_LIGHT);
should(Ti.UI.userInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_LIGHT);

Ti.UI.overrideUserInterfaceStyle = Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED;
should(Ti.UI.overrideUserInterfaceStyle).eql(Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED);
should(Ti.UI.userInterfaceStyle).eql(originalStyle);
}
});

describe('Semantic Colors', () => {
Expand Down

0 comments on commit 2a32030

Please sign in to comment.