Skip to content

Commit ba6b7bd

Browse files
authored
Merge pull request #15 from MillerTechnologyPeru/feature/android-skip
Add Skip Android app
2 parents 9418b33 + a600168 commit ba6b7bd

187 files changed

Lines changed: 3791 additions & 5513 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ xcuserdata/
2222
*.moved-aside
2323
*.xccheckout
2424
*.xcscmblueprint
25-
*.DS_Store
25+
.DS_Store
2626

2727
## Obj-C/Swift specific
2828
*.hmap
@@ -76,6 +76,7 @@ Android/app/src/main/swift/.swiftpm
7676

7777
# Android Studio
7878
.idea/*
79+
.gradle
7980

8081
# VS Code
8182
.vscode

Android/app/build.gradle.kts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import java.util.Properties
2+
3+
plugins {
4+
alias(libs.plugins.kotlin.android)
5+
alias(libs.plugins.kotlin.compose)
6+
alias(libs.plugins.android.application)
7+
id("skip-build-plugin")
8+
}
9+
10+
skip {
11+
}
12+
13+
kotlin {
14+
compilerOptions {
15+
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(libs.versions.jvm.get().toString())
16+
}
17+
}
18+
19+
android {
20+
namespace = group as String
21+
compileSdk = libs.versions.android.sdk.compile.get().toInt()
22+
compileOptions {
23+
sourceCompatibility = JavaVersion.toVersion(libs.versions.jvm.get())
24+
targetCompatibility = JavaVersion.toVersion(libs.versions.jvm.get())
25+
}
26+
packaging {
27+
jniLibs {
28+
keepDebugSymbols.add("**/*.so")
29+
pickFirsts.add("**/*.so")
30+
// this option will compress JNI .so files
31+
useLegacyPackaging = true
32+
}
33+
}
34+
35+
defaultConfig {
36+
minSdk = libs.versions.android.sdk.min.get().toInt()
37+
targetSdk = libs.versions.android.sdk.compile.get().toInt()
38+
// skip.tools.skip-build-plugin will automatically use Skip.env properties for:
39+
// applicationId = ANDROID_APPLICATION_ID ?? PRODUCT_BUNDLE_IDENTIFIER
40+
// versionCode = CURRENT_PROJECT_VERSION
41+
// versionName = MARKETING_VERSION
42+
}
43+
44+
buildFeatures {
45+
buildConfig = true
46+
}
47+
48+
lint {
49+
disable.add("Instantiatable")
50+
disable.add("MissingPermission")
51+
}
52+
53+
dependenciesInfo {
54+
// Disables dependency metadata when building APKs.
55+
includeInApk = false
56+
// Disables dependency metadata when building Android App Bundles.
57+
includeInBundle = false
58+
}
59+
60+
// default signing configuration tries to load from keystore.properties
61+
// see: https://skip.dev/docs/deployment/#export-signing
62+
signingConfigs {
63+
val keystorePropertiesFile = file("keystore.properties")
64+
create("release") {
65+
if (keystorePropertiesFile.isFile) {
66+
val keystoreProperties = Properties()
67+
keystoreProperties.load(keystorePropertiesFile.inputStream())
68+
keyAlias = keystoreProperties.getProperty("keyAlias")
69+
keyPassword = keystoreProperties.getProperty("keyPassword")
70+
storeFile = file(keystoreProperties.getProperty("storeFile"))
71+
storePassword = keystoreProperties.getProperty("storePassword")
72+
} else {
73+
// when there is no keystore.properties file, fall back to signing with debug config
74+
keyAlias = signingConfigs.getByName("debug").keyAlias
75+
keyPassword = signingConfigs.getByName("debug").keyPassword
76+
storeFile = signingConfigs.getByName("debug").storeFile
77+
storePassword = signingConfigs.getByName("debug").storePassword
78+
}
79+
}
80+
}
81+
82+
buildTypes {
83+
release {
84+
signingConfig = signingConfigs.findByName("release")
85+
isMinifyEnabled = true
86+
isShrinkResources = true
87+
isDebuggable = false // can be set to true for debugging release build, but needs to be false when uploading to store
88+
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
89+
}
90+
}
91+
}

