Search Unity

android:LaunchMode. Why does Unity have it as singleTask?

Discussion in 'Android' started by xVergilx, Oct 22, 2020.

  1. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    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?
     
    AntonPetrov likes this.
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    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.
     
  3. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    If there's anyone interested in the same hack:
    1. Modify AndroidManifest.xml UnityActivity to have these parameters:
    Code (CSharp):
    1.     <activity android:name="com.unity3d.player.UnityPlayerActivity"
    2.               android:label="@string/app_name"
    3.               android:alwaysRetainTaskState="true"
    4.               android:clearTaskOnLaunch="false"
    5.               android:launchMode="standard">
    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):
    Code (CSharp):
    1. // Override LibraryManifest.xml values and switch launchMode to standard
    2. android.applicationVariants.all { variant ->
    3.     variant.outputs.each { output ->
    4.         def processManifest = output.getProcessManifestProvider().get()
    5.         processManifest.doLast { task ->
    6.             def outputDir = task.getManifestOutputDirectory()
    7.             File outputDirectory
    8.             if (outputDir instanceof File) {
    9.                 outputDirectory = outputDir
    10.             } else {
    11.                 outputDirectory = outputDir.get().asFile
    12.             }
    13.    
    14.             File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
    15.  
    16.             if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
    17.                 def newManifest = manifestOutFile.getText().replace("android:launchMode=\"singleTask\"", "android:launchMode=\"standard\"")
    18.                 manifestOutFile.write(newManifest, 'UTF-8')
    19.             }
    20.    
    21.             // Make sure to modify bundle_manifest as well
    22.             outputDir = task.getBundleManifestOutputDirectory();
    23.    
    24.             if (outputDir instanceof File) {
    25.                 outputDirectory = outputDir
    26.             } else {
    27.                 outputDirectory = outputDir.get().asFile
    28.             }
    29.                    
    30.             manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
    31.  
    32.             if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
    33.                 def bundleManifest = manifestOutFile.getText().replace("android:launchMode=\"singleTask\"", "android:launchMode=\"standard\"")
    34.                 manifestOutFile.write(bundleManifest, 'UTF-8')
    35.             }
    36.         }
    37.     }
    38. }
    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.
     
    Last edited: Oct 26, 2020
    dyguests, Jumeuan and cn_zxcv like this.
  4. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    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.

    (Updated post above)
     
  5. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    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.


    Solved, see post bellow.
     
    Last edited: Nov 26, 2020
  6. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    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.
     
    Last edited: Nov 26, 2020
    AntonPetrov and cn_zxcv like this.
  7. AntonPetrov

    AntonPetrov

    Joined:
    Dec 27, 2013
    Posts:
    63
    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"
    .
     
  8. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    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.
     
  9. dyguests

    dyguests

    Joined:
    May 4, 2015
    Posts:
    16
    It save my life.

    But, Is there any others problem after remove singleTask?

    -------------------
    Edit at 2024/2/29

    Yes, there is some other problem!

    If change `singleTask` to `standard`, In Android, Clicking on a system notification to jump to the app will change the logic.

    - singleTask : works fine.
    - standard : (crash? then) restart the app.

    So,Is there a better solution?
     
    Last edited: Feb 29, 2024
  10. dyguests

    dyguests

    Joined:
    May 4, 2015
    Posts:
    16
    The code seems be added in `launcherTemplate.gradle` not `mainTemplate` ?
     
  11. dyguests

    dyguests

    Joined:
    May 4, 2015
    Posts:
    16
    There is a hack code for:
    • Unity >= 2022.3
    • Gradle >= 7.2.0
    launcherTemplate.gradle

    Code (CSharp):
    1. android.applicationVariants.all { variant ->
    2.     variant.outputs.each { output ->
    3.         def processManifest = output.getProcessManifestProvider().get()
    4.         processManifest.doLast { task ->
    5.             // def outputDir = task.getManifestOutputDirectory()
    6.             def outputDir = task.multiApkManifestOutputDirectory
    7.             File outputDirectory
    8.             if (outputDir instanceof File) {
    9.                 outputDirectory = outputDir
    10.             } else {
    11.                 outputDirectory = outputDir.get().asFile
    12.             }
    13.  
    14.             File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
    15.  
    16.             if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
    17.                 def newManifest = manifestOutFile.getText().replace("android:launchMode=\"singleTask\"", "android:launchMode=\"standard\"")
    18.                 manifestOutFile.write(newManifest, 'UTF-8')
    19.             }
    20.  
    21.             // Make sure to modify bundle_manifest as well
    22.             // outputDir = task.getBundleManifestOutputDirectory();
    23.             outputDir = task.multiApkManifestOutputDirectory
    24.  
    25.             if (outputDir instanceof File) {
    26.                 outputDirectory = outputDir
    27.             } else {
    28.                 outputDirectory = outputDir.get().asFile
    29.             }
    30.  
    31.             manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
    32.  
    33.             if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
    34.                 def bundleManifest = manifestOutFile.getText().replace("android:launchMode=\"singleTask\"", "android:launchMode=\"standard\"")
    35.                 manifestOutFile.write(bundleManifest, 'UTF-8')
    36.             }
    37.         }
    38.     }
    39. }
     
  12. korypse

    korypse

    Joined:
    Sep 16, 2017
    Posts:
    4
    Here is an improved version so as not to modify all the android:launchMode of the file.

    Code (CSharp):
    1. // Import classes from the Groovy XML library
    2. import groovy.xml.XmlUtil
    3. import groovy.xml.XmlParser
    4.  
    5. // This section is for code generated by Unity in the launcherTemplate.
    6. // ...
    7.  
    8. // End of Unity generated code.
    9.  
    10. // Iterate over all application variants. This is typical in Android build scripts where you might have different variants like debug, release, etc.
    11. android.applicationVariants.all { variant ->
    12.     // Iterate over each output of the variant. There could be multiple outputs for a variant.
    13.     variant.outputs.each { output ->
    14.         // Get the process manifest provider for the output and apply a closure in the last execution phase.
    15.         def processManifest = output.getProcessManifestProvider().get()
    16.         processManifest.doLast { task ->
    17.             // Get the directory for the output manifest files.
    18.             def outputDir = task.getMultiApkManifestOutputDirectory()
    19.             File outputDirectory
    20.             // Check the type of outputDir and assign it to outputDirectory.
    21.             if (outputDir instanceof File) {
    22.                 outputDirectory = outputDir
    23.             } else {
    24.                 outputDirectory = outputDir.get().asFile
    25.             }
    26.  
    27.             // Define the path to the AndroidManifest.xml file in the output directory.
    28.             File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
    29.  
    30.             // Parse the AndroidManifest.xml file using XmlSlurper, which is a lazy parser. Declare the namespace for Android.
    31.             def manifest = new XmlSlurper(false, true)
    32.                             .parse(manifestOutFile)
    33.                             .declareNamespace(android: 'http://schemas.android.com/apk/res/android')
    34.             // Get all activity nodes from the parsed manifest.
    35.             def activities =  manifest.application.activity;
    36.             // Iterate over each activity node.
    37.             activities.each { activity ->
    38.                 // If the activity's name is UnityPlayerActivity, set its launchMode to 'standard'.
    39.                 if(activity.'@android:name' == "com.unity3d.player.UnityPlayerActivity")
    40.                     activity.'@android:launchMode' = 'standard'
    41.             }
    42.             // Serialize the modified XML and write it back to the AndroidManifest.xml file.
    43.             manifestOutFile.text = XmlUtil.serialize(manifest)
    44.         }
    45.     }
    46. }
     
  13. dyguests

    dyguests

    Joined:
    May 4, 2015
    Posts:
    16
    I got a better solution:

    @korypse

    Code (CSharp):
    1.     /// <summary>
    2.     /// 将启动Activity的LaunchMode改为Standard
    3.     /// </summary>
    4.     public class StandardLaunchModeAndroidBuild : IPostGenerateGradleAndroidProject
    5.     {
    6.         private const string Tag = "[standardLaunchModeAndroidBuild]";
    7.  
    8.         public int callbackOrder => 0;
    9.  
    10.         public void OnPostGenerateGradleAndroidProject(string path)
    11.         {
    12.             Debug.Log(Tag + $" start.");
    13.  
    14.             Debug.Log(Tag + "unityLibraryPath : " + path);
    15.             var manifestPath = Path.Combine(path, "src/main/AndroidManifest.xml");
    16.  
    17.             var xmlDoc = new XmlDocument();
    18.             xmlDoc.Load(manifestPath);
    19.  
    20.             var activityNodes = xmlDoc.SelectNodes("/manifest/application/activity") ?? throw new InvalidDataException("Illegal xml.");
    21.             for (var i = 0; i < activityNodes.Count; i++)
    22.             {
    23.                 var activityNode = activityNodes[i];
    24.                 var nameAttribute = activityNode.Attributes!["android:name"];
    25.                 if (nameAttribute == null) continue;
    26.                 // if launcher Activity is not UnityPlayerActivity, change to actual Activity.
    27.                 // TODO compat
    28.                 if (nameAttribute.Value != "com.unity3d.player.UnityPlayerActivity") continue;
    29.  
    30.                 var launchAttribute = activityNode.Attributes!["android:launchMode"];
    31.                 if (launchAttribute != null)
    32.                 {
    33.                     launchAttribute.Value = "standard";
    34.                     Debug.Log(Tag + $"set launchMode to standard success.");
    35.                 }
    36.  
    37.                 Debug.Log(Tag + $" completed.");
    38.  
    39.                 break;
    40.             }
    41.  
    42.             xmlDoc.Save(manifestPath);
    43.  
    44.             Debug.Log(Tag + $" completed.");
    45.         }
    46.     }
    advantage:

    1. use C# in Unity
    2. 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:

    1. The xml file will be reformatted.
    2. Comments will be deleted. (Is there any way to keep it?)