Skip to content

Commit

Permalink
Add is-launcher to URL Argument processing for Android
Browse files Browse the repository at this point in the history
  • Loading branch information
jhugman committed Jul 28, 2023
1 parent 3aa422e commit 32fc1a5
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ private const val NIMBUS_FLAG = "nimbus-cli"
private const val EXPERIMENTS_KEY = "experiments"
private const val LOG_STATE_KEY = "log-state"
private const val RESET_DB_KEY = "reset-db"
private const val IS_LAUNCHER_KEY = "is-launcher"
private const val VERSION_KEY = "version"
private const val DATA_KEY = "data"

Expand Down Expand Up @@ -46,6 +47,11 @@ fun NimbusInterface.initializeTooling(context: Context, intent: Intent) {
if (args.logState) {
dumpStateToLog()
}

if (args.isLauncher) {
intent.action = Intent.ACTION_MAIN
intent.addCategory(Intent.CATEGORY_LAUNCHER)
}
}

@Suppress("ReturnCount")
Expand All @@ -67,7 +73,7 @@ private fun createCommandLineArgs(intent: Intent): CliArgs? {
val resetDatabase = intent.getBooleanExtra(RESET_DB_KEY, false)
val logState = intent.getBooleanExtra(LOG_STATE_KEY, false)

return check(CliArgs(resetDatabase, experiments, logState))
return check(CliArgs(resetDatabase, experiments, logState, false))
}

@Suppress("ReturnCount")
Expand Down Expand Up @@ -105,8 +111,9 @@ fun createCommandLineArgs(uri: Uri): CliArgs? {
val experiments = uri.getQueryParameter("--$EXPERIMENTS_KEY")
val resetDatabase = uri.getBooleanQueryParameter("--$RESET_DB_KEY", false)
val logState = uri.getBooleanQueryParameter("--$LOG_STATE_KEY", false)
val isLauncher = uri.getBooleanQueryParameter("--$IS_LAUNCHER_KEY", false)

return check(CliArgs(resetDatabase, experiments, logState))
return check(CliArgs(resetDatabase, experiments, logState, isLauncher))
}

data class CliArgs(val resetDatabase: Boolean, val experiments: String?, val logState: Boolean)
data class CliArgs(val resetDatabase: Boolean, val experiments: String?, val logState: Boolean, val isLauncher: Boolean)
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,27 @@ class ArgumentProcessorTest {
Uri.parse("my-app://foo?--nimbus-cli"),
)
assertNotNull(obs)
assertEquals(CliArgs(false, null, false), obs)
assertEquals(CliArgs(false, null, false, false), obs)

val obs1 = createCommandLineArgs(
Uri.parse("my-app://foo?--nimbus-cli&--reset-db"),
)
assertEquals(CliArgs(true, null, false), obs1)
assertEquals(CliArgs(true, null, false, false), obs1)

val obs2 = createCommandLineArgs(
Uri.parse("my-app://foo?--reset-db&--nimbus-cli&--log-state"),
)
assertEquals(CliArgs(true, null, true), obs2)
assertEquals(CliArgs(true, null, true, false), obs2)

val obs3 = createCommandLineArgs(
Uri.parse("my-app://foo?--nimbus-cli=true&--reset-db=1&--log-state"),
)
assertEquals(CliArgs(true, null, true), obs3)
assertEquals(CliArgs(true, null, true, false), obs3)

val obs4 = createCommandLineArgs(
Uri.parse("my-app://foo?--nimbus-cli&--reset-db=0&--log-state=false"),
)
assertEquals(CliArgs(false, null, false), obs4)
assertEquals(CliArgs(false, null, false, false), obs4)
}

@Test
Expand All @@ -51,7 +51,7 @@ class ArgumentProcessorTest {
Uri.parse("my-app://foo?--nimbus-cli&--experiments=$unenrollAll"),
)
assertNotNull(obs)
assertEquals(CliArgs(false, unenrollAll, false), obs)
assertEquals(CliArgs(false, unenrollAll, false, false), obs)

val encoded = URLEncoder.encode(unenrollAll, "UTF-8")
assertNotEquals(encoded, unenrollAll)
Expand All @@ -60,7 +60,7 @@ class ArgumentProcessorTest {
Uri.parse("my-app://foo?--nimbus-cli&--experiments=$encoded"),
)
assertNotNull(obs1)
assertEquals(CliArgs(false, unenrollAll, false), obs1)
assertEquals(CliArgs(false, unenrollAll, false, false), obs1)
}

