52 | git rebase -i --autosquash master
53 | git push --force-with-lease origin my-new-feature
54 | ```
55 |
56 | Be sure to add a comment to the PR indicating your new changes are ready to review, as GitHub does not generate a
57 | notification when you git push.
58 |
59 | ### Code Style
60 | The main code of Captive Web View is written in Kotlin, Swift, and ES6
61 | JavaScript. There is also an HTTP server, based on the version 3 Python module.
62 | The Python code makes use of the PEP 8 style guidelines.
63 |
64 | ### Formatting Commit Messages
65 |
66 | We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/).
67 |
68 | Be sure to include any related GitHub issue references in the commit message. See
69 | [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues
70 | and commits.
71 |
72 | ## Reporting Bugs and Creating Issues
73 |
74 | When opening a new issue, try to roughly follow the commit message format conventions above.
75 |
--------------------------------------------------------------------------------
/forApple/Captivity/Captivity/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Omnissa, LLC.
2 | // SPDX-License-Identifier: BSD-2-Clause
3 |
4 | import UIKit
5 | import CaptiveWebView
6 |
7 | @UIApplicationMain
8 | class AppDelegate: CaptiveWebView.ApplicationDelegate {
9 |
10 | // var window: UIWindow?
11 |
12 |
13 | func application(
14 | _ application: UIApplication,
15 | didFinishLaunchingWithOptions launchOptions:
16 | [UIApplication.LaunchOptionsKey: Any]?
17 | ) -> Bool
18 | {
19 | CaptiveWebView.DefaultViewController
20 | .viewControllerMap.merge([
21 | "Secondary": SecondaryViewController.self,
22 | "Spinner": SpinnerViewController.self
23 | ], uniquingKeysWith: {(first, _) in first})
24 | self.launch(MainViewController.self)
25 | // Override point for customization after application launch.
26 | return true
27 | }
28 |
29 | func applicationWillResignActive(_ application: UIApplication) {
30 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
31 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
32 | }
33 |
34 | func applicationDidEnterBackground(_ application: UIApplication) {
35 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
36 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
37 | }
38 |
39 | func applicationWillEnterForeground(_ application: UIApplication) {
40 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
41 | }
42 |
43 | func applicationDidBecomeActive(_ application: UIApplication) {
44 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
45 | }
46 |
47 | func applicationWillTerminate(_ application: UIApplication) {
48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
49 | }
50 |
51 |
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/forAndroid/captivewebview/build.gradle:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Omnissa, LLC.
2 | // SPDX-License-Identifier: BSD-2-Clause
3 |
4 | plugins {
5 | id 'com.android.library'
6 | id 'kotlin-android'
7 | id 'maven-publish'
8 |
9 | // The dokka plugin requires a version or it won't be found.
10 | // Adds the documentation/dokka task.
11 | // id 'org.jetbrains.dokka' version '0.9.18'
12 | id 'org.jetbrains.dokka-android' version '0.9.18'
13 | }
14 |
15 | android {
16 | namespace 'com.example.captivewebview'
17 | compileSdk 33
18 |
19 | sourceSets {
20 | main.assets.srcDirs += new RelativePath(
21 | false, "Sources", "CaptiveWebView", "Resources"
22 | ).getFile(buildscript.sourceFile.parentFile.parentFile.parentFile)
23 | }
24 |
25 | defaultConfig {
26 | minSdk 29
27 | targetSdk 33
28 | versionCode 1
29 | versionName captiveWebViewVersion
30 | }
31 |
32 | buildTypes {
33 | release {
34 | minifyEnabled false
35 | proguardFiles getDefaultProguardFile(
36 | 'proguard-android-optimize.txt'), 'proguard-rules.pro'
37 | }
38 | }
39 |
40 | }
41 |
42 | dependencies {
43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
44 | implementation 'androidx.webkit:webkit:1.7.0'
45 | }
46 |
47 | // Reference for Maven publication of Android library.
48 | // https://developer.android.com/studio/publish-library/upload-library
49 | //
50 | // The `publish` Gradle task seems to do what the old `uploadArchives` task did.
51 | publishing {
52 | publications {
53 | release(MavenPublication) {
54 | groupId = 'com.example.captivewebview'
55 | artifactId = 'captivewebview'
56 | version = captiveWebViewVersion
57 | afterEvaluate {
58 | from components.release
59 | }
60 | }
61 | }
62 | repositories {
63 | maven {
64 | url = uri(new File(rootDir, '../m2repository'))
65 | }
66 | }
67 | }
68 |
69 | // https://www.kotlinresources.com/library/dokka/
70 | // https://github.com/Kotlin/dokka/issues/224#issuecomment-383886215
71 |
72 | task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaAndroidTask) {
73 | outputFormat = 'javadoc'
74 | outputDirectory = "$buildDir/dokkaJavadoc"
75 | includes = ['src/documentation/readme.md', 'src/documentation/extra.md']
76 | }
77 |
78 | task dokkaHTML(type: org.jetbrains.dokka.gradle.DokkaAndroidTask) {
79 | outputFormat = 'html'
80 | outputDirectory = "$buildDir/dokkaHTML"
81 | includes = ['src/documentation/readme.md', 'src/documentation/extra.md']
82 | }
83 |
--------------------------------------------------------------------------------
/WebResources/Captivity/UserInterface/embeddedSVG.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
36 |
37 |
38 | Embedded SVG Images
The image is from the Material Design icon set:
https://material.io/tools/icons/?icon=visibility&style=baseline
Back to Captivity
63 |
--------------------------------------------------------------------------------
/WebResources/Captivity/UserInterface/three.html:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
34 | My first three.js app
35 |
41 |
42 |
43 |
44 |
70 | Back to Captivity
73 |
74 |
--------------------------------------------------------------------------------
/forAndroid/CaptiveCrypto/src/main/java/com/example/captivecrypto/storedkey/StoredKey.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Omnissa, LLC.
2 | // SPDX-License-Identifier: BSD-2-Clause
3 |
4 | package com.example.captivecrypto.storedkey
5 |
6 | import android.content.Context
7 | import android.os.Build
8 | import android.security.keystore.KeyProperties
9 | import java.security.Key
10 | import java.security.KeyStore
11 | import java.text.SimpleDateFormat
12 | import java.util.*
13 |
14 | // Single API object for convenience import.
15 | object StoredKey {
16 | fun capabilities(context: Context) = deviceCapabilities(context)
17 | fun describeAll() = describeAllStoredKeys()
18 | fun describeAll(providerName: String) = describeAllStoredKeys(providerName)
19 | fun deleteAll() = deleteAllStoredKeys()
20 | fun deleteAll(providerName: String) = deleteAllStoredKeys(providerName)
21 | fun generateKeyNamed(alias: String) = generateStoredKeyNamed(alias)
22 | fun generateKeyPairNamed(alias: String) = generateStoredKeyPairNamed(alias)
23 | fun describeKeyNamed(alias: String) = describeStoredKeyNamed(alias)
24 | fun encipherWithKeyNamed(plaintext:String, alias: String) =
25 | encipherWithStoredKey(plaintext, alias)
26 | fun decipherWithKeyNamed(ciphertext:EncipheredMessage, alias: String) =
27 | decipherWithStoredKey(ciphertext, alias)
28 | }
29 | // It'd be nicer to have single functions with optional parameters for
30 | // `providerName` but that seemed to generate errors like this.
31 | //
32 | // java.lang.NoSuchMethodError: No virtual method ...
33 | //
34 |
35 | // Common code used by more than one file in the package.
36 |
37 | enum class KEY {
38 | AndroidKeyStore;
39 | }
40 |
41 | fun loadKeyStore(name: String): KeyStore =
42 | KeyStore.getInstance(name).apply { load(null) }
43 |
44 | fun cipherSpecifier(key: Key): String = when (key.algorithm) {
45 | // For the "AES/CBC/PKCS5PADDING" magic, TOTH:
46 | // https://developer.android.com/guide/topics/security/cryptography#encrypt-message
47 | // KeyProperties.KEY_ALGORITHM_AES -> "AES/CBC/PKCS5PADDING"
48 | KeyProperties.KEY_ALGORITHM_AES -> "AES/GCM/NoPADDING"
49 |
50 | KeyProperties.KEY_ALGORITHM_RSA -> "RSA/ECB/OAEPPadding"
51 |
52 | else -> key.algorithm
53 | }
54 |
55 | fun unavailableMessage(requiredVersion: Int) = listOf(
56 | "Unavailable at build version ${Build.VERSION.SDK_INT}.",
57 | "Minimum build version ${requiredVersion}."
58 | ).joinToString(" ")
59 |
60 | fun formattedDate(date: Date, withZone:Boolean) =
61 | listOf("dd", "MMM", "yyyy HH:MM", " z")
62 | .filter { withZone || it.trim() != "z"}
63 | .map { SimpleDateFormat(it, Locale.getDefault()) }
64 | .map { it.format(date).run {
65 | if (it.toPattern() == "MMM") lowercase(Locale.getDefault())
66 | else this
67 | } }
68 | .joinToString("")
69 |
--------------------------------------------------------------------------------
/forApple/CaptiveCrypto/CaptiveCrypto/StoredKey/Decipher.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Omnissa, LLC.
2 | // SPDX-License-Identifier: BSD-2-Clause
3 |
4 | import Foundation
5 | import CryptoKit
6 |
7 | extension StoredKey {
8 | // Instance methods.
9 | func decipher(_ enciphered:Data) throws -> String {
10 | switch _storage {
11 | case .key:
12 | return try decipherWithPrivateKey(enciphered as CFData)
13 | case .generic:
14 | return try decipherWithSymmetricKey(enciphered)
15 | }
16 | }
17 | func decipher(_ enciphered:Enciphered) throws -> String {
18 | return try decipher(enciphered.message)
19 | }
20 |
21 | private func decipherWithSymmetricKey(_ enciphered:Data) throws -> String {
22 | let sealed = try AES.GCM.SealedBox(combined: enciphered)
23 | guard let key = symmetricKey else { throw StoredKeyError(
24 | "StoredKey instance isn't a symmetric key.")
25 | }
26 | let decipheredData = try AES.GCM.open(sealed, using: key)
27 | let message =
28 | String(data: decipheredData, encoding: .utf8) ?? "\(decipheredData)"
29 | return message
30 | }
31 |
32 | private func decipherWithPrivateKey(_ enciphered:CFData) throws -> String {
33 | guard let privateKey = secKey else { throw StoredKeyError(
34 | "StoredKey instance isn't a key pair.")
35 | }
36 | guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
37 | throw StoredKeyError("No public key.")
38 | }
39 | guard let algorithm = StoredKey.algorithms.first(
40 | where: { SecKeyIsAlgorithmSupported(publicKey, .encrypt, $0)}
41 | ) else
42 | {
43 | throw StoredKeyError("No algorithms supported.")
44 | }
45 |
46 | var error: Unmanaged?
47 | guard let decipheredBytes = SecKeyCreateDecryptedData(
48 | privateKey, algorithm, enciphered, &error) else {
49 | throw error?.takeRetainedValue() as? Error ?? StoredKeyError(
50 | "SecKeyCreateDecryptedData(\(privateKey),",
51 | " \(algorithm), \(enciphered),)",
52 | " returned null and set error \(String(describing: error)).")
53 | }
54 |
55 | let message = String(
56 | data: decipheredBytes as Data, encoding: .utf8)
57 | ?? "\(decipheredBytes)"
58 | return message
59 | }
60 |
61 | // Static methods that work with a key alias instead of a StoredKey
62 | // instance.
63 | static func decipher(
64 | _ enciphered:Enciphered, withFirstKeyNamed alias:String
65 | ) throws -> String
66 | {
67 | guard let key = try keysWithName(alias).first else {
68 | throw StoredKeyError(errSecItemNotFound)
69 | }
70 | return try key.decipher(enciphered)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/CaptiveWebView/CaptiveWebView/Constrain.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Omnissa, LLC.
2 | // SPDX-License-Identifier: BSD-2-Clause
3 |
4 | import Foundation
5 | import WebKit
6 |
7 | extension CaptiveWebView {
8 | #if os(macOS)
9 |
10 | // macOS version, takes two NSView parameters.
11 | public static func constrain(
12 | view left: NSView, to right: NSView, leftSide:Bool = false
13 | ) {
14 | left.translatesAutoresizingMaskIntoConstraints = false
15 | left.topAnchor.constraint(
16 | equalTo: right.topAnchor).isActive = true
17 | left.bottomAnchor.constraint(
18 | equalTo: right.bottomAnchor).isActive = true
19 | left.leftAnchor.constraint(
20 | equalTo: right.leftAnchor).isActive = true
21 | left.rightAnchor.constraint(
22 | equalTo: leftSide ? right.centerXAnchor : right.rightAnchor
23 | ).isActive = true
24 | }
25 | // TOTH:
26 | // https://github.com/dasher-project/redash/blob/master/Keyboard/foriOS/DasherApp/Keyboard/KeyboardViewController.swift#L129
27 |
28 | #else
29 |
30 | // iOS version, takes either of the following:
31 | //
32 | // - Two UIView parameters.
33 | // - One UIView and one UILayoutGuide.
34 | //
35 | // The UIView.safeAreaLayoutGuide property is a UILayoutGuide.
36 | public static func constrain(
37 | view left: UIView, to right: UIView, leftHalf:Bool = false
38 | ) {
39 | setAnchors(of: left,
40 | top: right.topAnchor,
41 | left: right.leftAnchor,
42 | bottom: right.bottomAnchor,
43 | right: leftHalf ? right.centerXAnchor : right.rightAnchor)
44 | }
45 |
46 | public static func constrain(
47 | view: UIView, to guide: UILayoutGuide, leftHalf:Bool = false
48 | ) {
49 | setAnchors(of: view,
50 | top: guide.topAnchor,
51 | left: guide.leftAnchor,
52 | bottom: guide.bottomAnchor,
53 | right: leftHalf ? guide.centerXAnchor : guide.rightAnchor)
54 | }
55 |
56 | public static func setAnchors(
57 | of view: UIView,
58 | top: NSLayoutYAxisAnchor,
59 | left:NSLayoutXAxisAnchor,
60 | bottom: NSLayoutYAxisAnchor,
61 | right: NSLayoutXAxisAnchor
62 | ) {
63 | view.translatesAutoresizingMaskIntoConstraints = false
64 | view.topAnchor.constraint(equalTo: top).isActive = true
65 | view.leftAnchor.constraint(equalTo: left).isActive = true
66 | view.bottomAnchor.constraint(equalTo: bottom).isActive = true
67 | view.rightAnchor.constraint(equalTo: right).isActive = true
68 | }
69 | // TOTH:
70 | // https://github.com/dasher-project/redash/blob/master/Keyboard/foriOS/DasherApp/Keyboard/KeyboardViewController.swift#L129
71 | #endif
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Captive Web View
2 | This repository holds the Captive Web View library code and sample applications.
3 |
4 | # Overview
5 | The Captive Web View library facilitates use of web technologies in mobile
6 | applications. It has the following features.
7 |
8 | - Web technologies support.
9 |
10 | The library facilitates use of Web View controls as the container for any of
11 | the following.
12 |
13 | - Whole application.
14 | - Whole user interface.
15 | - Part of user interface.
16 | - Headless application code.
17 |
18 | The user interface, and any other code running in a Web View, would be
19 | written in HTML5, CSS, and JavaScript.
20 |
21 | The Android and iOS versions of a Captive Web View application can share the
22 | same HTML5, CSS, and JavaScript code.
23 |
24 | - Object bridge.
25 |
26 | The library implements a simple bridge between JavaScript, running in the
27 | Web View, and Kotlin or Swift code, running natively.
28 |
29 | The bridge can be invoked from either the native end or the JavaScript end,
30 | and supports responses.
31 |
32 | The JavaScript ends of the bridge interface use JavaScript objects. The
33 | native ends use either JSONObject, for Android, or Dictionary, for iOS.
34 |
35 | - Native user interface division.
36 |
37 | The library can be used with applications that divide their user interface
38 | into multiple Activity or ViewController classes. A different HTML file can
39 | be associated with each native class.
40 |
41 | - Modern standards from built-in controls.
42 |
43 | The library utilises the built-in WebView, for Android, and WKWebView, for
44 | iOS. These classes support the latest web standards, such as HTML5 and ES6
45 | JavaScript. Support is maintained by the respective developer teams, i.e.
46 | the Chromium and WebKit projects.
47 |
48 | The library for Android is written in Kotlin; the library for iOS is written in
49 | Swift. There is also a small amount of JavaScript code in the library.
50 |
51 | Captive Web View can be seen as a simple version of platforms like Apache
52 | Cordova and Electron.
53 |
54 | # Usage
55 | - For Android, see the [forAndroid sub-directory](/forAndroid/).
56 | - For iOS, Catalyst, and native macOS, see the
57 | [forApple sub-directory](/forApple/).
58 |
59 | # Learn More
60 | - Reference documentation is in the
61 | [documentation/reference.md](documentation/reference.md) file.
62 |
63 | ## Contributing
64 | The Captive Web View project team welcomes contributions from the community.
65 | For more detailed information, refer to the
66 | [contributing.md](contributing.md) file.
67 |
68 | Check the [documentation/backlog.md](documentation/backlog.md) file for a list
69 | of work to be done.
70 |
71 | License
72 | =======
73 | Captive Web View, is:
74 | Copyright 2020 Omnissa, LLC.
75 | And licensed under a two-clause BSD license.
76 | SPDX-License-Identifier: BSD-2-Clause
77 |
--------------------------------------------------------------------------------