diff --git a/.gitignore b/.gitignore
index 2e9d231..ce0c66c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ xcuserdata/
*.moved-aside
*.xccheckout
*.xcscmblueprint
+*.DS_Store
## Obj-C/Swift specific
*.hmap
diff --git a/Android/.gitignore b/Android/.gitignore
deleted file mode 100644
index 70acd67..0000000
--- a/Android/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-*.iml
-.gradle
-/local.properties
-/.idea
-/build
-/captures
-.externalNativeBuild
diff --git a/Android/app/.gitignore b/Android/app/.gitignore
deleted file mode 100644
index 81cda7b..0000000
--- a/Android/app/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/build
-/src/main/jniLibs
-/src/main/swift/.build
-/src/main/swift/Package.resolved
\ No newline at end of file
diff --git a/Android/app/build.gradle b/Android/app/build.gradle
deleted file mode 100644
index 78595eb..0000000
--- a/Android/app/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-apply plugin: 'com.android.application'
-
-apply plugin: 'kotlin-android'
-
-apply plugin: 'kotlin-android-extensions'
-
-apply plugin: 'net.zhuoweizhang.swiftandroid'
-
-android {
- compileSdkVersion 28
- defaultConfig {
- applicationId "com.millertech.bluetoothexplorer"
- minSdkVersion 21
- targetSdkVersion 28
- versionCode 1
- versionName "1.0"
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-}
-
-dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-
- implementation 'com.github.PureSwift:SwiftAndroidSupport:0.2.4'
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'com.android.support.test:runner:1.0.2'
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
-}
diff --git a/Android/app/src/main/java/com/millertech/bluetoothexplorer/Application.kt b/Android/app/src/main/java/com/millertech/bluetoothexplorer/Application.kt
deleted file mode 100644
index ea86e6a..0000000
--- a/Android/app/src/main/java/com/millertech/bluetoothexplorer/Application.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.millertech.bluetoothexplorer
-
-import org.pureswift.swiftandroidsupport.app.SwiftApplication
-
-class Application: SwiftApplication() {
-
- companion object {
-
- init {
- System.loadLibrary("BluetoothExplorer")
- }
- }
-}
\ No newline at end of file
diff --git a/Android/app/src/main/java/com/millertech/bluetoothexplorer/MainActivity.kt b/Android/app/src/main/java/com/millertech/bluetoothexplorer/MainActivity.kt
deleted file mode 100644
index 0664887..0000000
--- a/Android/app/src/main/java/com/millertech/bluetoothexplorer/MainActivity.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.millertech.bluetoothexplorer
-
-import org.pureswift.swiftandroidsupport.app.SwiftAppCompatActivity
-
-class MainActivity : SwiftAppCompatActivity()
diff --git a/Android/app/src/main/java/com/millertech/bluetoothexplorer/swiftbindings/MainActivityBindings.java b/Android/app/src/main/java/com/millertech/bluetoothexplorer/swiftbindings/MainActivityBindings.java
deleted file mode 100644
index ceec0ee..0000000
--- a/Android/app/src/main/java/com/millertech/bluetoothexplorer/swiftbindings/MainActivityBindings.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.millertech.bluetoothexplorer.swiftbindings;
-
-public interface MainActivityBindings {
-
- // Messages from Java to Swift
- interface Listener {
-
- void hi();
-
- }
-
- // Messages from Swift back to Java Activity
- interface Responder{
-
- }
-}
diff --git a/Android/app/src/main/java/org/swiftjava/com_millertech/MainActivityBindings_ListenerProxy.java b/Android/app/src/main/java/org/swiftjava/com_millertech/MainActivityBindings_ListenerProxy.java
deleted file mode 100644
index 5392ae8..0000000
--- a/Android/app/src/main/java/org/swiftjava/com_millertech/MainActivityBindings_ListenerProxy.java
+++ /dev/null
@@ -1,32 +0,0 @@
-
-/// generated by: genswift.java 'java/lang|java/util|java/sql' 'Sources' '../java' ///
-
-/// interface com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings$Listener ///
-
-package org.swiftjava.com_millertech;
-
-@SuppressWarnings("JniMissingFunction")
-public class MainActivityBindings_ListenerProxy implements com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings.Listener {
-
- // address of proxy object
- long __swiftObject;
-
- MainActivityBindings_ListenerProxy( long __swiftObject ) {
- this.__swiftObject = __swiftObject;
- }
-
- /// public abstract void com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings$Listener.hi()
-
- public native void __hi( long __swiftObject );
-
- public void hi() {
- __hi( __swiftObject );
- }
-
- public native void __finalize( long __swiftObject );
-
- public void finalize() {
- __finalize( __swiftObject );
- }
-
-}
diff --git a/Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index c7bd21d..0000000
--- a/Android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/app/src/main/res/drawable/ic_arrow_back.xml b/Android/app/src/main/res/drawable/ic_arrow_back.xml
deleted file mode 100644
index d426473..0000000
--- a/Android/app/src/main/res/drawable/ic_arrow_back.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/Android/app/src/main/res/drawable/ic_bluetooth.xml b/Android/app/src/main/res/drawable/ic_bluetooth.xml
deleted file mode 100644
index cb32298..0000000
--- a/Android/app/src/main/res/drawable/ic_bluetooth.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/Android/app/src/main/res/layout/list_cell.xml b/Android/app/src/main/res/layout/list_cell.xml
deleted file mode 100644
index 9c176dd..0000000
--- a/Android/app/src/main/res/layout/list_cell.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/list_header.xml b/Android/app/src/main/res/layout/list_header.xml
deleted file mode 100644
index 0877334..0000000
--- a/Android/app/src/main/res/layout/list_header.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/peripheral_item.xml b/Android/app/src/main/res/layout/peripheral_item.xml
deleted file mode 100644
index 835b636..0000000
--- a/Android/app/src/main/res/layout/peripheral_item.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Android/app/src/main/res/layout/peripheral_item_2.xml b/Android/app/src/main/res/layout/peripheral_item_2.xml
deleted file mode 100644
index c155d47..0000000
--- a/Android/app/src/main/res/layout/peripheral_item_2.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100755
index 93690f9..0000000
Binary files a/Android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100755
index 6e2ae02..0000000
Binary files a/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100755
index a039e07..0000000
Binary files a/Android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100755
index e52115f..0000000
Binary files a/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100755
index 15dea2a..0000000
Binary files a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100755
index a9a8610..0000000
Binary files a/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100755
index 2175529..0000000
Binary files a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100755
index e4fd6c1..0000000
Binary files a/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100755
index bb63a5e..0000000
Binary files a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100755
index 4a6e4a8..0000000
Binary files a/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/Android/app/src/main/res/values/colors.xml b/Android/app/src/main/res/values/colors.xml
deleted file mode 100644
index 3ab3e9c..0000000
--- a/Android/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- #3F51B5
- #303F9F
- #FF4081
-
diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml
deleted file mode 100644
index 1c0f157..0000000
--- a/Android/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Bluetooth Explorer
-
diff --git a/Android/app/src/main/res/values/styles.xml b/Android/app/src/main/res/values/styles.xml
deleted file mode 100644
index c34b820..0000000
--- a/Android/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Android/app/src/main/swift/Package.resolved b/Android/app/src/main/swift/Package.resolved
deleted file mode 100644
index 67e1c7c..0000000
--- a/Android/app/src/main/swift/Package.resolved
+++ /dev/null
@@ -1,97 +0,0 @@
-{
- "object": {
- "pins": [
- {
- "package": "Android",
- "repositoryURL": "https://github.com/PureSwift/Android.git",
- "state": {
- "branch": "master",
- "revision": "937413a8f7f741466850eb792f4f162378bb7b65",
- "version": null
- }
- },
- {
- "package": "AndroidBluetooth",
- "repositoryURL": "https://github.com/PureSwift/AndroidBluetooth.git",
- "state": {
- "branch": "master",
- "revision": "fcc0c03c440cf6855b95936b7260497cbbbb532a",
- "version": null
- }
- },
- {
- "package": "AndroidUIKit",
- "repositoryURL": "https://github.com/PureSwift/AndroidUIKit.git",
- "state": {
- "branch": "master",
- "revision": "d23b4fe083d0a0f1f1767cb46e747efd69a8b96f",
- "version": null
- }
- },
- {
- "package": "Bluetooth",
- "repositoryURL": "https://github.com/PureSwift/Bluetooth.git",
- "state": {
- "branch": "master",
- "revision": "b03524c7a0c7eca048133a9df9dc4bbe727b38e0",
- "version": null
- }
- },
- {
- "package": "CJavaVM",
- "repositoryURL": "https://github.com/PureSwift/CJavaVM.git",
- "state": {
- "branch": "master",
- "revision": "3b19893aa50a724c467d341c482087c30323b190",
- "version": null
- }
- },
- {
- "package": "GATT",
- "repositoryURL": "https://github.com/PureSwift/GATT.git",
- "state": {
- "branch": "master",
- "revision": "955a19b2030e08666154f2ee52d7955f56bb6ff5",
- "version": null
- }
- },
- {
- "package": "java_lang",
- "repositoryURL": "https://github.com/PureSwift/java_lang.git",
- "state": {
- "branch": "master",
- "revision": "e308632de721a79119b4a76e3b62faf5893d4cad",
- "version": null
- }
- },
- {
- "package": "java_swift",
- "repositoryURL": "https://github.com/PureSwift/java_swift.git",
- "state": {
- "branch": "master",
- "revision": "65deecc81042d0839a7496450109cafd2a0e1741",
- "version": null
- }
- },
- {
- "package": "java_util",
- "repositoryURL": "https://github.com/PureSwift/java_util.git",
- "state": {
- "branch": "master",
- "revision": "5cad067437a2567ffcf0ba9ea1c2f19d8b414d61",
- "version": null
- }
- },
- {
- "package": "JNI",
- "repositoryURL": "https://github.com/PureSwift/JNI.git",
- "state": {
- "branch": "master",
- "revision": "29239efbdd3370dc17f06f9df5380ecd2997370c",
- "version": null
- }
- }
- ]
- },
- "version": 1
-}
diff --git a/Android/app/src/main/swift/Package.swift b/Android/app/src/main/swift/Package.swift
deleted file mode 100644
index 4469a59..0000000
--- a/Android/app/src/main/swift/Package.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-// swift-tools-version:5.0
-import PackageDescription
-
-let package = Package(
- name: "BluetoothExplorer",
- products: [
- .library(
- name: "BluetoothExplorer",
- type: .dynamic,
- targets: ["BluetoothExplorerAndroid"]
- )
- ],
- dependencies: [
- .package(
- url: "https://github.com/PureSwift/AndroidBluetooth.git",
- .branch("master")
- ),
- .package(
- url: "https://github.com/PureSwift/AndroidUIKit.git",
- .branch("master")
- )
- ],
- targets: [
- .target(
- name: "BluetoothExplorerAndroid",
- dependencies: [
- "AndroidBluetooth",
- "AndroidUIKit"
- ],
- path: "Sources"
- )
- ]
-)
diff --git a/Android/app/src/main/swift/Sources/ActivityIndicatorViewController.swift b/Android/app/src/main/swift/Sources/ActivityIndicatorViewController.swift
deleted file mode 100755
index dec5903..0000000
--- a/Android/app/src/main/swift/Sources/ActivityIndicatorViewController.swift
+++ /dev/null
@@ -1,151 +0,0 @@
-//
-// ActivityIndicatorViewController.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 6/19/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-#endif
-
-protocol ActivityIndicatorViewController: class {
-
- var view: UIView! { get }
-
- var navigationItem: UINavigationItem { get }
-
- var navigationController: UINavigationController? { get }
-
- func showActivity()
-
- func hideActivity(animated: Bool)
-}
-
-extension ActivityIndicatorViewController {
-
- func performActivity (showActivity: Bool = true,
- _ asyncOperation: @escaping () throws -> T,
- completion: ((Self, T) -> ())? = nil) {
-
- if showActivity { self.showActivity() }
-
- async {
-
- do {
-
- let value = try asyncOperation()
-
- mainQueue { [weak self] in
-
- guard let controller = self
- else { return }
-
- if showActivity { controller.hideActivity(animated: true) }
-
- // success
- completion?(controller, value)
- }
- }
-
- catch {
-
- mainQueue { [weak self] in
-
- guard let controller = self as? (UIViewController & ActivityIndicatorViewController)
- else { return }
-
- if showActivity { controller.hideActivity(animated: false) }
-
- // show error
-
- log("⚠️ Error: \(error)")
-
- if (controller as UIViewController).view.window != nil {
-
- controller.showErrorAlert(error.localizedDescription)
-
- } else {
-
- NativeAppDelegate.shared.window?.rootViewController?.showErrorAlert(error.localizedDescription)
- }
- }
- }
- }
- }
-}
-
-protocol TableViewActivityIndicatorViewController: ActivityIndicatorViewController {
-
- var tableView: UITableView! { get }
-
- var refreshControl: UIRefreshControl? { get }
-
- #if os(iOS)
- var activityIndicator: UIActivityIndicatorView { get }
- #elseif os(Android) || os(macOS)
- var progressDialog: AndroidProgressDialog { get }
- #endif
-}
-
-extension TableViewActivityIndicatorViewController {
-
- func showActivity() {
-
- self.view.isUserInteractionEnabled = false
-
- if refreshControl?.isRefreshing ?? false {
-
- // refresh control animating
- } else {
-
- #if os(iOS)
- activityIndicator.startAnimating()
- #else
- progressDialog.show()
- #endif
- }
- }
-
- func hideActivity(animated: Bool = true) {
-
- self.view.isUserInteractionEnabled = true
-
- if refreshControl?.isRefreshing ?? false {
-
- refreshControl?.endRefreshing()
- } else {
- #if os(iOS)
- activityIndicator.stopAnimating()
- #else
- progressDialog.dismiss()
- #endif
-
- }
- }
-}
-
-internal extension ActivityIndicatorViewController {
-
- #if os(iOS)
- func loadActivityIndicatorView() -> UIActivityIndicatorView {
-
- let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
- activityIndicator.frame.origin = CGPoint(x: 6.5, y: 15)
-
- let view = UIView(frame: CGRect(x: 0, y: 0, width: 33, height: 44))
- view.backgroundColor = .clear
- view.addSubview(activityIndicator)
-
- let barButtonItem = UIBarButtonItem(customView: view)
- self.navigationItem.rightBarButtonItem = barButtonItem
- return activityIndicator
- }
- #endif
-}
diff --git a/Android/app/src/main/swift/Sources/AndroidTableViewCell.swift b/Android/app/src/main/swift/Sources/AndroidTableViewCell.swift
deleted file mode 100644
index b49d554..0000000
--- a/Android/app/src/main/swift/Sources/AndroidTableViewCell.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-//
-// AndroidTableViewCell.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 4/30/19.
-// Copyright © 2019 PureSwift. All rights reserved.
-//
-
-import Foundation
-
-#if os(Android) || os(macOS)
-
-import Foundation
-import Android
-import AndroidUIKit
-import java_lang
-
-protocol AndroidTableViewCell: ReusableTableViewCell {
-
- static var layout: String { get }
-
- func awakeFromLayout()
-}
-
-extension AndroidTableViewCell {
-
- func awakeFromLayout() { }
-}
-
-extension AndroidTableViewCell where Self: AndroidUIKit.UITableViewCell {
-
- private func inflate() {
-
- let layout = Self.layout
-
- if self.layoutName != layout {
- inflateAndroidLayout(layout)
- awakeFromLayout()
- }
- }
-
- func view(for identifier: String) -> Android.View.View? {
-
- inflate()
- let activity = UIApplication.shared.androidActivity
- let androidIdentifier = activity.getIdentifier(name: identifier, type: "id")
- return androidView.findViewById(androidIdentifier)
- }
-
- subscript (identifier: String) -> Android.View.View {
-
- guard let view = self.view(for: identifier)
- else { fatalError("No view for \(identifier)") }
-
- return view
- }
-}
-
-#endif
diff --git a/Android/app/src/main/swift/Sources/AppDelegate.swift b/Android/app/src/main/swift/Sources/AppDelegate.swift
deleted file mode 100644
index 44921a2..0000000
--- a/Android/app/src/main/swift/Sources/AppDelegate.swift
+++ /dev/null
@@ -1,239 +0,0 @@
-//
-// AppDelegate.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 9/7/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-#endif
-
-import Bluetooth
-import GATT
-
-class AppDelegate: UIResponder, UIApplicationDelegate {
-
- final var window: UIWindow?
-
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
-
- #if os(iOS)
- log("Launching Bluetooth Explorer v\(AppVersion) Build \(AppBuild)")
- #elseif os(Android)
- log("Launching Bluetooth Explorer")
- #endif
-
- #if os(Android) || os(macOS)
- NSLog("UIScreen scale: \(UIScreen.main.scale)")
- NSLog("UIScreen native scale: \(UIScreen.main.nativeScale)")
- NSLog("UIScreen size: \(UIScreen.main.bounds.size)")
- NSLog("UIScreen native size: \(UIScreen.main.nativeBounds.size)")
- #endif
-
- // initalize BLE
- NativeCentral.shared.log = { log("Central: \($0)") }
-
- // load window and view controller
- let viewController = CentralViewController()
-
- // setup UI theme
- #if os(iOS)
- configureAppearance()
- #endif
-
- let navigationController = UINavigationController(rootViewController: viewController)
-
- self.window = UIWindow(frame: UIScreen.main.bounds)
- self.window?.rootViewController = navigationController
- self.window?.makeKeyAndVisible()
-
- return true
- }
-
- func applicationWillResignActive(_ application: UIApplication) {
-
- // 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.
- // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
-
- NSLog("\(#function)")
- }
-
- func applicationDidEnterBackground(_ application: UIApplication) {
- // 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.
- // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
-
- NSLog("\(#function)")
- }
-
- func applicationWillEnterForeground(_ application: UIApplication) {
- // 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.
-
- NSLog("\(#function)")
- }
-
- func applicationDidBecomeActive(_ application: UIApplication) {
- // 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.
-
- NSLog("\(#function)")
- }
-
- func applicationWillTerminate(_ application: UIApplication) {
- // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
-
- NSLog("\(#function)")
- }
-}
-
-// MARK: - iOS
-
-#if os(iOS)
-
-typealias NativeAppDelegate = iOSAppDelegate
-
-@UIApplicationMain
-final class iOSAppDelegate: AppDelegate {
-
- static var shared: iOSAppDelegate { return UIApplication.shared.delegate as! iOSAppDelegate }
-}
-
-// MARK: - Android
-
-#elseif os(Android) || os(macOS)
-
-typealias NativeAppDelegate = AndroidAppDelegate
-
-final class AndroidAppDelegate: AppDelegate {
-
- static var shared: AndroidAppDelegate { return UIApplication.shared.delegate as! AndroidAppDelegate }
-
- var bluetoothEnabled: (() -> ())?
-
- override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
-
- guard super.application(application, didFinishLaunchingWithOptions: launchOptions)
- else { return false }
-
- NSLog("UIScreen scale: \(UIScreen.main.scale)")
- NSLog("UIScreen native scale: \(UIScreen.main.nativeScale)")
- NSLog("UIScreen size: \(UIScreen.main.bounds.size)")
- NSLog("UIScreen native size: \(UIScreen.main.nativeBounds.size)")
-
- enableBluetooth()
-
- return true
- }
-}
-#endif
-
-// MARK: - iOS Info Plist
-
-#if os(iOS)
-/// Version of the app.
-public let AppVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
-
-/// Build of the app.
-public let AppBuild = Int(Bundle.main.infoDictionary!["CFBundleVersion"] as! String)!
-#endif
-
-// MARK: - Android Permissions
-
-#if os(Android) || os(macOS)
-
-extension AndroidAppDelegate {
-
- internal enum AndroidPermissionRequest {
-
- static let enableBluetooth = 1000
- static let gpsAndWriteStorage = 2000
- }
-}
-
-extension AndroidAppDelegate {
-
- func application(_ application: UIApplication, activityResult requestCode: Int, resultCode: Int, data: Android.Content.Intent?) {
-
- log("\(type(of: self)) \(#function) - requestCode = \(requestCode) - resultCode = \(resultCode)")
-
- if resultCode == AndroidPermissionRequest.enableBluetooth,
- resultCode == SwiftSupportAppCompatActivity.RESULT_OK {
-
- // no need to request permissions
- if requestLocationPermissions() {
-
- //
- bluetoothEnabled?()
- }
- }
- }
-
- func application(_ application: UIApplication, requestPermissionsResult requestCode: Int, permissions: [String], grantResults: [Int]) {
-
- log("\(type(of: self)) \(#function)")
-
- if requestCode == AndroidPermissionRequest.gpsAndWriteStorage {
-
- if grantResults[0] == Android.Content.PM.PackageManager.Permission.granted.rawValue {
-
- // permission granted, now we can scan
- bluetoothEnabled?()
-
- } else {
-
- log("\(type(of: self)) \(#function) GPS Permission is required")
- }
- }
- }
-}
-
-extension AndroidAppDelegate {
-
- /// Checks if permissions are needed.
- @discardableResult
- func enableBluetooth(hostController: Android.Bluetooth.Adapter = Android.Bluetooth.Adapter.default!) -> Bool {
-
- guard hostController.isEnabled() == false
- else { return requestLocationPermissions() }
-
- let enableBluetoothIntent = Android.Content.Intent(action: Android.Bluetooth.Adapter.Action.requestEnable.rawValue)
-
- UIApplication.shared.androidActivity.startActivityForResult(intent: enableBluetoothIntent,
- requestCode: AndroidPermissionRequest.enableBluetooth)
-
- log("\(type(of: self)) \(#function) enable Bluetooth")
-
- return false
- }
-
- @discardableResult
- func requestLocationPermissions() -> Bool {
-
- let activity = UIApplication.shared.androidActivity
-
- if Android.OS.Build.Version.Sdk.sdkInt.rawValue >= Android.OS.Build.VersionCodes.M,
- activity.checkSelfPermission(permission: Android.ManifestPermission.accessCoarseLocation.rawValue) != Android.Content.PM.PackageManager.Permission.granted.rawValue {
-
- log("\(type(of: self)) \(#function) request permission")
-
- let permissions = [Android.ManifestPermission.accessCoarseLocation.rawValue, Android.ManifestPermission.writeExternalStorage.rawValue]
-
- activity.requestPermissions(permissions: permissions, requestCode: AndroidPermissionRequest.gpsAndWriteStorage)
-
- return false
-
- } else {
-
- log("\(type(of: self)) \(#function) dont need to request permissions")
-
- return true
- }
- }
-}
-
-#endif
diff --git a/Android/app/src/main/swift/Sources/Appearance.swift b/Android/app/src/main/swift/Sources/Appearance.swift
deleted file mode 100644
index 3692129..0000000
--- a/Android/app/src/main/swift/Sources/Appearance.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// Appearance.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 9/23/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-#endif
-
-/// Configure the application's UI appearance
-func configureAppearance() {
-
- #if os(iOS)
-
- if #available(iOS 11.0, *) {
- UINavigationBar.appearance().prefersLargeTitles = true
- UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.white]
- }
-
- UINavigationBar.appearance().tintColor = .white
- UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white]
- UINavigationBar.appearance().barTintColor = UIColor(red: 0.386, green: 0.707, blue: 1.0, alpha: 1.0)
-
- #endif
-}
diff --git a/Android/app/src/main/swift/Sources/Async.swift b/Android/app/src/main/swift/Sources/Async.swift
deleted file mode 100755
index dd9d8d7..0000000
--- a/Android/app/src/main/swift/Sources/Async.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// Async.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 6/19/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-import Dispatch
-
-#if os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-#endif
-
-func mainQueue(_ block: @escaping () -> ()) {
-
- #if os(iOS)
- DispatchQueue.main.async(execute: block)
- #elseif os(Android) || os(macOS)
- UIApplication.shared.androidActivity.runOnMainThread(block)
- #endif
-}
-
-/// Perform a task on the internal queue.
-func async(_ block: @escaping () -> ()) {
-
- appQueue.async(execute: block)
-}
-
-let appQueue = DispatchQueue(label: "App Queue")
diff --git a/Android/app/src/main/swift/Sources/Central.swift b/Android/app/src/main/swift/Sources/Central.swift
deleted file mode 100644
index eea1e4d..0000000
--- a/Android/app/src/main/swift/Sources/Central.swift
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// Central.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 9/7/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-import Bluetooth
-import GATT
-
-typealias NativeScanData = ScanData
-
-#if os(iOS)
-
-import DarwinGATT
-
-typealias NativeCentral = DarwinCentral
-
-private struct CentralCache {
-
- static let options = DarwinCentral.Options(showPowerAlert: false, restoreIdentifier: nil)
-
- static let central = DarwinCentral(options: options)
-}
-
-#elseif os(Android) || os(macOS)
-
-import Android
-import AndroidBluetooth
-import AndroidUIKit
-
-typealias NativeCentral = AndroidCentral
-
-private struct CentralCache {
-
- static let hostController = Android.Bluetooth.Adapter.default!
-
- static let context = UIApplication.shared.androidActivity
-
- static let options = AndroidCentral.Options()
-
- static let central = AndroidCentral(hostController: hostController, context: context, options: options)
-}
-
-#endif
-
-internal extension NativeCentral {
-
- static var shared: NativeCentral {
-
- return CentralCache.central
- }
-}
diff --git a/Android/app/src/main/swift/Sources/CentralViewController.swift b/Android/app/src/main/swift/Sources/CentralViewController.swift
deleted file mode 100644
index cdd6b63..0000000
--- a/Android/app/src/main/swift/Sources/CentralViewController.swift
+++ /dev/null
@@ -1,265 +0,0 @@
-//
-// CentralViewController.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 9/7/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-import Bluetooth
-import GATT
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidBluetooth
-import AndroidUIKit
-#endif
-
-/// Scans for nearby BLE devices.
-final class CentralViewController: UITableViewController {
-
- // MARK: - Properties
-
- #if os(iOS)
- lazy var activityIndicator: UIActivityIndicatorView = self.loadActivityIndicatorView()
- #else
- lazy var progressDialog: AndroidProgressDialog = {
- let progressDialog = AndroidProgressDialog(context: UIApplication.shared.androidActivity)
- progressDialog.setIndeterminate(true)
- progressDialog.setTitle("Wait")
- progressDialog.setMessage("Scanning...")
- return progressDialog
- }()
- #endif
-
- private(set) var items = [NativeScanData]()
-
- let scanDuration: TimeInterval = 5.0
-
- let filterDuplicates: Bool = false
-
- // MARK: - Loading
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- // set title
- self.title = "Central"
-
- // setup table view
- self.tableView.estimatedRowHeight = 44
- self.tableView.rowHeight = UITableViewAutomaticDimension
- self.tableView.register(ScanDataTableViewCell.self)
-
- let refreshControl = UIRefreshControl(frame: .zero)
-
- #if os(Android) || os(macOS)
- refreshControl.addTarget(action: { [unowned self] in self.reloadData() }, for: .valueChanged)
- #else
- refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
- #endif
-
- self.refreshControl = refreshControl
-
- #if os(Android) || os(macOS)
- AndroidAppDelegate.shared.bluetoothEnabled = { [weak self] in self?.reloadData() }
- reloadData()
- #endif
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- reloadData()
- }
-
- // MARK: - Actions
-
- #if os(iOS)
- @objc func pullToRefresh(_ sender: UIRefreshControl) {
-
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { [weak self] in
-
- self?.reloadData()
- })
- }
- #endif
-
-
- // MARK: - Methods
-
- private subscript (indexPath: IndexPath) -> NativeScanData {
-
- @inline(__always)
- get { return self.items[indexPath.row] }
- }
-
- private final func endRefreshing() {
-
- if let refreshControl = self.refreshControl,
- refreshControl.isRefreshing{
-
- refreshControl.endRefreshing()
- }
- }
-
- private func reloadData() {
-
- log("\(type(of: self)) \(#function)")
-
- // clear table data
- self.items.removeAll()
- tableView.reloadData()
-
- // make sure its enabled
- #if os(Android)
- guard AndroidCentral.shared.hostController.isEnabled()
- else { return } // wait until enabled
- #endif
-
- // scan
- let scanDuration = self.scanDuration
- let filterDuplicates = self.filterDuplicates
-
- performActivity({
- try NativeCentral.shared.scan(duration: scanDuration, filterDuplicates: filterDuplicates) { [weak self] (device) in mainQueue { self?.foundDevice(device) } }
- })
- }
-
- private func foundDevice(_ scanData: NativeScanData) {
-
- // remove old value
- if let index = self.items.index(where: { $0.peripheral == scanData.peripheral }) {
- self.items.remove(at: index)
- }
-
- // add item
- self.items.append(scanData)
-
- // sort
- self.items.sort(by: { $0.peripheral.description < $1.peripheral.description })
-
- // update table view
- self.tableView.reloadData()
- }
-
- private func configure(cell: UITableViewCell, at indexPath: IndexPath) {
-
- let item = self[indexPath]
-
- #if os(iOS)
-
- cell.textLabel?.text = item.advertisementData.localName ?? item.peripheral.identifier.description
- cell.textLabel?.numberOfLines = 0
-
- #elseif os(Android) || os(macOS)
-
- if let localName = item.advertisementData.localName {
-
- let layoutName = "peripheral_item"
-
- if cell.layoutName != layoutName {
- cell.inflateAndroidLayout(layoutName)
- }
-
- let itemView = cell.androidView
-
- let tvNameId = UIApplication.shared.androidActivity.getIdentifier(name: "tvName", type: "id")
- let tvAddressId = UIApplication.shared.androidActivity.getIdentifier(name: "tvAddress", type: "id")
-
- guard let tvNameObject = itemView.findViewById(tvNameId)
- else { fatalError("No view for \(tvNameId)") }
-
- guard let tvAddressObject = itemView.findViewById(tvAddressId)
- else { fatalError("No view for \(tvAddressId)") }
-
- let tvName = Android.Widget.TextView(casting: tvNameObject)
- let tvAddress = Android.Widget.TextView(casting: tvAddressObject)
-
- tvName?.text = localName
- tvAddress?.text = item.peripheral.description
-
- } else {
-
- let layoutName = "peripheral_item_2"
-
- if cell.layoutName != layoutName {
- cell.inflateAndroidLayout(layoutName)
- }
-
- let itemView = cell.androidView
-
- let tvAddressId = UIApplication.shared.androidActivity.getIdentifier(name: "tvAddress", type: "id")
-
- guard let tvAddressObject = itemView.findViewById(tvAddressId)
- else { fatalError("No view for \(tvAddressId)") }
-
- let tvAddress = Android.Widget.TextView(casting: tvAddressObject)
-
- tvAddress?.text = item.peripheral.description
- }
-
- #endif
- }
-
- #if os(iOS)
- private func loadActivityIndicatorView() -> UIActivityIndicatorView {
-
- let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
- activityIndicator.frame.origin = CGPoint(x: 6.5, y: 15)
-
- let view = UIView(frame: CGRect(x: 0, y: 0, width: 33, height: 44))
- view.backgroundColor = .clear
- view.addSubview(activityIndicator)
-
- let barButtonItem = UIBarButtonItem(customView: view)
- self.navigationItem.rightBarButtonItem = barButtonItem
- return activityIndicator
- }
- #endif
-
- // MARK: - UITableViewDataSource
-
- override func numberOfSections(in tableView: UITableView) -> Int {
-
- return 1
- }
-
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-
- return items.count
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-
- let cell = tableView.dequeueReusableCell(ScanDataTableViewCell.self, for: indexPath)
- configure(cell: cell, at: indexPath)
- return cell
- }
-
- // MARK: - UITableViewDelegate
-
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-
- #if os(iOS)
- defer { tableView.deselectRow(at: indexPath, animated: true) }
- #endif
-
- let item = self[indexPath]
-
- self.endRefreshing()
-
- log("Selected \(item.peripheral) \(item.advertisementData.localName ?? "")")
-
- let viewController = ServicesViewController(scanData: item)
-
- self.show(viewController, sender: self)
- }
-}
-
-// MARK: - ActivityIndicatorViewController
-
-extension CentralViewController: TableViewActivityIndicatorViewController { }
diff --git a/Android/app/src/main/swift/Sources/CharacteristicViewController.swift b/Android/app/src/main/swift/Sources/CharacteristicViewController.swift
deleted file mode 100644
index 91e6bd3..0000000
--- a/Android/app/src/main/swift/Sources/CharacteristicViewController.swift
+++ /dev/null
@@ -1,470 +0,0 @@
-//
-// CharacteristicViewController.swift
-// BluetoothExplorerAndroid
-//
-// Created by Marco Estrella on 9/21/18.
-//
-
-import Foundation
-import Bluetooth
-import GATT
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-#endif
-
-/// Characteristic
-final class CharacteristicViewController: UITableViewController {
-
- typealias NativeService = Service
- typealias NativeCharacteristic = Characteristic
-
- // MARK: - Properties
-
- #if os(iOS)
- lazy var activityIndicator: UIActivityIndicatorView = self.loadActivityIndicatorView()
- #else
- lazy var progressDialog: AndroidProgressDialog = {
- let progressDialog = AndroidProgressDialog(context: UIApplication.shared.androidActivity)
- progressDialog.setIndeterminate(true)
- progressDialog.setTitle("Wait")
- progressDialog.setMessage("Loading...")
- return progressDialog
- }()
- #endif
-
- let service: NativeService
- let characteristic: NativeCharacteristic
-
- private var sections = [Section]()
-
- private(set) var characteristicValue = [Data]() {
- didSet { configureView() }
- }
-
- private var isNotifying = false {
- didSet { configureView() }
- }
-
- private let timeout: TimeInterval = .gattDefaultTimeout
-
- // MARK: - Initialization
-
- init(service: NativeService, characteristic: NativeCharacteristic) {
-
- self.characteristic = characteristic
- self.service = service
-
- super.init(style: .grouped)
- }
-
- deinit {
-
- // just in case we didnt stop notifications
- NativeCentral.shared.disconnect(peripheral: characteristic.peripheral)
- }
-
- #if os(iOS)
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- #endif
-
- private let cellReuseIdentifier = "Cell"
-
- // MARK: - Loading
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- NSLog("\(type(of: self)) \(#function)")
-
-
- // setup table view
- self.tableView.estimatedRowHeight = 44
- self.tableView.rowHeight = UITableViewAutomaticDimension
- #if os(iOS)
- self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier.uuid.rawValue)
- self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier.name.rawValue)
- self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier.value.rawValue)
- self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: CellIdentifier.property.rawValue)
- #else
- self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: self.cellReuseIdentifier)
- #endif
-
- // update UI
- self.configureView()
-
- // attempt to read
- self.readValue()
- }
-
- // MARK: - Methods
-
- private subscript (indexPath: IndexPath) -> Item {
-
- @inline(__always)
- get { return self.sections[indexPath.section].items[indexPath.row] }
- }
-
- private func configureView() {
-
- title = self.characteristic.uuid.description
-
- // configure table view
-
- sections = []
-
- do {
-
- var items = [Item]()
-
- if let name = characteristic.uuid.name {
-
- items.append(.name(name))
- }
-
- items.append(.uuid(characteristic.uuid))
-
- sections.append(Section(title: "Information", items: items))
- }
-
- if characteristic.properties.isEmpty == false {
-
- sections.append(Section(title: "Properties", items: characteristic.properties.map { Item.property($0) }))
- }
-
- if characteristicValue.isEmpty == false {
-
- sections.append(Section(title: "Value", items: characteristicValue.reversed().map { Item.value($0) }))
- }
-
- // update UI
- tableView.reloadData()
- }
-
- private func readValue() {
- NSLog("\(type(of: self)) \(#function)")
- let timeout = self.timeout
- let service = self.service
- let characteristic = self.characteristic
- let peripheral = self.service.peripheral
-
- guard characteristic.properties.contains(.read)
- else { return }
-
- performActivity({
- try NativeCentral.shared.connect(to: peripheral, timeout: timeout)
- defer { NativeCentral.shared.disconnect(peripheral: peripheral) }
- let _ = try NativeCentral.shared.discoverServices(for: peripheral, timeout: timeout)
- let _ = try NativeCentral.shared.discoverCharacteristics(for: service, timeout: timeout)
- return try NativeCentral.shared.readValue(for: characteristic, timeout: timeout)
- }, completion: {
- $0.characteristicValue.append($1)
- })
- }
-
- private func writeValue(_ newValue: Data, withResponse: Bool = true) {
-
- let timeout = self.timeout
- let service = self.service
- let characteristic = self.characteristic
- let peripheral = self.service.peripheral
-
- performActivity({
- try NativeCentral.shared.connect(to: peripheral, timeout: timeout)
- defer { NativeCentral.shared.disconnect(peripheral: peripheral) }
- let _ = try NativeCentral.shared.discoverServices(for: peripheral, timeout: timeout)
- let _ = try NativeCentral.shared.discoverCharacteristics(for: service, timeout: timeout)
- try NativeCentral.shared.writeValue(newValue, for: characteristic, withResponse: withResponse, timeout: timeout)
- }, completion: { (viewController: CharacteristicViewController, _) in
- viewController.characteristicValue.append(newValue)
- })
- }
-
- private func startNotifications() {
-
- let timeout = self.timeout
- let service = self.service
- let characteristic = self.characteristic
- let peripheral = self.service.peripheral
-
- performActivity({
- try NativeCentral.shared.connect(to: peripheral, timeout: timeout)
- let _ = try NativeCentral.shared.discoverServices(for: peripheral, timeout: timeout)
- let _ = try NativeCentral.shared.discoverCharacteristics(for: service, timeout: timeout)
- try NativeCentral.shared.notify({ [weak self] (newValue) in mainQueue { self?.notification(newValue) } }, for: characteristic)
- }, completion: { (viewController: CharacteristicViewController, _) in
- viewController.isNotifying = true
- })
- }
-
- private func notification(_ newValue: Data) {
-
- NSLog("\(#function) new characteristicValue: \(newValue)")
- self.characteristicValue.append(newValue)
- }
-
- private func stopNotifications() {
-
- let timeout = self.timeout
- let service = self.service
- let characteristic = self.characteristic
- let peripheral = self.service.peripheral
-
- performActivity({
- // should already be connected
- defer { NativeCentral.shared.disconnect(peripheral: peripheral) }
- let _ = try NativeCentral.shared.discoverServices(for: peripheral, timeout: timeout)
- let _ = try NativeCentral.shared.discoverCharacteristics(for: service, timeout: timeout)
- try NativeCentral.shared.notify(nil, for: characteristic)
- }, completion: { (viewController: CharacteristicViewController, _) in
- viewController.isNotifying = false
- })
- }
-
- private func configure(cell: UITableViewCell, with value: String) {
-
- cell.textLabel?.text = value
-
- #if os(iOS)
- cell.textLabel?.numberOfLines = 0
- #endif
- }
-
- private func configure(cell: UITableViewCell, with data: Data) {
-
- cell.textLabel?.text = data.isEmpty ? "No value" : "0x" + data.reduce("", { $0 + String($1, radix: 16) })
-
- #if os(iOS)
- cell.textLabel?.numberOfLines = 0
- #endif
- }
-
- // MARK: - UITableViewDataSource
-
- override func numberOfSections(in tableView: UITableView) -> Int {
- NSLog("count: \(sections.count)")
- return sections.count
- }
-
- override func tableView(_ tableView: UITableView, numberOfRowsInSection sectionIndex: Int) -> Int {
-
- let section = self.sections[sectionIndex]
-
- return section.items.count
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-
- #if os(iOS)
- let item = self[indexPath]
-
- switch item {
- case let .uuid(uuid):
- let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.uuid.rawValue, for: indexPath)
- configure(cell: cell, with: uuid.rawValue)
- return cell
- case let .name(name):
- let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.name.rawValue, for: indexPath)
- configure(cell: cell, with: name)
- return cell
- case let .value(data):
- let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.value.rawValue, for: indexPath)
- configure(cell: cell, with: data)
- return cell
- case let .property(property):
- let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.property.rawValue, for: indexPath)
- configure(cell: cell, with: property.description)
- return cell
- }
- #else
-
- let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
-
- let layoutName = "list_cell"
-
- if cell.layoutName != layoutName {
- cell.inflateAndroidLayout(layoutName)
- }
-
- let itemView = cell.androidView
-
- let tvItemId = UIApplication.shared.androidActivity.getIdentifier(name: "text_label", type: "id")
-
- guard let tvItemObject = itemView.findViewById(tvItemId)
- else { fatalError("No view for \(tvItemId)") }
-
- let tvItem = Android.Widget.TextView(casting: tvItemObject)
-
- let item = self.sections[indexPath.section].items[indexPath.row]
- NSLog("tableView \(indexPath.row) : \(item)")
- switch item {
- case let .uuid(uuid):
- tvItem?.text = uuid.rawValue
- case let .name(name):
- tvItem?.text = name
- case let .value(data):
- tvItem?.text = data.isEmpty ? "No value" : "0x" + data.reduce("", { $0 + String($1, radix: 16) }).uppercased()
- case let .property(property):
- tvItem?.text = property.description
- }
- return cell
- #endif
- }
-
- func openAlertForWrite(item: NativeCharacteristic){
-
- let alertController = UIAlertController.init(title: "Write Characteristic", message: nil, preferredStyle: UIAlertControllerStyle.alert)
-
- alertController.addTextField(configurationHandler: { text in text.placeholder = "New Value" })
-
- let action1 = UIAlertAction.init(title: "Write", style: UIAlertActionStyle.default) { action in
-
- let newValue = alertController.textFields![0].text
-
- guard let value = newValue, let data = Data(hexString: value) else {
-
- NSLog("Value is required")
- //AndroidToast.makeText(context: UIApplication.shared.androidActivity, text: "Value is required", duration: AndroidToast.Dutation.short).show()
- return
- }
-
- self.writeValue(data)
- }
-
- let action2 = UIAlertAction.init(title: "Cancel", style: UIAlertActionStyle.cancel)
-
- alertController.addAction(action1)
- alertController.addAction(action2)
-
- present(alertController, animated: false)
- }
-
- #if os(iOS)
- override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
-
- return self.sections[section].title
- }
- #else
- override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
-
- let peripheralViewLayoutId = UIApplication.shared.androidActivity.getIdentifier(name: "list_header", type: "layout")
-
- let layoutInflarer = Android.View.LayoutInflater.from(context: UIApplication.shared.androidActivity)
-
- let itemView = layoutInflarer.inflate(resource: Android.R.Layout(rawValue: peripheralViewLayoutId), root: nil, attachToRoot: false)
-
- let tvHeaderId = UIApplication.shared.androidActivity.getIdentifier(name: "text_label", type: "id")
-
- guard let tvHeaderObject = itemView.findViewById(tvHeaderId)
- else { fatalError("No view for \(tvHeaderId)") }
-
- let tvHeader = Android.Widget.TextView(casting: tvHeaderObject)
-
- tvHeader?.text = self.sections[section].title
-
- let uiView = UIView.init(androidViewChild: itemView)
-
- uiView.androidView.layoutParams = Android.Widget.FrameLayout.FLayoutParams(width: Android.Widget.FrameLayout.FLayoutParams.MATCH_PARENT, height: Android.Widget.FrameLayout.FLayoutParams.WRAP_CONTENT)
-
- return uiView
- }
- #endif
-
- // MARK: - UITableViewDelegate
-
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-
- #if os(iOS)
- defer { tableView.deselectRow(at: indexPath, animated: true) }
- #endif
-
- let item = self[indexPath]
-
- switch item {
-
- case .uuid,
- .name:
- break
-
- case let .value(data):
- break // TODO: Show expanded data
-
- case let .property(property):
-
- switch property {
- case .broadcast,
- .extendedProperties,
- .signedWrite:
- break // cant handle
- case .read:
- readValue()
- case .write:
- openAlertForWrite(item: characteristic)
- break
- case .writeWithoutResponse:
- // TODO: show UI to type new value
- writeValue(Data(), withResponse: false)
- break
- case .notify, .indicate:
- NSLog("notify")
- isNotifying ? stopNotifications() : startNotifications()
- }
- }
- }
-}
-
-// MARK: - ActivityIndicatorViewController
-
-extension CharacteristicViewController: TableViewActivityIndicatorViewController { }
-
-// MARK: - Supporting Types
-
-private extension CharacteristicViewController {
-
- struct Section {
-
- let title: String?
- let items: [Item]
- }
-
- enum Item {
-
- case uuid(BluetoothUUID)
- case name(String)
- case value(Data)
- case property(GATT.CharacteristicProperty)
- }
-
- enum CellIdentifier: String {
-
- case uuid
- case name
- case value
- case property
- }
-}
-
-
-//FIXME:
-extension Data {
- init?(hexString: String) {
- let length = hexString.count / 2
- var data = Data(capacity: length)
- for i in 0 ..< length {
- let j = hexString.index(hexString.startIndex, offsetBy: i * 2)
- let k = hexString.index(j, offsetBy: 2)
- let bytes = hexString[j..
- typealias NativeCharacteristic = Characteristic
-
- // MARK: - Properties
-
- #if os(iOS)
- lazy var activityIndicator: UIActivityIndicatorView = self.loadActivityIndicatorView()
- #else
- lazy var progressDialog: AndroidProgressDialog = {
- let progressDialog = AndroidProgressDialog(context: UIApplication.shared.androidActivity)
- progressDialog.setIndeterminate(true)
- progressDialog.setTitle("Wait")
- progressDialog.setMessage("Loading Characteristics...")
- return progressDialog
- }()
- #endif
-
- let service: NativeService
-
- private let cellReuseIdentifier = "Cell"
-
- private let timeout: TimeInterval = .gattDefaultTimeout
-
- private(set) var items = [NativeCharacteristic]() {
-
- didSet { self.tableView.reloadData() }
- }
-
- // MARK: - Initialization
-
- init(service: NativeService) {
-
- self.service = service
-
- super.init(style: .plain)
- }
-
- #if os(iOS)
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- #endif
-
- // MARK: - Loading
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- // setup table view
- self.tableView.estimatedRowHeight = 44
- self.tableView.rowHeight = UITableViewAutomaticDimension
- self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: self.cellReuseIdentifier)
-
- // add refresh control
- let refreshControl = UIRefreshControl(frame: .zero)
-
- #if os(Android) || os(macOS)
- refreshControl.addTarget(action: { [unowned self] in self.reloadData() }, for: .valueChanged)
- #else
- refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
- #endif
-
- self.refreshControl = refreshControl
-
- self.configureView()
- self.reloadData()
- }
-
- // MARK: - Methods
-
- private subscript (indexPath: IndexPath) -> NativeCharacteristic {
-
- @inline(__always)
- get { return self.items[indexPath.row] }
- }
-
- #if os(iOS) || os(macOS)
- @objc func pullToRefresh() {
-
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { [weak self] in
-
- self?.reloadData()
- })
- }
- #endif
-
- private func endRefreshing() {
-
- if let refreshControl = self.refreshControl,
- refreshControl.isRefreshing == true {
-
- refreshControl.endRefreshing()
- }
- }
-
- private func configureView() {
-
- self.title = self.service.uuid.description
- }
-
- private func reloadData() {
-
- let timeout = self.timeout
-
- let service = self.service
- let peripheral = self.service.peripheral
-
- performActivity({
- try NativeCentral.shared.connect(to: peripheral)
- defer { NativeCentral.shared.disconnect(peripheral: peripheral) }
- let _ = try NativeCentral.shared.discoverServices(for: peripheral, timeout: timeout)
- return try NativeCentral.shared.discoverCharacteristics(for: service, timeout: timeout)
- }, completion: {
- $0.items = $1
- })
- }
-
- private func configure(cell: UITableViewCell, at indexPath: IndexPath) {
-
- let item = self[indexPath]
-
- cell.textLabel?.text = item.uuid.description
-
- #if os(iOS)
- cell.textLabel?.numberOfLines = 0
- #endif
- }
-
- // MARK: - UITableViewDataSource
-
- override func numberOfSections(in tableView: UITableView) -> Int {
-
- return 1
- }
-
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-
- return items.count
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-
- let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
-
- configure(cell: cell, at: indexPath)
-
- return cell
- }
-
- // MARK: - UITableViewDelegate
-
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-
- #if os(iOS)
- defer { tableView.deselectRow(at: indexPath, animated: true) }
- #endif
-
- let characteristic = self[indexPath]
-
- log("Selected \(characteristic.uuid.description)")
-
- let viewController = CharacteristicViewController(service: service, characteristic: characteristic)
-
- self.show(viewController, sender: self)
- }
-}
-
-// MARK: - ActivityIndicatorViewController
-
-extension CharacteristicsViewController: TableViewActivityIndicatorViewController { }
diff --git a/Android/app/src/main/swift/Sources/ErrorAlert.swift b/Android/app/src/main/swift/Sources/ErrorAlert.swift
deleted file mode 100755
index 08ca46e..0000000
--- a/Android/app/src/main/swift/Sources/ErrorAlert.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// ErrorAlert.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 6/19/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-#endif
-
-public extension UIViewController {
-
- /// Presents an error alert controller with the specified completion handlers.
- func showErrorAlert(_ localizedText: String,
- okHandler: (() -> ())? = nil,
- retryHandler: (()-> ())? = nil) {
-
- let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"),
- message: localizedText,
- preferredStyle: .alert)
-
- alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "OK"), style: UIAlertActionStyle.`default`, handler: { (UIAlertAction) in
-
- okHandler?()
-
- alert.presentingViewController?.dismiss(animated: true, completion: nil)
- }))
-
- // optionally add retry button
-
- if retryHandler != nil {
-
- alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: "Retry"), style: UIAlertActionStyle.`default`, handler: { (UIAlertAction) in
-
- retryHandler!()
-
- alert.presentingViewController?.dismiss(animated: true, completion: nil)
- }))
- }
-
- self.present(alert, animated: true, completion: nil)
- }
-}
-
diff --git a/Android/app/src/main/swift/Sources/Log.swift b/Android/app/src/main/swift/Sources/Log.swift
deleted file mode 100644
index 8e9cdad..0000000
--- a/Android/app/src/main/swift/Sources/Log.swift
+++ /dev/null
@@ -1,19 +0,0 @@
-//
-// Log.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 9/7/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-
-/// app logger function
-func log(_ message: String) {
-
- #if os(Android)
- NSLog(message)
- #else
- print(message)
- #endif
-}
diff --git a/Android/app/src/main/swift/Sources/MainActivity.swift b/Android/app/src/main/swift/Sources/MainActivity.swift
deleted file mode 100644
index c6cde61..0000000
--- a/Android/app/src/main/swift/Sources/MainActivity.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-//
-// main.swift
-// AndroidUIKit
-//
-// Created by Marco Estrella on 9/6/18.
-//
-
-#if os(Android) || os(macOS)
-
-import Foundation
-import Android
-import AndroidUIKit
-
-/// Needs to be implemented by app.
-@_silgen_name("SwiftAndroidMainApplication")
-public func SwiftAndroidMainApplication() -> SwiftApplication.Type {
-
- NSLog("\(#function)")
-
- // initialize singleton App Delegate
- UIApplication.shared.delegate = AndroidAppDelegate()
-
- // return specialized Android Application
- return AndroidUIKitApplication.self
-}
-
-/// Needs to be implemented by app.
-@_silgen_name("SwiftAndroidMainActivity")
-public func SwiftAndroidMainActivity() -> SwiftSupportAppCompatActivity.Type {
-
- NSLog("\(#function)")
-
- // return specialized Android Activity
- return AndroidUIKitMainActivity.self
-}
-
-#endif
diff --git a/Android/app/src/main/swift/Sources/MainActivityBindings.swift b/Android/app/src/main/swift/Sources/MainActivityBindings.swift
deleted file mode 100644
index 502eb65..0000000
--- a/Android/app/src/main/swift/Sources/MainActivityBindings.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-
-import java_swift
-
-/// generated by: genswift.java 'java/lang|java/util|java/sql' 'Sources' '../java' ///
-
-/// interface com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings ///
-
-public protocol MainActivityBindings: JavaProtocol {
-
-}
-
-
-open class MainActivityBindingsForward: JNIObjectForward, MainActivityBindings {
-
- private static var MainActivityBindingsJNIClass: jclass?
-
-}
-
diff --git a/Android/app/src/main/swift/Sources/MainActivityBindings_Listener.swift b/Android/app/src/main/swift/Sources/MainActivityBindings_Listener.swift
deleted file mode 100644
index 7b43869..0000000
--- a/Android/app/src/main/swift/Sources/MainActivityBindings_Listener.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-
-import java_swift
-
-/// generated by: genswift.java 'java/lang|java/util|java/sql' 'Sources' '../java' ///
-
-/// interface com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings$Listener ///
-
-public protocol MainActivityBindings_Listener: JavaProtocol {
-
- /// public abstract void com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings$Listener.hi()
-
- func hi()
-
-}
-
-
-open class MainActivityBindings_ListenerForward: JNIObjectForward, MainActivityBindings_Listener {
-
- private static var MainActivityBindings_ListenerJNIClass: jclass?
-
- /// public abstract void com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings$Listener.hi()
-
- private static var hi_MethodID_2: jmethodID?
-
- open func hi() {
- var __locals = [jobject]()
- var __args = [jvalue]( repeating: jvalue(), count: 1 )
- JNIMethod.CallVoidMethod( object: javaObject, methodName: "hi", methodSig: "()V", methodCache: &MainActivityBindings_ListenerForward.hi_MethodID_2, args: &__args, locals: &__locals )
- }
-
-
-}
-
-private typealias MainActivityBindings_Listener_hi_0_type = @convention(c) ( _: UnsafeMutablePointer, _: jobject?, _: jlong ) -> ()
-
-private func MainActivityBindings_Listener_hi_0( _ __env: UnsafeMutablePointer, _ __this: jobject?, _ __swiftObject: jlong ) -> () {
- MainActivityBindings_ListenerLocal_.swiftObject( jniEnv: __env, javaObject: __this, swiftObject: __swiftObject ).hi( )
-}
-
-fileprivate class MainActivityBindings_ListenerLocal_: JNILocalProxy {
-
- fileprivate static let _proxyClass: jclass = {
- var natives = [JNINativeMethod]()
-
- let MainActivityBindings_Listener_hi_0_thunk: MainActivityBindings_Listener_hi_0_type = MainActivityBindings_Listener_hi_0
- natives.append( JNINativeMethod( name: strdup("__hi"), signature: strdup("(J)V"), fnPtr: unsafeBitCast( MainActivityBindings_Listener_hi_0_thunk, to: UnsafeMutableRawPointer.self ) ) )
-
- natives.append( JNINativeMethod( name: strdup("__finalize"), signature: strdup("(J)V"), fnPtr: unsafeBitCast( JNIReleasableProxy__finalize_thunk, to: UnsafeMutableRawPointer.self ) ) )
-
- let clazz = JNI.FindClass( proxyClassName() )
- natives.withUnsafeBufferPointer {
- nativesPtr in
- if JNI.api.RegisterNatives( JNI.env, clazz, nativesPtr.baseAddress, jint(nativesPtr.count) ) != jint(JNI_OK) {
- JNI.report( "Unable to register java natives" )
- }
- }
-
- defer { JNI.DeleteLocalRef( clazz ) }
- return JNI.api.NewGlobalRef( JNI.env, clazz )!
- }()
-
- override open class func proxyClassName() -> String { return "org/swiftjava/com_millertech/MainActivityBindings_ListenerProxy" }
- override open class func proxyClass() -> jclass? { return _proxyClass }
-
-}
-
-extension MainActivityBindings_Listener {
-
- public func localJavaObject( _ locals: UnsafeMutablePointer<[jobject]> ) -> jobject? {
- return MainActivityBindings_ListenerLocal_( owned: self, proto: self ).localJavaObject( locals )
- }
-
-}
-
-open class MainActivityBindings_ListenerBase: MainActivityBindings_Listener {
-
- public init() {}
-
- /// public abstract void com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings$Listener.hi()
-
- open func hi() /**/ {
- }
-
-
-}
diff --git a/Android/app/src/main/swift/Sources/MainActivityBindings_Responder.swift b/Android/app/src/main/swift/Sources/MainActivityBindings_Responder.swift
deleted file mode 100644
index 10402a5..0000000
--- a/Android/app/src/main/swift/Sources/MainActivityBindings_Responder.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-
-import java_swift
-
-/// generated by: genswift.java 'java/lang|java/util|java/sql' 'Sources' '../java' ///
-
-/// interface com.millertech.bluetoothexplorer.swiftbindings.MainActivityBindings$Responder ///
-
-public protocol MainActivityBindings_Responder: JavaProtocol {
-
-}
-
-
-open class MainActivityBindings_ResponderForward: JNIObjectForward, MainActivityBindings_Responder {
-
- private static var MainActivityBindings_ResponderJNIClass: jclass?
-
-}
-
diff --git a/Android/app/src/main/swift/Sources/NibTableViewCell.swift b/Android/app/src/main/swift/Sources/NibTableViewCell.swift
deleted file mode 100644
index f742bb2..0000000
--- a/Android/app/src/main/swift/Sources/NibTableViewCell.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-//
-// NibTableViewCell.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 4/30/19.
-// Copyright © 2019 PureSwift. All rights reserved.
-//
-
-#if os(iOS)
-import Foundation
-import UIKit
-
-/// NIB loading table view cell
-protocol NibTableViewCell: ReusableTableViewCell {
-
- static var nibName: String { get }
-}
-
-extension NibTableViewCell {
-
- static var nibName: String {
-
- return reuseIdentifier
- }
-}
-
-extension NibTableViewCell {
-
- static func register(tableView: UITableView) {
-
- let bundle = Bundle(for: self)
- let nib = UINib(nibName: nibName, bundle: bundle)
- tableView.register(nib, forCellReuseIdentifier: reuseIdentifier)
- }
-}
-
-#endif
diff --git a/Android/app/src/main/swift/Sources/ReusableTableViewCell.swift b/Android/app/src/main/swift/Sources/ReusableTableViewCell.swift
deleted file mode 100644
index 23c8c27..0000000
--- a/Android/app/src/main/swift/Sources/ReusableTableViewCell.swift
+++ /dev/null
@@ -1,66 +0,0 @@
-//
-// ReusableTableViewCell.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 4/30/19.
-// Copyright © 2019 PureSwift. All rights reserved.
-//
-
-import Foundation
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-import java_lang
-#endif
-
-/// Reusable Table View Cell
-protocol ReusableTableViewCell: class {
-
- /// Reusable Cell Identifier
- static var reuseIdentifier: String { get }
-
- /// Register table view cell for reuse.
- static func register(tableView: UITableView)
-}
-
-#if os(iOS)
-extension ReusableTableViewCell where Self: UITableViewCell {
-
- static func register(tableView: UITableView) {
-
- tableView.register(self, forCellReuseIdentifier: reuseIdentifier)
- }
-}
-#else
-extension ReusableTableViewCell {
-
- static func register(tableView: UITableView) {
-
- tableView.register(self as? UITableViewCell.Type, forCellReuseIdentifier: reuseIdentifier)
- }
-}
-#endif
-
-extension UITableView {
-
- func dequeueReusableCell (_ cell: T.Type, for indexPath: IndexPath) -> T {
-
- let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath)
-
- guard let reusableCell = cell as? T
- else { fatalError("Invalid cell \(cell) for \(T.reuseIdentifier)") }
-
- return reusableCell
- }
-}
-
-extension UITableView {
-
- func register (_ cell: T.Type) {
-
- T.register(tableView: self)
- }
-}
diff --git a/Android/app/src/main/swift/Sources/ScanDataTableViewCell.swift b/Android/app/src/main/swift/Sources/ScanDataTableViewCell.swift
deleted file mode 100644
index 69e6ba7..0000000
--- a/Android/app/src/main/swift/Sources/ScanDataTableViewCell.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-//
-// ScanDataTableViewCell.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 4/30/19.
-// Copyright © 2019 PureSwift. All rights reserved.
-//
-
-import Foundation
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-import java_lang
-#endif
-
-/// Table View Cell
-final class ScanDataTableViewCell: UITableViewCell {
-
- #if os(iOS)
-
- #elseif os(Android) || os(macOS)
-
- var titleLabel: Android.Widget.TextView! {
-
- return Android.Widget.TextView(casting: self["textLabel"])
- }
-
- var detailLabel: Android.Widget.TextView! {
-
- return Android.Widget.TextView(casting: self["detailTextLabel"])
- }
-
- var accessoryImageView: Android.Widget.ImageView! {
-
- return Android.Widget.ImageView(casting: self["accessoryImageView"])
- }
-
- var isAccessoryVisible: Bool = false {
- didSet { accessoryImageView.visibility = isAccessoryVisible ? .visible : .invisible }
- }
-
- #endif
-}
-
-// MARK: - ReusableTableViewCell
-
-extension ScanDataTableViewCell: ReusableTableViewCell {
- static let reuseIdentifier = "ScanDataTableViewCell"
-}
-
-#if os(iOS)
-extension ScanDataTableViewCell: NibTableViewCell { }
-#elseif os(Android) || os(macOS)
-extension ScanDataTableViewCell: AndroidTableViewCell {
- static let layout = "cell_right_detail"
-
- func awakeFromLayout() {
-
- }
-}
-#endif
diff --git a/Android/app/src/main/swift/Sources/ServicesViewController.swift b/Android/app/src/main/swift/Sources/ServicesViewController.swift
deleted file mode 100644
index 6db6dff..0000000
--- a/Android/app/src/main/swift/Sources/ServicesViewController.swift
+++ /dev/null
@@ -1,196 +0,0 @@
-//
-// ServicesViewController.swift
-// BluetoothExplorer
-//
-// Created by Alsey Coleman Miller on 9/7/18.
-// Copyright © 2018 PureSwift. All rights reserved.
-//
-
-import Foundation
-import Bluetooth
-import GATT
-
-#if os(iOS)
-import UIKit
-#elseif os(Android) || os(macOS)
-import Android
-import AndroidUIKit
-#endif
-
-/// Services
-final class ServicesViewController: UITableViewController {
-
- typealias NativeScanData = ScanData
-
- typealias NativeService = Service
-
- // MARK: - Properties
-
- let scanData: NativeScanData
-
- private(set) var items = [NativeService]() {
-
- didSet { self.tableView.reloadData() }
- }
-
- private let cellReuseIdentifier = "Cell"
-
- private let timeout: TimeInterval = .gattDefaultTimeout
-
- #if os(iOS)
- lazy var activityIndicator: UIActivityIndicatorView = self.loadActivityIndicatorView()
- #else
- lazy var progressDialog: AndroidProgressDialog = {
- let progressDialog = AndroidProgressDialog(context: UIApplication.shared.androidActivity)
- progressDialog.setIndeterminate(true)
- progressDialog.setTitle("Wait")
- progressDialog.setMessage("Loading Services...")
- return progressDialog
- }()
- #endif
-
- // MARK: - Loading
-
- init(scanData: NativeScanData) {
-
- self.scanData = scanData
-
- super.init(style: .plain)
- }
-
- #if os(iOS)
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- #endif
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- // setup table view
- self.tableView.estimatedRowHeight = 44
- self.tableView.rowHeight = UITableViewAutomaticDimension
- self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: self.cellReuseIdentifier)
-
- // add refresh control
-
- let actionRefresh: () -> () = {
-
- self.reloadData()
- }
-
- let refreshControl = UIRefreshControl(frame: .zero)
-
- #if os(Android) || os(macOS)
- refreshControl.addTarget(action: actionRefresh, for: .valueChanged)
- #else
- refreshControl.addTarget(self, action: #selector(pullToRefresh), for: UIControlEvents.valueChanged)
- #endif
-
- self.refreshControl = refreshControl
-
- self.configureView()
- self.reloadData()
- }
-
- // MARK: - Actions
-
- #if os(iOS) || os(macOS)
- @objc func pullToRefresh() {
-
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { [weak self] in
-
- self?.reloadData()
- })
- }
- #endif
-
- // MARK: - Methods
-
- private subscript (indexPath: IndexPath) -> NativeService {
-
- @inline(__always)
- get { return self.items[indexPath.row] }
- }
-
- private func configureView() {
-
- self.title = self.scanData.advertisementData.localName ?? self.scanData.peripheral.identifier.description
- }
-
- private func reloadData() {
-
- let peripheral = self.scanData.peripheral
- let timeout = self.timeout
-
- performActivity({
- try NativeCentral.shared.connect(to: peripheral)
- defer { NativeCentral.shared.disconnect(peripheral: peripheral) }
- return try NativeCentral.shared.discoverServices([], for: peripheral, timeout: timeout)
- }, completion: {
- $0.items = $1
- })
- }
-
- private func endRefreshing() {
-
- if let refreshControl = self.refreshControl,
- refreshControl.isRefreshing == true {
-
- refreshControl.endRefreshing()
- }
- }
-
- private func configure(cell: UITableViewCell, at indexPath: IndexPath) {
-
- let item = self[indexPath]
-
- cell.textLabel?.text = item.uuid.description
-
- #if os(iOS)
- cell.textLabel?.numberOfLines = 0
- #endif
- }
-
- // MARK: - UITableViewDataSource
-
- override func numberOfSections(in tableView: UITableView) -> Int {
-
- return 1
- }
-
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-
- return items.count
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-
- let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
-
- configure(cell: cell, at: indexPath)
-
- return cell
- }
-
- // MARK: - UITableViewDelegate
-
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-
- #if os(iOS)
- defer { tableView.deselectRow(at: indexPath, animated: true) }
- #endif
-
- let item = self[indexPath]
-
- log("Selected \(item.uuid.description)")
-
- let viewController = CharacteristicsViewController(service: item)
-
- self.show(viewController, sender: self)
- }
-}
-
-// MARK: - ActivityIndicatorViewController
-
-extension ServicesViewController: TableViewActivityIndicatorViewController { }
diff --git a/Android/build.gradle b/Android/build.gradle
deleted file mode 100644
index fadcee6..0000000
--- a/Android/build.gradle
+++ /dev/null
@@ -1,32 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
- ext.kotlin_version = '1.2.51'
- repositories {
- google()
- jcenter()
- mavenLocal()
- mavenCentral()
- maven { url 'https://jitpack.io' }
- }
- dependencies {
- classpath 'net.zhuoweizhang:swiftandroid:1.0.0'
- classpath 'com.android.tools.build:gradle:3.1.4'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
-}
-
-allprojects {
- repositories {
- google()
- jcenter()
- maven { url 'https://jitpack.io' }
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/Android/gradle.properties b/Android/gradle.properties
deleted file mode 100644
index 743d692..0000000
--- a/Android/gradle.properties
+++ /dev/null
@@ -1,13 +0,0 @@
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
diff --git a/Android/gradle/wrapper/gradle-wrapper.jar b/Android/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 7a3265e..0000000
Binary files a/Android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/Android/settings.gradle b/Android/settings.gradle
deleted file mode 100644
index e7b4def..0000000
--- a/Android/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-include ':app'
diff --git a/BluetoothExplorer.swiftpm/App.swift b/BluetoothExplorer.swiftpm/App.swift
index eae5d24..dae42ba 100644
--- a/BluetoothExplorer.swiftpm/App.swift
+++ b/BluetoothExplorer.swiftpm/App.swift
@@ -12,7 +12,7 @@ import SwiftUI
@main
struct BluetoothExplorerApp: App {
- let store = Store.shared
+ static let store = Store()
var body: some Scene {
WindowGroup {
@@ -20,7 +20,7 @@ struct BluetoothExplorerApp: App {
CentralList()
Text("Scan for devices")
}
- .environmentObject(Store.shared)
+ .environment(Self.store)
}
}
}
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Entity/CharacteristicEntity.swift b/BluetoothExplorer.swiftpm/AppIntents/Entity/CharacteristicEntity.swift
index adc5b9a..d22c50e 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Entity/CharacteristicEntity.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Entity/CharacteristicEntity.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/22/22.
//
+#if canImport(AppIntents)
import AppIntents
import Bluetooth
import GATT
@@ -25,18 +26,20 @@ extension CharacteristicEntity {
struct ID: Equatable, Hashable, EntityIdentifierConvertible, Sendable {
- let peripheral: UUID
+ typealias Peripheral = NativeCentral.Peripheral.ID
+
+ let peripheral: Peripheral
let attributeID: Int
var entityIdentifierString: String {
- return peripheral.uuidString + "/" + attributeID.description
+ return peripheral.description + "/" + attributeID.description
}
static func entityIdentifier(for string: String) -> ID? {
let components = string.components(separatedBy: "/")
guard components.count == 2,
- let peripheral = UUID(uuidString: components[0]),
+ let peripheral = Peripheral(components[0]),
let attributeID = Int(components[1]) else {
return nil
}
@@ -80,7 +83,7 @@ struct CharacteristicQuery: EntityQuery {
@MainActor
func entities(for identifiers: [CharacteristicEntity.ID]) -> [CharacteristicEntity] {
- let allServices = Store.shared.characteristics.values.lazy.reduce([], { $0 + $1 })
+ let allServices = BluetoothExplorerApp.store.characteristics.values.lazy.reduce([], { $0 + $1 })
return identifiers.compactMap { id in
allServices
.first(where: { $0.peripheral.id == id.peripheral && $0.id.hashValue == id.attributeID })
@@ -90,3 +93,4 @@ struct CharacteristicQuery: EntityQuery {
func suggestedEntities() -> [CharacteristicEntity] { [] }
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Entity/ManufacturerDataEntity.swift b/BluetoothExplorer.swiftpm/AppIntents/Entity/ManufacturerDataEntity.swift
index 7083458..206f035 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Entity/ManufacturerDataEntity.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Entity/ManufacturerDataEntity.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/20/22.
//
+#if canImport(AppIntents)
import AppIntents
import Bluetooth
import GATT
@@ -59,10 +60,11 @@ struct ManufacturerDataQuery: EntityQuery {
@MainActor
func suggestedEntities() throws -> [ManufacturerDataEntity] {
- return Store.shared.scanResults
+ return BluetoothExplorerApp.store.scanResults
.values
.lazy
.compactMap { $0.manufacturerData }
.map { .init($0) }
}
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Entity/PeripheralEntity.swift b/BluetoothExplorer.swiftpm/AppIntents/Entity/PeripheralEntity.swift
index 724e7df..60c5d6f 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Entity/PeripheralEntity.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Entity/PeripheralEntity.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/20/22.
//
+#if canImport(AppIntents)
import AppIntents
import Bluetooth
import GATT
@@ -12,7 +13,7 @@ import GATT
@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *)
struct PeripheralEntity: AppEntity, Identifiable {
- let id: UUID
+ let id: NativeCentral.Peripheral.ID
/// Timestamp for when device was scanned.
let date: Date
@@ -73,8 +74,8 @@ extension PeripheralEntity {
struct PeripheralQuery: EntityQuery {
@MainActor
- func entities(for identifiers: [UUID]) -> [PeripheralEntity] {
- let scanResults = Store.shared.scanResults.values
+ func entities(for identifiers: [PeripheralEntity.ID]) -> [PeripheralEntity] {
+ let scanResults = BluetoothExplorerApp.store.scanResults.values
return identifiers.compactMap { id in
scanResults.first(where: { $0.id == id })
.map { PeripheralEntity($0.scanData) }
@@ -83,9 +84,23 @@ struct PeripheralQuery: EntityQuery {
@MainActor
func suggestedEntities() throws -> [PeripheralEntity] {
- Store.shared.scanResults
+ BluetoothExplorerApp.store.scanResults
.values
.sorted(by: { ($0.name ?? $0.id.description) < ($1.name ?? $1.id.description) })
.map { .init($0.scanData) }
}
}
+
+// MARK: - EntityIdentifierConvertible
+
+extension BluetoothAddress: @retroactive EntityIdentifierConvertible {
+
+ public var entityIdentifierString: String {
+ rawValue
+ }
+
+ public static func entityIdentifier(for entityIdentifierString: String) -> Bluetooth.BluetoothAddress? {
+ .init(rawValue: entityIdentifierString)
+ }
+}
+#endif
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Entity/ServiceEntity.swift b/BluetoothExplorer.swiftpm/AppIntents/Entity/ServiceEntity.swift
index d46c8fa..9872cc3 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Entity/ServiceEntity.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Entity/ServiceEntity.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/22/22.
//
+#if canImport(AppIntents)
import AppIntents
import Bluetooth
import GATT
@@ -24,18 +25,20 @@ extension ServiceEntity {
struct ID: Equatable, Hashable, EntityIdentifierConvertible, Sendable {
- let peripheral: UUID
+ let peripheral: PeripheralEntity.ID
let attributeID: Int
var entityIdentifierString: String {
- return peripheral.uuidString + "/" + attributeID.description
+ return peripheral.description + "/" + attributeID.description
}
/// Identifiers should be able to initialize via a `String` format.
static func entityIdentifier(for string: String) -> ID? {
let components = string.components(separatedBy: "/")
- guard components.count == 2, let peripheral = UUID(uuidString: components[0]), let attributeID = Int(components[1]) else {
+ guard components.count == 2,
+ let peripheral = PeripheralEntity.ID(components[0]),
+ let attributeID = Int(components[1]) else {
return nil
}
return ServiceEntity.ID.init(peripheral: peripheral, attributeID: attributeID)
@@ -75,7 +78,7 @@ struct ServiceQuery: EntityQuery {
@MainActor
func entities(for identifiers: [ServiceEntity.ID]) -> [ServiceEntity] {
- let allServices = Store.shared.services.values.lazy.reduce([], { $0 + $1 })
+ let allServices = BluetoothExplorerApp.store.services.values.lazy.reduce([], { $0 + $1 })
return identifiers.compactMap { id in
allServices
.first(where: { $0.peripheral.id == id.peripheral && $0.id.hashValue == id.attributeID })
@@ -85,3 +88,4 @@ struct ServiceQuery: EntityQuery {
func suggestedEntities() -> [ServiceEntity] { [] }
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverCharacteristicsIntent.swift b/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverCharacteristicsIntent.swift
index 83b0016..a379272 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverCharacteristicsIntent.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverCharacteristicsIntent.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/22/22.
//
+#if canImport(AppIntents) && canImport(SwiftUI)
import AppIntents
import SwiftUI
import Bluetooth
@@ -31,15 +32,15 @@ struct DiscoverCharacteristicsIntent: AppIntent {
@MainActor
func perform() async throws -> some IntentResult {
- let store = Store.shared
+ let store = BluetoothExplorerApp.store
guard let peripheral = store.scanResults.keys.first(where: { $0.id == service.id.peripheral }) else {
throw CentralError.unknownPeripheral
}
guard let service = store.services[peripheral, default: []].first(where: { $0.id.hashValue == service.id.attributeID && $0.peripheral.id == service.id.peripheral }) else {
throw CentralError.invalidAttribute(BluetoothUUID(rawValue: service.uuid) ?? .bit128(.zero))
}
- try await store.central.wait(for: .poweredOn, warning: 1, timeout: 2)
- if store.connected.contains(peripheral) == false {
+ try await store.central.wait(warning: 1, timeout: 2)
+ if await store.connected.contains(peripheral) == false {
try await store.connect(to: peripheral)
}
try await store.discoverCharacteristics(for: service)
@@ -52,3 +53,4 @@ struct DiscoverCharacteristicsIntent: AppIntent {
)
}
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverServicesIntent.swift b/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverServicesIntent.swift
index 7bd4aa8..fd1287e 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverServicesIntent.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Intent/DiscoverServicesIntent.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/22/22.
//
+#if canImport(AppIntents) && canImport(SwiftUI)
import AppIntents
import SwiftUI
import Bluetooth
@@ -31,12 +32,12 @@ struct DiscoverServicesIntent: AppIntent {
@MainActor
func perform() async throws -> some IntentResult {
- let store = Store.shared
+ let store = BluetoothExplorerApp.store
guard let peripheral = store.scanResults.keys.first(where: { $0.id == device.id }) else {
throw CentralError.unknownPeripheral
}
- try await store.central.wait(for: .poweredOn, warning: 1, timeout: 2)
- if store.connected.contains(peripheral) == false {
+ try await store.central.wait(warning: 1, timeout: 2)
+ if await store.connected.contains(peripheral) == false {
try await store.connect(to: peripheral)
}
try await store.discoverServices(for: peripheral)
@@ -49,3 +50,4 @@ struct DiscoverServicesIntent: AppIntent {
)
}
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Intent/ScanIntent.swift b/BluetoothExplorer.swiftpm/AppIntents/Intent/ScanIntent.swift
index 4db5d3b..da58cb9 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Intent/ScanIntent.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Intent/ScanIntent.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/20/22.
//
+#if canImport(AppIntents) && canImport(SwiftUI)
import AppIntents
import SwiftUI
import Bluetooth
@@ -51,8 +52,8 @@ struct ScanIntent: AppIntent {
@MainActor
func perform() async throws -> some IntentResult {
let services = Set(self.services.compactMap({ BluetoothUUID(rawValue: $0) }))
- let store = Store.shared
- try await store.central.wait(for: .poweredOn, warning: 1, timeout: 2)
+ let store = BluetoothExplorerApp.store
+ try await store.central.wait(warning: 1, timeout: 2)
try await store.scan(
with: services,
filterDuplicates: filterDuplicates
@@ -115,3 +116,4 @@ private extension ScanIntent {
}.padding()
}
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/AppIntents/Shortcuts.swift b/BluetoothExplorer.swiftpm/AppIntents/Shortcuts.swift
index b822e3c..5a2190a 100644
--- a/BluetoothExplorer.swiftpm/AppIntents/Shortcuts.swift
+++ b/BluetoothExplorer.swiftpm/AppIntents/Shortcuts.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 11/20/22.
//
+#if canImport(AppIntents)
import AppIntents
@available(macOS 13, iOS 16, watchOS 9, tvOS 16, *)
@@ -26,3 +27,4 @@ struct AppShortcuts: AppShortcutsProvider {
)
}
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/Model/AttributeValue.swift b/BluetoothExplorer.swiftpm/Model/AttributeValue.swift
index 69b15bd..aa37a6d 100644
--- a/BluetoothExplorer.swiftpm/Model/AttributeValue.swift
+++ b/BluetoothExplorer.swiftpm/Model/AttributeValue.swift
@@ -7,25 +7,35 @@
import Foundation
-enum AttributeValueType: Equatable, Hashable {
+public enum AttributeValueType: Equatable, Hashable {
case read
case write
case notification
}
-struct AttributeValue: Equatable, Hashable {
+public struct AttributeValue: Equatable, Hashable {
- let date: Date
+ public let date: Date
- let type: AttributeValueType
+ public let type: AttributeValueType
- let data: Data
+ public let data: Data
+
+ public init(
+ date: Date,
+ type: AttributeValueType,
+ data: Data
+ ) {
+ self.date = date
+ self.type = type
+ self.data = data
+ }
}
extension AttributeValue: Identifiable {
- var id: Date {
+ public var id: Date {
date
}
}
diff --git a/BluetoothExplorer.swiftpm/Model/Mock/MockAdvertisement.swift b/BluetoothExplorer.swiftpm/Model/Mock/MockAdvertisement.swift
index e2f1671..be3c573 100644
--- a/BluetoothExplorer.swiftpm/Model/Mock/MockAdvertisement.swift
+++ b/BluetoothExplorer.swiftpm/Model/Mock/MockAdvertisement.swift
@@ -12,34 +12,35 @@ import Bluetooth
import GATT
/// Mock Advertisement Data
-struct MockAdvertisementData: AdvertisementData {
+internal struct MockAdvertisementData: AdvertisementData {
/// The local name of a peripheral.
- let localName: String?
+ internal let localName: String?
/// The Manufacturer data of a peripheral.
- let manufacturerData: ManufacturerSpecificData?
+ internal let manufacturerData: ManufacturerSpecificData?
/// This value is available if the broadcaster (peripheral) provides its Tx power level in its advertising packet.
/// Using the RSSI value and the Tx power level, it is possible to calculate path loss.
- let txPowerLevel: Double?
+ internal let txPowerLevel: Double?
/// Service-specific advertisement data.
- let serviceData: [BluetoothUUID: Data]?
+ internal let serviceData: [BluetoothUUID: Data]?
/// An array of service UUIDs
- let serviceUUIDs: [BluetoothUUID]?
+ internal let serviceUUIDs: [BluetoothUUID]?
/// An array of one or more `BluetoothUUID`, representing Service UUIDs.
- let solicitedServiceUUIDs: [BluetoothUUID]?
+ internal let solicitedServiceUUIDs: [BluetoothUUID]?
- init(localName: String? = nil,
- manufacturerData: ManufacturerSpecificData? = nil,
- txPowerLevel: Double? = nil,
- serviceData: [BluetoothUUID : Data]? = nil,
- serviceUUIDs: [BluetoothUUID]? = nil,
- solicitedServiceUUIDs: [BluetoothUUID]? = nil) {
-
+ internal init(
+ localName: String? = nil,
+ manufacturerData: ManufacturerSpecificData? = nil,
+ txPowerLevel: Double? = nil,
+ serviceData: [BluetoothUUID : Data]? = nil,
+ serviceUUIDs: [BluetoothUUID]? = nil,
+ solicitedServiceUUIDs: [BluetoothUUID]? = nil
+ ) {
self.localName = localName
self.manufacturerData = manufacturerData
self.txPowerLevel = txPowerLevel
@@ -51,7 +52,7 @@ struct MockAdvertisementData: AdvertisementData {
extension MockAdvertisementData {
- static let beacon = MockAdvertisementData(
+ internal static let beacon = MockAdvertisementData(
localName: nil,
manufacturerData: ManufacturerSpecificData(data: Data([0x4c, 0x00, 0x02, 0x15, 0xb9, 0x40, 0x7f, 0x30, 0xf5, 0xf8, 0x46, 0x6e, 0xaf, 0xf9, 0x25, 0x55, 0x6b, 0x57, 0xfe, 0x6d, 0x29, 0x4c, 0x90, 0x39, 0x74])),
txPowerLevel: nil,
@@ -60,7 +61,7 @@ extension MockAdvertisementData {
solicitedServiceUUIDs: nil
)
- static let smartThermostat = MockAdvertisementData(
+ internal static let smartThermostat = MockAdvertisementData(
localName: "CLI-W200",
manufacturerData: ManufacturerSpecificData(data: Data([0xd9, 0x01, 0x01, 0x02, 0x00, 0x00, 0x8c, 0x85, 0x90, 0xcb, 0x31, 0x74, 0x00, 0x60])),
txPowerLevel: nil,
diff --git a/BluetoothExplorer.swiftpm/Model/Mock/MockAttributes.swift b/BluetoothExplorer.swiftpm/Model/Mock/MockAttributes.swift
index a62aa01..3edfba1 100644
--- a/BluetoothExplorer.swiftpm/Model/Mock/MockAttributes.swift
+++ b/BluetoothExplorer.swiftpm/Model/Mock/MockAttributes.swift
@@ -6,15 +6,14 @@
//
#if DEBUG
-import SwiftUI
import Bluetooth
import GATT
-typealias MockService = GATT.Service
-typealias MockCharacteristic = GATT.Characteristic
-typealias MockDescriptor = GATT.Descriptor
+internal typealias MockService = GATT.Service
+internal typealias MockCharacteristic = GATT.Characteristic
+internal typealias MockDescriptor = GATT.Descriptor
-extension MockService {
+internal extension MockService {
static var deviceInformation: MockService {
Service(
@@ -41,7 +40,7 @@ extension MockService {
}
}
-extension MockCharacteristic {
+internal extension MockCharacteristic {
static var deviceName: MockCharacteristic {
Characteristic(
@@ -96,7 +95,7 @@ extension MockCharacteristic {
)
}
-extension MockDescriptor {
+internal extension MockDescriptor {
static func clientCharacteristicConfiguration(_ peripheral: Peripheral) -> MockDescriptor {
Descriptor(
diff --git a/BluetoothExplorer.swiftpm/Model/Mock/MockCentral.swift b/BluetoothExplorer.swiftpm/Model/Mock/MockCentral.swift
index 19cd8bb..1a0242e 100644
--- a/BluetoothExplorer.swiftpm/Model/Mock/MockCentral.swift
+++ b/BluetoothExplorer.swiftpm/Model/Mock/MockCentral.swift
@@ -9,77 +9,96 @@
import Foundation
import Bluetooth
import GATT
-import DarwinGATT
-internal final class MockCentral: CentralManager, @unchecked Sendable {
+@MainActor
+final class MockCentral: CentralManager, @unchecked Sendable {
/// Central Peripheral Type
typealias Peripheral = GATT.Peripheral
/// Central Advertisement Type
- typealias Advertisement = MockAdvertisementData
+ internal typealias Advertisement = MockAdvertisementData
/// Central Attribute ID (Handle)
- typealias AttributeID = UInt16
+ internal typealias AttributeID = UInt16
+
+ internal nonisolated(unsafe) var log: (@Sendable (String) -> ())?
- lazy var state = AsyncStream { [unowned self] in
- $0.yield(.poweredOn)
- }
-
- var log: (@Sendable (String) -> ())?
-
- var peripherals: [GATT.Peripheral : Bool] {
+ internal var peripherals: [GATT.Peripheral : Bool] {
get async {
var peripherals = [Peripheral: Bool]()
- for scanData in _state.scanData {
- peripherals[scanData.peripheral] = _state.connected.contains(scanData.peripheral)
+ for scanData in state.scanData {
+ peripherals[scanData.peripheral] = state.connected.contains(scanData.peripheral)
}
return peripherals
}
}
- var _state = State()
+ internal var isEnabled: Bool {
+ get async {
+ state.isEnabled
+ }
+ }
+
+ private var state = State()
private var continuation = Continuation()
- init() { }
+ internal init() {
+ Task {
+ try await Task.sleep(for: .seconds(1))
+ updateState {
+ $0.isEnabled = true
+ }
+ }
+ }
/// Scans for peripherals that are advertising services.
- func scan(filterDuplicates: Bool) -> AsyncCentralScan {
+ internal func scan(
+ with services: Set,
+ filterDuplicates: Bool
+ ) -> AsyncCentralScan {
return AsyncCentralScan { continuation in
- self._state.scanData.forEach {
- continuation($0)
+ for scanData in await self.state.scanData {
+ try await Task.sleep(for: .seconds(1))
+ continuation(scanData)
}
}
}
+ internal func scan(
+ filterDuplicates: Bool
+ ) -> AsyncCentralScan {
+ scan(with: [], filterDuplicates: filterDuplicates)
+ }
+
/// Connect to the specified device
- func connect(to peripheral: Peripheral) async throws {
- _state.connected.insert(peripheral)
+ internal func connect(to peripheral: Peripheral) async throws {
+ state.connected.insert(peripheral)
}
/// Disconnect the specified device.
- func disconnect(_ peripheral: Peripheral) {
- _state.connected.remove(peripheral)
+ internal func disconnect(_ peripheral: Peripheral) async {
+ state.connected.remove(peripheral)
}
/// Disconnect all connected devices.
- func disconnectAll() {
- _state.connected.removeAll()
+ internal func disconnectAll() {
+ state.connected.removeAll()
}
/// Discover Services
- func discoverServices(
+ internal func discoverServices(
_ services: Set = [],
for peripheral: Peripheral
) async throws -> [Service] {
- return _state.characteristics
+ return state.characteristics
.keys
.filter { $0.peripheral == peripheral }
.sorted(by: { $0.id < $1.id })
}
- public func discoverIncludedServices(
+ internal func discoverIncludedServices(
_ services: Set = [],
for service: Service
) async throws -> [Service] {
@@ -87,14 +106,14 @@ internal final class MockCentral: CentralManager, @unchecked Sendable {
}
/// Discover Characteristics for service
- func discoverCharacteristics(
+ internal nonisolated func discoverCharacteristics(
_ characteristics: Set = [],
for service: Service
) async throws -> [Characteristic] {
- guard _state.connected.contains(service.peripheral) else {
+ guard await state.connected.contains(service.peripheral) else {
throw CentralError.disconnected
}
- guard let characteristics = _state.characteristics[service] else {
+ guard let characteristics = await state.characteristics[service] else {
throw CentralError.invalidAttribute(service.uuid)
}
return characteristics
@@ -102,22 +121,22 @@ internal final class MockCentral: CentralManager, @unchecked Sendable {
}
/// Read Characteristic Value
- func readValue(
+ internal nonisolated func readValue(
for characteristic: Characteristic
) async throws -> Data {
- guard _state.connected.contains(characteristic.peripheral) else {
+ guard await state.connected.contains(characteristic.peripheral) else {
throw CentralError.disconnected
}
- return _state.characteristicValues[characteristic] ?? Data()
+ return await state.characteristicValues[characteristic] ?? Data()
}
/// Write Characteristic Value
- func writeValue(
+ internal nonisolated func writeValue(
_ data: Data,
for characteristic: Characteristic,
withResponse: Bool = true
) async throws {
- guard _state.connected.contains(characteristic.peripheral) else {
+ guard await state.connected.contains(characteristic.peripheral) else {
throw CentralError.disconnected
}
if withResponse {
@@ -130,50 +149,56 @@ internal final class MockCentral: CentralManager, @unchecked Sendable {
}
}
// write
- _state.characteristicValues[characteristic] = data
+ await updateState { state in
+ state.characteristicValues[characteristic] = data
+ }
}
/// Discover descriptors
- func discoverDescriptors(
+ internal nonisolated func discoverDescriptors(
for characteristic: Characteristic
) async throws -> [Descriptor] {
- guard _state.connected.contains(characteristic.peripheral) else {
+ guard await state.connected.contains(characteristic.peripheral) else {
throw CentralError.disconnected
}
- return _state.descriptors[characteristic] ?? []
+ return await state.descriptors[characteristic] ?? []
}
/// Read descriptor
- func readValue(
+ internal nonisolated func readValue(
for descriptor: Descriptor
) async throws -> Data {
- guard _state.connected.contains(descriptor.peripheral) else {
+ guard await state.connected.contains(descriptor.peripheral) else {
throw CentralError.disconnected
}
- return _state.descriptorValues[descriptor] ?? Data()
+ return await state.descriptorValues[descriptor] ?? Data()
}
/// Write descriptor
- func writeValue(
+ internal nonisolated func writeValue(
_ data: Data,
for descriptor: Descriptor
) async throws {
- guard _state.connected.contains(descriptor.peripheral) else {
+ guard await state.connected.contains(descriptor.peripheral) else {
throw CentralError.disconnected
}
- _state.descriptorValues[descriptor] = data
+ await updateState { state in
+ state.descriptorValues[descriptor] = data
+ }
}
- func notify(
+ internal nonisolated func notify(
for characteristic: GATT.Characteristic
) async throws -> AsyncCentralNotifications {
- guard _state.connected.contains(characteristic.peripheral) else {
+ guard await state.connected.contains(characteristic.peripheral) else {
throw CentralError.disconnected
}
return AsyncCentralNotifications { [unowned self] continuation in
- if let notifications = self._state.notifications[characteristic] {
+ if let notifications = await self.state.notifications[characteristic] {
for notification in notifications {
- try await Task.sleep(nanoseconds: 1_000_000_000)
+ if #available(iOS 16.0, *) {
+ try await Task.sleep(for: .seconds(1))
+ }
continuation(notification)
}
}
@@ -181,22 +206,30 @@ internal final class MockCentral: CentralManager, @unchecked Sendable {
}
/// Read MTU
- func maximumTransmissionUnit(for peripheral: Peripheral) async throws -> MaximumTransmissionUnit {
- guard _state.connected.contains(peripheral) else {
+ internal func maximumTransmissionUnit(for peripheral: Peripheral) async throws -> MaximumTransmissionUnit {
+ guard state.connected.contains(peripheral) else {
throw CentralError.disconnected
}
return .default
}
// Read RSSI
- func rssi(for peripheral: Peripheral) async throws -> RSSI {
+ internal func rssi(for peripheral: Peripheral) async throws -> RSSI {
return .init(rawValue: 127)!
}
}
+private extension MockCentral {
+
+ func updateState(_ body: (inout State) -> ()) {
+ body(&state)
+ }
+}
+
internal extension MockCentral {
struct State {
+ var isEnabled = false
var isScanning = false
var scanData: [MockScanData] = [.beacon, .smartThermostat]
var connected = Set()
@@ -253,7 +286,6 @@ internal extension MockCentral {
struct Continuation {
var scan: AsyncThrowingStream, Error>.Continuation?
- var isScanning: AsyncStream.Continuation?
}
}
#endif
diff --git a/BluetoothExplorer.swiftpm/Model/Mock/MockScanData.swift b/BluetoothExplorer.swiftpm/Model/Mock/MockScanData.swift
index 981808a..28c6925 100644
--- a/BluetoothExplorer.swiftpm/Model/Mock/MockScanData.swift
+++ b/BluetoothExplorer.swiftpm/Model/Mock/MockScanData.swift
@@ -11,9 +11,9 @@ import Foundation
import Bluetooth
import GATT
-typealias MockScanData = ScanData
+internal typealias MockScanData = ScanData
-extension MockScanData {
+internal extension MockScanData {
static let beacon = MockScanData(
peripheral: .beacon,
@@ -32,7 +32,7 @@ extension MockScanData {
)
}
-extension Peripheral {
+internal extension Peripheral {
static var beacon: Peripheral {
Peripheral(id: BluetoothAddress(rawValue: "00:AA:AB:03:10:01")!)
diff --git a/BluetoothExplorer.swiftpm/Model/NativeCentral.swift b/BluetoothExplorer.swiftpm/Model/NativeCentral.swift
index 387e126..9c79a08 100644
--- a/BluetoothExplorer.swiftpm/Model/NativeCentral.swift
+++ b/BluetoothExplorer.swiftpm/Model/NativeCentral.swift
@@ -7,73 +7,44 @@
//
import Foundation
-import SwiftUI
import Bluetooth
import GATT
+#if canImport(DarwinGATT)
import DarwinGATT
+#endif
-#if os(Android) || os(iOS) && targetEnvironment(simulator)
+#if os(Android)
+typealias NativeCentral = MockCentral
+#elseif os(iOS) && targetEnvironment(simulator)
typealias NativeCentral = MockCentral
-
-extension NativeCentral {
-
- private struct Cache {
- static let central = MockCentral()
- }
-
- static var shared: NativeCentral {
- return Cache.central
- }
-}
-
#elseif canImport(Darwin)
typealias NativeCentral = DarwinCentral
-
-extension NativeCentral {
-
- private struct Cache {
- static let central = DarwinCentral(
- options: .init(showPowerAlert: true)
- )
- }
-
- static var shared: NativeCentral {
- return Cache.central
- }
-}
#else
#error("Platform not supported")
#endif
-#if canImport(Darwin)
-public extension NativeCentral {
+extension NativeCentral {
/// Wait for CoreBluetooth to be ready.
func wait(
- for state: DarwinBluetoothState,
warning: Int = 3,
timeout: Int = 10
) async throws {
var powerOnWait = 0
- var currentState: DarwinBluetoothState
+ var currentState: Bool
repeat {
- currentState = await self.state
+ currentState = await self.isEnabled
// inform user after 3 seconds
if powerOnWait == warning {
- NSLog("Waiting for CoreBluetooth to be ready, please turn on Bluetooth")
+ NSLog("Waiting for Bluetooth to be ready, please turn on Bluetooth")
}
// sleep for 1s
- if #available(macOS 13, iOS 16, watchOS 9, tvOS 16, *) {
- try await Task.sleep(for: .seconds(1))
- } else {
- try await Task.sleep(nanoseconds: 1_000_000_000)
- }
+ try await Task.sleep(for: .seconds(1))
powerOnWait += 1
guard powerOnWait < timeout else {
- throw DarwinCentralError.invalidState(currentState)
+ throw CocoaError(.featureUnsupported) // TODO: Update error
}
- } while currentState != state
+ } while currentState != true
}
}
-#endif
diff --git a/BluetoothExplorer.swiftpm/Model/Store.swift b/BluetoothExplorer.swiftpm/Model/Store.swift
index 80ac143..8486882 100644
--- a/BluetoothExplorer.swiftpm/Model/Store.swift
+++ b/BluetoothExplorer.swiftpm/Model/Store.swift
@@ -7,15 +7,14 @@
//
import Foundation
-@preconcurrency import Combine
-import SwiftUI
+import Observation
import Bluetooth
import GATT
-import DarwinGATT
/// Store
@MainActor
-final class Store: ObservableObject, @unchecked Sendable {
+@Observable
+public final class Store: @unchecked Sendable {
typealias Central = NativeCentral
@@ -33,64 +32,49 @@ final class Store: ObservableObject, @unchecked Sendable {
// MARK: - Properties
- @Published
private(set) var activity = [Peripheral: Bool]()
- @Published
- private(set) var state: DarwinBluetoothState = .unknown
+ private(set) var isEnabled = false
- @Published
private(set) var scanResults = [Peripheral: ScanResult]()
var isScanning: Bool {
self.scanStream?.isScanning ?? false
}
- @Published
- private(set) var connected = Set()
+ private(set) var connected: Set = []
- @Published
private(set) var services = [Peripheral: [Service]]()
- @Published
private(set) var characteristics = [Service: [Characteristic]]()
- @Published
private(set) var includedServices = [Service: [Service]]()
-
- @Published
+
private(set) var descriptors = [Characteristic: [Descriptor]]()
-
- @Published
+
private(set) var characteristicValues = [Characteristic: Cache]()
- @Published
private(set) var descriptorValues = [Descriptor: Cache]()
-
- @Published
+
private(set) var isNotifying = [Characteristic: Bool]()
internal let central: Central
- @Published
private var scanStream: AsyncCentralScan?
-
- private var centralObserver: AnyCancellable?
-
+
// MARK: - Initialization
- deinit {
- centralObserver?.cancel()
+ public convenience init() {
+ let central = Central()
+ self.init(central: central)
}
init(central: Central) {
self.central = central
- observeValues()
setupLog()
+ observeValues()
}
-
- static let shared = Store(central: .shared)
-
+
// MARK: - Methods
private func setupLog() {
@@ -98,29 +82,30 @@ final class Store: ObservableObject, @unchecked Sendable {
}
private func observeValues() {
- centralObserver = central.objectWillChange
- .receive(on: DispatchQueue.main)
- .sink { [unowned self] _ in
- self.objectWillChange.send()
- Task { [unowned self] in
+ Task { [weak self] in
+ do {
+ while let self {
+ try await Task.sleep(for: .seconds(1))
await self.updateState()
- await self.updateConnected()
}
}
-
+ catch {
+
+ }
+ }
}
private func updateState() async {
assert(Thread.isMainThread)
- let oldValue = self.state
- let newValue = await self.central.state
+ let oldValue = self.isEnabled
+ let newValue = await self.central.isEnabled
guard newValue != oldValue else {
return
}
// update value
- self.state = newValue
+ self.isEnabled = newValue
// start scanning when powered on
- guard newValue == .poweredOn else {
+ guard newValue else {
return
}
do { try await self.scan() }
@@ -137,6 +122,14 @@ final class Store: ObservableObject, @unchecked Sendable {
self.connected = newValue
}
+ public func log(_ message: String) {
+ print(message)
+ }
+
+ func log(error: any Error) {
+ print("Error", error.localizedDescription)
+ }
+
func scan(
with services: Set = [],
filterDuplicates: Bool = true
@@ -152,6 +145,7 @@ final class Store: ObservableObject, @unchecked Sendable {
for try await scanData in stream {
await found(scanData: scanData)
}
+ self.scanStream = nil
}
}
@@ -159,7 +153,11 @@ final class Store: ObservableObject, @unchecked Sendable {
private func found(scanData: ScanData) async {
var cache = scanResults[scanData.peripheral] ?? ScanDataCache(scanData: scanData)
cache += scanData
- #if canImport(CoreBluetooth)
+ #if os(Android)
+
+ #elseif os(iOS) && targetEnvironment(simulator)
+
+ #elseif canImport(CoreBluetooth)
cache.name = try? await central.name(for: scanData.peripheral)
for serviceUUID in scanData.advertisementData.overflowServiceUUIDs ?? [] {
cache.overflowServiceUUIDs.insert(serviceUUID)
@@ -180,8 +178,6 @@ final class Store: ObservableObject, @unchecked Sendable {
scanStream?.stop()
}
try await central.connect(to: peripheral)
- assert(Thread.isMainThread)
- connected.insert(peripheral)
}
func disconnect(_ peripheral: Central.Peripheral) async {
diff --git a/BluetoothExplorer.swiftpm/Package.resolved b/BluetoothExplorer.swiftpm/Package.resolved
deleted file mode 100644
index a9b0dae..0000000
--- a/BluetoothExplorer.swiftpm/Package.resolved
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "originHash" : "6f441ddd41cfb68cb844b72f1d636b1c13c69022a658e28afd7edb91e9200e78",
- "pins" : [
- {
- "identity" : "bluetooth",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/PureSwift/Bluetooth.git",
- "state" : {
- "revision" : "52f228b819e8ef920e4232cc37c9a84ffbbc5707",
- "version" : "7.2.3"
- }
- },
- {
- "identity" : "gatt",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/PureSwift/GATT.git",
- "state" : {
- "branch" : "master",
- "revision" : "aacc05662e4fec5fdf22c666d14c0a79763f40f6"
- }
- },
- {
- "identity" : "swift-syntax",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/swiftlang/swift-syntax.git",
- "state" : {
- "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
- "version" : "601.0.1"
- }
- }
- ],
- "version" : 3
-}
diff --git a/BluetoothExplorer.swiftpm/Package.swift b/BluetoothExplorer.swiftpm/Package.swift
index 7cc0fec..a54f614 100644
--- a/BluetoothExplorer.swiftpm/Package.swift
+++ b/BluetoothExplorer.swiftpm/Package.swift
@@ -10,14 +10,14 @@ import AppleProductTypes
let package = Package(
name: "BluetoothExplorer",
platforms: [
- .iOS("15.2"),
- .macOS("12.0")
+ .iOS("17.0"),
+ .macOS("15.0")
],
products: [
.iOSApplication(
name: "BluetoothExplorer",
targets: ["BluetoothExplorer"],
- bundleIdentifier: "com.pureswift.bluetooth-explorer",
+ bundleIdentifier: "org.pureswift.bluetoothexplorer",
teamIdentifier: "4W79SG34MW",
displayVersion: "1.0",
bundleVersion: "5",
@@ -59,5 +59,5 @@ let package = Package(
// Xcode only settings
#if os(macOS)
package.dependencies[0] = .package(url: "https://github.com/PureSwift/GATT.git", branch: "master")
-package.platforms = [.iOS("15.0")]
+package.platforms = [.iOS("17.0")]
#endif
diff --git a/BluetoothExplorer.swiftpm/ViewModel/CentralListViewModel.swift b/BluetoothExplorer.swiftpm/ViewModel/CentralListViewModel.swift
new file mode 100644
index 0000000..2a421f2
--- /dev/null
+++ b/BluetoothExplorer.swiftpm/ViewModel/CentralListViewModel.swift
@@ -0,0 +1,196 @@
+//
+// CentralListViewModel.swift
+// BluetoothExplorer
+//
+// Created by Alsey Coleman Miller on 7/11/25.
+//
+
+import Foundation
+import Observation
+import Bluetooth
+import GATT
+
+@MainActor
+@Observable
+public final class CentralListViewModel {
+
+ let store: Store
+
+ var scanToggleTask: Task?
+
+ public init(store: Store) {
+ self.store = store
+ }
+
+ var state: State {
+ State(
+ .init(
+ store: store,
+ didToggle: scanToggleTask != nil
+ )
+ )
+ }
+
+ public var scanResults: [ScanResult] {
+ state.scanResults
+ }
+
+ public var isEnabled: Bool {
+ state.isEnabled
+ }
+
+ public var isScanning: Bool {
+ state.isScanning
+ }
+
+ public var canToggleScan: Bool {
+ state.canToggleScan
+ }
+
+ public func scanToggle() {
+ scanToggleTask = Task {
+ defer { scanToggleTask = nil }
+ if store.isScanning {
+ await store.stopScan()
+ } else {
+ do {
+ try await store.scan()
+ }
+ catch {
+ store.log(error: error)
+ }
+ }
+ }
+ }
+}
+
+public extension CentralListViewModel {
+
+ struct State: Equatable, Hashable, Sendable {
+
+ let input: Input
+
+ init(_ input: Input) {
+ self.input = input
+ }
+
+ public var scanResults: [ScanResult] {
+ input.scanResults
+ .values
+ .lazy
+ .sorted(by: { $0.id.description < $1.id.description })
+ .sorted(by: { ($0.name ?? "") < ($1.name ?? "") })
+ .sorted(by: { $0.name != nil && $1.name == nil })
+ .sorted(by: { $0.beacon != nil && $1.beacon == nil })
+ .map { ScanResult($0) }
+ }
+
+ public var isEnabled: Bool {
+ input.isEnabled
+ }
+
+ public var isScanning: Bool {
+ input.isScanning
+ }
+
+ public var canToggleScan: Bool {
+ input.didToggle == false && input.isEnabled
+ }
+ }
+}
+
+public extension CentralListViewModel.State {
+
+ struct Input: Equatable, Hashable, Sendable {
+
+ let scanResults: [Peripheral: Store.ScanResult]
+
+ let isEnabled: Bool
+
+ let isScanning: Bool
+
+ let didToggle: Bool
+
+ init(scanResults: [Peripheral : Store.ScanResult], isEnabled: Bool, isScanning: Bool, didToggle: Bool) {
+ self.scanResults = scanResults
+ self.isEnabled = isEnabled
+ self.isScanning = isScanning
+ self.didToggle = didToggle
+ }
+
+ @MainActor
+ init(store: Store, didToggle: Bool) {
+ self.scanResults = store.scanResults
+ self.isEnabled = store.isEnabled
+ self.isScanning = store.isScanning
+ self.didToggle = didToggle
+ }
+ }
+}
+
+public extension CentralListViewModel {
+
+ struct ScanResult: Equatable, Hashable, Sendable, Identifiable {
+
+ typealias ScanData = ScanDataCache
+
+ let scanData: ScanData
+
+ init(_ scanData: ScanData) {
+ self.scanData = scanData
+ }
+
+ public var id: String {
+ scanData.id.description
+ }
+
+ public var name: String {
+ scanData.name ?? (beacon != nil ? "iBeacon" : "Unknown")
+ }
+
+ public var company: String? {
+ scanData.manufacturerData?.companyIdentifier.name
+ }
+
+ public var services: String? {
+ let services = scanData.serviceUUIDs
+ .sorted(by: { $0.description < $1.description })
+ .map { $0.metadata?.name ?? $0.rawValue }
+ guard services.isEmpty == false
+ else { return nil }
+ return "Services: " + services.reduce("", { ($0.isEmpty ? "" : ", ") + $1 })
+ }
+
+ public var beacon: CentralListViewModel.Beacon? {
+ scanData.beacon.flatMap(Beacon.init)
+ }
+ }
+}
+
+public extension CentralListViewModel {
+
+ struct Beacon: Equatable, Hashable, Sendable {
+
+ let beacon: AppleBeacon
+
+ init(_ beacon: AppleBeacon) {
+ self.beacon = beacon
+ }
+
+ public var uuid: String {
+ beacon.uuid.uuidString
+ }
+
+ public var major: String {
+ "Major: 0x\(beacon.major.toHexadecimal())"
+ }
+
+ public var minor: String {
+ "Minor: 0x\(beacon.minor.toHexadecimal())"
+ }
+
+ public var rssi: String {
+ "RSSI: \(beacon.rssi)"
+ }
+ }
+}
diff --git a/BluetoothExplorer.swiftpm/ViewModel/PeripheralViewModel.swift b/BluetoothExplorer.swiftpm/ViewModel/PeripheralViewModel.swift
new file mode 100644
index 0000000..edd950a
--- /dev/null
+++ b/BluetoothExplorer.swiftpm/ViewModel/PeripheralViewModel.swift
@@ -0,0 +1,56 @@
+//
+// PeripheralViewModel.swift
+// bluetooth-explorer
+//
+// Created by Alsey Coleman Miller on 7/12/25.
+//
+
+import Foundation
+import Observation
+import Bluetooth
+import GATT
+
+@MainActor
+@Observable
+public final class PeripheralViewModel {
+
+ let store: Store
+
+ let peripheral: Store.Peripheral
+
+ init(store: Store, peripheral: Peripheral) {
+ self.store = store
+ self.peripheral = peripheral
+ }
+
+ public convenience init(store: Store, peripheral: String) {
+ guard let peripheral = store.scanResults.first(where: { $0.key.description == peripheral })?.key else {
+ fatalError("Invalid peripheral: \(peripheral)")
+ }
+ self.init(store: store, peripheral: peripheral)
+ }
+
+ var title: String {
+ store.scanResults[peripheral]?.name ?? "Device"
+ }
+
+ var isConnected: Bool {
+ store.connected.contains(peripheral)
+ }
+
+ var services: [Store.Service] {
+ store.services[peripheral] ?? []
+ }
+
+ var showActivity: Bool {
+ store.activity[peripheral] ?? false
+ }
+
+ public func connect() {
+
+ }
+
+ public func reload() {
+
+ }
+}
diff --git a/BluetoothExplorer.swiftpm/Views/AsyncButton.swift b/BluetoothExplorer.swiftpm/Views/AsyncButton.swift
index c635adb..9ac3a3a 100644
--- a/BluetoothExplorer.swiftpm/Views/AsyncButton.swift
+++ b/BluetoothExplorer.swiftpm/Views/AsyncButton.swift
@@ -5,16 +5,17 @@
// Created by Alsey Coleman Miller on 23/12/21.
//
+#if canImport(SwiftUI)
import SwiftUI
+enum ActionOption: CaseIterable {
+ case disableButton
+ case showProgressView
+}
+
// https://www.swiftbysundell.com/articles/building-an-async-swiftui-button/
struct AsyncButton: View {
- enum ActionOption: CaseIterable {
- case disableButton
- case showProgressView
- }
-
var action: () async -> Void
var actionOptions = Set(ActionOption.allCases)
@@ -60,3 +61,4 @@ struct AsyncButton: View {
.disabled(isDisabled)
}
}
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/AttributeCell.swift b/BluetoothExplorer.swiftpm/Views/AttributeCell.swift
index c074d8b..92ac40e 100644
--- a/BluetoothExplorer.swiftpm/Views/AttributeCell.swift
+++ b/BluetoothExplorer.swiftpm/Views/AttributeCell.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 19/12/21.
//
+#if canImport(SwiftUI)
import SwiftUI
import Bluetooth
import GATT
@@ -47,3 +48,4 @@ struct AttributeCell_Preview: PreviewProvider {
}
}
#endif
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/AttributeValueCell.swift b/BluetoothExplorer.swiftpm/Views/AttributeValueCell.swift
index 6bd45ca..3f825bf 100644
--- a/BluetoothExplorer.swiftpm/Views/AttributeValueCell.swift
+++ b/BluetoothExplorer.swiftpm/Views/AttributeValueCell.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 22/12/21.
//
+#if canImport(SwiftUI)
import Foundation
import Bluetooth
import SwiftUI
@@ -114,3 +115,4 @@ extension AttributeValueCell {
)
}
#endif
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/AttributeValuesSection.swift b/BluetoothExplorer.swiftpm/Views/AttributeValuesSection.swift
index 74f1a32..591bb2e 100644
--- a/BluetoothExplorer.swiftpm/Views/AttributeValuesSection.swift
+++ b/BluetoothExplorer.swiftpm/Views/AttributeValuesSection.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 22/12/21.
//
+#if canImport(SwiftUI)
import Foundation
import Bluetooth
import SwiftUI
@@ -100,3 +101,4 @@ struct AttributeValuesSection_Preview: PreviewProvider {
}
#endif
*/
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/CentralCell.swift b/BluetoothExplorer.swiftpm/Views/CentralCell.swift
index 20ebd26..5fed982 100644
--- a/BluetoothExplorer.swiftpm/Views/CentralCell.swift
+++ b/BluetoothExplorer.swiftpm/Views/CentralCell.swift
@@ -5,45 +5,48 @@
// Created by Alsey Coleman Miller on 18/12/21.
//
+#if canImport(SwiftUI)
import SwiftUI
-import Bluetooth
-import GATT
-struct CentralCell : View {
+public struct CentralCell: View {
- let scanData: ScanDataCache
+ let item: CentralListViewModel.ScanResult
- var body: some View {
+ public init(_ item: CentralListViewModel.ScanResult) {
+ self.item = item
+ }
+
+ public var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
- nameText
+ Text(verbatim: item.name)
.font(.title3)
.foregroundColor(.primary)
#if DEBUG
- Text(verbatim: "\(scanData.id)")
+ Text(verbatim: item.id)
.font(.footnote)
.foregroundColor(.secondary)
#endif
- if let beacon = self.beacon {
- Text("\(beacon.uuid)")
+ if let beacon = item.beacon {
+ Text(verbatim: beacon.uuid)
.font(.subheadline)
.foregroundColor(.primary)
- Text("Major: 0x\(beacon.major.toHexadecimal())")
+ Text(verbatim: beacon.major)
.font(.subheadline)
.foregroundColor(.primary)
- Text("Minor: 0x\(beacon.minor.toHexadecimal())")
+ Text(verbatim: beacon.minor)
.font(.subheadline)
.foregroundColor(.primary)
- Text("RSSI: \(beacon.rssi)")
+ Text(verbatim: beacon.rssi)
.font(.subheadline)
.foregroundColor(.primary)
}
- if let company = self.company, beacon == nil {
+ if let company = item.company, item.beacon == nil {
Text(verbatim: company)
.font(.subheadline)
.foregroundColor(.primary)
}
- if let services = services {
- Text("Services: \(services)")
+ if let services = item.services {
+ Text(verbatim: services)
.font(.subheadline)
.foregroundColor(.primary)
}
@@ -51,46 +54,4 @@ struct CentralCell : View {
.padding()
}
}
-
-private struct CentralCellCache {
- static var listFormatter: ListFormatter { ListFormatter() }
-}
-
-extension CentralCell {
-
- var nameText: Text {
- scanData.name.flatMap { Text(verbatim: $0) } ?? (beacon != nil ? Text("iBeacon") : Text("Unknown"))
- }
-
- var company: String? {
- scanData.manufacturerData?.companyIdentifier.name
- }
-
- var services: String? {
- let services = scanData.serviceUUIDs
- .sorted(by: { $0.description < $1.description })
- .map { $0.metadata?.name ?? $0.rawValue }
- guard services.isEmpty == false
- else { return nil }
- return CentralCellCache.listFormatter.string(from: services)
- }
-
- var beacon: AppleBeacon? {
- return scanData.beacon
- }
-}
-
-#if DEBUG
-struct CentralCell_Preview: PreviewProvider {
- static var previews: some View {
- NavigationView {
- List {
- CentralCell(scanData: .init(scanData: MockScanData.beacon))
- CentralCell(scanData: .init(scanData: MockScanData.beacon))
- CentralCell(scanData: .init(scanData: MockScanData.beacon))
- CentralCell(scanData: .init(scanData: MockScanData.smartThermostat))
- }
- }
- }
-}
#endif
diff --git a/BluetoothExplorer.swiftpm/Views/CentralList.swift b/BluetoothExplorer.swiftpm/Views/CentralList.swift
index d28fea0..677882d 100644
--- a/BluetoothExplorer.swiftpm/Views/CentralList.swift
+++ b/BluetoothExplorer.swiftpm/Views/CentralList.swift
@@ -6,89 +6,90 @@
// Copyright © 2019 Alsey Coleman Miller. All rights reserved.
//
+#if canImport(SwiftUI)
import SwiftUI
-import Bluetooth
-import GATT
-struct CentralList: View {
+public struct CentralList: View {
- @EnvironmentObject
+ typealias ViewModel = CentralListViewModel
+
+ @Environment(Store.self)
var store: Store
- var scanResults: [Store.ScanResult] {
- return store.scanResults
- .values
- .sorted(by: { $0.id.description < $1.id.description })
- .sorted(by: { ($0.name ?? "") < ($1.name ?? "") })
- .sorted(by: { $0.name != nil && $1.name == nil })
- .sorted(by: { $0.beacon != nil && $1.beacon == nil })
- }
+ public init() { }
- var body: some View {
- list
- .navigationTitle(Text("Central"))
- .toolbar { leftBarButtonItem }
+ public var body: some View {
+ ContentView(store)
}
-}
-
-extension CentralList {
- var list: some View {
- ScrollView {
- LazyVStack(alignment: .leading) {
- ForEach(scanResults) { item in
- NavigationLink(
- destination: { PeripheralView(peripheral: item.scanData.peripheral) },
- label: { CentralCell(scanData: item) }
- )
+ struct ContentView: View {
+
+ @State
+ var viewModel: ViewModel
+
+ init(_ store: Store) {
+ _viewModel = State(initialValue: CentralListViewModel(store: store))
+ }
+
+ var body: some View {
+ ListView(scanResults: viewModel.scanResults)
+ .navigationTitle(Text("Central"))
+ .toolbar {
+ leftBarButtonItem
}
- }
+ }
+
+ var leftBarButtonItem: CentralList.LeftBarButtonItem {
+ LeftBarButtonItem(
+ isEnabled: viewModel.isEnabled,
+ isScanning: viewModel.isScanning,
+ canToggleScan: viewModel.canToggleScan,
+ toggle: { viewModel.scanToggle() }
+ )
}
}
- var leftBarButtonItem: some View {
- switch store.state {
- case .unknown:
- return AnyView(EmptyView())
- case .poweredOff,
- .resetting,
- .unauthorized,
- .unsupported:
- return AnyView(
- Image(systemName: "exclamationmark.triangle.fill")
- .foregroundColor(.red)
- )
- case .poweredOn:
- if store.isScanning {
- return AnyView(Button(action: {
- Task {
- await self.store.stopScan()
+ struct ListView: View {
+
+ let scanResults: [CentralListViewModel.ScanResult]
+
+ var body: some View {
+ ScrollView {
+ LazyVStack(alignment: .leading) {
+ ForEach(scanResults) { item in
+ NavigationLink(destination: {
+ EmptyView()
+ }, label: {
+ CentralCell(item)
+ })
}
- }) {
- Text("Stop")
- })
- } else {
- return AnyView(Button(action: {
- Task {
- do { try await self.store.scan() }
- catch { print("Error scanning:", error) }
- }
- }) {
- Text("Scan")
- })
+ }
}
}
}
-}
-
-#if DEBUG
-struct CentralList_Preview: PreviewProvider {
- static var previews: some View {
- Group {
- NavigationView {
- CentralList()
+
+ struct LeftBarButtonItem: View {
+
+ let isEnabled: Bool
+
+ let isScanning: Bool
+
+ let canToggleScan: Bool
+
+ let toggle: () -> ()
+
+ var body: some View {
+ switch isEnabled {
+ case false:
+ return AnyView(
+ Image(systemName: "exclamationmark.triangle.fill")
+ .foregroundColor(.red)
+ )
+ case true:
+ return AnyView(Button(action: toggle) {
+ isScanning ? Text("Stop") : Text("Scan")
+ }.disabled(canToggleScan == false))
}
- .environmentObject(Store.shared)
}
}
}
diff --git a/BluetoothExplorer.swiftpm/Views/CharacteristicView.swift b/BluetoothExplorer.swiftpm/Views/CharacteristicView.swift
index 7506aa3..a9bbbd5 100644
--- a/BluetoothExplorer.swiftpm/Views/CharacteristicView.swift
+++ b/BluetoothExplorer.swiftpm/Views/CharacteristicView.swift
@@ -5,13 +5,14 @@
// Created by Alsey Coleman Miller on 22/12/21.
//
+#if canImport(SwiftUI)
import SwiftUI
import Bluetooth
import GATT
struct CharacteristicView: View {
- @EnvironmentObject
+ @Environment(Store.self)
var store: Store
let characteristic: Store.Characteristic
@@ -153,7 +154,8 @@ extension CharacteristicView {
}
var isConnected: Bool {
- store.connected.contains(peripheral)
+ //store.connected.contains(peripheral)
+ true
}
var descriptors: [Store.Descriptor] {
@@ -251,17 +253,19 @@ struct CharacteristicView_Preview: PreviewProvider {
Group {
NavigationView {
CharacteristicView(
- store: .shared,
characteristic: .deviceName
)
}
+ .environment(Store())
+
NavigationView {
CharacteristicView(
- store: .shared,
characteristic: .batteryLevel
)
}
+ .environment(Store())
}
}
}
#endif
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/DescriptorView.swift b/BluetoothExplorer.swiftpm/Views/DescriptorView.swift
index 7df7207..26ebcf5 100644
--- a/BluetoothExplorer.swiftpm/Views/DescriptorView.swift
+++ b/BluetoothExplorer.swiftpm/Views/DescriptorView.swift
@@ -5,13 +5,14 @@
// Created by Alsey Coleman Miller on 23/12/21.
//
+#if canImport(SwiftUI)
import SwiftUI
import Bluetooth
import GATT
struct DescriptorView: View {
- @EnvironmentObject
+ @Environment(Store.self)
var store: Store
let descriptor: Store.Descriptor
@@ -128,7 +129,9 @@ extension DescriptorView {
}
var isConnected: Bool {
- store.connected.contains(peripheral)
+ // FIXME
+ //store.connected.contains(peripheral)
+ false
}
var showActivity: Bool {
@@ -191,12 +194,13 @@ struct DescriptorView_Preview: PreviewProvider {
Group {
NavigationView {
DescriptorView(
- store: .shared,
descriptor: .clientCharacteristicConfiguration(.beacon)
)
+ .environment(Store())
}
}
}
}
#endif
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/PeripheralView.swift b/BluetoothExplorer.swiftpm/Views/PeripheralView.swift
index 3f409ed..fe1ce7b 100644
--- a/BluetoothExplorer.swiftpm/Views/PeripheralView.swift
+++ b/BluetoothExplorer.swiftpm/Views/PeripheralView.swift
@@ -6,13 +6,14 @@
// Copyright © 2021 Alsey Coleman Miller. All rights reserved.
//
+#if canImport(SwiftUI)
import SwiftUI
import Bluetooth
import GATT
struct PeripheralView: View {
- @EnvironmentObject
+ @Environment(Store.self)
var store: Store
let peripheral: Store.Peripheral
@@ -84,7 +85,8 @@ extension PeripheralView {
}
var isConnected: Bool {
- store.connected.contains(peripheral)
+ //store.connected.contains(peripheral)
+ true
}
var services: [Store.Service] {
@@ -152,11 +154,12 @@ struct PeripheralView_Preview: PreviewProvider {
Group {
NavigationView {
PeripheralView(
- store: .shared,
peripheral: .beacon
)
}
+ .environment(Store())
}
}
}
#endif
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/ServiceView.swift b/BluetoothExplorer.swiftpm/Views/ServiceView.swift
index fb1c8e9..6c759eb 100644
--- a/BluetoothExplorer.swiftpm/Views/ServiceView.swift
+++ b/BluetoothExplorer.swiftpm/Views/ServiceView.swift
@@ -5,13 +5,14 @@
// Created by Alsey Coleman Miller on 22/12/21.
//
+#if canImport(SwiftUI)
import SwiftUI
import Bluetooth
import GATT
struct ServiceView: View {
- @EnvironmentObject
+ @Environment(Store.self)
var store: Store
let service: Store.Service
@@ -88,7 +89,8 @@ extension ServiceView {
}
var isConnected: Bool {
- store.connected.contains(peripheral)
+ //store.connected.contains(peripheral)
+ true
}
var characteristics: [Store.Characteristic] {
@@ -134,11 +136,12 @@ struct ServiceView_Preview: PreviewProvider {
Group {
NavigationView {
ServiceView(
- store: .shared,
service: .deviceInformation
)
}
+ .environment(Store())
}
}
}
#endif
+#endif
diff --git a/BluetoothExplorer.swiftpm/Views/WriteAttributeView.swift b/BluetoothExplorer.swiftpm/Views/WriteAttributeView.swift
index 3d7a15b..d8963b0 100644
--- a/BluetoothExplorer.swiftpm/Views/WriteAttributeView.swift
+++ b/BluetoothExplorer.swiftpm/Views/WriteAttributeView.swift
@@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 23/12/21.
//
+#if canImport(SwiftUI)
import Foundation
import SwiftUI
import Bluetooth
@@ -66,3 +67,4 @@ internal extension WriteAttributeView {
return Data(hexadecimal: text)
}
}
+#endif
diff --git a/Cartfile b/Cartfile
deleted file mode 100644
index 522eae6..0000000
--- a/Cartfile
+++ /dev/null
@@ -1 +0,0 @@
-github "PureSwift/GATT" "master"
\ No newline at end of file
diff --git a/Cartfile.resolved b/Cartfile.resolved
deleted file mode 100644
index 85cfa95..0000000
--- a/Cartfile.resolved
+++ /dev/null
@@ -1,2 +0,0 @@
-github "PureSwift/Bluetooth" "cd5d3fa8a243d405c024a907481a6179c46a9cd6"
-github "PureSwift/GATT" "e6ac711ce0ebafd30d8d8ba11b9015a9f1d0a5fb"
diff --git a/bluetooth-explorer-android/.gitignore b/bluetooth-explorer-android/.gitignore
new file mode 100644
index 0000000..10cf596
--- /dev/null
+++ b/bluetooth-explorer-android/.gitignore
@@ -0,0 +1,6 @@
+.build
+.swiftpm
+.idea
+.gradle
+*.so
+*.jar
diff --git a/bluetooth-explorer-android/Package.swift b/bluetooth-explorer-android/Package.swift
new file mode 100644
index 0000000..e4352a4
--- /dev/null
+++ b/bluetooth-explorer-android/Package.swift
@@ -0,0 +1,65 @@
+// swift-tools-version: 6.1
+import PackageDescription
+
+let package = Package(
+ name: "bluetooth-explorer-android",
+ platforms: [
+ .macOS(.v15),
+ ],
+ products: [
+ .library(
+ name: "BluetoothExplorerApp",
+ type: .dynamic,
+ targets: ["BluetoothExplorerApp"]
+ ),
+ ],
+ dependencies: [
+ .package(
+ url: "https://github.com/PureSwift/swift-java.git",
+ branch: "feature/android"
+ ),
+ .package(
+ url: "https://github.com/PureSwift/Android.git",
+ branch: "master"
+ ),
+ .package(
+ url: "https://github.com/PureSwift/GATT.git",
+ branch: "master"
+ ),
+ ],
+ targets: [
+ .target(
+ name: "BluetoothExplorerApp",
+ dependencies: [
+ .product(
+ name: "AndroidKit",
+ package: "Android"
+ ),
+ "BluetoothExplorerModel"
+ ],
+ path: "./app/src/main/swift/app",
+ swiftSettings: [
+ .swiftLanguageMode(.v5)
+ ]
+ ),
+ .target(
+ name: "BluetoothExplorerModel",
+ dependencies: [
+ .product(
+ name: "GATT",
+ package: "GATT"
+ ),
+ .product(
+ name: "JavaKit",
+ package: "swift-java"
+ )
+ ],
+ swiftSettings: [
+ .swiftLanguageMode(.v5)
+ ],
+ plugins: [
+ .plugin(name: "JExtractSwiftPlugin", package: "swift-java")
+ ]
+ )
+ ]
+)
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/DataExtensions.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/DataExtensions.swift
new file mode 100644
index 0000000..1af1e7b
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/DataExtensions.swift
@@ -0,0 +1,74 @@
+//
+// Data.swift
+// Bluetooth
+//
+// Created by Alsey Coleman Miller on 5/30/18.
+// Copyright © 2018 PureSwift. All rights reserved.
+//
+
+import Foundation
+
+internal extension Data {
+
+ @usableFromInline
+ func subdataNoCopy(in range: Range) -> Data {
+
+ // stored in heap, can reuse buffer
+ if count > Data.inlineBufferSize {
+
+ return withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
+ Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer.baseAddress!.advanced(by: range.lowerBound)),
+ count: range.count,
+ deallocator: .none)
+ }
+
+ } else {
+
+ // stored in stack, must copy
+ return subdata(in: range)
+ }
+ }
+
+ @usableFromInline
+ func suffixNoCopy(from index: Int) -> Data {
+ return subdataNoCopy(in: index ..< count)
+ }
+
+ @usableFromInline
+ func suffixCheckingBounds(from start: Int) -> Data {
+
+ if count > start {
+ return Data(suffix(from: start))
+ } else {
+ return Data()
+ }
+ }
+}
+
+internal extension Data {
+
+ /// Size of the inline buffer for `Foundation.Data` used in Swift 5.
+ ///
+ /// Used to determine wheather data is stored on stack or in heap.
+ @usableFromInline
+ static var inlineBufferSize: Int {
+
+ // Keep up to date
+ // https://github.com/apple/swift/blob/aa3e5904f8ba8bf9ae06d96946774d171074f6e5/stdlib/public/Darwin/Foundation/Data.swift#L656
+ // https://github.com/apple/swift-corelibs-foundation/blob/76068b8caf54f250a7be5336a7c6bb97f55469f8/Sources/Foundation/Data.swift#L649
+ #if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
+ typealias Buffer = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
+ UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) //len //enum
+ #elseif arch(i386) || arch(arm)
+ typealias Buffer = (UInt8, UInt8, UInt8, UInt8,
+ UInt8, UInt8) //len //enum
+ #elseif os(watchOS) // arm64_32
+ typealias Buffer = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
+ UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) //len //enum
+ #else
+ #error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
+ #endif
+
+ return MemoryLayout.size
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/Hexadecimal.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/Hexadecimal.swift
new file mode 100644
index 0000000..0bfa5b0
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/Hexadecimal.swift
@@ -0,0 +1,67 @@
+//
+// Hexadecimal.swift
+// Bluetooth
+//
+// Created by Alsey Coleman Miller on 3/2/16.
+// Copyright © 2016 PureSwift. All rights reserved.
+//
+
+import Foundation
+
+internal extension FixedWidthInteger {
+
+ func toHexadecimal() -> String {
+
+ var string = String(self, radix: 16)
+ while string.utf8.count < (MemoryLayout.size * 2) {
+ string = "0" + string
+ }
+ assert(string.utf8.count == MemoryLayout.size * 2)
+ return string.uppercased()
+ }
+
+ init?(hexadecimal string: T) {
+ self.init(string, radix: 16)
+ }
+}
+
+internal extension Collection where Element: FixedWidthInteger {
+
+ func toHexadecimal() -> String {
+ let length = count * MemoryLayout.size * 2
+ var string = ""
+ string.reserveCapacity(length)
+ string = reduce(into: string) { $0 += $1.toHexadecimal() }
+ assert(string.count == length)
+ return string
+ }
+}
+
+internal extension Data {
+
+ init?(hexadecimal string: String) {
+ let elementStringSize = MemoryLayout.size * 2 // 2 for UInt8
+ guard string.isEmpty == false else {
+ self.init()
+ return
+ }
+ guard string.count % elementStringSize == 0 else {
+ return nil
+ }
+ let elementsCount = string.count / elementStringSize
+ let elements = (0 ..< elementsCount)
+ .lazy
+ .map { ($0 * elementStringSize, ($0+1) * elementStringSize) }
+ .map { string.index(string.startIndex, offsetBy: $0.0) ..< string.index(string.startIndex, offsetBy: $0.1) }
+ .map { string[$0] }
+ self.init()
+ self.reserveCapacity(elementsCount)
+ for substring in elements {
+ guard let element = Element.init(hexadecimal: substring) else {
+ return nil
+ }
+ self.append(element)
+ }
+ assert(self.count == elementsCount)
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/Integer.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/Integer.swift
new file mode 100644
index 0000000..63ac05c
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/Integer.swift
@@ -0,0 +1,54 @@
+//
+// Integer.swift
+// Bluetooth
+//
+// Created by Alsey Coleman Miller on 8/24/15.
+// Copyright © 2015 PureSwift. All rights reserved.
+//
+
+import Foundation
+
+internal extension UInt16 {
+
+ /// Initializes value from two bytes.
+ init(bytes: (UInt8, UInt8)) {
+
+ self = unsafeBitCast(bytes, to: UInt16.self)
+ }
+
+ /// Converts to two bytes.
+ var bytes: (UInt8, UInt8) {
+
+ return unsafeBitCast(self, to: (UInt8, UInt8).self)
+ }
+}
+
+internal extension UInt32 {
+
+ /// Initializes value from four bytes.
+ init(bytes: (UInt8, UInt8, UInt8, UInt8)) {
+
+ self = unsafeBitCast(bytes, to: UInt32.self)
+ }
+
+ /// Converts to four bytes.
+ var bytes: (UInt8, UInt8, UInt8, UInt8) {
+
+ return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8).self)
+ }
+}
+
+internal extension UInt64 {
+
+ /// Initializes value from four bytes.
+ init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
+
+ self = unsafeBitCast(bytes, to: UInt64.self)
+ }
+
+ /// Converts to eight bytes.
+ var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) {
+
+ return unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8).self)
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/UUID.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/UUID.swift
new file mode 100644
index 0000000..535bde5
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/UUID.swift
@@ -0,0 +1,51 @@
+//
+// UUID.swift
+// Bluetooth
+//
+// Created by Alsey Coleman Miller on 4/5/17.
+// Copyright © 2017 PureSwift. All rights reserved.
+//
+
+import Foundation
+
+internal extension UUID {
+
+ static var length: Int { return 16 }
+}
+
+internal extension UUID { // ByteValue
+
+ typealias ByteValue = uuid_t
+
+ static var bitWidth: Int { return 128 }
+
+ @inline(__always)
+ init(bytes: uuid_t) {
+
+ self.init(uuid: bytes)
+ }
+
+ var bytes: uuid_t {
+
+ @inline(__always)
+ get { return uuid }
+
+ @inline(__always)
+ set { self = UUID(uuid: newValue) }
+ }
+}
+
+internal extension UUID {
+
+ init?(data: Data) {
+
+ guard data.count == UUID.length else { return nil }
+
+ self.init(bytes: (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]))
+ }
+
+ var data: Data {
+
+ return Data([bytes.0, bytes.1, bytes.2, bytes.3, bytes.4, bytes.5, bytes.6, bytes.7, bytes.8, bytes.9, bytes.10, bytes.11, bytes.12, bytes.13, bytes.14, bytes.15])
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/iBeacon.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/iBeacon.swift
new file mode 100644
index 0000000..8c08b94
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Extensions/iBeacon.swift
@@ -0,0 +1,58 @@
+//
+// iBeacon.swift
+//
+//
+// Created by Alsey Coleman Miller on 3/26/23.
+//
+
+import Foundation
+import Bluetooth
+import GATT
+
+internal extension AppleBeacon {
+
+ init?(manufacturerData: GATT.ManufacturerSpecificData) where Data: DataContainer {
+
+ let data = manufacturerData.additionalData
+
+ guard manufacturerData.companyIdentifier == type(of: self).companyIdentifier,
+ data.count > 2
+ else { return nil }
+
+ let dataType = data[0]
+
+ guard dataType == type(of: self).appleDataType
+ else { return nil }
+
+ let length = data[1]
+
+ guard length == type(of: self).length,
+ data.count == type(of: self).additionalDataLength
+ else { return nil }
+
+ let uuid = UUID(UInt128(bigEndian: UInt128(data: data.subdata(in: 2 ..< 18))!))
+ let major = UInt16(bigEndian: UInt16(bytes: (data[18], data[19])))
+ let minor = UInt16(bigEndian: UInt16(bytes: (data[20], data[21])))
+ let rssi = Int8(bitPattern: data[22])
+
+ self.init(uuid: uuid, major: major, minor: minor, rssi: rssi)
+ }
+}
+
+internal extension AppleBeacon {
+
+ /// Apple iBeacon data type.
+ static var appleDataType: UInt8 { 0x02 } // iBeacon
+
+ /// The length of the TLV encoded data.
+ static var length: UInt8 { 0x15 } // length: 21 = 16 byte UUID + 2 bytes major + 2 bytes minor + 1 byte RSSI
+
+ static var additionalDataLength: Int { Int(length) + 2 }
+}
+
+internal extension GATT.AdvertisementData {
+
+ var beacon: AppleBeacon? {
+ manufacturerData.flatMap { AppleBeacon(manufacturerData: $0) }
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/AttributeValue.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/AttributeValue.swift
new file mode 100644
index 0000000..73b60b4
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/AttributeValue.swift
@@ -0,0 +1,41 @@
+//
+// AttributeValue.swift
+//
+//
+// Created by Alsey Coleman Miller on 22/12/21.
+//
+
+import Foundation
+
+public enum AttributeValueType: Equatable, Hashable {
+
+ case read
+ case write
+ case notification
+}
+
+public struct AttributeValue: Equatable, Hashable {
+
+ public let date: Date
+
+ public let type: AttributeValueType
+
+ internal let data: Data
+
+ internal init(
+ date: Date,
+ type: AttributeValueType,
+ data: Data
+ ) {
+ self.date = date
+ self.type = type
+ self.data = data
+ }
+}
+
+extension AttributeValue: Identifiable {
+
+ public var id: Date {
+ date
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/BluetoothUUID.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/BluetoothUUID.swift
new file mode 100644
index 0000000..91dd137
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/BluetoothUUID.swift
@@ -0,0 +1,31 @@
+//
+// BluetoothUUID.swift
+//
+//
+// Created by Alsey Coleman Miller on 22/12/21.
+//
+
+import Foundation
+import Bluetooth
+
+internal extension BluetoothUUID {
+
+ func description(for value: Data) -> String? {
+ switch self {
+ case BluetoothUUID.Characteristic.batteryLevel:
+ return value.first.flatMap { $0.description + "%" }
+ case BluetoothUUID.Characteristic.currentTime:
+ return nil
+ case BluetoothUUID.Characteristic.deviceName,
+ BluetoothUUID.Characteristic.serialNumberString,
+ BluetoothUUID.Characteristic.firmwareRevisionString,
+ BluetoothUUID.Characteristic.softwareRevisionString,
+ BluetoothUUID.Characteristic.hardwareRevisionString,
+ BluetoothUUID.Characteristic.modelNumberString,
+ BluetoothUUID.Characteristic.manufacturerNameString:
+ return String(data: value, encoding: .utf8)
+ default:
+ return nil
+ }
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Cache.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Cache.swift
new file mode 100644
index 0000000..4fe0952
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Cache.swift
@@ -0,0 +1,31 @@
+//
+// Cache.swift
+//
+//
+// Created by Alsey Coleman Miller on 22/12/21.
+//
+
+internal struct Cache {
+
+ let capacity: Int
+
+ private(set) var values: [T]
+
+ init(capacity: Int) {
+ assert(capacity > 0)
+ self.capacity = capacity
+ self.values = [T]()
+ self.values.reserveCapacity(capacity)
+ }
+
+ mutating func append(_ value: T) {
+ values.append(value)
+ if values.count > capacity {
+ values.removeFirst()
+ }
+ }
+
+ mutating func removeAll() {
+ values.removeAll(keepingCapacity: true)
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockAdvertisement.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockAdvertisement.swift
new file mode 100644
index 0000000..be3c573
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockAdvertisement.swift
@@ -0,0 +1,74 @@
+//
+// MockAdvertisement.swift
+// BluetoothExplorer
+//
+// Created by Alsey Coleman Miller on 31/10/21.
+// Copyright © 2021 Alsey Coleman Miller. All rights reserved.
+//
+
+#if DEBUG
+import Foundation
+import Bluetooth
+import GATT
+
+/// Mock Advertisement Data
+internal struct MockAdvertisementData: AdvertisementData {
+
+ /// The local name of a peripheral.
+ internal let localName: String?
+
+ /// The Manufacturer data of a peripheral.
+ internal let manufacturerData: ManufacturerSpecificData?
+
+ /// This value is available if the broadcaster (peripheral) provides its Tx power level in its advertising packet.
+ /// Using the RSSI value and the Tx power level, it is possible to calculate path loss.
+ internal let txPowerLevel: Double?
+
+ /// Service-specific advertisement data.
+ internal let serviceData: [BluetoothUUID: Data]?
+
+ /// An array of service UUIDs
+ internal let serviceUUIDs: [BluetoothUUID]?
+
+ /// An array of one or more `BluetoothUUID`, representing Service UUIDs.
+ internal let solicitedServiceUUIDs: [BluetoothUUID]?
+
+ internal init(
+ localName: String? = nil,
+ manufacturerData: ManufacturerSpecificData? = nil,
+ txPowerLevel: Double? = nil,
+ serviceData: [BluetoothUUID : Data]? = nil,
+ serviceUUIDs: [BluetoothUUID]? = nil,
+ solicitedServiceUUIDs: [BluetoothUUID]? = nil
+ ) {
+ self.localName = localName
+ self.manufacturerData = manufacturerData
+ self.txPowerLevel = txPowerLevel
+ self.serviceData = serviceData
+ self.serviceUUIDs = serviceUUIDs
+ self.solicitedServiceUUIDs = solicitedServiceUUIDs
+ }
+}
+
+extension MockAdvertisementData {
+
+ internal static let beacon = MockAdvertisementData(
+ localName: nil,
+ manufacturerData: ManufacturerSpecificData(data: Data([0x4c, 0x00, 0x02, 0x15, 0xb9, 0x40, 0x7f, 0x30, 0xf5, 0xf8, 0x46, 0x6e, 0xaf, 0xf9, 0x25, 0x55, 0x6b, 0x57, 0xfe, 0x6d, 0x29, 0x4c, 0x90, 0x39, 0x74])),
+ txPowerLevel: nil,
+ serviceData: nil,
+ serviceUUIDs: nil,
+ solicitedServiceUUIDs: nil
+ )
+
+ internal static let smartThermostat = MockAdvertisementData(
+ localName: "CLI-W200",
+ manufacturerData: ManufacturerSpecificData(data: Data([0xd9, 0x01, 0x01, 0x02, 0x00, 0x00, 0x8c, 0x85, 0x90, 0xcb, 0x31, 0x74, 0x00, 0x60])),
+ txPowerLevel: nil,
+ serviceData: nil,
+ serviceUUIDs: [BluetoothUUID.Member.savantSystems2],
+ solicitedServiceUUIDs: nil
+ )
+}
+
+#endif
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockAttributes.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockAttributes.swift
new file mode 100644
index 0000000..3edfba1
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockAttributes.swift
@@ -0,0 +1,109 @@
+//
+// MockService.swift
+//
+//
+// Created by Alsey Coleman Miller on 18/12/21.
+//
+
+#if DEBUG
+import Bluetooth
+import GATT
+
+internal typealias MockService = GATT.Service
+internal typealias MockCharacteristic = GATT.Characteristic
+internal typealias MockDescriptor = GATT.Descriptor
+
+internal extension MockService {
+
+ static var deviceInformation: MockService {
+ Service(
+ id: 10,
+ uuid: BluetoothUUID.Service.deviceInformation,
+ peripheral: .beacon
+ )
+ }
+
+ static var battery: MockService {
+ Service(
+ id: 20,
+ uuid: BluetoothUUID.Service.battery,
+ peripheral: .beacon
+ )
+ }
+
+ static var savantSystems: MockService {
+ Service(
+ id: 30,
+ uuid: BluetoothUUID.Member.savantSystems2,
+ peripheral: .smartThermostat
+ )
+ }
+}
+
+internal extension MockCharacteristic {
+
+ static var deviceName: MockCharacteristic {
+ Characteristic(
+ id: 11,
+ uuid: BluetoothUUID.Characteristic.deviceName,
+ peripheral: .beacon,
+ properties: [.read]
+ )
+ }
+
+ static var manufacturerName: MockCharacteristic {
+ Characteristic(
+ id: 12,
+ uuid: BluetoothUUID.Characteristic.manufacturerNameString,
+ peripheral: .beacon,
+ properties: [.read]
+ )
+ }
+
+ static var modelNumber: MockCharacteristic {
+ Characteristic(
+ id: 13,
+ uuid: BluetoothUUID.Characteristic.modelNumberString,
+ peripheral: .beacon,
+ properties: [.read]
+ )
+ }
+
+ static var serialNumber: MockCharacteristic {
+ Characteristic(
+ id: 14,
+ uuid: BluetoothUUID.Characteristic.serialNumberString,
+ peripheral: .beacon,
+ properties: [.read]
+ )
+ }
+
+ static var batteryLevel: MockCharacteristic {
+ Characteristic(
+ id: 21,
+ uuid: BluetoothUUID.Characteristic.batteryLevel,
+ peripheral: .beacon,
+ properties: [.read, .notify]
+ )
+ }
+
+ static let savantTest: MockCharacteristic = Characteristic(
+ id: 31,
+ uuid: BluetoothUUID(),
+ peripheral: .smartThermostat,
+ properties: [.read, .write, .writeWithoutResponse, .notify]
+ )
+}
+
+internal extension MockDescriptor {
+
+ static func clientCharacteristicConfiguration(_ peripheral: Peripheral) -> MockDescriptor {
+ Descriptor(
+ id: 99,
+ uuid: BluetoothUUID.Descriptor.clientCharacteristicConfiguration,
+ peripheral: peripheral
+ )
+ }
+}
+
+#endif
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockCentral.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockCentral.swift
new file mode 100644
index 0000000..1f8513e
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockCentral.swift
@@ -0,0 +1,290 @@
+//
+// MockCentral.swift
+//
+//
+// Created by Alsey Coleman Miller on 22/12/21.
+//
+
+#if DEBUG
+import Foundation
+import Bluetooth
+import GATT
+
+@MainActor
+final class MockCentral: CentralManager, @unchecked Sendable {
+
+ /// Central Peripheral Type
+ typealias Peripheral = GATT.Peripheral
+
+ /// Central Advertisement Type
+ internal typealias Advertisement = MockAdvertisementData
+
+ /// Central Attribute ID (Handle)
+ internal typealias AttributeID = UInt16
+
+ internal nonisolated(unsafe) var log: (@Sendable (String) -> ())?
+
+ internal var peripherals: [GATT.Peripheral : Bool] {
+ get async {
+ var peripherals = [Peripheral: Bool]()
+ for scanData in state.scanData {
+ peripherals[scanData.peripheral] = state.connected.contains(scanData.peripheral)
+ }
+ return peripherals
+ }
+ }
+
+ internal var isEnabled: Bool {
+ get async {
+ state.isEnabled
+ }
+ }
+
+ private var state = State()
+
+ private var continuation = Continuation()
+
+ internal init() {
+ Task {
+ try await Task.sleep(for: .seconds(1))
+ updateState {
+ $0.isEnabled = true
+ }
+ }
+ }
+
+ /// Scans for peripherals that are advertising services.
+ internal func scan(
+ with services: Set,
+ filterDuplicates: Bool
+ ) -> AsyncCentralScan {
+ return AsyncCentralScan { continuation in
+ await self.state.scanData.forEach {
+ continuation($0)
+ }
+ }
+ }
+
+ internal func scan(
+ filterDuplicates: Bool
+ ) -> AsyncCentralScan {
+ scan(with: [], filterDuplicates: filterDuplicates)
+ }
+
+ /// Connect to the specified device
+ internal func connect(to peripheral: Peripheral) async throws {
+ state.connected.insert(peripheral)
+ }
+
+ /// Disconnect the specified device.
+ internal func disconnect(_ peripheral: Peripheral) async {
+ state.connected.remove(peripheral)
+ }
+
+ /// Disconnect all connected devices.
+ internal func disconnectAll() {
+ state.connected.removeAll()
+ }
+
+ /// Discover Services
+ internal func discoverServices(
+ _ services: Set = [],
+ for peripheral: Peripheral
+ ) async throws -> [Service] {
+ return state.characteristics
+ .keys
+ .filter { $0.peripheral == peripheral }
+ .sorted(by: { $0.id < $1.id })
+ }
+
+ internal func discoverIncludedServices(
+ _ services: Set = [],
+ for service: Service
+ ) async throws -> [Service] {
+ return []
+ }
+
+ /// Discover Characteristics for service
+ internal nonisolated func discoverCharacteristics(
+ _ characteristics: Set = [],
+ for service: Service
+ ) async throws -> [Characteristic] {
+ guard await state.connected.contains(service.peripheral) else {
+ throw CentralError.disconnected
+ }
+ guard let characteristics = await state.characteristics[service] else {
+ throw CentralError.invalidAttribute(service.uuid)
+ }
+ return characteristics
+ .sorted(by: { $0.id < $1.id })
+ }
+
+ /// Read Characteristic Value
+ internal nonisolated func readValue(
+ for characteristic: Characteristic
+ ) async throws -> Data {
+ guard await state.connected.contains(characteristic.peripheral) else {
+ throw CentralError.disconnected
+ }
+ return await state.characteristicValues[characteristic] ?? Data()
+ }
+
+ /// Write Characteristic Value
+ internal nonisolated func writeValue(
+ _ data: Data,
+ for characteristic: Characteristic,
+ withResponse: Bool = true
+ ) async throws {
+ guard await state.connected.contains(characteristic.peripheral) else {
+ throw CentralError.disconnected
+ }
+ if withResponse {
+ guard characteristic.properties.contains(.write) else {
+ throw CentralError.invalidAttribute(characteristic.uuid)
+ }
+ } else {
+ guard characteristic.properties.contains(.writeWithoutResponse) else {
+ throw CentralError.invalidAttribute(characteristic.uuid)
+ }
+ }
+ // write
+ await updateState { state in
+ state.characteristicValues[characteristic] = data
+ }
+ }
+
+ /// Discover descriptors
+ internal nonisolated func discoverDescriptors(
+ for characteristic: Characteristic
+ ) async throws -> [Descriptor] {
+ guard await state.connected.contains(characteristic.peripheral) else {
+ throw CentralError.disconnected
+ }
+ return await state.descriptors[characteristic] ?? []
+ }
+
+ /// Read descriptor
+ internal nonisolated func readValue(
+ for descriptor: Descriptor
+ ) async throws -> Data {
+ guard await state.connected.contains(descriptor.peripheral) else {
+ throw CentralError.disconnected
+ }
+ return await state.descriptorValues[descriptor] ?? Data()
+ }
+
+ /// Write descriptor
+ internal nonisolated func writeValue(
+ _ data: Data,
+ for descriptor: Descriptor
+ ) async throws {
+ guard await state.connected.contains(descriptor.peripheral) else {
+ throw CentralError.disconnected
+ }
+ await updateState { state in
+ state.descriptorValues[descriptor] = data
+ }
+ }
+
+ internal nonisolated func notify(
+ for characteristic: GATT.Characteristic
+ ) async throws -> AsyncCentralNotifications {
+ guard await state.connected.contains(characteristic.peripheral) else {
+ throw CentralError.disconnected
+ }
+ return AsyncCentralNotifications { [unowned self] continuation in
+ if let notifications = await self.state.notifications[characteristic] {
+ for notification in notifications {
+ if #available(iOS 16.0, *) {
+ try await Task.sleep(for: .seconds(1))
+ }
+ continuation(notification)
+ }
+ }
+ }
+ }
+
+ /// Read MTU
+ internal func maximumTransmissionUnit(for peripheral: Peripheral) async throws -> MaximumTransmissionUnit {
+ guard state.connected.contains(peripheral) else {
+ throw CentralError.disconnected
+ }
+ return .default
+ }
+
+ // Read RSSI
+ internal func rssi(for peripheral: Peripheral) async throws -> RSSI {
+ return .init(rawValue: 127)!
+ }
+}
+
+private extension MockCentral {
+
+ func updateState(_ body: (inout State) -> ()) {
+ body(&state)
+ }
+}
+
+internal extension MockCentral {
+
+ struct State {
+ var isEnabled = false
+ var isScanning = false
+ var scanData: [MockScanData] = [.beacon, .smartThermostat]
+ var connected = Set()
+ var characteristics: [MockService: [MockCharacteristic]] = [
+ .deviceInformation: [
+ .deviceName,
+ .manufacturerName,
+ .modelNumber,
+ .serialNumber
+ ],
+ .battery: [
+ .batteryLevel
+ ],
+ .savantSystems: [
+ .savantTest
+ ]
+ ]
+ var descriptors: [MockCharacteristic: [MockDescriptor]] = [
+ .batteryLevel: [.clientCharacteristicConfiguration(.beacon)],
+ .savantTest: [.clientCharacteristicConfiguration(.smartThermostat)],
+ ]
+ var characteristicValues: [MockCharacteristic: Data] = [
+ .deviceName: Data("iBeacon".utf8),
+ .manufacturerName: Data("Apple Inc.".utf8),
+ .modelNumber: Data("iPhone11.8".utf8),
+ .serialNumber: Data(UUID().uuidString.utf8),
+ .batteryLevel: Data([100]),
+ .savantTest: Data(UUID().uuidString.utf8)
+ ]
+ var descriptorValues: [MockDescriptor: Data] = [
+ .clientCharacteristicConfiguration(.beacon): Data([0x00]),
+ .clientCharacteristicConfiguration(.smartThermostat): Data([0x00]),
+ ]
+ var notifications: [MockCharacteristic: [Data]] = [
+ .batteryLevel: [
+ Data([99]),
+ Data([98]),
+ Data([95]),
+ Data([80]),
+ Data([75]),
+ Data([25]),
+ Data([20]),
+ Data([5]),
+ Data([1]),
+ ],
+ .savantTest: [
+ Data(UUID().uuidString.utf8),
+ Data(UUID().uuidString.utf8),
+ Data(UUID().uuidString.utf8),
+ Data(UUID().uuidString.utf8),
+ ]
+ ]
+ }
+
+ struct Continuation {
+ var scan: AsyncThrowingStream, Error>.Continuation?
+ }
+}
+#endif
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockScanData.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockScanData.swift
new file mode 100644
index 0000000..28c6925
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Mock/MockScanData.swift
@@ -0,0 +1,46 @@
+//
+// MockScanData.swift
+// BluetoothExplorer
+//
+// Created by Alsey Coleman Miller on 31/10/21.
+// Copyright © 2021 Alsey Coleman Miller. All rights reserved.
+//
+
+#if DEBUG
+import Foundation
+import Bluetooth
+import GATT
+
+internal typealias MockScanData = ScanData
+
+internal extension MockScanData {
+
+ static let beacon = MockScanData(
+ peripheral: .beacon,
+ date: Date(timeIntervalSinceReferenceDate: 10_000),
+ rssi: -20,
+ advertisementData: .beacon,
+ isConnectable: true
+ )
+
+ static let smartThermostat = MockScanData(
+ peripheral: .smartThermostat,
+ date: Date(timeIntervalSinceReferenceDate: 10_100),
+ rssi: -127,
+ advertisementData: .smartThermostat,
+ isConnectable: true
+ )
+}
+
+internal extension Peripheral {
+
+ static var beacon: Peripheral {
+ Peripheral(id: BluetoothAddress(rawValue: "00:AA:AB:03:10:01")!)
+ }
+
+ static var smartThermostat: Peripheral {
+ Peripheral(id: BluetoothAddress(rawValue: "00:1A:7D:DA:71:13")!)
+ }
+}
+
+#endif
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/NativeCentral.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/NativeCentral.swift
new file mode 100644
index 0000000..9c79a08
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/NativeCentral.swift
@@ -0,0 +1,50 @@
+//
+// Central.swift
+// BluetoothExplorer
+//
+// Created by Alsey Coleman Miller on 6/9/19.
+// Copyright © 2019 Alsey Coleman Miller. All rights reserved.
+//
+
+import Foundation
+import Bluetooth
+import GATT
+#if canImport(DarwinGATT)
+import DarwinGATT
+#endif
+
+#if os(Android)
+typealias NativeCentral = MockCentral
+#elseif os(iOS) && targetEnvironment(simulator)
+typealias NativeCentral = MockCentral
+#elseif canImport(Darwin)
+typealias NativeCentral = DarwinCentral
+#else
+#error("Platform not supported")
+#endif
+
+extension NativeCentral {
+
+ /// Wait for CoreBluetooth to be ready.
+ func wait(
+ warning: Int = 3,
+ timeout: Int = 10
+ ) async throws {
+
+ var powerOnWait = 0
+ var currentState: Bool
+ repeat {
+ currentState = await self.isEnabled
+ // inform user after 3 seconds
+ if powerOnWait == warning {
+ NSLog("Waiting for Bluetooth to be ready, please turn on Bluetooth")
+ }
+ // sleep for 1s
+ try await Task.sleep(for: .seconds(1))
+ powerOnWait += 1
+ guard powerOnWait < timeout else {
+ throw CocoaError(.featureUnsupported) // TODO: Update error
+ }
+ } while currentState != true
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Store.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Store.swift
new file mode 100644
index 0000000..8486882
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/Model/Store.swift
@@ -0,0 +1,363 @@
+//
+// Store.swift
+// BluetoothExplorer
+//
+// Created by Alsey Coleman Miller on 6/9/19.
+// Copyright © 2019 Alsey Coleman Miller. All rights reserved.
+//
+
+import Foundation
+import Observation
+import Bluetooth
+import GATT
+
+/// Store
+@MainActor
+@Observable
+public final class Store: @unchecked Sendable {
+
+ typealias Central = NativeCentral
+
+ typealias Peripheral = Central.Peripheral
+
+ typealias ScanData = GATT.ScanData
+
+ typealias Service = GATT.Service
+
+ typealias Characteristic = GATT.Characteristic
+
+ typealias Descriptor = GATT.Descriptor
+
+ typealias ScanResult = ScanDataCache
+
+ // MARK: - Properties
+
+ private(set) var activity = [Peripheral: Bool]()
+
+ private(set) var isEnabled = false
+
+ private(set) var scanResults = [Peripheral: ScanResult]()
+
+ var isScanning: Bool {
+ self.scanStream?.isScanning ?? false
+ }
+
+ private(set) var connected: Set = []
+
+ private(set) var services = [Peripheral: [Service]]()
+
+ private(set) var characteristics = [Service: [Characteristic]]()
+
+ private(set) var includedServices = [Service: [Service]]()
+
+ private(set) var descriptors = [Characteristic: [Descriptor]]()
+
+ private(set) var characteristicValues = [Characteristic: Cache]()
+
+ private(set) var descriptorValues = [Descriptor: Cache]()
+
+ private(set) var isNotifying = [Characteristic: Bool]()
+
+ internal let central: Central
+
+ private var scanStream: AsyncCentralScan?
+
+ // MARK: - Initialization
+
+ public convenience init() {
+ let central = Central()
+ self.init(central: central)
+ }
+
+ init(central: Central) {
+ self.central = central
+ setupLog()
+ observeValues()
+ }
+
+ // MARK: - Methods
+
+ private func setupLog() {
+ central.log = { print("Central: \($0)") }
+ }
+
+ private func observeValues() {
+ Task { [weak self] in
+ do {
+ while let self {
+ try await Task.sleep(for: .seconds(1))
+ await self.updateState()
+ }
+ }
+ catch {
+
+ }
+ }
+ }
+
+ private func updateState() async {
+ assert(Thread.isMainThread)
+ let oldValue = self.isEnabled
+ let newValue = await self.central.isEnabled
+ guard newValue != oldValue else {
+ return
+ }
+ // update value
+ self.isEnabled = newValue
+ // start scanning when powered on
+ guard newValue else {
+ return
+ }
+ do { try await self.scan() }
+ catch { } // ignore error
+ }
+
+ private func updateConnected() async {
+ assert(Thread.isMainThread)
+ let oldValue = self.connected
+ let newValue = await Set(central.peripherals.compactMap { $0.value ? $0.key : nil })
+ guard oldValue != newValue else {
+ return
+ }
+ self.connected = newValue
+ }
+
+ public func log(_ message: String) {
+ print(message)
+ }
+
+ func log(error: any Error) {
+ print("Error", error.localizedDescription)
+ }
+
+ func scan(
+ with services: Set = [],
+ filterDuplicates: Bool = true
+ ) async throws {
+ scanResults.removeAll(keepingCapacity: true)
+ self.scanStream = nil // end previous scan
+ let stream = central.scan(
+ with: services,
+ filterDuplicates: filterDuplicates
+ )
+ self.scanStream = stream
+ Task {
+ for try await scanData in stream {
+ await found(scanData: scanData)
+ }
+ self.scanStream = nil
+ }
+ }
+
+ /// Cache discovered values
+ private func found(scanData: ScanData) async {
+ var cache = scanResults[scanData.peripheral] ?? ScanDataCache(scanData: scanData)
+ cache += scanData
+ #if os(Android)
+
+ #elseif os(iOS) && targetEnvironment(simulator)
+
+ #elseif canImport(CoreBluetooth)
+ cache.name = try? await central.name(for: scanData.peripheral)
+ for serviceUUID in scanData.advertisementData.overflowServiceUUIDs ?? [] {
+ cache.overflowServiceUUIDs.insert(serviceUUID)
+ }
+ #endif
+ scanResults[scanData.peripheral] = cache
+ }
+
+ func stopScan() async {
+ scanStream?.stop()
+ scanStream = nil
+ }
+
+ func connect(to peripheral: Central.Peripheral) async throws {
+ activity[peripheral] = true
+ defer { activity[peripheral] = false }
+ if isScanning {
+ scanStream?.stop()
+ }
+ try await central.connect(to: peripheral)
+ }
+
+ func disconnect(_ peripheral: Central.Peripheral) async {
+ await central.disconnect(peripheral)
+ }
+
+ func discoverServices(for peripheral: Central.Peripheral) async throws {
+ activity[peripheral] = true
+ defer { activity[peripheral] = false }
+ let services = try await central.discoverServices(for: peripheral)
+ assert(Thread.isMainThread)
+ self.services[peripheral] = services
+ }
+
+ func discoverCharacteristics(for service: Service) async throws {
+ activity[service.peripheral] = true
+ defer { activity[service.peripheral] = false }
+ let characteristics = try await central.discoverCharacteristics([], for: service)
+ assert(Thread.isMainThread)
+ self.characteristics[service] = characteristics
+ }
+
+ func discoverIncludedServices(for service: Service) async throws {
+ activity[service.peripheral] = true
+ defer { activity[service.peripheral] = false }
+ let includedServices = try await central.discoverIncludedServices(for: service)
+ assert(Thread.isMainThread)
+ self.includedServices[service] = includedServices
+ }
+
+ func discoverDescriptors(for characteristic: Characteristic) async throws {
+ activity[characteristic.peripheral] = true
+ defer { activity[characteristic.peripheral] = false }
+ let includedServices = try await central.discoverDescriptors(for: characteristic)
+ assert(Thread.isMainThread)
+ self.descriptors[characteristic] = includedServices
+ }
+
+ func readValue(for characteristic: Characteristic) async throws {
+ activity[characteristic.peripheral] = true
+ defer { activity[characteristic.peripheral] = false }
+ let data = try await central.readValue(for: characteristic)
+ assert(Thread.isMainThread)
+ let value = AttributeValue(
+ date: Date(),
+ type: .read,
+ data: data
+ )
+ self.characteristicValues[characteristic, default: .init(capacity: 10)].append(value)
+ }
+
+ func writeValue(_ data: Data, for characteristic: Characteristic, withResponse: Bool = true) async throws {
+ activity[characteristic.peripheral] = true
+ defer { activity[characteristic.peripheral] = false }
+ try await central.writeValue(data, for: characteristic, withResponse: withResponse)
+ assert(Thread.isMainThread)
+ let value = AttributeValue(
+ date: Date(),
+ type: .write,
+ data: data
+ )
+ self.characteristicValues[characteristic, default: .init(capacity: 10)].append(value)
+ }
+
+ func notify(_ isEnabled: Bool, for characteristic: Characteristic) async throws {
+ activity[characteristic.peripheral] = true
+ defer { activity[characteristic.peripheral] = false }
+ if isEnabled {
+ let stream = try await central.notify(for: characteristic)
+ isNotifying[characteristic] = isEnabled
+ Task.detached(priority: .low) { [unowned self] in
+ for try await notification in stream {
+ await self.notification(notification, for: characteristic)
+ }
+ }
+ } else {
+ //try await central.stopNotifications(for: characteristic)
+ isNotifying[characteristic] = false
+ }
+ }
+
+ private func notification(_ data: Data, for characteristic: Characteristic) async {
+ assert(Thread.isMainThread)
+ let value = AttributeValue(
+ date: Date(),
+ type: .notification,
+ data: data
+ )
+ self.characteristicValues[characteristic, default: .init(capacity: 10)].append(value)
+ }
+
+ func readValue(for descriptor: Descriptor) async throws {
+ activity[descriptor.peripheral] = true
+ defer { activity[descriptor.peripheral] = false }
+ let data = try await central.readValue(for: descriptor)
+ assert(Thread.isMainThread)
+ let value = AttributeValue(
+ date: Date(),
+ type: .read,
+ data: data
+ )
+ self.descriptorValues[descriptor, default: .init(capacity: 10)].append(value)
+ }
+
+ func writeValue(_ data: Data, for descriptor: Descriptor) async throws {
+ activity[descriptor.peripheral] = true
+ defer { activity[descriptor.peripheral] = false }
+ try await central.writeValue(data, for: descriptor)
+ assert(Thread.isMainThread)
+ let value = AttributeValue(
+ date: Date(),
+ type: .write,
+ data: data
+ )
+ self.descriptorValues[descriptor, default: .init(capacity: 10)].append(value)
+ }
+}
+
+// MARK: - Supporting Types
+
+struct ScanDataCache : Equatable, Hashable {
+
+ var scanData: GATT.ScanData
+
+ /// GAP or advertised name
+ var name: String?
+
+ /// Advertised name
+ var advertisedName: String?
+
+ var manufacturerData: GATT.ManufacturerSpecificData?
+
+ /// This value is available if the broadcaster (peripheral) provides its Tx power level in its advertising packet.
+ /// Using the RSSI value and the Tx power level, it is possible to calculate path loss.
+ var txPowerLevel: Double?
+
+ /// Service-specific advertisement data.
+ var serviceData = [BluetoothUUID: Advertisement.Data]()
+
+ /// An array of service UUIDs
+ var serviceUUIDs = Set()
+
+ /// An array of one or more ``BluetoothUUID``, representing Service UUIDs.
+ var solicitedServiceUUIDs = Set()
+
+ /// An array of one or more ``BluetoothUUID``, representing Service UUIDs that were found in the “overflow” area of the advertisement data.
+ var overflowServiceUUIDs = Set()
+
+ /// Advertised iBeacon
+ var beacon: AppleBeacon?
+
+ init(scanData: GATT.ScanData) {
+ self.scanData = scanData
+ self += scanData
+ }
+
+ static func += (cache: inout ScanDataCache, scanData: GATT.ScanData) {
+ cache.scanData = scanData
+ cache.advertisedName = scanData.advertisementData.localName
+ if cache.name == nil {
+ cache.name = scanData.advertisementData.localName
+ }
+ cache.txPowerLevel = scanData.advertisementData.txPowerLevel
+ if let beacon = scanData.advertisementData.beacon {
+ cache.beacon = beacon
+ } else {
+ cache.manufacturerData = scanData.advertisementData.manufacturerData
+ }
+ for serviceUUID in scanData.advertisementData.serviceUUIDs ?? [] {
+ cache.serviceUUIDs.insert(serviceUUID)
+ }
+ for (serviceUUID, serviceData) in scanData.advertisementData.serviceData ?? [:] {
+ cache.serviceData[serviceUUID] = serviceData
+ }
+ }
+}
+
+extension ScanDataCache: Identifiable {
+
+ var id: Peripheral.ID {
+ scanData.id
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift
new file mode 100644
index 0000000..e2c61d6
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift
@@ -0,0 +1,196 @@
+//
+// CentralListViewModel.swift
+// BluetoothExplorer
+//
+// Created by Alsey Coleman Miller on 7/11/25.
+//
+
+import Foundation
+import Observation
+import Bluetooth
+import GATT
+
+@MainActor
+@Observable
+public final class CentralListViewModel {
+
+ let store: Store
+
+ var scanToggleTask: Task?
+
+ public init(store: Store) {
+ self.store = store
+ }
+
+ var state: State {
+ State(
+ .init(
+ store: store,
+ didToggle: scanToggleTask != nil
+ )
+ )
+ }
+
+ public var scanResults: [ScanResult] {
+ state.scanResults
+ }
+
+ public var isEnabled: Bool {
+ state.isEnabled
+ }
+
+ public var isScanning: Bool {
+ state.isScanning
+ }
+
+ public var canToggleScan: Bool {
+ state.canToggleScan
+ }
+
+ public func scanToggle() {
+ scanToggleTask = Task {
+ defer { scanToggleTask = nil }
+ if store.isScanning {
+ await store.stopScan()
+ } else {
+ do {
+ try await store.scan()
+ }
+ catch {
+ store.log(error: error)
+ }
+ }
+ }
+ }
+}
+
+public extension CentralListViewModel {
+
+ struct State: Sendable {
+
+ let input: Input
+
+ init(_ input: Input) {
+ self.input = input
+ }
+
+ public var scanResults: [ScanResult] {
+ input.scanResults
+ .values
+ .lazy
+ .sorted(by: { $0.id.description < $1.id.description })
+ .sorted(by: { ($0.name ?? "") < ($1.name ?? "") })
+ .sorted(by: { $0.name != nil && $1.name == nil })
+ .sorted(by: { $0.beacon != nil && $1.beacon == nil })
+ .map { ScanResult($0) }
+ }
+
+ public var isEnabled: Bool {
+ input.isEnabled
+ }
+
+ public var isScanning: Bool {
+ input.isScanning
+ }
+
+ public var canToggleScan: Bool {
+ input.didToggle == false && input.isEnabled
+ }
+ }
+}
+
+public extension CentralListViewModel.State {
+
+ struct Input: Sendable {
+
+ let scanResults: [Store.Peripheral: Store.ScanResult]
+
+ let isEnabled: Bool
+
+ let isScanning: Bool
+
+ let didToggle: Bool
+
+ init(scanResults: [Store.Peripheral : Store.ScanResult], isEnabled: Bool, isScanning: Bool, didToggle: Bool) {
+ self.scanResults = scanResults
+ self.isEnabled = isEnabled
+ self.isScanning = isScanning
+ self.didToggle = didToggle
+ }
+
+ @MainActor
+ init(store: Store, didToggle: Bool) {
+ self.scanResults = store.scanResults
+ self.isEnabled = store.isEnabled
+ self.isScanning = store.isScanning
+ self.didToggle = didToggle
+ }
+ }
+}
+
+public extension CentralListViewModel {
+
+ struct ScanResult: Identifiable {
+
+ typealias ScanData = ScanDataCache
+
+ let scanData: ScanData
+
+ init(_ scanData: ScanData) {
+ self.scanData = scanData
+ }
+
+ public var id: String {
+ scanData.id.description
+ }
+
+ public var name: String {
+ scanData.name ?? (beacon != nil ? "iBeacon" : "Unknown")
+ }
+
+ public var company: String? {
+ scanData.manufacturerData?.companyIdentifier.name
+ }
+
+ public var services: String? {
+ let services = scanData.serviceUUIDs
+ .sorted(by: { $0.description < $1.description })
+ .map { $0.metadata?.name ?? $0.rawValue }
+ guard services.isEmpty == false
+ else { return nil }
+ return "Services: " + services.reduce("", { ($0.isEmpty ? "" : ", ") + $1 })
+ }
+
+ public var beacon: CentralListViewModel.Beacon? {
+ scanData.beacon.flatMap(Beacon.init)
+ }
+ }
+}
+
+public extension CentralListViewModel {
+
+ struct Beacon: Sendable {
+
+ let beacon: AppleBeacon
+
+ init(_ beacon: AppleBeacon) {
+ self.beacon = beacon
+ }
+
+ public var uuid: String {
+ beacon.uuid.uuidString
+ }
+
+ public var major: String {
+ "Major: 0x\(beacon.major.toHexadecimal())"
+ }
+
+ public var minor: String {
+ "Minor: 0x\(beacon.minor.toHexadecimal())"
+ }
+
+ public var rssi: String {
+ "RSSI: \(beacon.rssi)"
+ }
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift
new file mode 100644
index 0000000..edd950a
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift
@@ -0,0 +1,56 @@
+//
+// PeripheralViewModel.swift
+// bluetooth-explorer
+//
+// Created by Alsey Coleman Miller on 7/12/25.
+//
+
+import Foundation
+import Observation
+import Bluetooth
+import GATT
+
+@MainActor
+@Observable
+public final class PeripheralViewModel {
+
+ let store: Store
+
+ let peripheral: Store.Peripheral
+
+ init(store: Store, peripheral: Peripheral) {
+ self.store = store
+ self.peripheral = peripheral
+ }
+
+ public convenience init(store: Store, peripheral: String) {
+ guard let peripheral = store.scanResults.first(where: { $0.key.description == peripheral })?.key else {
+ fatalError("Invalid peripheral: \(peripheral)")
+ }
+ self.init(store: store, peripheral: peripheral)
+ }
+
+ var title: String {
+ store.scanResults[peripheral]?.name ?? "Device"
+ }
+
+ var isConnected: Bool {
+ store.connected.contains(peripheral)
+ }
+
+ var services: [Store.Service] {
+ store.services[peripheral] ?? []
+ }
+
+ var showActivity: Bool {
+ store.activity[peripheral] ?? false
+ }
+
+ public func connect() {
+
+ }
+
+ public func reload() {
+
+ }
+}
diff --git a/bluetooth-explorer-android/Sources/BluetoothExplorerModel/swift-java.config b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/swift-java.config
new file mode 100644
index 0000000..f6db9ce
--- /dev/null
+++ b/bluetooth-explorer-android/Sources/BluetoothExplorerModel/swift-java.config
@@ -0,0 +1,4 @@
+{
+ "javaPackage": "org.pureswift.bluetoothexplorer",
+ "mode": "jni"
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/.gitignore b/bluetooth-explorer-android/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/bluetooth-explorer-android/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/build.gradle.kts b/bluetooth-explorer-android/app/build.gradle.kts
new file mode 100644
index 0000000..0fb11be
--- /dev/null
+++ b/bluetooth-explorer-android/app/build.gradle.kts
@@ -0,0 +1,89 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "com.pureswift.swiftandroid"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "com.pureswift.swiftandroid"
+ minSdk = 29
+ targetSdk = 35
+ versionCode = 1
+ versionName = "1.0"
+ ndk {
+ //noinspection ChromeOsAbiSupport
+ abiFilters += listOf("arm64-v8a")
+ }
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")
+ }
+ jniLibs {
+ keepDebugSymbols += listOf(
+ "*/arm64-v8a/*.so",
+ "*/armeabi-v7a/*.so",
+ "*/x86_64/*.so"
+ )
+ }
+ }
+/*
+ // Custom Swift build task
+ val buildSwift by tasks.registering(Exec::class) {
+ group = "build"
+ description = "Build Swift sources"
+ workingDir("$projectDir")
+ commandLine("bash", "build-swift.sh")
+ }
+
+ tasks.withType {
+ dependsOn(buildSwift)
+ }*/
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.recyclerview)
+ implementation(libs.androidx.navigation.runtime)
+ implementation(libs.androidx.material3)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
\ No newline at end of file
diff --git a/Android/app/proguard-rules.pro b/bluetooth-explorer-android/app/proguard-rules.pro
similarity index 94%
rename from Android/app/proguard-rules.pro
rename to bluetooth-explorer-android/app/proguard-rules.pro
index f1b4245..481bb43 100644
--- a/Android/app/proguard-rules.pro
+++ b/bluetooth-explorer-android/app/proguard-rules.pro
@@ -18,4 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/Android/app/src/androidTest/java/com/millertech/bluetoothexplorer/ExampleInstrumentedTest.kt b/bluetooth-explorer-android/app/src/androidTest/java/com/pureswift/swiftandroid/ExampleInstrumentedTest.kt
similarity index 54%
rename from Android/app/src/androidTest/java/com/millertech/bluetoothexplorer/ExampleInstrumentedTest.kt
rename to bluetooth-explorer-android/app/src/androidTest/java/com/pureswift/swiftandroid/ExampleInstrumentedTest.kt
index 9f8b95d..0b3aecb 100644
--- a/Android/app/src/androidTest/java/com/millertech/bluetoothexplorer/ExampleInstrumentedTest.kt
+++ b/bluetooth-explorer-android/app/src/androidTest/java/com/pureswift/swiftandroid/ExampleInstrumentedTest.kt
@@ -1,7 +1,7 @@
-package com.millertech.bluetoothexplorer
+package com.pureswift.swiftandroid
-import android.support.test.InstrumentationRegistry
-import android.support.test.runner.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
@@ -18,7 +18,7 @@ class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
- val appContext = InstrumentationRegistry.getTargetContext()
- assertEquals("com.millertech.bluetoothexplorer", appContext.packageName)
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.pureswift.swiftandroid", appContext.packageName)
}
-}
+}
\ No newline at end of file
diff --git a/Android/app/src/main/AndroidManifest.xml b/bluetooth-explorer-android/app/src/main/AndroidManifest.xml
similarity index 65%
rename from Android/app/src/main/AndroidManifest.xml
rename to bluetooth-explorer-android/app/src/main/AndroidManifest.xml
index 5601e0b..b361577 100644
--- a/Android/app/src/main/AndroidManifest.xml
+++ b/bluetooth-explorer-android/app/src/main/AndroidManifest.xml
@@ -1,26 +1,27 @@
-
-
+ xmlns:tools="http://schemas.android.com/tools">
-
-
-
+ android:theme="@style/Theme.SwiftAndroid"
+ tools:targetApi="31">
+
diff --git a/bluetooth-explorer-android/app/src/main/java/com/example/swift/HelloSubclass.java b/bluetooth-explorer-android/app/src/main/java/com/example/swift/HelloSubclass.java
new file mode 100644
index 0000000..2312d8f
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/example/swift/HelloSubclass.java
@@ -0,0 +1,27 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+public class HelloSubclass extends HelloSwift {
+ private String greeting;
+
+ public HelloSubclass(String greeting) {
+ this.greeting = greeting;
+ }
+
+ public void greetMe() {
+ super.greet(greeting);
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/java/com/example/swift/HelloSwift.java b/bluetooth-explorer-android/app/src/main/java/com/example/swift/HelloSwift.java
new file mode 100644
index 0000000..b04f53a
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/example/swift/HelloSwift.java
@@ -0,0 +1,64 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+import java.util.function.Predicate;
+
+public class HelloSwift {
+ public double value;
+ public static double initialValue = 3.14159;
+ public String name = "Java";
+
+ static {
+ System.loadLibrary("BluetoothExplorerApp");
+ }
+
+ public HelloSwift() {
+ this.value = initialValue;
+ }
+
+ public native int sayHello(int x, int y);
+ public native String throwMessageFromSwift(String message) throws Exception;
+
+ // To be called back by the native code
+ public double sayHelloBack(int i) {
+ System.out.println("And hello back from " + name + "! You passed me " + i);
+ return value;
+ }
+
+ public void greet(String name) {
+ System.out.println("Salutations, " + name);
+ }
+
+ public Predicate lessThanTen() {
+ Predicate predicate = i -> (i < 10);
+ return predicate;
+ }
+
+ public String[] doublesToStrings(double[] doubles) {
+ int size = doubles.length;
+ String[] strings = new String[size];
+
+ for(int i = 0; i < size; i++) {
+ strings[i] = "" + doubles[i];
+ }
+
+ return strings;
+ }
+
+ public void throwMessage(String message) throws Exception {
+ throw new Exception(message);
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/java/com/example/swift/ThreadSafe.java b/bluetooth-explorer-android/app/src/main/java/com/example/swift/ThreadSafe.java
new file mode 100644
index 0000000..2b1b358
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/example/swift/ThreadSafe.java
@@ -0,0 +1,22 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ThreadSafe {
+}
diff --git a/bluetooth-explorer-android/app/src/main/java/com/example/swift/ThreadSafeHelperClass.java b/bluetooth-explorer-android/app/src/main/java/com/example/swift/ThreadSafeHelperClass.java
new file mode 100644
index 0000000..3b7793f
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/example/swift/ThreadSafeHelperClass.java
@@ -0,0 +1,41 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+import java.util.Optional;
+import java.util.OptionalLong;
+import java.util.OptionalInt;
+import java.util.OptionalDouble;
+
+@ThreadSafe
+public class ThreadSafeHelperClass {
+ public ThreadSafeHelperClass() { }
+
+ public Optional text = Optional.of("");
+
+ public final OptionalDouble val = OptionalDouble.of(2);
+
+ public String getValue(Optional name) {
+ return name.orElse("");
+ }
+
+ public Optional getText() {
+ return text;
+ }
+
+ public OptionalLong from(OptionalInt value) {
+ return OptionalLong.of(value.getAsInt());
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Application.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Application.kt
new file mode 100644
index 0000000..4075c97
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Application.kt
@@ -0,0 +1,29 @@
+package com.pureswift.swiftandroid
+
+import com.example.swift.HelloSubclass
+
+class Application: android.app.Application() {
+
+ init {
+ NativeLibrary.shared()
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ onCreateSwift()
+ }
+
+ private external fun onCreateSwift()
+
+ override fun onTerminate() {
+ super.onTerminate()
+ onTerminateSwift()
+ }
+
+ private external fun onTerminateSwift()
+
+ fun sayHello() {
+ val result = HelloSubclass("Swift").sayHello(17, 25)
+ println("sayHello(17, 25) = $result")
+ }
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/EmbeddedAndroidViewDemo.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/EmbeddedAndroidViewDemo.kt
new file mode 100644
index 0000000..5b20617
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/EmbeddedAndroidViewDemo.kt
@@ -0,0 +1,63 @@
+package com.pureswift.swiftandroid
+
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.material3.Text
+import androidx.compose.material3.Button
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+
+@Preview(showBackground = true)
+@Composable
+fun EmbeddedAndroidViewDemo() {
+ Column {
+ val state = remember { mutableIntStateOf(0) }
+
+ //widget.ImageView
+ AndroidView(factory = { ctx ->
+ ImageView(ctx).apply {
+ val drawable = ContextCompat.getDrawable(ctx, R.drawable.ic_launcher_foreground)
+ setImageDrawable(drawable)
+ }
+ })
+
+ //Compose Button
+ Button(onClick = { state.value++ }) {
+ Text("MyComposeButton")
+ }
+
+ //widget.Button
+ AndroidView(factory = { ctx ->
+ //Here you can construct your View
+ android.widget.Button(ctx).apply {
+ text = "MyAndroidButton"
+ layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ setOnClickListener {
+ state.value++
+ }
+ }
+ }, modifier = Modifier.padding(8.dp))
+
+ //widget.TextView
+ AndroidView(factory = { ctx ->
+ //Here you can construct your View
+ TextView(ctx).apply {
+ layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ }
+ }, update = {
+ it.text = "You have clicked the buttons: " + state.value.toString() + " times"
+ })
+ }
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Fragment.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Fragment.kt
new file mode 100644
index 0000000..26a6c39
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Fragment.kt
@@ -0,0 +1,24 @@
+package com.pureswift.swiftandroid
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+
+class Fragment(val swiftObject: SwiftObject): android.app.Fragment() {
+
+ @Deprecated("Deprecated in Java")
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val context = this.context
+ checkNotNull(context)
+ val linearLayout = LinearLayout(context)
+ return linearLayout
+ }
+
+ external override fun onViewCreated(view: View, savedInstanceState: Bundle?)
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ListViewAdapter.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ListViewAdapter.kt
new file mode 100644
index 0000000..722b2b0
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ListViewAdapter.kt
@@ -0,0 +1,14 @@
+package com.pureswift.swiftandroid
+
+import android.R
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.recyclerview.widget.RecyclerView
+
+class ListViewAdapter(context: Context, val swiftObject: SwiftObject, val objects: ArrayList) :
+ ArrayAdapter(context, 0, objects) {
+
+ external override fun getView(position: Int, convertView: View?, parent: ViewGroup): View
+}
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/MainActivity.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/MainActivity.kt
new file mode 100644
index 0000000..249104b
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/MainActivity.kt
@@ -0,0 +1,76 @@
+package com.pureswift.swiftandroid
+
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import java.util.Date
+
+class MainActivity : ComponentActivity() {
+
+ init {
+ NativeLibrary.shared()
+ }
+
+ val emitter = UnitEmitter()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ onCreateSwift(savedInstanceState)
+ //enableEdgeToEdge()
+
+ // set composable root view
+ setContent {
+ EventReceiver(emitter = emitter)
+ }
+ }
+
+ external fun onCreateSwift(savedInstanceState: Bundle?)
+
+ fun setRootView(view: View) {
+ Log.d("MainActivity", "AndroidSwiftUI.MainActivity.setRootView(_:)")
+ setContentView(view)
+ }
+}
+
+@Composable
+fun EventReceiver(emitter: UnitEmitter) {
+
+ val tick by emitter.flow.collectAsState(initial = Unit)
+
+ var date by remember { mutableStateOf(Date()) }
+
+ LaunchedEffect(Unit) {
+ emitter.flow.collect {
+ date = Date()
+ }
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Hello Swift!")
+ Text(date.toString())
+ }
+ }
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/NativeLibrary.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/NativeLibrary.kt
new file mode 100644
index 0000000..c4bc3f4
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/NativeLibrary.kt
@@ -0,0 +1,34 @@
+package com.pureswift.swiftandroid
+
+import android.util.Log
+
+class NativeLibrary private constructor() {
+
+ companion object {
+
+ @Volatile
+ var shared : NativeLibrary? = null
+
+ fun shared(): NativeLibrary {
+ return shared?: synchronized(this){
+ val instance = NativeLibrary()
+ shared = instance
+ return instance
+ }
+ }
+ }
+
+ init {
+ loadNativeLibrary()
+ }
+
+ private fun loadNativeLibrary() {
+ try {
+ System.loadLibrary("BluetoothExplorerApp")
+ } catch (error: UnsatisfiedLinkError) {
+ Log.e("NativeLibrary", "Unable to load native libraries: $error")
+ return
+ }
+ Log.d("NativeLibrary", "Loaded Swift library")
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/NavigationBarViewOnItemSelectedListener.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/NavigationBarViewOnItemSelectedListener.kt
new file mode 100644
index 0000000..cf8014f
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/NavigationBarViewOnItemSelectedListener.kt
@@ -0,0 +1,9 @@
+package com.pureswift.swiftandroid
+
+import android.view.MenuItem
+import com.google.android.material.navigation.NavigationBarView
+
+class NavigationBarViewOnItemSelectedListener(val action: SwiftObject): NavigationBarView.OnItemSelectedListener {
+
+ external override fun onNavigationItemSelected(menuItem: MenuItem): Boolean
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/RecyclerViewAdapter.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/RecyclerViewAdapter.kt
new file mode 100644
index 0000000..f069972
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/RecyclerViewAdapter.kt
@@ -0,0 +1,40 @@
+package com.pureswift.swiftandroid
+
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+class RecyclerViewAdapter(val swiftObject: SwiftObject) :
+ RecyclerView.Adapter() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+
+ }
+
+ // Create new views (invoked by the layout manager)
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewAdapter.ViewHolder {
+ Log.d("RecyclerViewAdapter", "BluetoothExplorerApp.RecyclerViewAdapter.onCreateViewHolderSwift(_:_:) $viewType")
+ val view = LinearLayout(parent.context)
+ val viewHolder = ViewHolder(view)
+ checkNotNull(viewHolder)
+ checkNotNull(viewHolder.itemView)
+ return viewHolder
+ }
+
+ // Replace the contents of a view (invoked by the layout manager)
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ onBindViewHolderSwift(holder as RecyclerViewAdapter.ViewHolder, position)
+ }
+
+ external fun onBindViewHolderSwift(holder: RecyclerViewAdapter.ViewHolder, position: Int)
+
+ // Return the size of your dataset (invoked by the layout manager)
+ override fun getItemCount(): Int {
+ return getItemCountSwift()
+ }
+
+ external fun getItemCountSwift(): Int
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Runnable.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Runnable.kt
new file mode 100644
index 0000000..08a0987
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/Runnable.kt
@@ -0,0 +1,6 @@
+package com.pureswift.swiftandroid
+
+class Runnable(val block: SwiftObject): java.lang.Runnable {
+
+ external override fun run()
+}
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/SwiftObject.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/SwiftObject.kt
new file mode 100644
index 0000000..94ff2db
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/SwiftObject.kt
@@ -0,0 +1,17 @@
+package com.pureswift.swiftandroid
+
+/// Swift object retained by JVM
+class SwiftObject(val swiftObject: Long, val type: String) {
+
+ override fun toString(): String {
+ return toStringSwift()
+ }
+
+ external fun toStringSwift(): String
+
+ fun finalize() {
+ finalizeSwift()
+ }
+
+ external fun finalizeSwift()
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/UnitEmitter.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/UnitEmitter.kt
new file mode 100644
index 0000000..93af004
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/UnitEmitter.kt
@@ -0,0 +1,15 @@
+package com.pureswift.swiftandroid
+
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+
+class UnitEmitter() {
+
+ private val _flow = MutableSharedFlow(extraBufferCapacity = 64)
+ val flow: SharedFlow get() = _flow
+
+ fun emit() {
+ //println("Emit")
+ _flow.tryEmit(Unit)
+ }
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ViewOnClickListener.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ViewOnClickListener.kt
new file mode 100644
index 0000000..39b5ce3
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ViewOnClickListener.kt
@@ -0,0 +1,8 @@
+package com.pureswift.swiftandroid
+
+import android.view.View
+
+class ViewOnClickListener(val action: SwiftObject): View.OnClickListener {
+
+ external override fun onClick(view: View)
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Color.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Color.kt
new file mode 100644
index 0000000..4a8371b
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.pureswift.swiftandroid.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Theme.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Theme.kt
new file mode 100644
index 0000000..47f8f81
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package com.pureswift.swiftandroid.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun SwiftAndroidTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Type.kt b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Type.kt
new file mode 100644
index 0000000..4de94b7
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/java/com/pureswift/swiftandroid/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.pureswift.swiftandroid.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/Android/app/src/main/res/drawable/ic_launcher_background.xml b/bluetooth-explorer-android/app/src/main/res/drawable/ic_launcher_background.xml
similarity index 54%
rename from Android/app/src/main/res/drawable/ic_launcher_background.xml
rename to bluetooth-explorer-android/app/src/main/res/drawable/ic_launcher_background.xml
index d5fccc5..07d5da9 100644
--- a/Android/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/bluetooth-explorer-android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -2,169 +2,169 @@
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
diff --git a/bluetooth-explorer-android/app/src/main/res/drawable/ic_launcher_foreground.xml b/bluetooth-explorer-android/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/res/layout/list_item.xml b/bluetooth-explorer-android/app/src/main/res/layout/list_item.xml
new file mode 100644
index 0000000..eaf5504
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/res/layout/list_item.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/bluetooth-explorer-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 79%
rename from Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to bluetooth-explorer-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index eca70cf..6f3b755 100644
--- a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/bluetooth-explorer-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/bluetooth-explorer-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 79%
rename from Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to bluetooth-explorer-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index eca70cf..6f3b755 100644
--- a/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/bluetooth-explorer-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -2,4 +2,5 @@
+
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/bluetooth-explorer-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/bluetooth-explorer-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/bluetooth-explorer-android/app/src/main/res/values/colors.xml b/bluetooth-explorer-android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/res/values/strings.xml b/bluetooth-explorer-android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7de10d2
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ SwiftAndroid
+
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/res/values/themes.xml b/bluetooth-explorer-android/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..c81f2c7
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/res/xml/backup_rules.xml b/bluetooth-explorer-android/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/res/xml/data_extraction_rules.xml b/bluetooth-explorer-android/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/Application.swift b/bluetooth-explorer-android/app/src/main/swift/app/Application.swift
new file mode 100644
index 0000000..1b0fd74
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/Application.swift
@@ -0,0 +1,74 @@
+//
+// SwiftApp.swift
+// BluetoothExplorerApp
+//
+// Created by Alsey Coleman Miller on 6/8/25.
+//
+
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.Application")
+public class Application: AndroidApp.Application {
+
+ @JavaMethod
+ public func sayHello()
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.Application")
+public extension Application {
+
+ @JavaMethod
+ func onCreateSwift() {
+ log("\(self).\(#function)")
+
+ printAPIVersion()
+ sayHello()
+ }
+
+ @JavaMethod
+ func onTerminateSwift() {
+ log("\(self).\(#function)")
+ }
+}
+
+private extension Application {
+
+ func printAPIVersion() {
+
+ do {
+ let api = try AndroidOS.AndroidAPI.current
+ Self.logInfo("Running on Android API \(api)")
+ }
+ catch {
+ Self.logError("\(error)")
+ }
+ }
+}
+
+extension Application {
+
+ static var logTag: LogTag { "Application" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/Fragment.swift b/bluetooth-explorer-android/app/src/main/swift/app/Fragment.swift
new file mode 100644
index 0000000..9044e46
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/Fragment.swift
@@ -0,0 +1,86 @@
+//
+// Fragment.swift
+// BluetoothExplorerApp
+//
+// Created by Alsey Coleman Miller on 6/22/25.
+//
+
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.Fragment")
+open class Fragment: AndroidApp.Fragment {
+
+ @JavaMethod
+ @_nonoverride public convenience init(swiftObject: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getSwiftObject() -> SwiftObject?
+}
+
+public extension Fragment {
+
+ struct Callback {
+
+ var onViewCreated: ((AndroidView.View, AndroidOS.Bundle?) -> ())?
+ }
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.Fragment")
+extension Fragment {
+
+ @JavaMethod
+ func onViewCreated(
+ view: AndroidView.View?,
+ savedInstanceState: AndroidOS.Bundle?
+ ) {
+ log("\(self).\(#function)")
+ guard let onViewCreated = callback.onViewCreated else {
+ return
+ }
+ guard let view else {
+ assertionFailure("Missing view")
+ return
+ }
+ onViewCreated(view, savedInstanceState)
+ }
+}
+
+public extension Fragment {
+
+ convenience init(callback: Callback, environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(callback, environment: environment)
+ self.init(swiftObject: object, environment: environment)
+ }
+
+ var callback: Callback {
+ getSwiftObject()!.valueObject().value as! Callback
+ }
+}
+
+extension Fragment {
+
+ static var logTag: LogTag { "Fragment" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/HelloSubclass.swift b/bluetooth-explorer-android/app/src/main/swift/app/HelloSubclass.swift
new file mode 100644
index 0000000..eebd1b7
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/HelloSubclass.swift
@@ -0,0 +1,16 @@
+// Auto-generated by Java-to-Swift wrapper generator.
+import JavaKit
+import JavaRuntime
+
+@JavaClass("com.example.swift.HelloSubclass")
+open class HelloSubclass: HelloSwift {
+ @JavaMethod
+ @_nonoverride public convenience init(_ greeting: String, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func greetMe()
+}
+extension JavaClass {
+ @JavaStaticField(isFinal: false)
+ public var initialValue: Double
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/HelloSwift.swift b/bluetooth-explorer-android/app/src/main/swift/app/HelloSwift.swift
new file mode 100644
index 0000000..3e4f64c
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/HelloSwift.swift
@@ -0,0 +1,46 @@
+// Auto-generated by Java-to-Swift wrapper generator.
+import JavaKit
+import JavaKitFunction
+import JavaRuntime
+
+@JavaClass("com.example.swift.HelloSwift")
+open class HelloSwift: JavaObject {
+ @JavaField(isFinal: false)
+ public var value: Double
+
+ @JavaField(isFinal: false)
+ public var name: String
+
+ @JavaMethod
+ @_nonoverride public convenience init(environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func greet(_ name: String)
+
+ @JavaMethod
+ open func sayHelloBack(_ i: Int32) -> Double
+
+ @JavaMethod
+ open func lessThanTen() -> JavaPredicate!
+
+ @JavaMethod
+ open func doublesToStrings(_ doubles: [Double]) -> [String]
+
+ @JavaMethod
+ open func throwMessage(_ message: String) throws
+}
+extension JavaClass {
+ @JavaStaticField(isFinal: false)
+ public var initialValue: Double
+}
+/// Describes the Java `native` methods for ``HelloSwift``.
+///
+/// To implement all of the `native` methods for HelloSwift in Swift,
+/// extend HelloSwift to conform to this protocol and mark
+/// each implementation of the protocol requirement with
+/// `@JavaMethod`.
+protocol HelloSwiftNativeMethods {
+ func throwMessageFromSwift(_ message: String) throws -> String
+
+ func sayHello(_ x: Int32, _ y: Int32) -> Int32
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/JavaKitExample.swift b/bluetooth-explorer-android/app/src/main/swift/app/JavaKitExample.swift
new file mode 100644
index 0000000..7b53f19
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/JavaKitExample.swift
@@ -0,0 +1,115 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import JavaKit
+import JavaKitFunction
+import AndroidUtil
+import AndroidLogging
+
+enum SwiftWrappedError: Error {
+ case message(String)
+}
+
+@JavaImplementation("com.example.swift.HelloSwift")
+extension HelloSwift: HelloSwiftNativeMethods {
+ @JavaMethod
+ func sayHello(_ i: Int32, _ j: Int32) -> Int32 {
+ print("Hello from Swift!")
+ let answer = self.sayHelloBack(i + j)
+ print("Swift got back \(answer) from Java")
+
+ print("We expect the above value to be the initial value, \(self.javaClass.initialValue)")
+
+ print("Updating Java field value to something different")
+ self.value = 2.71828
+
+ let newAnswer = self.sayHelloBack(17)
+ print("Swift got back updated \(newAnswer) from Java")
+
+ let newHello = HelloSwift(environment: javaEnvironment)
+ print("Swift created a new Java instance with the value \(newHello.value)")
+
+ let name = newHello.name
+ print("Hello to \(name)")
+ newHello.greet("Swift 👋🏽 How's it going")
+
+ self.name = "a 🗑️-collected language"
+ _ = self.sayHelloBack(42)
+
+ let predicate: JavaPredicate = self.lessThanTen()!
+ let value = predicate.test(JavaInteger(3).as(JavaObject.self))
+ print("Running a JavaPredicate from swift 3 < 10 = \(value)")
+
+ let strings = doublesToStrings([3.14159, 2.71828])
+ print("Converting doubles to strings: \(strings)")
+
+ // Try downcasting
+ if let helloSub = self.as(HelloSubclass.self) {
+ print("Hello from the subclass!")
+ helloSub.greetMe()
+
+ assert(helloSub.value == 2.71828)
+ } else {
+ fatalError("Expected subclass here")
+ }
+
+ // Check "is" behavior
+ assert(newHello.is(HelloSwift.self))
+ assert(!newHello.is(HelloSubclass.self))
+
+ // Create a new instance.
+ let helloSubFromSwift = HelloSubclass("Hello from Swift", environment: javaEnvironment)
+ helloSubFromSwift.greetMe()
+
+ do {
+ try throwMessage("I am an error")
+ } catch {
+ print("Caught Java error: \(error)")
+ }
+
+ // Make sure that the thread safe class is sendable
+ let helper = ThreadSafeHelperClass(environment: javaEnvironment)
+ let threadSafe: Sendable = helper
+
+ checkOptionals(helper: helper)
+
+ return i * j
+ }
+
+ func checkOptionals(helper: ThreadSafeHelperClass) {
+ let text: JavaString? = helper.textOptional
+ let value: String? = helper.getValueOptional(Optional.none)
+ let textFunc: JavaString? = helper.getTextOptional()
+ let doubleOpt: Double? = helper.valOptional
+ let longOpt: Int64? = helper.fromOptional(21 as Int32?)
+ print("Optional text = \(text.debugDescription)")
+ print("Optional string value = \(value.debugDescription)")
+ print("Optional text function returned \(textFunc.debugDescription)")
+ print("Optional double function returned \(doubleOpt.debugDescription)")
+ print("Optional long function returned \(longOpt.debugDescription)")
+ }
+
+ @JavaMethod
+ func throwMessageFromSwift(_ message: String) throws -> String {
+ throw SwiftWrappedError.message(message)
+ }
+}
+
+internal extension HelloSwift {
+
+ func print(_ string: String) {
+ try? AndroidLogger(tag: "HelloSwift", priority: .verbose)
+ .log(string)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/JavaRetainedValue.swift b/bluetooth-explorer-android/app/src/main/swift/app/JavaRetainedValue.swift
new file mode 100644
index 0000000..8840eb5
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/JavaRetainedValue.swift
@@ -0,0 +1,89 @@
+//
+// JavaRetainedValue.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import JavaKit
+import JavaRuntime
+
+/// Java class that retains a Swift value for the duration of its lifetime.
+@JavaClass("com.pureswift.swiftandroid.SwiftObject")
+open class SwiftObject: JavaObject {
+
+ @JavaMethod
+ @_nonoverride public convenience init(swiftObject: Int64, type: String, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func getSwiftObject() -> Int64
+
+ @JavaMethod
+ open func getType() -> String
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.SwiftObject")
+extension SwiftObject {
+
+ @JavaMethod
+ public func toStringSwift() -> String {
+ "\(valueObject().value)"
+ }
+
+ @JavaMethod
+ public func finalizeSwift() {
+ // release owned swift value
+ release()
+ }
+}
+
+extension SwiftObject {
+
+ convenience init(_ value: T, environment: JNIEnvironment? = nil) {
+ let box = JavaRetainedValue(value)
+ let type = box.type
+ self.init(swiftObject: box.id, type: type, environment: environment)
+ // retain value
+ retain(box)
+ }
+
+ func valueObject() -> JavaRetainedValue {
+ let id = getSwiftObject()
+ guard let object = Self.retained[id] else {
+ fatalError()
+ }
+ return object
+ }
+}
+
+private extension SwiftObject {
+
+ static var retained = [JavaRetainedValue.ID: JavaRetainedValue]()
+
+ func retain(_ value: JavaRetainedValue) {
+ Self.retained[value.id] = value
+ }
+
+ func release() {
+ let id = getSwiftObject()
+ Self.retained[id] = nil
+ }
+}
+
+/// Swift Object retained until released by Java object.
+final class JavaRetainedValue: Identifiable {
+
+ var value: Any
+
+ var type: String {
+ String(describing: Swift.type(of: value))
+ }
+
+ var id: Int64 {
+ Int64(ObjectIdentifier(self).hashValue)
+ }
+
+ init(_ value: T) {
+ self.value = value
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/ListViewAdapter.swift b/bluetooth-explorer-android/app/src/main/swift/app/ListViewAdapter.swift
new file mode 100644
index 0000000..0c7eb8a
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/ListViewAdapter.swift
@@ -0,0 +1,85 @@
+//
+// ListViewAdapter.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import Foundation
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.ListViewAdapter", extends: ListAdapter.self)
+open class ListViewAdapter: JavaObject {
+
+ @JavaMethod
+ @_nonoverride public convenience init(
+ context: AndroidContent.Context?,
+ swiftObject: SwiftObject?,
+ objects: ArrayList?,
+ environment: JNIEnvironment? = nil
+ )
+
+ @JavaMethod
+ func getSwiftObject() -> SwiftObject!
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.ListViewAdapter")
+extension ListViewAdapter {
+
+ @JavaMethod
+ func getView(position: Int32, convertView: AndroidView.View?, parent: ViewGroup?) -> AndroidView.View? {
+ log("\(self).\(#function) \(position)")
+ return getView(position, convertView, parent)
+ }
+}
+
+public extension ListViewAdapter {
+
+ typealias GetView = (Int32, AndroidView.View?, ViewGroup?) -> AndroidView.View?
+
+ var getView: GetView {
+ get {
+ getSwiftObject().valueObject().value as! GetView
+ }
+ set {
+ getSwiftObject().valueObject().value = newValue
+ }
+ }
+
+ convenience init(
+ context: AndroidContent.Context,
+ getView: @escaping (Int32, AndroidView.View?, ViewGroup?) -> AndroidView.View?,
+ objects: ArrayList,
+ environment: JNIEnvironment? = nil
+ ) {
+ self.init(context: context, swiftObject: SwiftObject(getView), objects: objects, environment: environment)
+ }
+}
+
+extension ListViewAdapter {
+
+ static var logTag: LogTag { "ListViewAdapter" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/MainActivity.swift b/bluetooth-explorer-android/app/src/main/swift/app/MainActivity.swift
new file mode 100644
index 0000000..ee966a1
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/MainActivity.swift
@@ -0,0 +1,383 @@
+//
+// Activity.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/8/25.
+//
+
+import Foundation
+import AndroidKit
+import JavaLang
+import Binder
+
+@JavaClass("com.pureswift.swiftandroid.MainActivity")
+open class MainActivity: AndroidApp.Activity {
+
+ @JavaMethod
+ open func setRootView(_ view: AndroidView.View?)
+
+ @JavaMethod
+ open func getEmitter() -> UnitEmitter!
+
+ static private(set) var shared: MainActivity!
+
+ lazy var textView = TextView(self)
+
+ lazy var listView = ListView(self)
+
+ lazy var recyclerView = RecyclerView(self)
+
+ lazy var button = AndroidWidget.Button(self)
+
+ lazy var emitter = getEmitter()!
+
+ lazy var rootViewID: Int32 = try! JavaClass().generateViewId()
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.MainActivity")
+extension MainActivity {
+
+ @JavaMethod
+ public func onCreateSwift(_ savedInstanceState: BaseBundle?) {
+ log("\(self).\(#function)")
+
+ _onCreate(savedInstanceState)
+ }
+}
+
+private extension MainActivity {
+
+ #if os(Android)
+ typealias MainActor = AndroidMainActor
+ #endif
+
+ func _onCreate(_ savedInstanceState: BaseBundle?) {
+
+ // setup singletons
+ if savedInstanceState == nil, MainActivity.shared == nil {
+ MainActivity.shared = self
+ startMainRunLoop()
+ runAsync()
+ }
+
+ // need to recreate views
+ //setRootView()
+ startEmitterTimer()
+
+ Task {
+ printBinderVersion()
+ }
+ }
+
+ func runAsync() {
+ RunLoop.main.run(until: Date() + 0.1)
+ DispatchQueue.main.async {
+ Self.log("\(self).\(#function) Main Thread Async")
+ }
+ DispatchQueue.global(qos: .default).async {
+ Self.log("\(self).\(#function) Default Dispatch Queue Async")
+ }
+ Task {
+ Self.log("\(self).\(#function) Task Started")
+ }
+ }
+
+ func startMainRunLoop() {
+ #if os(Android)
+ guard AndroidMainActor.setupMainLooper() else {
+ fatalError("Unable to setup main loop")
+ }
+ #endif
+ }
+
+ func setRootView() {
+ setTextView()
+ }
+
+ func setTextView() {
+ let linearLayout = LinearLayout(self)
+ linearLayout.orientation = .vertical
+ linearLayout.gravity = .center
+ linearLayout.addView(textView)
+ configureButton()
+ linearLayout.addView(button)
+ setRootView(linearLayout)
+ // update view on timer
+ Task { [weak self] in
+ while let self {
+ await self.updateTextView()
+ try? await Task.sleep(for: .seconds(1))
+ }
+ }
+ }
+
+ func startEmitterTimer() {
+ // update view on timer
+ Task { [weak self] in
+ while let self {
+ await emit()
+ try? await Task.sleep(for: .seconds(1))
+ }
+ }
+ }
+
+ @MainActor
+ func emit() {
+ Self.log("\(self).\(#function)")
+ emitter.emit()
+ }
+
+ func setupNavigationStack() {
+
+ let fragmentContainer = FrameLayout(self)
+ fragmentContainer.setId(rootViewID)
+ let matchParent = try! JavaClass().MATCH_PARENT
+ fragmentContainer.setLayoutParams(ViewGroup.LayoutParams(matchParent, matchParent))
+
+ let homeFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+
+ let linearLayout = LinearLayout(self)
+ linearLayout.setLayoutParams(ViewGroup.LayoutParams(matchParent, matchParent))
+ linearLayout.orientation = .vertical
+ linearLayout.gravity = .center
+
+ let label = TextView(context)
+ label.text = "Home View"
+ label.gravity = .center
+ linearLayout.addView(label)
+
+ let button = Button(context)
+ button.text = "Push"
+ label.gravity = .center
+ let listener = ViewOnClickListener {
+ self.didPushButton()
+ }
+ button.setOnClickListener(listener.as(View.OnClickListener.self))
+ linearLayout.addView(button)
+
+ view.as(ViewGroup.self)!.addView(linearLayout)
+ }))
+
+ // setup initial fragment
+ _ = getFragmentManager()
+ .beginTransaction()
+ .replace(rootViewID, homeFragment)
+ .commit()
+
+ // Set as the content view
+ setRootView(fragmentContainer)
+ }
+
+ func configureButton() {
+ button.text = "Push"
+ let listener = ViewOnClickListener {
+ self.didPushButton()
+ }
+ button.setOnClickListener(listener.as(View.OnClickListener.self))
+ }
+
+ func didPushButton() {
+
+ let counter = getFragmentManager().getBackStackEntryCount() + 1
+
+ let detailFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+
+ let matchParent = try! JavaClass().MATCH_PARENT
+
+ let linearLayout = LinearLayout(self)
+ linearLayout.setLayoutParams(ViewGroup.LayoutParams(matchParent, matchParent))
+ linearLayout.orientation = .vertical
+ linearLayout.gravity = .center
+
+ let label = TextView(context)
+ label.text = "Detail View \(counter)"
+ label.gravity = .center
+ linearLayout.addView(label)
+
+ let button = Button(context)
+ button.text = "Push"
+ button.gravity = .center
+ let listener = ViewOnClickListener {
+ self.didPushButton()
+ }
+ button.setOnClickListener(listener.as(View.OnClickListener.self))
+ linearLayout.addView(button)
+
+ view.as(ViewGroup.self)!.addView(linearLayout)
+ }))
+
+ push(detailFragment, name: "Detail \(counter)")
+ }
+
+ func push(_ fragment: AndroidApp.Fragment, name: String) {
+ log("\(self).\(#function) \(name)")
+ _ = getFragmentManager()
+ .beginTransaction()
+ .replace(rootViewID, fragment)
+ .addToBackStack(name)
+ .commit()
+ }
+
+ func setListView() {
+ let items = [
+ "Row 1",
+ "Row 2",
+ "Row 3",
+ "Row 4",
+ "Row 5"
+ ]
+ let layout = try! JavaClass()
+ let resource = layout.simple_list_item_1
+ assert(resource != 0)
+ let objects: [JavaObject?] = items.map { JavaString($0) }
+ let adapter = ArrayAdapter(
+ context: self,
+ resource: resource,
+ objects: objects
+ )
+ listView.setAdapter(adapter.as(Adapter.self))
+
+ setRootView(listView)
+ }
+
+ func setRecyclerView() {
+ let items = [
+ "Row 1",
+ "Row 2",
+ "Row 3",
+ "Row 4",
+ "Row 5"
+ ]
+ let callback = RecyclerViewAdapter.Callback(
+ onBindViewHolder: { (holder, position) in
+ guard let viewHolder = holder.as(RecyclerViewAdapter.ViewHolder.self) else {
+ return
+ }
+ // get view
+ let linearLayout = viewHolder.itemView.as(LinearLayout.self)!
+ let textView: TextView
+ if linearLayout.getChildCount() == 0 {
+ textView = TextView(self)
+ linearLayout.addView(textView)
+ } else {
+ textView = linearLayout.getChildAt(0).as(TextView.self)!
+ }
+ // set data
+ let data = items[Int(position)]
+ textView.text = data
+ },
+ getItemCount: {
+ Int32(items.count)
+ }
+ )
+ let adapter = RecyclerViewAdapter(callback)
+ recyclerView.setLayoutManager(LinearLayoutManager(self))
+ recyclerView.setAdapter(adapter)
+ setRootView(recyclerView)
+ }
+
+ @MainActor
+ func updateTextView() {
+ log("\(self).\(#function)")
+ textView.text = "Hello Swift!\n\(Date().formatted(date: .numeric, time: .complete))"
+ }
+
+ func setTabBar() {
+ let layout = LinearLayout(self)
+ layout.orientation = .vertical
+
+ let container = FrameLayout(self)
+ container.setId(2001)
+
+ let bottomNav = BottomNavigationView(self)
+ _ = bottomNav.getMenu().add(0, 1, 0, JavaString("Home").as(CharSequence.self)).setIcon(17301543)
+ _ = bottomNav.getMenu().add(0, 2, 1, JavaString("Profile").as(CharSequence.self)).setIcon(17301659)
+
+ let homeFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+ let label = TextView(context)
+ label.text = "Home View"
+ label.gravity = .center
+ view.as(ViewGroup.self)!.addView(label)
+ }))
+
+ let profileFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+ let label = TextView(context)
+ label.text = "Profile"
+ label.gravity = .center
+ view.as(ViewGroup.self)!.addView(label)
+ }))
+
+ let fragment1 = homeFragment
+ let fragment2 = profileFragment
+
+ let listener = NavigationBarViewOnItemSelectedListener { item in
+ guard let item else { return false }
+ let fragment: AndroidApp.Fragment = (item.getItemId() == 1) ? fragment1 : fragment2
+ _ = self.getFragmentManager().beginTransaction()
+ .replace(2001, fragment)
+ .commit()
+ return true
+ }
+ bottomNav.setOnItemSelectedListener(listener.as(NavigationBarView.OnItemSelectedListener.self))
+
+ let matchParent = try! JavaClass().MATCH_PARENT
+ let wrapContent = try! JavaClass().WRAP_CONTENT
+
+ layout.addView(container as AndroidView.View, ViewGroup.LayoutParams(matchParent, 1))
+ layout.addView(bottomNav as AndroidView.View, ViewGroup.LayoutParams(matchParent, wrapContent))
+
+ self.setRootView(layout)
+
+ // Default to Home
+ _ = self.getFragmentManager().beginTransaction()
+ .add(2001, fragment1)
+ .commit()
+ }
+
+ private func printBinderVersion() {
+ // Print Binder version
+ do {
+ let version = try BinderVersion.current
+ logInfo("Binder Version: \(version)")
+ }
+ catch {
+ logError("Unable to read binder: \(error)")
+ }
+ }
+}
+
+extension MainActivity {
+
+ static var logTag: LogTag { "MainActivity" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+
+ func logInfo(_ string: String) {
+ Self.logInfo(string)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/NavigationBarViewOnItemSelectedListener.swift b/bluetooth-explorer-android/app/src/main/swift/app/NavigationBarViewOnItemSelectedListener.swift
new file mode 100644
index 0000000..1585fc2
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/NavigationBarViewOnItemSelectedListener.swift
@@ -0,0 +1,76 @@
+//
+// OnItemSelectedListener.swift
+// BluetoothExplorerApp
+//
+// Created by Alsey Coleman Miller on 6/21/25.
+//
+
+import Foundation
+import AndroidKit
+import AndroidMaterial
+
+@JavaClass("com.pureswift.swiftandroid.NavigationBarViewOnItemSelectedListener", extends: AndroidMaterial.NavigationView.OnNavigationItemSelectedListener.self)
+open class NavigationBarViewOnItemSelectedListener: JavaObject {
+
+ public typealias Action = (MenuItem?) -> (Bool)
+
+ @JavaMethod
+ @_nonoverride public convenience init(action: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getAction() -> SwiftObject?
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.NavigationBarViewOnItemSelectedListener")
+extension NavigationBarViewOnItemSelectedListener {
+
+ @JavaMethod
+ func onNavigationItemSelected(menuItem: MenuItem?) -> Bool {
+ log("\(self).\(#function)")
+ // drain queue
+ RunLoop.main.run(until: Date() + 0.01)
+ let result = action(menuItem)
+ RunLoop.main.run(until: Date() + 0.01)
+ return result
+ }
+}
+
+public extension NavigationBarViewOnItemSelectedListener {
+
+ convenience init(action: @escaping Action, environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(action, environment: environment)
+ self.init(action: object, environment: environment)
+ }
+
+ var action: Action {
+ getAction()!.valueObject().value as! Action
+ }
+}
+
+extension NavigationBarViewOnItemSelectedListener {
+
+ static var logTag: LogTag { "NavigationBarViewOnItemSelectedListener" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/OnClickListener.swift b/bluetooth-explorer-android/app/src/main/swift/app/OnClickListener.swift
new file mode 100644
index 0000000..c57c8fe
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/OnClickListener.swift
@@ -0,0 +1,74 @@
+//
+// OnClickListener.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import Foundation
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.ViewOnClickListener", extends: AndroidView.View.OnClickListener.self)
+open class ViewOnClickListener: JavaObject {
+
+ public typealias Action = () -> ()
+
+ @JavaMethod
+ @_nonoverride public convenience init(action: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getAction() -> SwiftObject?
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.ViewOnClickListener")
+extension ViewOnClickListener {
+
+ @JavaMethod
+ func onClick() {
+ log("\(self).\(#function)")
+ // drain queue
+ RunLoop.main.run(until: Date() + 0.01)
+ action()
+ RunLoop.main.run(until: Date() + 0.01)
+ }
+}
+
+public extension ViewOnClickListener {
+
+ convenience init(action: @escaping () -> (), environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(action, environment: environment)
+ self.init(action: object, environment: environment)
+ }
+
+ var action: (() -> ()) {
+ getAction()!.valueObject().value as! Action
+ }
+}
+
+extension ViewOnClickListener {
+
+ static var logTag: LogTag { "ViewOnClickListener" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/RecyclerView.swift b/bluetooth-explorer-android/app/src/main/swift/app/RecyclerView.swift
new file mode 100644
index 0000000..2460f08
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/RecyclerView.swift
@@ -0,0 +1,107 @@
+//
+// RecyclerView.swift
+// BluetoothExplorerApp
+//
+// Created by Alsey Coleman Miller on 6/13/25.
+//
+
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.RecyclerViewAdapter")
+open class RecyclerViewAdapter: RecyclerView.Adapter {
+
+ @JavaMethod
+ @_nonoverride public convenience init(swiftObject: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ func getSwiftObject() -> SwiftObject!
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.RecyclerViewAdapter")
+extension RecyclerViewAdapter {
+
+ @JavaMethod
+ public func onBindViewHolderSwift(_ viewHolder: RecyclerViewAdapter.ViewHolder?, _ position: Int32) {
+ log("\(self).\(#function) \(position)")
+ callback.onBindViewHolder(viewHolder!, position)
+ }
+
+ @JavaMethod
+ public func getItemCountSwift() -> Int32 {
+ log("\(self).\(#function)")
+ return callback.getItemCount()
+ }
+}
+
+extension RecyclerViewAdapter {
+
+ static var logTag: LogTag { "RecyclerViewAdapter" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
+
+public extension RecyclerViewAdapter {
+
+ struct Callback {
+
+ var onBindViewHolder: ((RecyclerViewAdapter.ViewHolder, Int32) -> ())
+
+ var getItemCount: () -> Int32
+
+ public init(
+ onBindViewHolder: @escaping ((RecyclerViewAdapter.ViewHolder, Int32) -> Void),
+ getItemCount: @escaping () -> Int32 = { return 0 }
+ ) {
+ self.onBindViewHolder = onBindViewHolder
+ self.getItemCount = getItemCount
+ }
+ }
+}
+
+public extension RecyclerViewAdapter {
+
+ convenience init(_ callback: Callback, environment: JNIEnvironment? = nil) {
+ let swiftObject = SwiftObject(callback, environment: environment)
+ self.init(swiftObject: swiftObject, environment: environment)
+ }
+
+ var callback: Callback {
+ get {
+ getSwiftObject().valueObject().value as! Callback
+ }
+ set {
+ getSwiftObject().valueObject().value = newValue
+ }
+ }
+}
+
+extension RecyclerViewAdapter {
+
+ @JavaClass("com.pureswift.swiftandroid.RecyclerViewAdapter$ViewHolder")
+ open class ViewHolder: RecyclerView.ViewHolder {
+
+ @JavaMethod
+ @_nonoverride public convenience init(view: AndroidView.View?, environment: JNIEnvironment? = nil)
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/Runnable.swift b/bluetooth-explorer-android/app/src/main/swift/app/Runnable.swift
new file mode 100644
index 0000000..7381969
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/Runnable.swift
@@ -0,0 +1,50 @@
+//
+// Runnable.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import JavaKit
+import JavaRuntime
+import AndroidKit
+import JavaLang
+
+@JavaClass("com.pureswift.swiftandroid.Runnable", extends: JavaLang.Runnable.self)
+open class Runnable: JavaObject {
+
+ public typealias Block = () -> ()
+
+ @JavaMethod
+ @_nonoverride public convenience init(block: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getBlock() -> SwiftObject?
+}
+
+public extension Runnable {
+
+ convenience init(_ block: @escaping () -> Void, environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(block, environment: environment)
+ self.init(block: object, environment: environment)
+ }
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.Runnable")
+extension Runnable {
+
+ @JavaMethod
+ func run() {
+ block()
+ }
+}
+
+private extension Runnable {
+
+ var block: Block {
+ guard let block = getBlock()?.valueObject().value as? Block else {
+ fatalError()
+ }
+ return block
+ }
+}
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/ThreadSafeHelperClass.swift b/bluetooth-explorer-android/app/src/main/swift/app/ThreadSafeHelperClass.swift
new file mode 100644
index 0000000..38786a0
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/ThreadSafeHelperClass.swift
@@ -0,0 +1,54 @@
+// Auto-generated by Java-to-Swift wrapper generator.
+import JavaKit
+import JavaRuntime
+
+@JavaClass("com.example.swift.ThreadSafeHelperClass")
+open class ThreadSafeHelperClass: JavaObject {
+ @JavaField(isFinal: false)
+ public var text: JavaOptional!
+
+
+ public var textOptional: JavaString? {
+ get {
+ Optional(javaOptional: text)
+ }
+ set {
+ text = newValue.toJavaOptional()
+ }
+ }
+
+ @JavaField(isFinal: true)
+ public var val: JavaOptionalDouble!
+
+
+ public var valOptional: Double? {
+ get {
+ Optional(javaOptional: val)
+ }
+ }
+
+ @JavaMethod
+ @_nonoverride public convenience init(environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func getValue(_ name: JavaOptional?) -> String
+
+ open func getValueOptional(_ name: JavaString?) -> String {
+ getValue(name.toJavaOptional())
+ }
+
+ @JavaMethod
+ open func from(_ value: JavaOptionalInt?) -> JavaOptionalLong!
+
+ open func fromOptional(_ value: Int32?) -> Int64? {
+ Optional(javaOptional: from(value.toJavaOptional()))
+ }
+
+ @JavaMethod
+ open func getText() -> JavaOptional!
+
+ open func getTextOptional() -> JavaString? {
+ Optional(javaOptional: getText())
+ }
+}
+extension ThreadSafeHelperClass: @unchecked Swift.Sendable { }
diff --git a/bluetooth-explorer-android/app/src/main/swift/app/UnitEmitter.swift b/bluetooth-explorer-android/app/src/main/swift/app/UnitEmitter.swift
new file mode 100644
index 0000000..81ca4eb
--- /dev/null
+++ b/bluetooth-explorer-android/app/src/main/swift/app/UnitEmitter.swift
@@ -0,0 +1,19 @@
+//
+// UnitEmitter.swift
+// BluetoothExplorerApp
+//
+// Created by Alsey Coleman Miller on 7/13/25.
+//
+
+import JavaKit
+
+/// Bridge from Swift to Kotlin Coroutines
+@JavaClass("com.pureswift.swiftandroid.UnitEmitter")
+open class UnitEmitter: JavaObject {
+
+ @JavaMethod
+ @_nonoverride public convenience init(environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ func emit()
+}
diff --git a/Android/app/src/test/java/com/millertech/bluetoothexplorer/ExampleUnitTest.kt b/bluetooth-explorer-android/app/src/test/java/com/pureswift/swiftandroid/ExampleUnitTest.kt
similarity index 87%
rename from Android/app/src/test/java/com/millertech/bluetoothexplorer/ExampleUnitTest.kt
rename to bluetooth-explorer-android/app/src/test/java/com/pureswift/swiftandroid/ExampleUnitTest.kt
index a89df4a..2d1f328 100644
--- a/Android/app/src/test/java/com/millertech/bluetoothexplorer/ExampleUnitTest.kt
+++ b/bluetooth-explorer-android/app/src/test/java/com/pureswift/swiftandroid/ExampleUnitTest.kt
@@ -1,4 +1,4 @@
-package com.millertech.bluetoothexplorer
+package com.pureswift.swiftandroid
import org.junit.Test
@@ -14,4 +14,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
-}
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/build-swift.sh b/bluetooth-explorer-android/build-swift.sh
new file mode 100755
index 0000000..9c64f90
--- /dev/null
+++ b/bluetooth-explorer-android/build-swift.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+source swift-define
+
+# Build with SwiftPM
+ANDROID_NDK_ROOT="" ANDROID_SDK_VERSION=$ANDROID_SDK_VERSION skip android build --arch $SWIFT_TARGET_ARCH --android-api-level $ANDROID_SDK_VERSION
+
+# Copy compiled Swift package
+mkdir -p $SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH/
+cp -rf $SWIFT_PACKAGE_SRC/.build/$SWIFT_TARGET_NAME/debug/libBluetoothExplorerApp.so \
+ $SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH/
diff --git a/bluetooth-explorer-android/build.gradle.kts b/bluetooth-explorer-android/build.gradle.kts
new file mode 100644
index 0000000..952b930
--- /dev/null
+++ b/bluetooth-explorer-android/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+}
\ No newline at end of file
diff --git a/bluetooth-explorer-android/gradle.properties b/bluetooth-explorer-android/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/bluetooth-explorer-android/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/bluetooth-explorer-android/gradle/libs.versions.toml b/bluetooth-explorer-android/gradle/libs.versions.toml
new file mode 100644
index 0000000..792411d
--- /dev/null
+++ b/bluetooth-explorer-android/gradle/libs.versions.toml
@@ -0,0 +1,38 @@
+[versions]
+agp = "8.10.1"
+kotlin = "2.0.21"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.8.0"
+composeBom = "2024.09.00"
+material = "1.12.0"
+navigationRuntime = "2.9.0"
+recyclerview = "1.4.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "navigationRuntime" }
+androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+material = { module = "com.google.android.material:material", version.ref = "material" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/Android/gradle/wrapper/gradle-wrapper.properties b/bluetooth-explorer-android/gradle/wrapper/gradle-wrapper.properties
similarity index 79%
rename from Android/gradle/wrapper/gradle-wrapper.properties
rename to bluetooth-explorer-android/gradle/wrapper/gradle-wrapper.properties
index d5b3c01..3473e96 100644
--- a/Android/gradle/wrapper/gradle-wrapper.properties
+++ b/bluetooth-explorer-android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Sep 05 14:57:00 PET 2018
+#Sat Jun 07 21:03:00 EDT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/Android/gradlew b/bluetooth-explorer-android/gradlew
similarity index 75%
rename from Android/gradlew
rename to bluetooth-explorer-android/gradlew
index cccdd3d..4f906e0 100755
--- a/Android/gradlew
+++ b/bluetooth-explorer-android/gradlew
@@ -1,5 +1,21 @@
#!/usr/bin/env sh
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
- i=$((i+1))
+ i=`expr $i + 1`
done
case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
-APP_ARGS=$(save "$@")
+APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
-fi
-
exec "$JAVACMD" "$@"
diff --git a/Android/gradlew.bat b/bluetooth-explorer-android/gradlew.bat
similarity index 64%
rename from Android/gradlew.bat
rename to bluetooth-explorer-android/gradlew.bat
index f955316..ac1b06f 100644
--- a/Android/gradlew.bat
+++ b/bluetooth-explorer-android/gradlew.bat
@@ -1,84 +1,89 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/bluetooth-explorer-android/local.properties b/bluetooth-explorer-android/local.properties
new file mode 100644
index 0000000..e018a92
--- /dev/null
+++ b/bluetooth-explorer-android/local.properties
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/Users/coleman/Library/Android/sdk
\ No newline at end of file
diff --git a/bluetooth-explorer-android/settings.gradle.kts b/bluetooth-explorer-android/settings.gradle.kts
new file mode 100644
index 0000000..4447e1b
--- /dev/null
+++ b/bluetooth-explorer-android/settings.gradle.kts
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "SwiftAndroid"
+include(":app")
diff --git a/bluetooth-explorer-android/setup.sh b/bluetooth-explorer-android/setup.sh
new file mode 100755
index 0000000..db8ba9a
--- /dev/null
+++ b/bluetooth-explorer-android/setup.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -e
+source swift-define
+
+# Install macOS dependencies
+if [[ $OSTYPE == 'darwin'* ]]; then
+ echo "Install macOS build dependencies"
+ brew install skiptools/skip/skip
+ skip android sdk install
+ brew update
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install wget cmake ninja android-ndk
+fi
+
+# Copy Swift libraries
+rm -rf $SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH/
+mkdir -p $SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH/
+cp -rf $SWIFT_ANDROID_LIBS/*.so \
+ $SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH/
+# Copy C stdlib
+cp -rf $SWIFT_ANDROID_SYSROOT/usr/lib/$ANDROID_LIB/libc++_shared.so \
+ $SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH/
+echo "Copied Swift libraries"
diff --git a/bluetooth-explorer-android/swift-define b/bluetooth-explorer-android/swift-define
new file mode 100644
index 0000000..66efe0b
--- /dev/null
+++ b/bluetooth-explorer-android/swift-define
@@ -0,0 +1,24 @@
+# Configurable
+SRC_ROOT="${SRC_ROOT:=$(pwd)}"
+SWIFT_TARGET_ARCH="${SWIFT_TARGET_ARCH:=aarch64}"
+ANDROID_ARCH="${ANDROID_ARCH:=arm64-v8a}"
+ANDROID_LIB="${ANDROID_LIB:=aarch64-linux-android}"
+SWIFT_COMPILATION_MODE="${SWIFT_COMPILATION_MODE:=debug}"
+
+# Version
+ANDROID_SDK_VERSION=29
+SWIFT_VERSION_SHORT=6.1.1
+SWIFT_VERSION=swift-$SWIFT_VERSION_SHORT-RELEASE
+SWIFT_TARGET_NAME=$SWIFT_TARGET_ARCH-unknown-linux-android$ANDROID_SDK_VERSION
+XCTOOLCHAIN=/Library/Developer/Toolchains/$SWIFT_VERSION.xctoolchain
+SWIFT_ARTIFACT_BUNDLE=swift-$SWIFT_VERSION_SHORT-RELEASE-android-0.1.artifactbundle
+
+# Paths
+SWIFT_SDK=swift-$SWIFT_VERSION_SHORT-release-android-$ANDROID_SDK_VERSION-sdk
+SWIFT_ANDROID_SYSROOT=~/.swiftpm/swift-sdks/$SWIFT_ARTIFACT_BUNDLE/swift-android/ndk-sysroot
+SWIFT_ANDROID_LIBS=~/.swiftpm/swift-sdks/$SWIFT_ARTIFACT_BUNDLE/swift-android/swift-resources/usr/lib/swift-$SWIFT_TARGET_ARCH/android/
+SWIFT_PACKAGE_SRC=$SRC_ROOT
+JAVA_HOME=$SWIFT_ANDROID_SYSROOT/usr
+
+# Configurable
+SWIFT_NATIVE_PATH="${SWIFT_NATIVE_PATH:=$XCTOOLCHAIN/usr/bin}"
diff --git a/iOS/Old/BluetoothExplorer.xcodeproj/project.pbxproj b/iOS/Old/BluetoothExplorer.xcodeproj/project.pbxproj
deleted file mode 100644
index 4c173d8..0000000
--- a/iOS/Old/BluetoothExplorer.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,774 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 50;
- objects = {
-
-/* Begin PBXBuildFile section */
- 51B6F9B3215567E800251524 /* CharacteristicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B6F9A7215567E800251524 /* CharacteristicsViewController.swift */; };
- 6E0596AD2158448B00BB43DC /* CharacteristicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0596A12158448B00BB43DC /* CharacteristicViewController.swift */; };
- 6E0596EC2158A88500BB43DC /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0596EB2158A88500BB43DC /* Appearance.swift */; };
- 6E303E072142398F0034A6C1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6E303E062142398F0034A6C1 /* Assets.xcassets */; };
- 6E303E0A2142398F0034A6C1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6E303E082142398F0034A6C1 /* LaunchScreen.storyboard */; };
- 6E303E1921423AED0034A6C1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E303E1421423AED0034A6C1 /* AppDelegate.swift */; };
- 6E38F60F2278DAB200925952 /* ScanDataTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6E38F60E2278DAB200925952 /* ScanDataTableViewCell.xib */; };
- 6E38F6112278DE3F00925952 /* ReusableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E38F6102278DE3F00925952 /* ReusableTableViewCell.swift */; };
- 6E38F6132278DE5B00925952 /* NibTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E38F6122278DE5B00925952 /* NibTableViewCell.swift */; };
- 6E38F6152278DE8400925952 /* ScanDataTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E38F6142278DE8400925952 /* ScanDataTableViewCell.swift */; };
- 6E38F6172278DEA600925952 /* AndroidTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E38F6162278DEA600925952 /* AndroidTableViewCell.swift */; };
- 6E733FE121423DFE001B8682 /* GATT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E733FD221423DEC001B8682 /* GATT.framework */; };
- 6E733FE221423DFE001B8682 /* GATT.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E733FD221423DEC001B8682 /* GATT.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 6E733FE521423DFE001B8682 /* DarwinGATT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E733FDC21423DEC001B8682 /* DarwinGATT.framework */; };
- 6E733FE621423DFE001B8682 /* DarwinGATT.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E733FDC21423DEC001B8682 /* DarwinGATT.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 6E733FE921423DFE001B8682 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E733F9E21423DE0001B8682 /* Bluetooth.framework */; };
- 6E733FEA21423DFE001B8682 /* Bluetooth.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E733F9E21423DE0001B8682 /* Bluetooth.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 6E73401F21424322001B8682 /* CentralViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E73401E21424322001B8682 /* CentralViewController.swift */; };
- 6E73402C21424437001B8682 /* Central.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E73402B21424437001B8682 /* Central.swift */; };
- 6EAEEE3C2142470C009A7A9F /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EAEEE302142470C009A7A9F /* ErrorAlert.swift */; };
- 6EAEEE3E21424717009A7A9F /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EAEEE3D21424717009A7A9F /* ActivityIndicatorViewController.swift */; };
- 6EAEEE4021424790009A7A9F /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EAEEE3F21424790009A7A9F /* Async.swift */; };
- 6EAEEE42214247F3009A7A9F /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EAEEE41214247F3009A7A9F /* Log.swift */; };
- 6EAEEED221437F2B009A7A9F /* ServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EAEEED121437F2B009A7A9F /* ServicesViewController.swift */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- 6E733F9B21423DE0001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EE84DB71CAF5C7C00A40C4D;
- remoteInfo = "Bluetooth-macOS";
- };
- 6E733F9D21423DE0001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EF45FBD1CC6D04D001F7A39;
- remoteInfo = "Bluetooth-iOS";
- };
- 6E733F9F21423DE0001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EB2EA1D1CD5A8A7000CF975;
- remoteInfo = "Bluetooth-watchOS";
- };
- 6E733FA121423DE0001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6E49B26520532D45002EA5DC;
- remoteInfo = "Bluetooth-tvOS";
- };
- 6E733FA321423DE0001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EE84DC11CAF5C7C00A40C4D;
- remoteInfo = BluetoothTests;
- };
- 6E733FCF21423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EE84D841CAF419D00A40C4D;
- remoteInfo = "GATT-macOS";
- };
- 6E733FD121423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EF45FCF1CC6D355001F7A39;
- remoteInfo = "GATT-iOS";
- };
- 6E733FD321423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EE9103A1FDE5C17007AD3EA;
- remoteInfo = "GATT-watchOS";
- };
- 6E733FD521423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6E49B23A20532A94002EA5DC;
- remoteInfo = "GATT-tvOS";
- };
- 6E733FD721423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6E0FC45A20F873B1009269B4;
- remoteInfo = GATTTests;
- };
- 6E733FD921423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EA5D1562107DE66009998FD;
- remoteInfo = "DarwinGATT-macOS";
- };
- 6E733FDB21423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EA5D16D2107DE6A009998FD;
- remoteInfo = "DarwinGATT-iOS";
- };
- 6E733FDD21423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EA5D1842107DE6D009998FD;
- remoteInfo = "DarwinGATT-watchOS";
- };
- 6E733FDF21423DEC001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = 6EA5D19B2107DE70009998FD;
- remoteInfo = "DarwinGATT-tvOS";
- };
- 6E733FE321423DFE001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 1;
- remoteGlobalIDString = 6EF45FBF1CC6D355001F7A39;
- remoteInfo = "GATT-iOS";
- };
- 6E733FE721423DFE001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- proxyType = 1;
- remoteGlobalIDString = 6EA5D1582107DE6A009998FD;
- remoteInfo = "DarwinGATT-iOS";
- };
- 6E733FEB21423DFE001B8682 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */;
- proxyType = 1;
- remoteGlobalIDString = 6EF45FA21CC6D04D001F7A39;
- remoteInfo = "Bluetooth-iOS";
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- 6E733FED21423DFE001B8682 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- 6E733FEA21423DFE001B8682 /* Bluetooth.framework in Embed Frameworks */,
- 6E733FE221423DFE001B8682 /* GATT.framework in Embed Frameworks */,
- 6E733FE621423DFE001B8682 /* DarwinGATT.framework in Embed Frameworks */,
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 51B6F9A7215567E800251524 /* CharacteristicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CharacteristicsViewController.swift; sourceTree = ""; };
- 6E0596A12158448B00BB43DC /* CharacteristicViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CharacteristicViewController.swift; sourceTree = ""; };
- 6E0596EB2158A88500BB43DC /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; };
- 6E303DFC2142398E0034A6C1 /* BluetoothExplorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BluetoothExplorer.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 6E303E062142398F0034A6C1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 6E303E092142398F0034A6C1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
- 6E303E0B2142398F0034A6C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 6E303E1421423AED0034A6C1 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- 6E303E1521423AED0034A6C1 /* MainActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainActivity.swift; sourceTree = ""; };
- 6E38F60E2278DAB200925952 /* ScanDataTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ScanDataTableViewCell.xib; sourceTree = ""; };
- 6E38F6102278DE3F00925952 /* ReusableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableTableViewCell.swift; sourceTree = ""; };
- 6E38F6122278DE5B00925952 /* NibTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibTableViewCell.swift; sourceTree = ""; };
- 6E38F6142278DE8400925952 /* ScanDataTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanDataTableViewCell.swift; sourceTree = ""; };
- 6E38F6162278DEA600925952 /* AndroidTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidTableViewCell.swift; sourceTree = ""; };
- 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Bluetooth.xcodeproj; path = ../Carthage/Checkouts/Bluetooth/Xcode/Bluetooth.xcodeproj; sourceTree = ""; };
- 6E733FC321423DEC001B8682 /* GATT.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GATT.xcodeproj; path = ../Carthage/Checkouts/GATT/Xcode/GATT.xcodeproj; sourceTree = ""; };
- 6E73401E21424322001B8682 /* CentralViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CentralViewController.swift; sourceTree = ""; };
- 6E73402B21424437001B8682 /* Central.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Central.swift; sourceTree = ""; };
- 6EAEEE302142470C009A7A9F /* ErrorAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = ""; };
- 6EAEEE3D21424717009A7A9F /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = ""; };
- 6EAEEE3F21424790009A7A9F /* Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Async.swift; sourceTree = ""; };
- 6EAEEE41214247F3009A7A9F /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; };
- 6EAEEED121437F2B009A7A9F /* ServicesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesViewController.swift; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 6E303DF92142398E0034A6C1 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 6E733FE921423DFE001B8682 /* Bluetooth.framework in Frameworks */,
- 6E733FE121423DFE001B8682 /* GATT.framework in Frameworks */,
- 6E733FE521423DFE001B8682 /* DarwinGATT.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 6E0596EA2158A87600BB43DC /* View */ = {
- isa = PBXGroup;
- children = (
- 6E0596EB2158A88500BB43DC /* Appearance.swift */,
- 6E38F6102278DE3F00925952 /* ReusableTableViewCell.swift */,
- 6E38F6122278DE5B00925952 /* NibTableViewCell.swift */,
- 6E38F6162278DEA600925952 /* AndroidTableViewCell.swift */,
- 6E38F6142278DE8400925952 /* ScanDataTableViewCell.swift */,
- );
- name = View;
- sourceTree = "";
- };
- 6E303DF32142398E0034A6C1 = {
- isa = PBXGroup;
- children = (
- 6E733F9221423DD8001B8682 /* Dependencies */,
- 6E303E1121423AED0034A6C1 /* Sources */,
- 6E303DFE2142398E0034A6C1 /* BluetoothExplorer */,
- 6E303DFD2142398E0034A6C1 /* Products */,
- );
- sourceTree = "";
- };
- 6E303DFD2142398E0034A6C1 /* Products */ = {
- isa = PBXGroup;
- children = (
- 6E303DFC2142398E0034A6C1 /* BluetoothExplorer.app */,
- );
- name = Products;
- sourceTree = "";
- };
- 6E303DFE2142398E0034A6C1 /* BluetoothExplorer */ = {
- isa = PBXGroup;
- children = (
- 6E303E062142398F0034A6C1 /* Assets.xcassets */,
- 6E303E082142398F0034A6C1 /* LaunchScreen.storyboard */,
- 6E303E0B2142398F0034A6C1 /* Info.plist */,
- 6E38F60E2278DAB200925952 /* ScanDataTableViewCell.xib */,
- );
- path = BluetoothExplorer;
- sourceTree = "";
- };
- 6E303E1121423AED0034A6C1 /* Sources */ = {
- isa = PBXGroup;
- children = (
- 6E303E1521423AED0034A6C1 /* MainActivity.swift */,
- 6E303E1421423AED0034A6C1 /* AppDelegate.swift */,
- 6EAEEE3F21424790009A7A9F /* Async.swift */,
- 6EAEEE41214247F3009A7A9F /* Log.swift */,
- 6E0596EA2158A87600BB43DC /* View */,
- 6E73402D21424456001B8682 /* Controller */,
- 6E73402E21424460001B8682 /* Model */,
- );
- name = Sources;
- path = ../Android/app/src/main/swift/Sources;
- sourceTree = "";
- };
- 6E733F9221423DD8001B8682 /* Dependencies */ = {
- isa = PBXGroup;
- children = (
- 6E733FC321423DEC001B8682 /* GATT.xcodeproj */,
- 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */,
- );
- name = Dependencies;
- sourceTree = "";
- };
- 6E733F9421423DE0001B8682 /* Products */ = {
- isa = PBXGroup;
- children = (
- 6E733F9C21423DE0001B8682 /* Bluetooth.framework */,
- 6E733F9E21423DE0001B8682 /* Bluetooth.framework */,
- 6E733FA021423DE0001B8682 /* Bluetooth.framework */,
- 6E733FA221423DE0001B8682 /* Bluetooth.framework */,
- 6E733FA421423DE0001B8682 /* BluetoothTests.xctest */,
- );
- name = Products;
- sourceTree = "";
- };
- 6E733FC421423DEC001B8682 /* Products */ = {
- isa = PBXGroup;
- children = (
- 6E733FD021423DEC001B8682 /* GATT.framework */,
- 6E733FD221423DEC001B8682 /* GATT.framework */,
- 6E733FD421423DEC001B8682 /* GATT.framework */,
- 6E733FD621423DEC001B8682 /* GATT.framework */,
- 6E733FD821423DEC001B8682 /* GATTTests.xctest */,
- 6E733FDA21423DEC001B8682 /* DarwinGATT.framework */,
- 6E733FDC21423DEC001B8682 /* DarwinGATT.framework */,
- 6E733FDE21423DEC001B8682 /* DarwinGATT.framework */,
- 6E733FE021423DEC001B8682 /* DarwinGATT.framework */,
- );
- name = Products;
- sourceTree = "";
- };
- 6E73402D21424456001B8682 /* Controller */ = {
- isa = PBXGroup;
- children = (
- 6EAEEE4421424869009A7A9F /* Protocols */,
- 6EAEEE432142485F009A7A9F /* Extensions */,
- 6E73401E21424322001B8682 /* CentralViewController.swift */,
- 6EAEEED121437F2B009A7A9F /* ServicesViewController.swift */,
- 51B6F9A7215567E800251524 /* CharacteristicsViewController.swift */,
- 6E0596A12158448B00BB43DC /* CharacteristicViewController.swift */,
- );
- name = Controller;
- sourceTree = "";
- };
- 6E73402E21424460001B8682 /* Model */ = {
- isa = PBXGroup;
- children = (
- 6E73402B21424437001B8682 /* Central.swift */,
- );
- name = Model;
- sourceTree = "";
- };
- 6EAEEE432142485F009A7A9F /* Extensions */ = {
- isa = PBXGroup;
- children = (
- 6EAEEE302142470C009A7A9F /* ErrorAlert.swift */,
- );
- name = Extensions;
- sourceTree = "";
- };
- 6EAEEE4421424869009A7A9F /* Protocols */ = {
- isa = PBXGroup;
- children = (
- 6EAEEE3D21424717009A7A9F /* ActivityIndicatorViewController.swift */,
- );
- name = Protocols;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- 6E303DFB2142398E0034A6C1 /* BluetoothExplorer */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 6E303E0E2142398F0034A6C1 /* Build configuration list for PBXNativeTarget "BluetoothExplorer" */;
- buildPhases = (
- 6E303DF82142398E0034A6C1 /* Sources */,
- 6E303DF92142398E0034A6C1 /* Frameworks */,
- 6E303DFA2142398E0034A6C1 /* Resources */,
- 6E733FED21423DFE001B8682 /* Embed Frameworks */,
- 6E0596E92158A46500BB43DC /* Increment Build */,
- );
- buildRules = (
- );
- dependencies = (
- 6E733FEC21423DFE001B8682 /* PBXTargetDependency */,
- 6E733FE421423DFE001B8682 /* PBXTargetDependency */,
- 6E733FE821423DFE001B8682 /* PBXTargetDependency */,
- );
- name = BluetoothExplorer;
- productName = BluetoothExplorer;
- productReference = 6E303DFC2142398E0034A6C1 /* BluetoothExplorer.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 6E303DF42142398E0034A6C1 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- LastSwiftUpdateCheck = 0940;
- LastUpgradeCheck = 0940;
- ORGANIZATIONNAME = PureSwift;
- TargetAttributes = {
- 6E303DFB2142398E0034A6C1 = {
- CreatedOnToolsVersion = 9.4.1;
- };
- };
- };
- buildConfigurationList = 6E303DF72142398E0034A6C1 /* Build configuration list for PBXProject "BluetoothExplorer" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = 6E303DF32142398E0034A6C1;
- productRefGroup = 6E303DFD2142398E0034A6C1 /* Products */;
- projectDirPath = "";
- projectReferences = (
- {
- ProductGroup = 6E733F9421423DE0001B8682 /* Products */;
- ProjectRef = 6E733F9321423DE0001B8682 /* Bluetooth.xcodeproj */;
- },
- {
- ProductGroup = 6E733FC421423DEC001B8682 /* Products */;
- ProjectRef = 6E733FC321423DEC001B8682 /* GATT.xcodeproj */;
- },
- );
- projectRoot = "";
- targets = (
- 6E303DFB2142398E0034A6C1 /* BluetoothExplorer */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXReferenceProxy section */
- 6E733F9C21423DE0001B8682 /* Bluetooth.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = Bluetooth.framework;
- remoteRef = 6E733F9B21423DE0001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733F9E21423DE0001B8682 /* Bluetooth.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = Bluetooth.framework;
- remoteRef = 6E733F9D21423DE0001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FA021423DE0001B8682 /* Bluetooth.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = Bluetooth.framework;
- remoteRef = 6E733F9F21423DE0001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FA221423DE0001B8682 /* Bluetooth.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = Bluetooth.framework;
- remoteRef = 6E733FA121423DE0001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FA421423DE0001B8682 /* BluetoothTests.xctest */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.cfbundle;
- path = BluetoothTests.xctest;
- remoteRef = 6E733FA321423DE0001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FD021423DEC001B8682 /* GATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = GATT.framework;
- remoteRef = 6E733FCF21423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FD221423DEC001B8682 /* GATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = GATT.framework;
- remoteRef = 6E733FD121423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FD421423DEC001B8682 /* GATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = GATT.framework;
- remoteRef = 6E733FD321423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FD621423DEC001B8682 /* GATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = GATT.framework;
- remoteRef = 6E733FD521423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FD821423DEC001B8682 /* GATTTests.xctest */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.cfbundle;
- path = GATTTests.xctest;
- remoteRef = 6E733FD721423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FDA21423DEC001B8682 /* DarwinGATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = DarwinGATT.framework;
- remoteRef = 6E733FD921423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FDC21423DEC001B8682 /* DarwinGATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = DarwinGATT.framework;
- remoteRef = 6E733FDB21423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FDE21423DEC001B8682 /* DarwinGATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = DarwinGATT.framework;
- remoteRef = 6E733FDD21423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
- 6E733FE021423DEC001B8682 /* DarwinGATT.framework */ = {
- isa = PBXReferenceProxy;
- fileType = wrapper.framework;
- path = DarwinGATT.framework;
- remoteRef = 6E733FDF21423DEC001B8682 /* PBXContainerItemProxy */;
- sourceTree = BUILT_PRODUCTS_DIR;
- };
-/* End PBXReferenceProxy section */
-
-/* Begin PBXResourcesBuildPhase section */
- 6E303DFA2142398E0034A6C1 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 6E303E0A2142398F0034A6C1 /* LaunchScreen.storyboard in Resources */,
- 6E38F60F2278DAB200925952 /* ScanDataTableViewCell.xib in Resources */,
- 6E303E072142398F0034A6C1 /* Assets.xcassets in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- 6E0596E92158A46500BB43DC /* Increment Build */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputPaths = (
- );
- name = "Increment Build";
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "#!/bin/bash\n\n# update_build_number.sh\n# Usage: `update_build_number.sh [branch]`\n# Run this script after the 'Copy Bundle Resources' build phase\n# Ref: http://tgoode.com/2014/06/05/sensible-way-increment-bundle-version-cfbundleversion-xcode/\n\nif [ $TRAVIS == \"true\" ]\nthen echo \"Updating build number to TRAVISCI.\"\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion TRAVISCI\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\nelse branch=${1:-'master'}\nbuildNumber=$(expr $(git rev-list $branch --count) - $(git rev-list HEAD..$branch --count))\necho \"Updating build number to $buildNumber using branch '$branch'.\"\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\nfi";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 6E303DF82142398E0034A6C1 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 6E38F6112278DE3F00925952 /* ReusableTableViewCell.swift in Sources */,
- 6E0596AD2158448B00BB43DC /* CharacteristicViewController.swift in Sources */,
- 6E38F6132278DE5B00925952 /* NibTableViewCell.swift in Sources */,
- 6E38F6172278DEA600925952 /* AndroidTableViewCell.swift in Sources */,
- 6E303E1921423AED0034A6C1 /* AppDelegate.swift in Sources */,
- 6EAEEE3E21424717009A7A9F /* ActivityIndicatorViewController.swift in Sources */,
- 6EAEEE3C2142470C009A7A9F /* ErrorAlert.swift in Sources */,
- 6EAEEE4021424790009A7A9F /* Async.swift in Sources */,
- 6E73402C21424437001B8682 /* Central.swift in Sources */,
- 6E38F6152278DE8400925952 /* ScanDataTableViewCell.swift in Sources */,
- 6E73401F21424322001B8682 /* CentralViewController.swift in Sources */,
- 6E0596EC2158A88500BB43DC /* Appearance.swift in Sources */,
- 6EAEEED221437F2B009A7A9F /* ServicesViewController.swift in Sources */,
- 6EAEEE42214247F3009A7A9F /* Log.swift in Sources */,
- 51B6F9B3215567E800251524 /* CharacteristicsViewController.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 6E733FE421423DFE001B8682 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- name = "GATT-iOS";
- targetProxy = 6E733FE321423DFE001B8682 /* PBXContainerItemProxy */;
- };
- 6E733FE821423DFE001B8682 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- name = "DarwinGATT-iOS";
- targetProxy = 6E733FE721423DFE001B8682 /* PBXContainerItemProxy */;
- };
- 6E733FEC21423DFE001B8682 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- name = "Bluetooth-iOS";
- targetProxy = 6E733FEB21423DFE001B8682 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin PBXVariantGroup section */
- 6E303E082142398F0034A6C1 /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- 6E303E092142398F0034A6C1 /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- 6E303E0C2142398F0034A6C1 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- MTL_ENABLE_DEBUG_INFO = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- };
- name = Debug;
- };
- 6E303E0D2142398F0034A6C1 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- 6E303E0F2142398F0034A6C1 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = DGYBYARDK9;
- INFOPLIST_FILE = BluetoothExplorer/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = org.pureswift.BluetoothExplorer;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 4.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- 6E303E102142398F0034A6C1 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = DGYBYARDK9;
- INFOPLIST_FILE = BluetoothExplorer/Info.plist;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = org.pureswift.BluetoothExplorer;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_VERSION = 4.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 6E303DF72142398E0034A6C1 /* Build configuration list for PBXProject "BluetoothExplorer" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 6E303E0C2142398F0034A6C1 /* Debug */,
- 6E303E0D2142398F0034A6C1 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- 6E303E0E2142398F0034A6C1 /* Build configuration list for PBXNativeTarget "BluetoothExplorer" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 6E303E0F2142398F0034A6C1 /* Debug */,
- 6E303E102142398F0034A6C1 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 6E303DF42142398E0034A6C1 /* Project object */;
-}
diff --git a/iOS/Old/BluetoothExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iOS/Old/BluetoothExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 97fe2a5..0000000
--- a/iOS/Old/BluetoothExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/iOS/Old/BluetoothExplorer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/iOS/Old/BluetoothExplorer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/iOS/Old/BluetoothExplorer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/iOS/Old/BluetoothExplorer.xcodeproj/xcshareddata/xcschemes/BluetoothExplorer.xcscheme b/iOS/Old/BluetoothExplorer.xcodeproj/xcshareddata/xcschemes/BluetoothExplorer.xcscheme
deleted file mode 100644
index af6222e..0000000
--- a/iOS/Old/BluetoothExplorer.xcodeproj/xcshareddata/xcschemes/BluetoothExplorer.xcscheme
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100755
index 7130009..0000000
--- a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,158 +0,0 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "NotificationIcon@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "NotificationIcon@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-Small.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-Small@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-Small@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "57x57",
- "idiom" : "iphone",
- "filename" : "Icon.png",
- "scale" : "1x"
- },
- {
- "size" : "57x57",
- "idiom" : "iphone",
- "filename" : "Icon@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "NotificationIcon~ipad.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "NotificationIcon~ipad@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-Small.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-Small@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-40.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "50x50",
- "idiom" : "ipad",
- "filename" : "Icon-Small-50.png",
- "scale" : "1x"
- },
- {
- "size" : "50x50",
- "idiom" : "ipad",
- "filename" : "Icon-Small-50@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "72x72",
- "idiom" : "ipad",
- "filename" : "Icon-72.png",
- "scale" : "1x"
- },
- {
- "size" : "72x72",
- "idiom" : "ipad",
- "filename" : "Icon-72@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-76.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "ios-marketing.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40.png
deleted file mode 100755
index 51a4bc5..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
deleted file mode 100755
index b3759fa..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
deleted file mode 100755
index 4ebe5fb..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
deleted file mode 100755
index 4ebe5fb..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
deleted file mode 100755
index 62fb260..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-72.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-72.png
deleted file mode 100755
index f9672e1..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-72.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png
deleted file mode 100755
index 5e07378..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-76.png
deleted file mode 100755
index d616d1a..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-76.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
deleted file mode 100755
index 8efa586..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
deleted file mode 100755
index a3d6ab6..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png
deleted file mode 100755
index e348d96..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
deleted file mode 100755
index 2738a82..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
deleted file mode 100755
index 510277e..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
deleted file mode 100755
index 7db25bf..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
deleted file mode 100755
index be83614..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon.png
deleted file mode 100755
index 39191e4..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon@2x.png
deleted file mode 100755
index 3190c5d..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/Icon@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png
deleted file mode 100755
index 51a4bc5..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png
deleted file mode 100755
index 310927a..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png
deleted file mode 100755
index f8148fa..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png
deleted file mode 100755
index 51a4bc5..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/ios-marketing.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/ios-marketing.png
deleted file mode 100755
index da84bda..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/AppIcon.appiconset/ios-marketing.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Contents.json b/iOS/Old/BluetoothExplorer/Assets.xcassets/Contents.json
deleted file mode 100755
index da4a164..0000000
--- a/iOS/Old/BluetoothExplorer/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Logo.imageset/Contents.json b/iOS/Old/BluetoothExplorer/Assets.xcassets/Logo.imageset/Contents.json
deleted file mode 100755
index e4678c4..0000000
--- a/iOS/Old/BluetoothExplorer/Assets.xcassets/Logo.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "PureSwiftBluetooth.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Logo.imageset/PureSwiftBluetooth.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/Logo.imageset/PureSwiftBluetooth.png
deleted file mode 100755
index 155393a..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/Logo.imageset/PureSwiftBluetooth.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Contents.json b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Contents.json
deleted file mode 100755
index da4a164..0000000
--- a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Contents.json b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Contents.json
deleted file mode 100755
index b87c9da..0000000
--- a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "Near.png",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "Near@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "Near@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near.png
deleted file mode 100755
index ee9521e..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near@2x.png
deleted file mode 100755
index f23864f..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near@3x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near@3x.png
deleted file mode 100755
index 1d3d5d8..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/Near.imageset/Near@3x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/Contents.json b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/Contents.json
deleted file mode 100755
index f5ddf8e..0000000
--- a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "NearSelected.png",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "filename" : "NearSelected@2x.png",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "filename" : "NearSelected@3x.png",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected.png
deleted file mode 100755
index 040509a..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected@2x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected@2x.png
deleted file mode 100755
index 9305aeb..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected@2x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected@3x.png b/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected@3x.png
deleted file mode 100755
index c1163b2..0000000
Binary files a/iOS/Old/BluetoothExplorer/Assets.xcassets/Tab Bar/NearSelected.imageset/NearSelected@3x.png and /dev/null differ
diff --git a/iOS/Old/BluetoothExplorer/Base.lproj/LaunchScreen.storyboard b/iOS/Old/BluetoothExplorer/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index f83f6fd..0000000
--- a/iOS/Old/BluetoothExplorer/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/iOS/Old/BluetoothExplorer/Info.plist b/iOS/Old/BluetoothExplorer/Info.plist
deleted file mode 100644
index af9d510..0000000
--- a/iOS/Old/BluetoothExplorer/Info.plist
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Bluetooth
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- 1.0.0
- CFBundleVersion
- 1
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIRequiredDeviceCapabilities
-
- armv7
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
-
-
diff --git a/iOS/Old/BluetoothExplorer/ScanDataTableViewCell.xib b/iOS/Old/BluetoothExplorer/ScanDataTableViewCell.xib
deleted file mode 100644
index 696c272..0000000
--- a/iOS/Old/BluetoothExplorer/ScanDataTableViewCell.xib
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-