Skip to content

Commit

Permalink
feat(android): icon splash screen support
Browse files Browse the repository at this point in the history
Fixes TIMOB-28473
  • Loading branch information
jquick-axway authored and ewanharris committed Aug 24, 2021
1 parent cce763a commit 2baef1e
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 53 deletions.
3 changes: 0 additions & 3 deletions android/app/src/main/assets/Resources/app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
'use strict';

// this sets the background color of the master UIView (when there are no windows/tab groups on it)
Titanium.UI.backgroundColor = '#000';

// create tab group
const tabGroup = Titanium.UI.createTabGroup();

Expand Down
2 changes: 1 addition & 1 deletion android/app/src/main/res/values/theme.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Titanium" parent="@style/Base.Theme.Titanium.Splash">
<item name="android:windowBackground">@drawable/background</item>
<item name="titaniumSplashIcon">@drawable/appicon</item>
</style>
</resources>
129 changes: 95 additions & 34 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -1398,17 +1398,34 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) {
}

// make sure we have an icon
this.appIconManifestValue = null;
this.appRoundIconManifestValue = null;
if (this.customAndroidManifest) {
const appIconValue = this.customAndroidManifest.getAppAttribute('android:icon');
if (appIconValue) {
cli.tiapp.icon = appIconValue.replace(/^@drawable\//, '') + '.png';
// Fetch the app "icon" and "roundIcon" attributes as-is from the "AndroidManfiest.xml".
this.appIconManifestValue = this.customAndroidManifest.getAppAttribute('android:icon');
this.appRoundIconManifestValue = this.customAndroidManifest.getAppAttribute('android:roundIcon');
if (this.appIconManifestValue) {
// Turn the "android:icon" value to an image file name. Remove the "@drawable/" or "@mipmap/" prefix.
let appIconName = this.appIconManifestValue;
const index = appIconName.lastIndexOf('/');
if (index >= 0) {
appIconName = appIconName.substring(index + 1);
}
cli.tiapp.icon = appIconName + '.png';
}
}
if (!cli.tiapp.icon || ![ 'Resources', 'Resources/android' ].some(function (p) {
return fs.existsSync(cli.argv['project-dir'], p, cli.tiapp.icon);
})) {
cli.tiapp.icon = 'appicon.png';
}
if (!this.appIconManifestValue) {
this.appIconManifestValue = '@drawable/' + cli.tiapp.icon;
const index = this.appIconManifestValue.indexOf('.');
if (index >= 0) {
this.appIconManifestValue = this.appIconManifestValue.substring(0, index);
}
}

return function (callback) {
this.validateTiModules('android', this.deployType, function validateTiModulesCallback(err, modules) {
Expand Down Expand Up @@ -2719,12 +2736,12 @@ AndroidBuilder.prototype.copyResources = async function copyResources() {
this.copyUnmodifiedResources(gatheredResults.resourcesToCopy), // copies any other files that don't require special handling (like JS/CSS do)
]);

// Then do the rest of the shit...
// Finish doing the following after the above tasks have copied files to the build folder.
const templateDir = path.join(this.platformPath, 'templates', 'app', 'default', 'template', 'Resources', 'android');
return Promise.all([
this.encryptJSFiles(),
this.ensureAppIcon(templateDir),
this.ensureSplashScreen(templateDir),
this.detectLegacySplashImage(),
]);
};

Expand All @@ -2745,31 +2762,26 @@ AndroidBuilder.prototype.copyUnmodifiedResources = async function copyUnmodified
};

/**
* Ensures the generated app has a splash screen image
* @param {string} templateDir the filepath to the Titanium SDK's app template for Android apps
* Checks if a legacy splash screen "background.png" exists in generated build folder.
* Note: As of Titanium 10.1.0, this image is optional and will use the app icon instead if not found.
*/
AndroidBuilder.prototype.ensureSplashScreen = async function ensureSplashScreen(templateDir) {
// make sure we have a splash screen
AndroidBuilder.prototype.detectLegacySplashImage = async function detectLegacySplashImage() {
// Check if a "background" splash image exists under one of the "res/drawable" folders.
this.hasSplashBackgroundImage = false;
const backgroundRegExp = /^background(\.9)?\.(png|jpg)$/;
const destBg = path.join(this.buildAppMainResDrawableDir, 'background.png');
const nodpiDir = path.join(this.buildAppMainResDir, 'drawable-nodpi');
if (!(await fs.readdir(this.buildAppMainResDrawableDir)).some(name => {
if (backgroundRegExp.test(name)) {
this.unmarkBuildDirFile(path.join(this.buildAppMainResDrawableDir, name));
return true;
}
return false;
}, this)) {
// no background image in drawable, but what about drawable-nodpi?
if (!(await fs.exists(nodpiDir)) || !(await fs.readdir(nodpiDir)).some(name => {
if (backgroundRegExp.test(name)) {
this.unmarkBuildDirFile(path.join(nodpiDir, name));
return true;
for (const dirName of await fs.readdir(this.buildAppMainResDir)) {
if (dirName.startsWith('drawable')) {
const drawableDirPath = path.join(this.buildAppMainResDir, dirName);
for (const fileName of await fs.readdir(drawableDirPath)) {
if (backgroundRegExp.test(fileName)) {
this.hasSplashBackgroundImage = true;
this.unmarkBuildDirFile(path.join(drawableDirPath, fileName));
break;
}
}
if (this.hasSplashBackgroundImage) {
break;
}
return false;
}, this)) {
this.unmarkBuildDirFile(destBg);
this.copyFileSync(path.join(templateDir, 'default.png'), destBg);
}
}
};
Expand Down Expand Up @@ -3279,9 +3291,8 @@ AndroidBuilder.prototype.generateSemanticColors = async function generateSemanti

AndroidBuilder.prototype.generateTheme = async function generateTheme() {
// Log the theme XML file we're about to generate.
const valuesDirPath = path.join(this.buildAppMainResDir, 'values');
const xmlFilePath = path.join(valuesDirPath, 'ti_styles.xml');
this.logger.info(__('Generating theme file: %s', xmlFilePath.cyan));
const xmlFileName = 'ti_styles.xml';
this.logger.info(__('Generating theme file: %s', xmlFileName.cyan));

// Set default theme to be used in "AndroidManifest.xml" and style resources.
let defaultAppThemeName = 'Theme.Titanium.DayNight';
Expand All @@ -3300,22 +3311,72 @@ AndroidBuilder.prototype.generateTheme = async function generateTheme() {
}
}

// Use background/default PNG for splash if found. Otherwise theme will default to using app icon.
// Also show semi-transparent status/navigation bar if image is set, which was the 10.0.0 behavior.
const translucentXmlValue = this.hasSplashBackgroundImage ? 'true' : 'false';
let windowBackgroundImageXmlString = '';
if (this.hasSplashBackgroundImage) {
windowBackgroundImageXmlString = '<item name="android:windowBackground">@drawable/background</item>';
}

// Create the theme XML file with above activity style.
// Also apply app's background image to root splash activity theme.
let valuesDirPath = path.join(this.buildAppMainResDir, 'values');
let xmlLines = [
'<?xml version="1.0" encoding="utf-8"?>',
'<resources>',
` <style name="Theme.Titanium.App" parent="${defaultAppThemeName}"/>`,
` <style name="Theme.AppDerived" parent="${actualAppTheme}"/>`,
'',
' <!-- Theme used by "TiRootActivity" derived class which displays the splash screen. -->',
' <style name="Theme.Titanium" parent="Base.Theme.Titanium.Splash">',
' <item name="android:windowBackground">@drawable/background</item>',
` <item name="titaniumSplashIcon">${this.appIconManifestValue}</item>`,
` <item name="android:windowTranslucentNavigation">${translucentXmlValue}</item>`,
` <item name="android:windowTranslucentStatus">${translucentXmlValue}</item>`,
` ${windowBackgroundImageXmlString}`,
' </style>',
'</resources>'
];
await fs.ensureDir(valuesDirPath);
await fs.writeFile(xmlFilePath, xmlLines.join('\n'));
await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n'));

// Create a theme XML for different Android OS versions depending on how the splash is configured.
const iconDrawable = '@drawable/titanium_splash_icon_background';
const adaptiveIconDrawable = '@drawable/titanium_splash_adaptive_icon_background';
if (this.hasSplashBackgroundImage) {
// Project uses background/default PNG for splash, but we will ignore it on Android 12 and higher.
// Note: Android 12 forces all apps to use an icon for splash screen. Cannot opt-out.
const iconValue = this.appRoundIconManifestValue ? this.appRoundIconManifestValue : this.appIconManifestValue;
const windowBackgroundValue = this.appRoundIconManifestValue ? adaptiveIconDrawable : iconDrawable;
valuesDirPath = path.join(this.buildAppMainResDir, 'values-v31');
xmlLines = [
'<?xml version="1.0" encoding="utf-8"?>',
'<resources>',
' <style name="Theme.Titanium" parent="Base.Theme.Titanium.Splash">',
` <item name="titaniumSplashIcon">${iconValue}</item>`,
` <item name="android:windowBackground">${windowBackgroundValue}</item>`,
' <item name="android:windowTranslucentNavigation">false</item>',
' <item name="android:windowTranslucentStatus">false</item>',
' </style>',
'</resources>'
];
await fs.ensureDir(valuesDirPath);
await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n'));
} else if (this.appRoundIconManifestValue) {
// Project is set up to use app icon for the splash on all Android OS versions. (No fullscreen splash image.)
// Since manifest has an "android:roundIcon" adaptive icon defined, use it on Android 8 and higher.
valuesDirPath = path.join(this.buildAppMainResDir, 'values-v26');
xmlLines = [
'<?xml version="1.0" encoding="utf-8"?>',
'<resources>',
' <style name="Theme.Titanium" parent="Base.Theme.Titanium.Splash">',
` <item name="titaniumSplashIcon">${this.appRoundIconManifestValue}</item>`,
` <item name="android:windowBackground">${adaptiveIconDrawable}</item>`,
' </style>',
'</resources>'
];
await fs.ensureDir(valuesDirPath);
await fs.writeFile(path.join(valuesDirPath, xmlFileName), xmlLines.join('\n'));
}
};

AndroidBuilder.prototype.fetchNeededManifestSettings = function fetchNeededManifestSettings() {
Expand Down Expand Up @@ -3544,7 +3605,7 @@ AndroidBuilder.prototype.generateAndroidManifest = async function generateAndroi
let mainManifestContent = await fs.readFile(path.join(this.templatesDir, 'AndroidManifest.xml'));
mainManifestContent = ejs.render(mainManifestContent.toString(), {
appChildXmlLines: appChildXmlLines,
appIcon: '@drawable/' + this.tiapp.icon.replace(/((\.9)?\.(png|jpg))$/, ''),
appIcon: this.appIconManifestValue,
appLabel: this.tiapp.name,
classname: this.classname,
storagePermissionMaxSdkVersion: neededManifestSettings.storagePermissionMaxSdkVersion,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TitaniumAngular" parent="Theme.MaterialComponents.Light.DarkActionBar.Bridge">
<style name="TitaniumAngular" parent="Theme.Titanium.Light">
<item name="colorPrimary">@color/angularBlue</item>
<item name="colorPrimaryDark">@color/angularDarkBlue</item>
<item name="colorAccent">@color/angularRed</item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar.Bridge">
<style name="AppTheme" parent="Theme.Titanium.Light">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Mimics Android 12 splash screen used by apps without a "roundIcon". -->
<!-- Shows an app icon within a white circle like how it appears in the apps screen. -->
<item android:drawable="?android:attr/colorBackground"/>
<item android:gravity="center">
<shape android:shape="oval">
<size android:width="160dp" android:height="160dp"/>
<solid android:color="@android:color/white"/>
</shape>
</item>
<item
android:gravity="center"
android:width="90dp"
android:height="90dp"
android:drawable="?attr/titaniumSplashIcon"/>
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Mimics Android 12 splash screen used by apps with a "roundIcon". -->
<!-- Expected to be set to an <adaptive-icon/> drawable which is supported as of Android 8.0. -->
<item android:drawable="?android:attr/colorBackground"/>
<item
android:gravity="center"
android:width="160dp"
android:height="160dp"
android:drawable="?attr/titaniumSplashIcon"/>
</layer-list>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:attr/colorBackground"/>
<item>
<bitmap android:gravity="center" android:src="?attr/titaniumSplashIcon"/>
</item>
</layer-list>
20 changes: 20 additions & 0 deletions android/titanium/res/values-v31/values.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Base.Theme.Titanium.Splash" parent="Theme.AppDerived">
<item name="android:navigationBarColor">?android:attr/colorBackground</item>
<item name="android:statusBarColor">?android:attr/colorBackground</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@drawable/titanium_splash_icon_background</item>
<item name="android:windowLightNavigationBar">?attr/isLightTheme</item>
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenBackground">?android:attr/colorBackground</item>
<item name="android:windowSplashScreenIconBackgroundColor">@android:color/white</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="titaniumSplashIcon">@drawable/titanium_icon_splash_empty</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>

9 changes: 7 additions & 2 deletions android/titanium/res/values/values.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
<!-- See "values-land/values.xml" which fixes action bar height for landscape. -->
<dimen name="ti_action_bar_height">@dimen/mtrl_toolbar_default_height</dimen>

<!-- Define theme attribute used to customize Titanium's splash screen background drawable. -->
<declare-styleable name="TitaniumSplashBackground">
<attr name="titaniumSplashIcon" format="reference"/>
</declare-styleable>

<!-- Titanium's button text style is not all-caps just like Google's apps. -->
<style name="TextAppearance.Titanium.Button" parent="TextAppearance.MaterialComponents.Button">
<item name="android:textAllCaps">false</item>
Expand Down Expand Up @@ -114,11 +119,11 @@
<!-- Base theme to be used by the "TiRootActivity" class. -->
<style name="Base.Theme.Titanium.Splash" parent="Theme.AppDerived">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@drawable/titanium_splash_icon_background</item>
<item name="android:windowNoTitle">true</item>
<item name="titaniumSplashIcon">@drawable/titanium_icon_splash_empty</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>

<!-- Theme to be replaced by app project and have a "windowBackground" image applied to it. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.appcelerator.kroll.util.KrollAssetHelper;
import org.appcelerator.titanium.proxy.IntentProxy;
import org.appcelerator.titanium.util.TiActivitySupport;
import org.appcelerator.titanium.util.TiRHelper;
import org.json.JSONException;
import org.json.JSONObject;

Expand All @@ -29,10 +28,14 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
import androidx.appcompat.content.res.AppCompatResources;

import ti.modules.titanium.ui.ShortcutItemProxy;

Expand Down Expand Up @@ -334,6 +337,12 @@ public void onDisposing(KrollRuntime runtime)
Log.e(TAG, "Failed to parse: " + TiC.EXTRA_TI_NEW_INTENT, ex);
}
}

// As of Android 12, the OS automatically shows a splash screen for us.
// Adding the following listener prevents the splash from being dismissed.
if (Build.VERSION.SDK_INT >= 31) {
getSplashScreen().setOnExitAnimationListener((splashView) -> {});
}
}

@Override
Expand Down Expand Up @@ -520,18 +529,18 @@ protected void onResume()
public void onConfigurationChanged(Configuration newConfig)
{
super.onConfigurationChanged(newConfig);
try {
int backgroundId = TiRHelper.getResource("drawable.background");
if (backgroundId != 0) {
Drawable d = this.getResources().getDrawable(backgroundId);
if (d != null) {
Drawable bg = getWindow().getDecorView().getBackground();
getWindow().setBackgroundDrawable(d);
bg.setCallback(null);

// Update background in case it uses different images/layouts for new config, such as after orientation change.
View layout = getLayout();
if (layout != null) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true);
if (typedValue.resourceId != 0) {
Drawable drawable = AppCompatResources.getDrawable(this, typedValue.resourceId);
if (drawable != null) {
layout.setBackground(drawable);
}
}
} catch (Exception e) {
Log.e(TAG, "Resource not found 'drawable.background': " + e.getMessage());
}
}

Expand Down

0 comments on commit 2baef1e

Please sign in to comment.