Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(android): override user interface style #14113

Merged
merged 17 commits into from
Nov 23, 2024

Conversation

AbdullahFaqeir
Copy link
Contributor

@AbdullahFaqeir AbdullahFaqeir commented Sep 15, 2024

Fixes an unexpected behaviour when trying to change the dark/light mode multiple times which causes the screen to go blank and made the change between both animated.

@m1ga
Copy link
Contributor

m1ga commented Sep 16, 2024

The last changes make the app restart correctly now 👍 Which makes me wonder: are the other changes needed at all? Do we need a default windowAnimationStyle ?

At some point we'll need to find the source why it is behaving like this after 4-5 clicks but at least it will show the app again.

Would love to have some community members to test this as there are some overrideUserInterfaceStyle users out there.

@AbdullahFaqeir
Copy link
Contributor Author

I'll explain why it's happening when I get on my laptop

@AbdullahFaqeir
Copy link
Contributor Author

@m1ga
The whole story begins here

At some point while clicking, the activity gets duplicated, which is the root activity of the app, when going inside this condition, we lose the state of the activity.

@AbdullahFaqeir
Copy link
Contributor Author

@m1ga Here's what's happening.

I assumed that since you keep clicking on the button and internally the activity is being recreated which in turn starts the while life cycle of the activity which is the root activity, at some point, the root activity gets duplicated. which leads us to the following

// Determine if a Titanium root activity already exists.
// Only 1 root activity is allowed at a time to host the one and only Titanium JavaScript runtime.
// Note: Android will create a new activity instance if intent is different than last activity's intent.
TiApplication tiApp = getTiApp();
TiRootActivity rootActivity = tiApp.getRootActivity();
this.isDuplicateInstance = (rootActivity != null);
// Determine if this activity is being restored. This will happen when:
// - Developer option "Don't keep activities" is enabled and user navigated back to this activity.
// - The OS forced-quit this app due to low memory or system's "Background process limit" was exceeded.
boolean isNotRestoringActivity = (savedInstanceState == null);
// Determine if this activity was created via startActivityForResult().
// In this case, this activity needs to respond with setResult() and finish().
boolean isActivityForResult = (getCallingActivity() != null);
// Handle the duplicate root activity instance case. (Only 1 is allowed at a time.)
if (this.isDuplicateInstance) {
// Call this instance's Activity.onCreate() method, bypassing TiBaseActivity.onCreate() method.
activityOnCreate(savedInstanceState);

if you go over the comments, you'll see at the very last line before calling activityOnCreate() that is bypassing the TiBaseActivity.onCreate() thus, after jumping to specially the TiBaseActivity.onCreate(), we'll see the real effect of what happened.

See below:

// If activity is being recreated/restored, then copy last saved intent extras.
// This is needed to acquire the Ti.UI.Window proxy ID this activity should be assigned to.
if ((this.launchIntent != null) && (savedInstanceState != null)) {
Bundle oldExtras = savedInstanceState.getBundle("tiLaunchIntentExtras");
if (oldExtras != null) {
Bundle newExtras = this.launchIntent.getExtras();
if (newExtras != null) {
oldExtras.putAll(newExtras);
}
this.launchIntent.putExtras(oldExtras);
}
}

By bypassing the TiBaseActivity.onCreate() we are stepping over the bunch of code that actually recreate the window and brings back whatever views the window had in it.

We ara stepping over this as well

// Create the root content layout, if not done already.
if (layout == null) {
layout = createLayout();
}

And I believe many other things that would cause the window/activity to lose it's state.

I don't know if all of this makes since to you!, let me know

@AbdullahFaqeir
Copy link
Contributor Author

@m1ga any updates on this one?

@m1ga
Copy link
Contributor

m1ga commented Sep 19, 2024

@AbdullahFaqeir as mentioned on Slack: I'll take a look at it again at the weekend.

And I think we should just use the one line from the last commit as that was fixing the issue, not the other default parts. But I'll test all at the weekend again and give more feedback

@m1ga
Copy link
Contributor

m1ga commented Sep 22, 2024

Ok, I'll tested it again and I think we can reduce this PR to just one line:
just adding getTiApp().setRootActivity(null); before:
https://github.com/tidev/titanium-sdk/blob/master/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java#L1242

fixes the issue already. That would reduces the possible site effects as we don't change the XML or anything else in the BaseActivity besides the part inside the if condition (which is only triggered when you change the interface style)

@AbdullahFaqeir
Copy link
Contributor Author

@m1ga
I've created a new method to handle the recreation because there's another places where the activity is recreated for the style https://github.com/tidev/titanium-sdk/blob/e1212e9fccd79f5c302a8380157288df356081da/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java#L1259C4-L1259C33

@m1ga
Copy link
Contributor

m1ga commented Sep 22, 2024

I saw that but it didn't go into that method so it wasn't needed there in my tests. It was only needed in the one place.

@AbdullahFaqeir
Copy link
Contributor Author

@m1ga it would in other cases, I'm not only fixing your case, I'm doing something to handle all cases in a proper way.

@m1ga
Copy link
Contributor

m1ga commented Sep 27, 2024

I totally understand that 👍 Just want to make sure that the setTheme parts or the default transition will not introduce any change will make a default app behave different with 12.6.0 compared to 12.5.0.GA. E.g. in the onCreate you set setTheme(selectedTheme); which is R.style.Theme_Titanium_Light. But what if the user sets a custom theme in tiapp.xml or js? Will that work correctly? I didn't notice any difference with or without it, so if you have a test case where it is needed I would be very happy to test it.

The updateActivity() part works great

@AbdullahFaqeir
Copy link
Contributor Author

@m1ga Can we conclude this? like do a final review showing me what you want and don't want

@m1ga
Copy link
Contributor

m1ga commented Oct 14, 2024

I think for this issue the two switches from

ActivityCompat.recreate(this);
getTiApp().setRootActivity(null);
ActivityCompat.recreate(this);

should be enough (so your updateActivity() method). This will fix the initial issue and can be tested with the code.

Keep the other layout/style/transition changes for another PR so we can test that with another example. As it doesn't change anything for the issue I'm a bit concerned that it might add changes to existing apps and that is not what we want. I just can't test it to see what it will fix.

@m1ga
Copy link
Contributor

m1ga commented Nov 2, 2024

@AbdullahFaqeir just a quick bump to see if you could update the PR as describe in the comment above (just keep the this.updateActivity(); + method in this PR, move the other fixes [theme stuff] into another PR). Then we can finish this one and test it quicker.

It's a big one again (even if it's just a few lines of code 😄 ) and improves the SDK a lot 👍

