Fix Android Launch From Home
note
This documentation is a work in progress. It should suffice to fix this particular issue but additional changes are likely to be made.
Android Launch From Home
Cordova and Capacitor applications, when launched on an Android device, will startup and display the webview your application is served in. However, for Auth Connect this may not be the behavior you want when a user is on the login page: you will likely want your application to return to the login page.
To make this behavior possible you'll need to alter your Cordova or Capacitor application using the following instructions.
Cordova
There are a few steps to apply on a Cordova project.
- Create a
scripts
folder in your project. - Create a file called
LaunchActivity.java
in thescripts
folder and paste below in the contents. - Replace the line
package io.ionic.starter
with the id from yourconfig.xml
package io.ionic.starter;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class LauncherActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent i = new Intent(this, MainActivity.class);
i.replaceExtras(this.getIntent());
startActivity(i);
finish();
}
}
- Create a file called
update-launcher.js
in thescripts
folder and paste the below file - Set the path of
LAUNCHER_DESTINATION
inupdate-launcher.js
to match your id fromconfig.xml
var ANDROID_PROJECT_ROOT = "platforms/android/app/src/main";
var LAUNCHER_DESTINATION =
"platforms/android/app/src/main/java/io/ionic/starter";
var LAUNCHER_SOURCE = "scripts/LauncherActivity.java";
var fs = require("fs");
var path = require("path");
var xml2js = require("xml2js");
function copyJavaLauncher() {
return new Promise((resolve, reject) => {
fs.access(LAUNCHER_DESTINATION, (err) => {
if (err) {
reject("INVALID LAUNCHER DESTINATION");
return;
}
copyFile(
LAUNCHER_SOURCE,
path.join(LAUNCHER_DESTINATION, "LauncherActivity.java")
);
});
});
}
function copyFile(src, dest) {
return new Promise((resolve, reject) => {
let readStream = fs.createReadStream(src);
readStream.once("error", (err) => {
reject(err);
});
readStream.once("end", () => {
resolve("done");
});
readStream.pipe(fs.createWriteStream(dest));
});
}
function readManifest() {
return new Promise((resolve, reject) => {
fs.readFile(
path.join(ANDROID_PROJECT_ROOT, "AndroidManifest.xml"),
"utf-8",
(err, input) => {
if (!!err) {
reject(err);
} else {
resolve(input);
}
}
);
});
}
function writeManifest(data) {
return new Promise((resolve, reject) => {
fs.writeFile(
path.join(ANDROID_PROJECT_ROOT, "AndroidManifest.xml"),
data,
(err) => {
if (!!err) {
reject(err);
} else {
resolve();
}
}
);
});
}
function convertToJson(input) {
return new Promise((resolve, reject) => {
xml2js.parseString(input, (err, result) => {
if (!!err) {
reject(err);
} else {
resolve(result);
}
});
});
}
function convertToXML(input) {
return new Promise((resolve, reject) => {
let builder = new xml2js.Builder();
let xml = builder.buildObject(input);
resolve(xml);
});
}
function removeLegacyActivityIntent(data) {
return new Promise((resolve, reject) => {
let applications = data.manifest.application;
if (!applications) {
reject();
return;
}
applications.forEach((application) => {
if (!!application.activity) {
application.activity.forEach((activity) => {
if (activity["intent-filter"]) {
activity["intent-filter"].forEach((intent, idx) => {
let shouldRemove = false;
if (intent.action) {
intent.action.forEach((action) => {
if (action["$"]["android:name"].includes("MAIN")) {
shouldRemove = true;
}
});
}
if (shouldRemove) {
delete activity["intent-filter"][idx];
}
});
}
});
}
});
resolve(data);
});
}
function addLauncherActivityIntent(data) {
return new Promise((resolve, reject) => {
let applications = data.manifest.application;
if (!applications) {
reject();
return;
}
applications.forEach((application) => {
if (typeof application.activity === "undefined") {
application.activity = [];
}
application.activity.push({
$: {
"android:name": "LauncherActivity",
"android:label": "@string/app_name",
"android:theme": "@android:style/Theme.DeviceDefault.NoActionBar",
},
"intent-filter": [
{
action: {
$: {
"android:name": "android.intent.action.MAIN",
},
},
category: {
$: {
"android:name": "android.intent.category.LAUNCHER",
},
},
},
],
});
});
resolve(data);
});
}
module.exports = function (context) {
return new Promise((resolve, reject) => {
readManifest()
.then((input) => convertToJson(input))
.then((data) => removeLegacyActivityIntent(data))
.then((data) => addLauncherActivityIntent(data))
.then((data) => convertToXML(data))
.then((input) => writeManifest(input))
.then(() => copyJavaLauncher())
.then((data) => {
resolve("done");
})
.catch((err) => {
console.log(err);
reject("done");
});
});
};
- Next step is to install xml2js with:
npm install xml2js --save-dev
Finally update config.xml
by adding the following line underneath <platform name="android">
:
<hook src="scripts/update-launcher.js" type="after_platform_add" />
This script will run when you add the Android platform is added so to test it you can run:
ionic cordova platform remove android
ionic cordova platform add android
note
Be sure that the package line in LaunchActivity.java
and the LAUNCHER_DESTINATION
match what is used for your id
in config.xml
.
Capacitor
In your Capacitor project create a new activity with the following code replacing io.ionic.starter
with your projects identifier.
package io.ionic.starter;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class LauncherActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent i = new Intent(this, MainActivity.class);
i.replaceExtras(this.getIntent());
startActivity(i);
finish();
}
}
Update the AndroidManifest.xml
to set the new Activity as the launcher activity, and keep the MainActivity launchMode as singleTask
note
make sure to remove the intent-filter on the MainActivity that defines it as the launcher
<activity android:name="io.ionic.starter.LauncherActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="io.ionic.starter.MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="my-scheme" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/*"/>
</intent-filter>
</activity>
Be sure to run npx cap sync
after applying these changes.
Testing
To test that you have correctly applied these changes:
- Launch your application with an Android device
- Go to the login page
- Return to the home screen of your Android device
- Launch your app again by pressing the icon
- Your application should now open displaying the login page in the state it was left in