Add Android app: phone UI for hosts, SSH terminal with extra key row, keys, snippets, LAN sync and app lock (password + fingerprint), reusing the desktop Node modules via capacitor-nodejs; APKs per ABI in releases for Obtainium
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -0,0 +1,2 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
@@ -0,0 +1,82 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace = "de.mrterm.app"
|
||||
compileSdk = rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "de.mrterm.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
// Version kommt aus der package.json des Desktop-Projekts (build.js setzt -PmrtermVersion)
|
||||
def v = (project.findProperty('mrtermVersion') ?: '0.0.1').toString()
|
||||
def p = v.tokenize('.-')
|
||||
versionCode (p[0].toInteger() * 10000 + p[1].toInteger() * 100 + p[2].toInteger())
|
||||
versionName v
|
||||
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:!*~'
|
||||
}
|
||||
}
|
||||
// Signatur: keystore.properties (nicht im Repo) mit storeFile, storePassword, keyAlias, keyPassword
|
||||
def ksFile = rootProject.file('keystore.properties')
|
||||
signingConfigs {
|
||||
release {
|
||||
if (ksFile.exists()) {
|
||||
def ks = new Properties()
|
||||
ksFile.withInputStream { ks.load(it) }
|
||||
storeFile rootProject.file(ks['storeFile'])
|
||||
storePassword ks['storePassword']
|
||||
keyAlias ks['keyAlias']
|
||||
keyPassword ks['keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
// Eine APK je Prozessorarchitektur (libnode ist groß); Obtainium wählt die passende automatisch
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'arm64-v8a', 'armeabi-v7a'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
if (ksFile.exists()) 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')
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
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")
|
||||
}
|
||||
@@ -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 {
|
||||
implementation project(':capacitor-nodejs')
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
@@ -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,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
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.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.mrterm.app;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
registerPlugin(MrTermNative.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
WebView web = getBridge().getWebView();
|
||||
|
||||
// Zurück-Taste an die Oberfläche geben (schließt Dialoge, verlässt das Terminal …)
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
web.evaluateJavascript("window.mrtermBack && window.mrtermBack()", null);
|
||||
}
|
||||
});
|
||||
|
||||
// Edge-to-Edge: Bei offener Tastatur den WebView um die Tastaturhöhe verkleinern,
|
||||
// damit Terminal und Zusatztasten sichtbar bleiben
|
||||
View parent = (View) web.getParent();
|
||||
ViewCompat.setOnApplyWindowInsetsListener(parent, (v, insets) -> {
|
||||
int ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
|
||||
v.setPadding(0, 0, 0, ime);
|
||||
web.evaluateJavascript("window.mrtermIme && window.mrtermIme(" + (ime > 0) + ")", null);
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package de.mrterm.app;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
/**
|
||||
* Plattformfunktionen für MrTerm:
|
||||
* - Tresorschlüssel: zufällige 32 Byte, verpackt mit einem nicht exportierbaren AES-Schlüssel im Android Keystore
|
||||
* - Fingerabdruck: Geheimnis, das nur nach erfolgreicher biometrischer Bestätigung entschlüsselt werden kann
|
||||
* - Multicast-Lock (sonst verwirft WLAN die UDP-Broadcasts der LAN-Synchronisation), Zwischenablage, Gerätename
|
||||
*/
|
||||
@CapacitorPlugin(name = "MrTermNative")
|
||||
public class MrTermNative extends Plugin {
|
||||
private static final String KS = "AndroidKeyStore";
|
||||
private static final String VAULT_ALIAS = "mrterm_vault";
|
||||
private static final String BIO_ALIAS = "mrterm_bio";
|
||||
private WifiManager.MulticastLock multicastLock;
|
||||
|
||||
private SharedPreferences prefs() {
|
||||
return getContext().getSharedPreferences("mrterm", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static String b64(byte[] b) { return Base64.encodeToString(b, Base64.NO_WRAP); }
|
||||
private static byte[] unb64(String s) { return Base64.decode(s, Base64.NO_WRAP); }
|
||||
|
||||
private SecretKey keystoreKey(String alias, boolean userAuth) throws Exception {
|
||||
KeyStore ks = KeyStore.getInstance(KS);
|
||||
ks.load(null);
|
||||
if (ks.containsAlias(alias)) return (SecretKey) ks.getKey(alias, null);
|
||||
KeyGenerator kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KS);
|
||||
KeyGenParameterSpec.Builder spec = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setKeySize(256);
|
||||
if (userAuth) {
|
||||
spec.setUserAuthenticationRequired(true).setInvalidatedByBiometricEnrollment(true);
|
||||
if (Build.VERSION.SDK_INT >= 30) spec.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG);
|
||||
}
|
||||
kg.init(spec.build());
|
||||
return kg.generateKey();
|
||||
}
|
||||
|
||||
private static void deleteKey(String alias) {
|
||||
try { KeyStore ks = KeyStore.getInstance(KS); ks.load(null); ks.deleteEntry(alias); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ---------- Tresorschlüssel ----------
|
||||
@PluginMethod
|
||||
public void vaultKey(PluginCall call) {
|
||||
try {
|
||||
SecretKey k = keystoreKey(VAULT_ALIAS, false);
|
||||
String stored = prefs().getString("vaultKey", null);
|
||||
byte[] plain;
|
||||
if (stored == null) {
|
||||
plain = new byte[32];
|
||||
new SecureRandom().nextBytes(plain);
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.ENCRYPT_MODE, k);
|
||||
prefs().edit().putString("vaultKey", b64(c.getIV()) + ":" + b64(c.doFinal(plain))).apply();
|
||||
} else {
|
||||
String[] p = stored.split(":");
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.DECRYPT_MODE, k, new GCMParameterSpec(128, unb64(p[0])));
|
||||
plain = c.doFinal(unb64(p[1]));
|
||||
}
|
||||
JSObject r = new JSObject();
|
||||
r.put("key", b64(plain));
|
||||
call.resolve(r);
|
||||
} catch (Exception e) {
|
||||
call.reject("Keystore: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Fingerabdruck ----------
|
||||
@PluginMethod
|
||||
public void bioAvailable(PluginCall call) {
|
||||
int s = BiometricManager.from(getContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG);
|
||||
JSObject r = new JSObject();
|
||||
r.put("available", s == BiometricManager.BIOMETRIC_SUCCESS);
|
||||
r.put("enrolled", prefs().getString("bio", null) != null);
|
||||
call.resolve(r);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void bioEnroll(PluginCall call) {
|
||||
try {
|
||||
deleteKey(BIO_ALIAS);
|
||||
SecretKey k = keystoreKey(BIO_ALIAS, true);
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.ENCRYPT_MODE, k);
|
||||
prompt(call, c, call.getString("title", "MrTerm"), call.getString("cancel", "Cancel"), (cipher) -> {
|
||||
byte[] secret = new byte[32];
|
||||
new SecureRandom().nextBytes(secret);
|
||||
byte[] ct = cipher.doFinal(secret);
|
||||
prefs().edit().putString("bio", b64(cipher.getIV()) + ":" + b64(ct)).apply();
|
||||
JSObject r = new JSObject();
|
||||
r.put("secret", b64(secret));
|
||||
call.resolve(r);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
call.reject(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void bioUnlock(PluginCall call) {
|
||||
String stored = prefs().getString("bio", null);
|
||||
if (stored == null) { call.reject("not-enrolled"); return; }
|
||||
try {
|
||||
String[] p = stored.split(":");
|
||||
SecretKey k = keystoreKey(BIO_ALIAS, true);
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.DECRYPT_MODE, k, new GCMParameterSpec(128, unb64(p[0])));
|
||||
prompt(call, c, call.getString("title", "MrTerm"), call.getString("cancel", "Cancel"), (cipher) -> {
|
||||
JSObject r = new JSObject();
|
||||
r.put("secret", b64(cipher.doFinal(unb64(p[1]))));
|
||||
call.resolve(r);
|
||||
});
|
||||
} catch (KeyPermanentlyInvalidatedException e) {
|
||||
// Neuer Fingerabdruck registriert → Schlüssel ungültig, Nutzer muss neu einrichten
|
||||
prefs().edit().remove("bio").apply();
|
||||
deleteKey(BIO_ALIAS);
|
||||
call.reject("invalidated");
|
||||
} catch (Exception e) {
|
||||
call.reject(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void bioDisable(PluginCall call) {
|
||||
prefs().edit().remove("bio").apply();
|
||||
deleteKey(BIO_ALIAS);
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
private interface CipherUse { void run(Cipher c) throws Exception; }
|
||||
|
||||
private void prompt(PluginCall call, Cipher cipher, String title, String cancel, CipherUse use) {
|
||||
FragmentActivity act = getActivity();
|
||||
act.runOnUiThread(() -> {
|
||||
BiometricPrompt bp = new BiometricPrompt(act, ContextCompat.getMainExecutor(act), new BiometricPrompt.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
|
||||
try { use.run(result.getCryptoObject().getCipher()); }
|
||||
catch (Exception e) { call.reject(e.getMessage()); }
|
||||
}
|
||||
@Override
|
||||
public void onAuthenticationError(int code, CharSequence msg) {
|
||||
call.reject(code == BiometricPrompt.ERROR_USER_CANCELED || code == BiometricPrompt.ERROR_NEGATIVE_BUTTON ? "cancelled" : msg.toString());
|
||||
}
|
||||
});
|
||||
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(title)
|
||||
.setNegativeButtonText(cancel)
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.build();
|
||||
bp.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Sonstiges ----------
|
||||
@PluginMethod
|
||||
public void info(PluginCall call) {
|
||||
String name = null;
|
||||
try { name = Settings.Global.getString(getContext().getContentResolver(), "device_name"); } catch (Exception ignored) {}
|
||||
if (name == null || name.isEmpty()) name = Build.MANUFACTURER + " " + Build.MODEL;
|
||||
JSObject r = new JSObject();
|
||||
r.put("device", name);
|
||||
r.put("locale", Locale.getDefault().toLanguageTag());
|
||||
try { r.put("version", getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0).versionName); }
|
||||
catch (Exception e) { r.put("version", "0.0.0"); }
|
||||
call.resolve(r);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void multicast(PluginCall call) {
|
||||
boolean on = Boolean.TRUE.equals(call.getBoolean("on", true));
|
||||
WifiManager wm = (WifiManager) getContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
if (on && multicastLock == null && wm != null) {
|
||||
multicastLock = wm.createMulticastLock("mrterm-sync");
|
||||
multicastLock.setReferenceCounted(false);
|
||||
multicastLock.acquire();
|
||||
} else if (!on && multicastLock != null) {
|
||||
multicastLock.release();
|
||||
multicastLock = null;
|
||||
}
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void background(PluginCall call) {
|
||||
getActivity().runOnUiThread(() -> getActivity().moveTaskToBack(true));
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void copy(PluginCall call) {
|
||||
ClipboardManager cm = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText("MrTerm", call.getString("text", "")));
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void paste(PluginCall call) {
|
||||
ClipboardManager cm = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
String text = "";
|
||||
if (cm.hasPrimaryClip() && cm.getPrimaryClip().getItemCount() > 0) {
|
||||
CharSequence t = cm.getPrimaryClip().getItemAt(0).coerceToText(getContext());
|
||||
if (t != null) text = t.toString();
|
||||
}
|
||||
JSObject r = new JSObject();
|
||||
r.put("text", text);
|
||||
call.resolve(r);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 798 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 794 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>
|
||||
|
After Width: | Height: | Size: 58 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: 8.6 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#111728</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">MrTerm</string>
|
||||
<string name="title_activity_main">MrTerm</string>
|
||||
<string name="package_name">de.mrterm.app</string>
|
||||
<string name="custom_url_scheme">de.mrterm.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>
|
||||