I’ve encountered multiple times similar bugs, where Unity is automatically destroying activities upon re-opening application through the launcher.
For example:
Open Unity application, start a different activity through native plugin, switch via home to the launcher screen. Then open Unity application through the launcher. singleTask destroys activity on top and the application never returns required callbacks, or worse - the native plugin itself is bricked.
I’ve tried using “standard” launchmode via a gradle groovy script hack (since there’s no way to modify LibraryManifest.xml in legit reliable way), but that results in everything that is 3D to be just pink (probably 3d engine is not initialized correctly).
So. The questions. Why does Unity still uses “singleTask” in 2020 (w/o “standard” support)?
How to handle bricked plugins this way without having a source code for them?
Okay, nevermind. Everything being pink is the result of me upgrading and downgrading project.
launchMode:standard does seems to work and activities no longer destroyed upon launch. Which good. Really good. Almost too good to be the truth.
android:launchMode is unfortunately overritten after merging all manifests (from LibraryManifest.xml, which is supplied from the Unity Editor installation folder @ PlaybackEngines), so to override it, use this gradle script (add to the mainTemplate.gradle at android section):
Note: This script may be gradle version specific. If it doesn’t work for you, something has probably changed. The idea is simple, find merged manifest, and replace launchMode value “singleTask” with “standard”.
To double check if patch is applied, check your resulting output at %ProjectDirectory%/Temp/gradleOut/build/intermediates/merged_manifests/release/AndroidManifest.xml.
And bundle_manifest directory for AAB (or use Android Studio & check via Analyze APK feature)
Man, I wish Unity has provided custom LibraryManifest.xml override.
With this, I was able to have native plugins to keep their intents on top when user opens application via launcher icon.
Also, don’t forget to modify bundle manifest for AAB (like I did), AAB manifest is located at different location.
And task.getBundleManifestOutputDirectory() should be used to fetch path intead of getManifestOutputDirectory.
Above doesn’t work for 2019.3+ unfortunately (due to mainTemplate now being :unityLibrary). If anyone knows how to modify LibraryManifest / merged manifest in 2019.3+ please let me know.
Okay, so after lots of trial and errors, I’ve managed to use same groovy script for the application as a whole.
In newer versions (2019.3+) according application section has been moved to LauncherManifest.xml.
And as a result - all logic for postprocessing of end APK / AAB should go to LauncherManifest.xml instead of mainTemplate.xml.
(LauncherManifest.xml can be generated automatically via Project Settings → Player → Publishing Settings → Custom Launcher Gradle Template, don’t forget to remove comment at the top of the file)
Above scripts work just fine, exactly the same way as in prior Unity versions.
For those who investigates this topic.
In the UnityEditor.Android.Extensions assembly there is a UnityEditor.Android.PostProcessor.Tasks.GenerateManifest class which patches the AndroidManifest.xml in the Temp/StaginArea folder during the build and adds launchMode="singleTask".
Just be aware, some devices does not treat different launchMode properly.
Which may cause ANR’s and crashes [Mainly caused by jobs & graphics issues].
We’ve decided not to change it at all once this cropped up and reverted back to singleTask.
While it is great to fix broken SDK’s as a last measure workaround, crashes that degrade store rating make it not worth overall.
Plus, most of the issues related to the invalid use of intent callbacks turned out to be quite fixable, once we’ve communicated with the SDK’s developers. (Such as issues with ads skips, etc)
So unless its properly supported by Unity in future, I’d suggest keeping launchMode as is.
Here is an improved version so as not to modify all the android:launchMode of the file.
// Import classes from the Groovy XML library
import groovy.xml.XmlUtil
import groovy.xml.XmlParser
// This section is for code generated by Unity in the launcherTemplate.
// ...
// End of Unity generated code.
// Iterate over all application variants. This is typical in Android build scripts where you might have different variants like debug, release, etc.
android.applicationVariants.all { variant ->
// Iterate over each output of the variant. There could be multiple outputs for a variant.
variant.outputs.each { output ->
// Get the process manifest provider for the output and apply a closure in the last execution phase.
def processManifest = output.getProcessManifestProvider().get()
processManifest.doLast { task ->
// Get the directory for the output manifest files.
def outputDir = task.getMultiApkManifestOutputDirectory()
File outputDirectory
// Check the type of outputDir and assign it to outputDirectory.
if (outputDir instanceof File) {
outputDirectory = outputDir
} else {
outputDirectory = outputDir.get().asFile
}
// Define the path to the AndroidManifest.xml file in the output directory.
File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
// Parse the AndroidManifest.xml file using XmlSlurper, which is a lazy parser. Declare the namespace for Android.
def manifest = new XmlSlurper(false, true)
.parse(manifestOutFile)
.declareNamespace(android: 'http://schemas.android.com/apk/res/android')
// Get all activity nodes from the parsed manifest.
def activities = manifest.application.activity;
// Iterate over each activity node.
activities.each { activity ->
// If the activity's name is UnityPlayerActivity, set its launchMode to 'standard'.
if(activity.'@android:name' == "com.unity3d.player.UnityPlayerActivity")
activity.'@android:launchMode' = 'standard'
}
// Serialize the modified XML and write it back to the AndroidManifest.xml file.
manifestOutFile.text = XmlUtil.serialize(manifest)
}
}
}
/// <summary>
/// 将启动Activity的LaunchMode改为Standard
/// </summary>
public class StandardLaunchModeAndroidBuild : IPostGenerateGradleAndroidProject
{
private const string Tag = "[standardLaunchModeAndroidBuild]";
public int callbackOrder => 0;
public void OnPostGenerateGradleAndroidProject(string path)
{
Debug.Log(Tag + $" start.");
Debug.Log(Tag + "unityLibraryPath : " + path);
var manifestPath = Path.Combine(path, "src/main/AndroidManifest.xml");
var xmlDoc = new XmlDocument();
xmlDoc.Load(manifestPath);
var activityNodes = xmlDoc.SelectNodes("/manifest/application/activity") ?? throw new InvalidDataException("Illegal xml.");
for (var i = 0; i < activityNodes.Count; i++)
{
var activityNode = activityNodes[i];
var nameAttribute = activityNode.Attributes!["android:name"];
if (nameAttribute == null) continue;
// if launcher Activity is not UnityPlayerActivity, change to actual Activity.
// TODO compat
if (nameAttribute.Value != "com.unity3d.player.UnityPlayerActivity") continue;
var launchAttribute = activityNode.Attributes!["android:launchMode"];
if (launchAttribute != null)
{
launchAttribute.Value = "standard";
Debug.Log(Tag + $"set launchMode to standard success.");
}
Debug.Log(Tag + $" completed.");
break;
}
xmlDoc.Save(manifestPath);
Debug.Log(Tag + $" completed.");
}
}
advantage:
use C# in Unity
Not affected by the gradle version (the code previously modified in gradle is different on gradle>=7.2.0 and gradle <7.2.0.)
shortcoming:
The xml file will be reformatted.
Comments will be deleted. (Is there any way to keep it?)