Initial commit

This commit is contained in:
Eddy de Vink 2026-09-21 22:54:02 +02:00
commit 9fc4e462d0
8 changed files with 278 additions and 0 deletions

118
.gitignore vendored Normal file
View file

@ -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

30
AndroidManifest.xml Normal file
View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="nl.eddy.tailscalehalauncher"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="26" android:targetSdkVersion="34" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
<application
android:label="HA starten"
android:icon="@mipmap/ic_launcher"
android:allowBackup="false">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -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);
}
}