commit 9fc4e462d02eb89bdfc1395bf91df752d629b805 Author: Eddy de Vink Date: Mon Sep 21 22:54:02 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0a8a87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +.yolo.json +credentials.json +verzendlijst.md + +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build output +dist/ +build/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Test files +test/fixtures/ +test/sample-pdfs/ +test/output/ + +# Temporary files +temp/ +tmp/ +*.tmp% + +# Android build output (tailscale-ha-launcher) +out/ +classes/ +dex/ +*.keystore diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..d2a7513 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/mipmap-hdpi/ic_launcher.png b/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..4b7a5df Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher.png differ diff --git a/res/mipmap-mdpi/ic_launcher.png b/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..87a8326 Binary files /dev/null and b/res/mipmap-mdpi/ic_launcher.png differ diff --git a/res/mipmap-xhdpi/ic_launcher.png b/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..9cf7194 Binary files /dev/null and b/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/res/mipmap-xxhdpi/ic_launcher.png b/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..cff7ac8 Binary files /dev/null and b/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/res/mipmap-xxxhdpi/ic_launcher.png b/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..9891cee Binary files /dev/null and b/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/nl/eddy/tailscalehalauncher/MainActivity.java b/src/nl/eddy/tailscalehalauncher/MainActivity.java new file mode 100644 index 0000000..9c51cd1 --- /dev/null +++ b/src/nl/eddy/tailscalehalauncher/MainActivity.java @@ -0,0 +1,130 @@ +package nl.eddy.tailscalehalauncher; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.Toast; + +public class MainActivity extends Activity { + + private static final String TAILSCALE_PKG = "com.tailscale.ipn"; + private static final String TAILSCALE_RECEIVER = TAILSCALE_PKG + ".IPNReceiver"; + private static final String CONNECT_ACTION = "com.tailscale.ipn.CONNECT_VPN"; + private static final String DISCONNECT_ACTION = "com.tailscale.ipn.DISCONNECT_VPN"; + private static final String HA_PKG = "io.homeassistant.companion.android"; + private static final long WAIT_FOR_VPN_MS = 3000; + + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Button connectButton = new Button(this); + connectButton.setText("VPN aan + HA openen"); + connectButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onConnectClicked(); + } + }); + + Button disconnectButton = new Button(this); + disconnectButton.setText("VPN uit"); + disconnectButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onDisconnectClicked(); + } + }); + + int density = (int) getResources().getDisplayMetrics().density; + int sidePad = 24 * density; + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setGravity(Gravity.CENTER_HORIZONTAL); + layout.setPadding(sidePad, 0, sidePad, 0); + + // Connect knop bovenaan (met wat marge onder de statusbalk) + LinearLayout.LayoutParams connectLp = + new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + connectLp.setMargins(0, 90 * density, 0, 0); + layout.addView(connectButton, connectLp); + + // Flexibele ruimte: duwt de onderste knop tot onderaan + View spacer = new View(this); + layout.addView(spacer, new LinearLayout.LayoutParams(0, 0, 1f)); + + // Disconnect knop onderaan + LinearLayout.LayoutParams disconnectLp = + new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + disconnectLp.setMargins(0, 0, 0, 90 * density); + layout.addView(disconnectButton, disconnectLp); + + setContentView(layout); + } + + private void onConnectClicked() { + if (!sendTailscaleCommand(CONNECT_ACTION)) { + return; + } + Toast.makeText(this, "Tailscale wordt aangezet…", Toast.LENGTH_SHORT).show(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + launchHomeAssistant(); + } + }, WAIT_FOR_VPN_MS); + } + + private void onDisconnectClicked() { + sendTailscaleCommand(DISCONNECT_ACTION); + Toast.makeText(this, "Tailscale uitgezet", Toast.LENGTH_SHORT).show(); + // korte delay zodat de broadcast verwerkt is, dan app afsluiten + handler.postDelayed(new Runnable() { + @Override + public void run() { + finish(); + } + }, 400); + } + + /** Returns false when Tailscale does not have a broadcast receiver. */ + private boolean sendTailscaleCommand(String action) { + Intent intent = new Intent(action); + intent.setComponent(new ComponentName(TAILSCALE_PKG, TAILSCALE_RECEIVER)); + try { + sendBroadcast(intent); + } catch (SecurityException e) { + Toast.makeText(this, "Kon Tailscale-commando niet sturen: " + e.getMessage(), + Toast.LENGTH_LONG).show(); + return false; + } + return true; + } + + private void launchHomeAssistant() { + PackageManager pm = getPackageManager(); + Intent launchIntent = pm.getLaunchIntentForPackage(HA_PKG); + if (launchIntent == null) { + Toast.makeText(this, "Home Assistant app is niet geïnstalleerd", Toast.LENGTH_LONG).show(); + return; + } + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(launchIntent); + } +} \ No newline at end of file