@Test
Expand All @@ -70,7 +70,7 @@ class ArgumentProcessorTest {
Uri.parse("my-app://foo?--nimbus-cli&--experiments=$good"),
)
assertNotNull(obs)
assertEquals(CliArgs(false, good, false), obs)
assertEquals(CliArgs(false, good, false, false), obs)

fun isInvalid(bad: String) {
val obs0 = createCommandLineArgs(
Expand Down Expand Up @@ -121,4 +121,12 @@ class ArgumentProcessorTest {

isNotForNimbus("https://example.com/webpage")
}

@Test
fun `test from Rust`() {
val url = "fenix-dev://open?--nimbus-cli&--experiments=%7B%22data%22%3A[%7B%22appId%22%3A%22org.mozilla.firefox%22,%22appName%22%3A%22fenix%22,%22application%22%3A%22org.mozilla.firefox%22,%22arguments%22%3A%7B%7D,%22branches%22%3A[%7B%22feature%22%3A%7B%22enabled%22%3Afalse,%22featureId%22%3A%22this-is-included-for-mobile-pre-96-support%22,%22value%22%3A%7B%7D%7D,%22features%22%3A[%7B%22enabled%22%3Atrue,%22featureId%22%3A%22juno-onboarding%22,%22value%22%3A%7B%22enabled%22%3Atrue%7D%7D],%22ratio%22%3A0,%22slug%22%3A%22control%22%7D,%7B%22feature%22%3A%7B%22enabled%22%3Afalse,%22featureId%22%3A%22this-is-included-for-mobile-pre-96-support%22,%22value%22%3A%7B%7D%7D,%22features%22%3A[%7B%22enabled%22%3Atrue,%22featureId%22%3A%22juno-onboarding%22,%22value%22%3A%7B%22cards%22%3A%7B%22default-browser%22%3A%7B%22body%22%3A%22Nimm%20nicht%20das%20Erstbeste,%20sondern%20das%20Beste%20f%C3%BCr%20dich%3A%20Firefox%20sch%C3%BCtzt%20deine%20Privatsph%C3%A4re.\n\nLies%20unseren%20Datenschutzhinweis.%22,%22image-res%22%3A%22onboarding_ctd_default_browser%22,%22link-text%22%3A%22Datenschutzhinweis%22,%22title%22%3A%22Du%20entscheidest,%20was%20Standard%20ist%22%7D,%22notification-permission%22%3A%7B%22body%22%3A%22Benachrichtigungen%20helfen%20dabei,%20Downloads%20zu%20managen%20und%20Tabs%20zwischen%20Ger%C3%A4ten%20zu%20senden.%22,%22image-res%22%3A%22onboarding_ctd_notification%22,%22title%22%3A%22Du%20bestimmst,%20was%20Firefox%20kann%22%7D,%22sync-sign-in%22%3A%7B%22body%22%3A%22Wenn%20du%20willst,%20bringt%20Firefox%20deine%20Tabs%20und%20Passw%C3%B6rter%20auf%20all%20deine%20Ger%C3%A4te.%22,%22image-res%22%3A%22onboarding_ctd_sync%22,%22title%22%3A%22Alles%20ist%20dort,%20wo%20du%20es%20brauchst%22%7D%7D,%22enabled%22%3Atrue%7D%7D],%22ratio%22%3A100,%22slug%22%3A%22treatment-a%22%7D],%22bucketConfig%22%3A%7B%22count%22%3A10000,%22namespace%22%3A%22fenix-juno-onboarding-release-3%22,%22randomizationUnit%22%3A%22nimbus_id%22,%22start%22%3A0,%22total%22%3A10000%7D,%22channel%22%3A%22developer%22,%22endDate%22%3Anull,%22enrollmentEndDate%22%3A%222023-07-18%22,%22featureIds%22%3A[%22juno-onboarding%22],%22featureValidationOptOut%22%3Afalse,%22id%22%3A%22on-boarding-challenge-the-default%22,%22isEnrollmentPaused%22%3Afalse,%22isRollout%22%3Afalse,%22locales%22%3Anull,%22localizations%22%3Anull,%22outcomes%22%3A[%7B%22priority%22%3A%22primary%22,%22slug%22%3A%22default-browser%22%7D],%22probeSets%22%3A[],%22proposedDuration%22%3A30,%22proposedEnrollment%22%3A14,%22referenceBranch%22%3A%22control%22,%22schemaVersion%22%3A%221.12.0%22,%22slug%22%3A%22on-boarding-challenge-the-default%22,%22startDate%22%3A%222023-06-21%22,%22targeting%22%3A%22true%22,%22userFacingDescription%22%3A%22Testing%20copy%20and%20images%20in%20the%20first%20run%20onboarding%20that%20is%20consistent%20with%20marketing%20messaging.%22,%22userFacingName%22%3A%22On-boarding%20Challenge%20the%20Default%22%7D]%7D&--reset-db&--log-state"
val observed = createCommandLineArgs(Uri.parse(url))
assertNotNull(observed)
assertNotNull(observed?.experiments)
}
}
23 changes: 21 additions & 2 deletions components/nimbus/ios/Nimbus/ArgumentProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ enum ArgumentProcessor {
if args.logState {
nimbus.dumpStateToLog()
}
// We have isLauncher here doing nothing; this is to match the Android implementation.
// There is nothing to do at this point, because we're unable to affect the flow of the app.
if args.isLauncher {
() // NOOP.
}
}

static func createCommandLineArgs(url: URL) -> CliArgs? {
Expand All @@ -34,6 +39,7 @@ enum ArgumentProcessor {
var experiments: String?
var resetDatabase = false
var logState = false
var isLauncher = false
var meantForUs = false

func flag(_ v: String?) -> Bool {
Expand All @@ -53,6 +59,8 @@ enum ArgumentProcessor {
resetDatabase = flag(item.value)
case "--log-state":
logState = flag(item.value)
case "--is-launcher":
isLauncher = flag(item.value)
default:
() // NOOP
}
Expand All @@ -62,7 +70,12 @@ enum ArgumentProcessor {
return nil
}

return check(args: CliArgs(resetDatabase: resetDatabase, experiments: experiments, logState: logState))
return check(args: CliArgs(
resetDatabase: resetDatabase,
experiments: experiments,
logState: logState,
isLauncher: isLauncher
))
}

static func createCommandLineArgs(args: [String]?) -> CliArgs? {
Expand Down Expand Up @@ -106,7 +119,12 @@ enum ArgumentProcessor {

let experiments = argMap["experiments"]

return check(args: CliArgs(resetDatabase: resetDatabase, experiments: experiments, logState: logState))
return check(args: CliArgs(
resetDatabase: resetDatabase,
experiments: experiments,
logState: logState,
isLauncher: false
))
}

static func check(args: CliArgs) -> CliArgs? {
Expand All @@ -124,6 +142,7 @@ struct CliArgs: Equatable {
let resetDatabase: Bool
let experiments: String?
let logState: Bool
let isLauncher: Bool
}

public extension NimbusInterface {
Expand Down
28 changes: 20 additions & 8 deletions components/support/nimbus-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,29 @@ use anyhow::{bail, Result};
impl TryFrom<&Cli> for LaunchableApp {
type Error = anyhow::Error;
fn try_from(value: &Cli) -> Result<Self> {
let device_id = value.device_id.clone();
Self::try_from_app_channel_device(
value.app.as_deref(),
value.channel.as_deref(),
value.device_id.as_deref(),
)
}
}

match (&value.app, &value.channel) {
impl LaunchableApp {
pub(crate) fn try_from_app_channel_device(
app: Option<&str>,
channel: Option<&str>,
device_id: Option<&str>,
) -> Result<Self> {
match (&app, &channel) {
(None, None) => anyhow::bail!("A value for --app and --channel must be specified. Supported apps are: fenix, focus_android, firefox_ios and focus_ios"),
(None, _) => anyhow::bail!("A value for --app must be specified. One of: fenix, focus_android, firefox_ios and focus_ios are currently supported"),
(_, None) => anyhow::bail!("A value for --channel must be specified. Supported channels are: developer, nightly, beta and release"),
_ => (),
}

let app = value.app.as_deref().unwrap();
let channel = value.channel.as_deref().unwrap();
let app = app.unwrap();
let channel = channel.unwrap();

let prefix = match app {
"fenix" => Some("org.mozilla"),
Expand Down Expand Up @@ -97,25 +109,25 @@ impl TryFrom<&Cli> for LaunchableApp {
("fenix", Some(prefix), Some(suffix)) => Self::Android {
package_name: format!("{}.{}", prefix, suffix),
activity_name: ".App".to_string(),
device_id,
device_id: device_id.map(str::to_string),
scheme,
open_deeplink: Some("open".to_string()),
},
("focus_android", Some(prefix), Some(suffix)) => Self::Android {
package_name: format!("{}.{}", prefix, suffix),
activity_name: "org.mozilla.focus.activity.MainActivity".to_string(),
device_id,
device_id: device_id.map(str::to_string),
scheme,
open_deeplink: None,
},
("firefox_ios", Some(prefix), Some(suffix)) => Self::Ios {
app_id: format!("{}.{}", prefix, suffix),
device_id: device_id.unwrap_or_else(|| "booted".to_string()),
device_id: device_id.unwrap_or("booted").to_string(),
scheme,
},
("focus_ios", Some(prefix), Some(suffix)) => Self::Ios {
app_id: format!("{}.{}", prefix, suffix),
device_id: device_id.unwrap_or_else(|| "booted".to_string()),
device_id: device_id.unwrap_or("booted").to_string(),
scheme,
},
_ => unreachable!(),
Expand Down
65 changes: 59 additions & 6 deletions components/support/nimbus-cli/src/output/deeplink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ impl LaunchableApp {
open: &AppOpenArgs,
) -> Result<String> {
let deeplink = match (&open.deeplink, self.app_opening_deeplink()) {
(Some(deeplink), _) => deeplink.as_ref(),
(_, Some(deeplink)) => deeplink,
(Some(deeplink), _) => deeplink.to_owned(),
(_, Some(deeplink)) => join_query(deeplink, "--nimbus-cli&--is-launcher"),
_ => anyhow::bail!("A deeplink must be provided"),
};

let url = longform_deeplink_url(deeplink, app_protocol)?;
let url = longform_deeplink_url(deeplink.as_str(), app_protocol)?;

self.prepend_scheme(url.as_str())
}
Expand Down Expand Up @@ -105,7 +105,10 @@ pub(crate) fn longform_deeplink_url(
return Ok(deeplink.to_string());
}

let mut parts: Vec<_> = vec!["--nimbus-cli".to_string()];
let mut parts: Vec<_> = Default::default();
if !deeplink.contains("--nimbus-cli") {
parts.push("--nimbus-cli".to_string());
}
if let Some(v) = experiments {
let json = serde_json::to_string(v)?;
let string = percent_encoding::utf8_percent_encode(&json, QUERY).to_string();
Expand All @@ -119,9 +122,12 @@ pub(crate) fn longform_deeplink_url(
parts.push("--log-state".to_string());
}

let suffix = if deeplink.contains('?') { '&' } else { '?' };
Ok(join_query(deeplink, &parts.join("&")))
}

Ok(format!("{deeplink}{suffix}{args}", args = parts.join("&")))
fn join_query(url: &str, item: &str) -> String {
let suffix = if url.contains('?') { '&' } else { '?' };
format!("{url}{suffix}{item}")
}

fn set_clipboard(contents: String) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Expand All @@ -133,6 +139,7 @@ fn set_clipboard(contents: String) -> Result<(), Box<dyn std::error::Error + Sen

#[cfg(test)]
mod unit_tests {

use super::*;
use serde_json::json;

Expand Down Expand Up @@ -211,4 +218,50 @@ mod unit_tests {

Ok(())
}

#[test]
fn test_deeplink_has_is_launcher_param_if_no_deeplink_is_specified() -> Result<()> {
let app =
LaunchableApp::try_from_app_channel_device(Some("fenix"), Some("developer"), None)?;

// No payload, or command line param for deeplink.
let payload: StartAppProtocol = Default::default();
let open: AppOpenArgs = Default::default();
assert_eq!(
"fenix-dev://open?--nimbus-cli&--is-launcher".to_string(),
app.longform_url(payload.clone(), &open)?
);

// A command line param for deeplink.
let open = AppOpenArgs {
deeplink: Some("deeplink".to_string()),
..Default::default()
};
assert_eq!(
"fenix-dev://deeplink".to_string(),
app.longform_url(payload, &open)?
);

// A parameter from the payload, but no deeplink.
let payload = StartAppProtocol {
log_state: true,
..Default::default()
};
assert_eq!(
"fenix-dev://open?--nimbus-cli&--is-launcher&--log-state".to_string(),
app.longform_url(payload.clone(), &Default::default())?
);

// A deeplink from the command line, and an extra param from the payload.
let open = AppOpenArgs {
deeplink: Some("deeplink".to_string()),
..Default::default()
};
assert_eq!(
"fenix-dev://deeplink?--nimbus-cli&--log-state".to_string(),
app.longform_url(payload, &open)?
);

Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@ final class NimbusArgumentProcessorTests: XCTestCase {

XCTAssertEqual(
ArgumentProcessor.createCommandLineArgs(args: ["--nimbus-cli", "--version", "1", "--experiments", unenrollExperiments]),
CliArgs(resetDatabase: false, experiments: unenrollExperiments, logState: false)
CliArgs(resetDatabase: false, experiments: unenrollExperiments, logState: false, isLauncher: false)
)

XCTAssertEqual(
ArgumentProcessor.createCommandLineArgs(args: ["--nimbus-cli", "--version", "1", "--experiments", unenrollExperiments, "--reset-db"]),
CliArgs(resetDatabase: true, experiments: unenrollExperiments, logState: false)
CliArgs(resetDatabase: true, experiments: unenrollExperiments, logState: false, isLauncher: false)
)

XCTAssertEqual(
ArgumentProcessor.createCommandLineArgs(args: ["--nimbus-cli", "--version", "1", "--reset-db"]),
CliArgs(resetDatabase: true, experiments: nil, logState: false)
CliArgs(resetDatabase: true, experiments: nil, logState: false, isLauncher: false)
)

XCTAssertEqual(
ArgumentProcessor.createCommandLineArgs(args: ["--nimbus-cli", "--version", "1", "--log-state"]),
CliArgs(resetDatabase: false, experiments: nil, logState: true)
CliArgs(resetDatabase: false, experiments: nil, logState: true, isLauncher: false)
)
}

Expand All @@ -56,15 +56,19 @@ final class NimbusArgumentProcessorTests: XCTestCase {
let percentEncoded = experiments.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics)!
let arg0 = ArgumentProcessor.createCommandLineArgs(url: URL(string: "my-app://deeplink?--nimbus-cli&--experiments=\(percentEncoded)&--reset-db")!)
XCTAssertNotNil(arg0)
XCTAssertEqual(arg0, CliArgs(resetDatabase: true, experiments: experiments, logState: false))
XCTAssertEqual(arg0, CliArgs(resetDatabase: true, experiments: experiments, logState: false, isLauncher: false))

let arg1 = ArgumentProcessor.createCommandLineArgs(url: URL(string: "my-app://deeplink?--nimbus-cli=1&--experiments=\(percentEncoded)&--reset-db=1")!)
XCTAssertNotNil(arg1)
XCTAssertEqual(arg1, CliArgs(resetDatabase: true, experiments: experiments, logState: false))
XCTAssertEqual(arg1, CliArgs(resetDatabase: true, experiments: experiments, logState: false, isLauncher: false))

let arg2 = ArgumentProcessor.createCommandLineArgs(url: URL(string: "my-app://deeplink?--nimbus-cli=true&--experiments=\(percentEncoded)&--reset-db=true")!)
XCTAssertNotNil(arg1)
XCTAssertEqual(arg1, CliArgs(resetDatabase: true, experiments: experiments, logState: false))
XCTAssertNotNil(arg2)
XCTAssertEqual(arg2, CliArgs(resetDatabase: true, experiments: experiments, logState: false, isLauncher: false))

let arg3 = ArgumentProcessor.createCommandLineArgs(url: URL(string: "my-app://deeplink?--nimbus-cli&--is-launcher")!)
XCTAssertNotNil(arg3)
XCTAssertEqual(arg3, CliArgs(resetDatabase: false, experiments: nil, logState: false, isLauncher: true))

let httpArgs = ArgumentProcessor.createCommandLineArgs(url: URL(string: "https://example.com?--nimbus-cli=true&--experiments=\(percentEncoded)&--reset-db=true")!)
XCTAssertNil(httpArgs)
Expand Down

0 comments on commit 32fc1a5

Please sign in to comment.