Android/app/proguard-rules.pro

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-keeppackagenames **
2+
-keep class skip.** { *; }
3+
-keep class tools.skip.** { *; }
4+
-keep class kotlin.jvm.functions.** {*;}
5+
-keep class com.sun.jna.** { *; }
6+
-dontwarn java.awt.**
7+
-keep class * implements com.sun.jna.** { *; }
8+
-keep class * implements skip.bridge.** { *; }
9+
-keep class **._ModuleBundleAccessor_* { *; }
10+
-keep class bluetooth.explorer.** { *; }
11+
-keep class org.pureswift.bluetooth.** { *; }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- This AndroidManifest.xml template was generated by Skip -->
3+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
4+
<!-- Android 12+ (API 31+) -->
5+
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
6+
android:usesPermissionFlags="neverForLocation" />
7+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
8+
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
9+
10+
<!-- Legacy support for Android 11 and below -->
11+
<uses-permission android:name="android.permission.BLUETOOTH"
12+
android:maxSdkVersion="30" />
13+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
14+
android:maxSdkVersion="30" />
15+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
16+
android:maxSdkVersion="30" />
17+
18+
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
19+
20+
<!-- permissions needed for using the internet or an embedded WebKit browser -->
21+
<uses-permission android:name="android.permission.INTERNET" />
22+
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
23+
24+
<application
25+
android:label="${PRODUCT_NAME}"
26+
android:name=".AndroidAppMain"
27+
android:supportsRtl="true"
28+
android:allowBackup="true"
29+
>
30+
<activity
31+
android:name=".MainActivity"
32+
android:exported="true"
33+
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
34+
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
35+
android:windowSoftInputMode="adjustResize">
36+
<intent-filter>
37+
<action android:name="android.intent.action.MAIN" />
38+
<category android:name="android.intent.category.LAUNCHER" />
39+
</intent-filter>
40+
</activity>
41+
</application>
42+
</manifest>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package bluetooth.explorer
2+
3+
import skip.lib.*
4+
import skip.model.*
5+
import skip.foundation.*
6+
import skip.ui.*
7+
8+
import android.Manifest
9+
import android.app.Application
10+
import android.graphics.Color as AndroidColor
11+
import androidx.activity.compose.setContent
12+
import androidx.activity.enableEdgeToEdge
13+
import androidx.activity.SystemBarStyle
14+
import androidx.activity.ComponentActivity
15+
import androidx.appcompat.app.AppCompatActivity
16+
import androidx.compose.foundation.isSystemInDarkTheme
17+
import androidx.compose.foundation.layout.fillMaxSize
18+
import androidx.compose.foundation.layout.Box
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.DisposableEffect
21+
import androidx.compose.runtime.SideEffect
22+
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
23+
import androidx.compose.ui.Alignment
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.graphics.luminance
26+
import androidx.compose.ui.platform.LocalContext
27+
import androidx.compose.material3.MaterialTheme
28+
import androidx.core.app.ActivityCompat
29+
30+
internal val logger: SkipLogger = SkipLogger(subsystem = "bluetooth.explorer", category = "BluetoothExplorer")
31+
32+
private typealias AppRootView = BluetoothExplorerRootView
33+
private typealias AppDelegate = BluetoothExplorerAppDelegate
34+
35+
/// AndroidAppMain is the `android.app.Application` entry point, and must match `application android:name` in the AndroidMainfest.xml file.
36+
open class AndroidAppMain: Application {
37+
constructor() {
38+
}
39+
40+
override fun onCreate() {
41+
super.onCreate()
42+
logger.info("starting app")
43+
try {
44+
// Ensure SwiftJNI JNI_OnLoad runs before Skip bridge bootstrap converts Java strings.
45+
java.lang.System.loadLibrary("SwiftJava")
46+
java.lang.System.loadLibrary("SkipBridge")
47+
java.lang.System.loadLibrary("SwiftJNI")
48+
java.lang.System.loadLibrary("BluetoothExplorer")
49+
} catch (error: Throwable) {
50+
logger.warning("SwiftJNI load skipped or failed: ${error.message ?: error::class.java.name}")
51+
}
52+
ProcessInfo.launch(applicationContext)
53+
AppDelegate.shared.onInit()
54+
}
55+
56+
companion object {
57+
}
58+
}
59+
60+
/// AndroidAppMain is initial `androidx.appcompat.app.AppCompatActivity`, and must match `activity android:name` in the AndroidMainfest.xml file.
61+
open class MainActivity: AppCompatActivity {
62+
constructor() {
63+
}
64+
65+
override fun onCreate(savedInstanceState: android.os.Bundle?) {
66+
super.onCreate(savedInstanceState)
67+
logger.info("starting activity")
68+
UIApplication.launch(this)
69+
enableEdgeToEdge()
70+
71+
setContent {
72+
val saveableStateHolder = rememberSaveableStateHolder()
73+
saveableStateHolder.SaveableStateProvider(true) {
74+
PresentationRootView(ComposeContext())
75+
SideEffect { saveableStateHolder.removeState(true) }
76+
}
77+
}
78+
79+
AppDelegate.shared.onLaunch()
80+
81+
// Request permissions on startup.
82+
val permissions = listOf(
83+
Manifest.permission.BLUETOOTH_SCAN,
84+
Manifest.permission.BLUETOOTH_CONNECT,
85+
Manifest.permission.BLUETOOTH_ADVERTISE,
86+
Manifest.permission.INTERNET
87+
)
88+
val requestTag = 1
89+
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), requestTag)
90+
}
91+
92+
override fun onStart() {
93+
logger.info("onStart")
94+
super.onStart()
95+
}
96+
97+
override fun onResume() {
98+
super.onResume()
99+
AppDelegate.shared.onResume()
100+
}
101+
102+
override fun onPause() {
103+
super.onPause()
104+
AppDelegate.shared.onPause()
105+
}
106+
107+
override fun onStop() {
108+
super.onStop()
109+
AppDelegate.shared.onStop()
110+
}
111+
112+
override fun onDestroy() {
113+
super.onDestroy()
114+
AppDelegate.shared.onDestroy()
115+
}
116+
117+
override fun onLowMemory() {
118+
super.onLowMemory()
119+
AppDelegate.shared.onLowMemory()
120+
}
121+
122+
override fun onRestart() {
123+
logger.info("onRestart")
124+
super.onRestart()
125+
}
126+
127+
override fun onSaveInstanceState(outState: android.os.Bundle): Unit = super.onSaveInstanceState(outState)
128+
129+
override fun onRestoreInstanceState(bundle: android.os.Bundle) {
130+
// Usually you restore your state in onCreate(). It is possible to restore it in onRestoreInstanceState() as well, but not very common. (onRestoreInstanceState() is called after onStart(), whereas onCreate() is called before onStart().
131+
logger.info("onRestoreInstanceState")
132+
super.onRestoreInstanceState(bundle)
133+
}
134+
135+
override fun onRequestPermissionsResult(requestCode: Int, permissions: kotlin.Array<String>, grantResults: IntArray) {
136+
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
137+
logger.info("onRequestPermissionsResult: ${requestCode}")
138+
}
139+
140+
companion object {
141+
}
142+
}
143+
144+
@Composable
145+
internal fun SyncSystemBarsWithTheme() {
146+
val dark = MaterialTheme.colorScheme.background.luminance() < 0.5f
147+
148+
val transparent = AndroidColor.TRANSPARENT
149+
val style = if (dark) {
150+
SystemBarStyle.dark(transparent)
151+
} else {
152+
SystemBarStyle.light(transparent, transparent)
153+
}
154+
155+
val activity = LocalContext.current as? ComponentActivity
156+
DisposableEffect(style) {
157+
activity?.enableEdgeToEdge(
158+
statusBarStyle = style,
159+
navigationBarStyle = style
160+
)
161+
onDispose { }
162+
}
163+
}
164+
165+
@Composable
166+
internal fun PresentationRootView(context: ComposeContext) {
167+
val colorScheme = if (isSystemInDarkTheme()) ColorScheme.dark else ColorScheme.light
168+
PresentationRoot(defaultColorScheme = colorScheme, context = context) { ctx ->
169+
SyncSystemBarsWithTheme()
170+
val contentContext = ctx.content()
171+
Box(modifier = ctx.modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
172+
AppRootView().Compose(context = contentContext)
173+
}
174+
}
175+
}

Android/fastlane/Appfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This file contains the app distribution configuration
2+
# for the Android half of the Skip app.
3+
# You can find the documentation at https://docs.fastlane.tools
4+
5+
# Load the shared Skip.env properties with the app info
6+
require('dotenv')
7+
Dotenv.load('../../Skip.env')
8+
package_name(ENV['PRODUCT_BUNDLE_IDENTIFIER'].sub("-", "_"))
9+
10+
# Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
11+
json_key_file("fastlane/apikey.json")

0 commit comments

Comments
 (0)