@m1ga m1ga self-requested a review November 2, 2024 12:16
Copy link
Contributor

@m1ga m1ga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the places that should be in a different PR

@AbdullahFaqeir AbdullahFaqeir requested a review from m1ga November 9, 2024 21:11
Copy link
Contributor

@m1ga m1ga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Example code in #13825 works fine. Kitchensink and Hyperloop-example still works without an issue

@hansemannn hansemannn merged commit 5bebfdb into tidev:master Nov 23, 2024
5 checks passed
@prashantsaini1
Copy link
Contributor

@m1ga @AbdullahFaqeir I doubt that setting the rootActivity to null would break notification/data-intent/shortcut handling as rootActivity is responsible to fire the newintent event for such scenarios via Ti.Android.rootActivity.addEventListener('newintent', () => ());, which is why rootActivity is never set to null.

@prashantsaini1
Copy link
Contributor

@m1ga @AbdullahFaqeir As I mentioned in last comment too, today I found a scenario where it's breaking the app (almost badly). The most common use case is to let an app remain in some fixed light/dark mode irrespective of the system mode. Very common case with many Android apps which allow users to opt among Same as system/ Light / Dark modes.

alloy.js

// set user defined style
Ti.UI.overrideUserInterfaceStyle = Ti.UI.USER_INTERFACE_STYLE_DARK;

// Above code makes `rootActivity` null, thus the below code crash the app.
Ti.Android.rootActivity.addEventListener('newintent', () => {});

// open first window afterwards...

However, if we alter the sequence of above code,

alloy.js

// `rootActivity` is not null as of now
Ti.Android.rootActivity.addEventListener('newintent', () => {});

// Set user defined style.
Ti.UI.overrideUserInterfaceStyle = Ti.UI.USER_INTERFACE_STYLE_DARK;

// open first window afterwards...
  • 2nd snippet makes rootActivity null again and newintent event will not be fired.
  • The only way to get newintent event fired is to add it again but we cannot precisely know when the rootActivity becomes available again.
  • Though after changing style, rootActivity becomes available after sometime via setTimeout, but this is not a consolidated approach to run rootActivity related code again.
  • I almost found a way to get rootActivity, it's available again in Ti.UI.addEventListener('userinterfacestyle', () => {}); method, but again rootActivity is null if the style is now changed via system (not the user one).

So this PR actually makes rootActivity null which must not be done until the app is meant to be totally restarted. I highly doubt that it would break some other codes/modules which generally rely on rootActivity.

@m1ga
Copy link
Contributor

