Add Capacitor Android project and native Tiptoi plugin

Checked-in android/ Capacitor project plus the native TiptoiPlugin
(Kotlin): SAF directory picker for the pen, listing/copying/deleting
.gme files on the pen, and downloading GME/cover files into
app-private storage.
This commit is contained in:
2026-08-12 11:54:39 +02:00
parent 30170436fa
commit 0188d5210e
58 changed files with 1353 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
+69
View File
@@ -0,0 +1,69 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace "com.tiptoisync.app"
compileSdk rootProject.ext.compileSdkVersion
signingConfigs {
release {
storeFile file('tiptoi-debug.keystore')
storePassword 'tiptoisync'
keyAlias 'tiptoisync'
keyPassword 'tiptoisync'
}
}
defaultConfig {
applicationId "com.tiptoisync.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
debug {
signingConfig signingConfigs.release
}
release {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
+19
View File
@@ -0,0 +1,19 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.getcapacitor.app", appContext.getPackageName());
}
}
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
</manifest>
@@ -0,0 +1,31 @@
package com.tiptoisync.app
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
@CapacitorPlugin(name = "Dev")
class DevPlugin : Plugin() {
private val prefs get() = context.getSharedPreferences("CapacitorStorage", 0)
/** Speichert die Dev-Server-URL und startet die Activity neu,
* damit die Bridge mit der neuen URL initialisiert wird. */
@PluginMethod
fun activate(call: PluginCall) {
val url = call.getString("url") ?: return call.reject("url required")
prefs.edit().putString("devServerUrl", url).apply()
activity.runOnUiThread { activity.recreate() }
call.resolve()
}
/** Entfernt die Dev-Server-URL und startet die Activity neu,
* damit die Bridge wieder die gebundelte App lädt. */
@PluginMethod
fun deactivate(call: PluginCall) {
prefs.edit().remove("devServerUrl").apply()
activity.runOnUiThread { activity.recreate() }
call.resolve()
}
}
@@ -0,0 +1,13 @@
package com.tiptoisync.app
import android.os.Bundle
import androidx.core.view.WindowCompat
import com.getcapacitor.BridgeActivity
class MainActivity : BridgeActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
registerPlugin(TiptoiPlugin::class.java)
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
}
@@ -0,0 +1,264 @@
package com.tiptoisync.app
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import androidx.activity.result.ActivityResult
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.ActivityCallback
import com.getcapacitor.annotation.CapacitorPlugin
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
@CapacitorPlugin(name = "Tiptoi")
class TiptoiPlugin : Plugin() {
private val downloadsDir get() = File(context.filesDir, "downloads").also { it.mkdirs() }
private val prefs get() = context.getSharedPreferences("tiptoi", 0)
// ── Stift (SAF) ──────────────────────────────────────────────────────────
@PluginMethod
fun requestPenAccess(call: PluginCall) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
)
}
startActivityForResult(call, intent, "onPenDirectoryPicked")
}
@ActivityCallback
private fun onPenDirectoryPicked(call: PluginCall?, result: ActivityResult) {
val uri = result.data?.data ?: run { call?.reject("Abgebrochen"); return }
context.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
prefs.edit().putString("pen_uri", uri.toString()).apply()
call?.resolve(JSObject().put("uri", uri.toString()))
}
@PluginMethod
fun getPenUri(call: PluginCall) {
val uri = prefs.getString("pen_uri", null)
val result = JSObject()
if (uri != null) result.put("uri", uri)
call.resolve(result)
}
@PluginMethod
fun listPenFiles(call: PluginCall) {
val uriStr = call.getString("uri") ?: return call.reject("uri fehlt")
val treeUri = Uri.parse(uriStr)
val childUri = DocumentsContract.buildChildDocumentsUriUsingTree(
treeUri, DocumentsContract.getTreeDocumentId(treeUri)
)
val cursor = context.contentResolver.query(
childUri,
arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME),
null, null, null
) ?: return call.reject("Verzeichnis nicht lesbar")
val files = JSArray()
cursor.use {
while (it.moveToNext()) {
val name = it.getString(0)
if (name.endsWith(".gme", ignoreCase = true)) files.put(name)
}
}
call.resolve(JSObject().put("files", files))
}
@PluginMethod
fun copyToPen(call: PluginCall) {
val uriStr = call.getString("uri") ?: return call.reject("uri fehlt")
val filename = call.getString("filename") ?: return call.reject("filename fehlt")
val src = File(downloadsDir, filename)
if (!src.exists()) return call.reject("Datei nicht im Cache: $filename")
Thread {
try {
val treeUri = Uri.parse(uriStr)
val treeDocUri = DocumentsContract.buildDocumentUriUsingTree(
treeUri, DocumentsContract.getTreeDocumentId(treeUri)
)
// Existierende Datei löschen (z. B. 0-Byte-Rest eines Fehlversuchs)
val existingUri = findDocInTree(treeUri, filename)
if (existingUri != null) {
DocumentsContract.deleteDocument(context.contentResolver, existingUri)
}
// Neue, leere Datei anlegen
val targetUri = DocumentsContract.createDocument(
context.contentResolver,
treeDocUri,
"application/octet-stream",
filename
) ?: throw IOException("Konnte Datei nicht anlegen: $filename")
val total = src.length()
var bytesCopied = 0L
val buffer = ByteArray(256 * 1024)
// openFileDescriptor statt openOutputStream: gibt echten fd zurück,
// damit wir sync() = fsync() aufrufen können
val pfd = context.contentResolver.openFileDescriptor(targetUri, "wt")
?: throw IOException("Konnte Datei nicht öffnen: $filename")
pfd.use { descriptor ->
FileOutputStream(descriptor.fileDescriptor).use { out ->
FileInputStream(src).use { input ->
var read: Int
while (input.read(buffer).also { read = it } != -1) {
out.write(buffer, 0, read)
bytesCopied += read
notifyListeners("copyProgress", JSObject()
.put("filename", filename)
.put("bytes", bytesCopied)
.put("total", total))
}
out.flush()
}
}
// fsync: Kernel-Pagecache → physische SD-Karte flushen.
// Für MTP-Geräte wirft dies (kein echtes fd) ignorieren,
// MTP committed bereits beim close() des Streams.
try { descriptor.fileDescriptor.sync() } catch (_: Exception) {}
}
// Verifikation: tatsächlich geschriebene Bytes prüfen
val written = context.contentResolver.query(
targetUri,
arrayOf(DocumentsContract.Document.COLUMN_SIZE),
null, null, null
)?.use { c -> if (c.moveToFirst()) c.getLong(0) else -1L } ?: -1L
if (written != total) {
throw IOException("Schreibfehler: $written von $total Bytes angekommen")
}
call.resolve()
} catch (e: Exception) {
call.reject(e.message ?: "Fehler beim Kopieren")
}
}.start()
}
private fun findDocInTree(treeUri: Uri, filename: String): Uri? {
val childUri = DocumentsContract.buildChildDocumentsUriUsingTree(
treeUri, DocumentsContract.getTreeDocumentId(treeUri)
)
val cursor = context.contentResolver.query(
childUri,
arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
),
null, null, null
) ?: return null
cursor.use {
while (it.moveToNext()) {
if (it.getString(1) == filename) {
return DocumentsContract.buildDocumentUriUsingTree(treeUri, it.getString(0))
}
}
}
return null
}
// ── Downloads (App-Cache) ────────────────────────────────────────────────
@PluginMethod
fun listDownloads(call: PluginCall) {
val gmeFiles = downloadsDir
.listFiles { f -> f.extension.equals("gme", ignoreCase = true) }
?: emptyArray()
val books = JSArray()
for (gme in gmeFiles) {
val base = gme.nameWithoutExtension
val cover = downloadsDir.listFiles { f ->
f.nameWithoutExtension == base && !f.extension.equals("gme", ignoreCase = true)
}?.firstOrNull()
books.put(JSObject().put("filename", gme.name).put("coverPath", cover?.absolutePath).put("size", gme.length()))
}
call.resolve(JSObject().put("books", books))
}
@PluginMethod
fun downloadGme(call: PluginCall) {
val url = call.getString("url") ?: return call.reject("url fehlt")
val filename = call.getString("filename") ?: return call.reject("filename fehlt")
Thread {
try {
downloadToFile(url, File(downloadsDir, filename))
call.resolve(JSObject().put("filename", filename))
} catch (e: Exception) {
call.reject(e.message ?: "Download fehlgeschlagen")
}
}.start()
}
@PluginMethod
fun downloadCover(call: PluginCall) {
val url = call.getString("url") ?: return call.reject("url fehlt")
val filename = call.getString("filename") ?: return call.reject("filename fehlt")
Thread {
try { downloadToFile(url, File(downloadsDir, filename)) }
catch (_: Exception) { /* Cover ist optional */ }
call.resolve()
}.start()
}
// ── Löschen ────────────────────────────────────────────────────────────
@PluginMethod
fun deleteDownload(call: PluginCall) {
val filename = call.getString("filename") ?: return call.reject("filename fehlt")
val gme = File(downloadsDir, File(filename).name)
if (!gme.exists()) return call.reject("Datei nicht gefunden: $filename")
val base = gme.nameWithoutExtension
gme.delete()
downloadsDir.listFiles { f ->
f.nameWithoutExtension == base && !f.extension.equals("gme", ignoreCase = true)
}?.forEach { it.delete() }
call.resolve()
}
@PluginMethod
fun deletePenFile(call: PluginCall) {
val uriStr = call.getString("uri") ?: return call.reject("uri fehlt")
val filename = call.getString("filename") ?: return call.reject("filename fehlt")
val treeUri = Uri.parse(uriStr)
val docUri = findDocInTree(treeUri, filename)
?: return call.reject("Datei nicht auf Stift gefunden: $filename")
val deleted = DocumentsContract.deleteDocument(context.contentResolver, docUri)
if (deleted) call.resolve() else call.reject("Löschen fehlgeschlagen")
}
// ── Hilfsfunktionen ──────────────────────────────────────────────────────
private fun downloadToFile(url: String, dest: File) {
val conn = URL(url).openConnection() as HttpURLConnection
conn.connectTimeout = 15_000
conn.readTimeout = 60_000
conn.connect()
if (conn.responseCode != 200) throw IOException("HTTP ${conn.responseCode}")
conn.inputStream.use { input -> FileOutputStream(dest).use { input.copyTo(it) } }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">TipToi Sync</string>
<string name="title_activity_main">TipToi Sync</string>
<string name="package_name">com.tiptoisync.app</string>
<string name="custom_url_scheme">com.tiptoisync.app</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -0,0 +1,18 @@
package com.getcapacitor.myapp;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
Binary file not shown.