Flatten repo: move client/ contents to repo root
The project is no longer a monorepo/workspace - there is only a single
app package. Remove the redundant client/ nesting: all source, the
Capacitor Android project, config files and package.json/tsconfig now
live directly at the repo root.
- Merge client/package.json into root package.json (drop the
workspaces field and the now-unnecessary bun --cwd client wrappers).
- Merge client/bun.lock (the actual dependency lockfile) into the
root, replacing the stale workspace-only root lockfile.
- Move src/, public/, android/, index.html, vite.config.ts,
capacitor.config.ts, tsconfig.{app,node}.json to the repo root.
- Update Dockerfile.android to build from the repo root instead of
cd'ing into client/.
- Update .github/workflows/ci.yml type-check step to run bun run build
directly instead of bun --cwd client run build.
- Update README.md and .github/copilot-instructions.md to drop
references to the client/ subdirectory.
- Drop the stale server/downloads/ entry from .gitignore (leftover
from an old server-based architecture that no longer exists).
@@ -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,29 @@
|
||||
package com.tiptoisync.app
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.getcapacitor.BridgeActivity
|
||||
import com.getcapacitor.CapConfig
|
||||
|
||||
class MainActivity : BridgeActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Dev-URL aus SharedPreferences lesen, bevor die Bridge initialisiert wird.
|
||||
// "CapacitorStorage" ist der von @capacitor/preferences genutzte SharedPreferences-Name.
|
||||
val devUrl = getSharedPreferences("CapacitorStorage", MODE_PRIVATE)
|
||||
.getString("devServerUrl", null)
|
||||
|
||||
if (devUrl != null) {
|
||||
// CapConfig mit der Dev-Server-URL überschreiben – die Bridge lädt dann
|
||||
// direkt vom Vite Dev-Server und injiziert die Bridge trotzdem korrekt.
|
||||
config = CapConfig.Builder(this)
|
||||
.setServerUrl(devUrl)
|
||||
.setAllowMixedContent(true)
|
||||
.create()
|
||||
}
|
||||
|
||||
registerPlugin(TiptoiPlugin::class.java)
|
||||
registerPlugin(DevPlugin::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) } }
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.2 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>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#12181f</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);
|
||||
}
|
||||
}
|
||||