m1ga commented Jan 2, 2025

Hi @prashantsaini1 as you can see in the initial issue: #13825 (comment)
the test code had to do with Ti.UI.overrideUserInterfaceStyle already. Changing Ti.UI.overrideUserInterfaceStyle was breaking the apps before already that is why this PR started in the first place. BTW: I don't think you should run that in alloy.js even with 12.5.1 it was restarting the app for people and ended up in a non starting state.

@prashantsaini1
Copy link
Contributor

@m1ga Actually this is not related to alloy.js, but accessing the rootActivity code after we change style via Ti.UI.overrideUserInterfaceStyle as it makes rootActivity null.

Fact is that apps do not break by setting Ti.UI.overrideUserInterfaceStyle, they break by setting it too frequently as in the initial issue. I tested all cases (except setting it too fast) with 12.5.1.GA, and all works fine.

I even figured out a more general use case without even setting Ti.UI.overrideUserInterfaceStyle at all. Just change the system theme in a simple test app and it would make rootActivity null forever.

Run this app and change theme from system settings:

const win = Titanium.UI.createWindow({backgroundColor: "red"});
const btn = Ti.UI.createButton({title: "change"});
win.add(btn);
win.open();

Ti.UI.addEventListener('userinterfacestyle', (e) => {
	// Ti.Android.rootActivity is reported null if system theme is changed.
});

I believe we need to reassess this PR with mentioned use cases that some apps may want to provide user specific light/dark modes, or may be relying on rootActivity.

@m1ga
Copy link
Contributor

m1ga commented Jan 2, 2025

I think we should continue this in the old issue (i can reopen it) or create a new issue for it 😄

I was digging through some old Slack logs:

2023/06/11: After a lot of testing, I found that the following instruction (I was executing it in alloy.js) creates problems on Android when the app is started (not simply resumed) by a notification click: Ti.UI.overrideUserInterfaceStyle = Ti.UI.USER_INTERFACE_STYLE_LIGHT;

2024/05/03: I found another bugs; the app will hang if I click the notification while the app is closed or killed. This line will make the app hang->. Android only. Ti.UI.overrideUserInterfaceStyle = Ti.UI.USER_INTERFACE_STYLE_LIGHT;

and I had some private chats like this one:
so i kind of found the cause of it, its caused by Ti.UI.overrideUserInterfaceStyle. commenting it out fixes it

overrideUserInterfaceStyle always broke apps if you set it by hand in some cases. The "quick" switch was the only way I could reproduce it. It will restart the activities in a different order at some point (not sure if it's speed but only doing it more then 4 times).

I'm more than happy if you find a way without resetting the rootActivity and making it work as expected! It looks like more people are using this and not Ti.Android.rootActivity. Didn't see any complaint so far with the new SDK.

@prashantsaini1
Copy link
Contributor

@m1ga Just a quick update - I did lots of testing on weekend to get the proper fixes to following problems:

  1. Set theme for entire app upon fresh launch (before any window is opened).
  2. Override theme frequently.

Solution for # 1:

  • Set the app theme in TiApplication.java class, which is actually the most correct place. Native Android apps also set whole app theme in Application class as well.
  • Since alloy.js code cannot be run before rootActivity is created, we can use Ti.App.Properties using a pre-defined key-name to set the user defined theme for entire app on launch, rather than setting Ti.UI.overrideUserInterfaceStyle in alloy.js.
  • This avoids the app restart again right after the launch and keeps rootActivity always alive.
  • It will solve problems for many people like you mentioned who got stuck on this.
  • I have tested this solution and works fine normally with single app start, with notification and intent launchers cases as well.

Notes for # 2:

  • Setting rootActivity null is only required if rootActivity itself is the visible screen, which was the case with the initial sample.
  • I further found that if user directly changes device theme, rootActivity becomes null due to the same method updateActivity being called there as well.
  • We should ideally set rootActivity to null only if the currently visible activity is the rootActivity.
  • I am running more tests around this solution.

I'd appreciate other suggestions!

@m1ga
Copy link
Contributor

m1ga commented Jan 6, 2025

@prashantsaini1 that sounds great! Thanks for putting so much effort in it.

I know there was some theme setting in this PR in the first commits. As they didn't make any difference in my testes I've requested to removed those but it looks like I was wrong about this.

Looking forward to do some tests here with your solutions

@prashantsaini1
Copy link
Contributor

@m1ga Thanks for the hint on theme settings earlier in this PR. I will review them as well to cover up more scenarios!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Android: overrideUserInterfaceStyle makes issues changing it multiple times
4 participants