From e0b97aab890944ac6932a4cf8919472eedc22b27 Mon Sep 17 00:00:00 2001 From: Marcel <40896559+hackthedev@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:43:56 +0100 Subject: [PATCH 1/2] Working QR Code Scanning --- app/build.gradle.kts | 4 + app/src/main/AndroidManifest.xml | 17 ++ .../java/community/dcts/app/InboxFetcher.java | 3 + .../dcts/app/InboxFetcherService.java | 86 ++++++ .../java/community/dcts/app/MainActivity.java | 26 ++ .../java/community/dcts/app/QRScanner.java | 265 ++++++++++++++++++ gradle/libs.versions.toml | 7 + 7 files changed, 408 insertions(+) create mode 100644 app/src/main/java/community/dcts/app/InboxFetcherService.java create mode 100644 app/src/main/java/community/dcts/app/QRScanner.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e603692..07b7636 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,10 +37,14 @@ android { } dependencies { + implementation(libs.camera.camera2) implementation(libs.appcompat) implementation(libs.material) implementation(libs.activity) implementation(libs.constraintlayout) + implementation(libs.play.services.mlkit.barcode.scanning) + implementation(libs.camera.view) + implementation(libs.camera.lifecycle) testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4efdea0..5645efa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,9 +2,17 @@ + + + + + + + + + + + diff --git a/app/src/main/java/community/dcts/app/InboxFetcher.java b/app/src/main/java/community/dcts/app/InboxFetcher.java index eb245ce..59e6f87 100644 --- a/app/src/main/java/community/dcts/app/InboxFetcher.java +++ b/app/src/main/java/community/dcts/app/InboxFetcher.java @@ -123,6 +123,9 @@ private void fetchInbox(String host) { conn.setRequestProperty("Content-Type", "application/json"); conn.setDoOutput(true); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + JSONObject creds = getAccountCredentials(host); if (creds == null) return; diff --git a/app/src/main/java/community/dcts/app/InboxFetcherService.java b/app/src/main/java/community/dcts/app/InboxFetcherService.java new file mode 100644 index 0000000..36982db --- /dev/null +++ b/app/src/main/java/community/dcts/app/InboxFetcherService.java @@ -0,0 +1,86 @@ +package community.dcts.app; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import android.content.pm.ServiceInfo; + +public class InboxFetcherService extends Service { + + private static final String TAG = "InboxFetcherService"; + private static final String CHANNEL_ID = "fetcher_service_channel"; + private InboxFetcher inboxFetcher; + + @Override + public void onCreate() { + super.onCreate(); + Log.d(TAG, "onCreate"); + createServiceChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "onStartCommand"); + + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle("DCTS") + .setContentText("Running in background") + .setPriority(NotificationCompat.PRIORITY_MIN) + .setSilent(true) + .setOngoing(true) + .build(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground(9999, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); + } else { + startForeground(9999, notification); + } + + if (inboxFetcher == null) { + inboxFetcher = new InboxFetcher(this); + inboxFetcher.start(); + } + + // restart service if android kills it + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "onDestroy"); + if (inboxFetcher != null) { + inboxFetcher.stop(); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void createServiceChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "Background Service", + NotificationManager.IMPORTANCE_MIN + ); + channel.setShowBadge(false); + channel.setDescription("Keeps DCTS running for message notifications"); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/community/dcts/app/MainActivity.java b/app/src/main/java/community/dcts/app/MainActivity.java index e0408cc..2f8ea77 100644 --- a/app/src/main/java/community/dcts/app/MainActivity.java +++ b/app/src/main/java/community/dcts/app/MainActivity.java @@ -1,9 +1,13 @@ package community.dcts.app; import android.Manifest; +import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; import android.util.Log; import android.webkit.ConsoleMessage; import android.webkit.WebChromeClient; @@ -19,6 +23,8 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import org.json.JSONObject; + public class MainActivity extends AppCompatActivity { private WebView webView; @@ -41,6 +47,14 @@ protected void onCreate(Bundle savedInstanceState) { .apply(); */ + // ask for power shit because android is aids + PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + if (!pm.isIgnoringBatteryOptimizations(getPackageName())) { + Intent batteryIntent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + batteryIntent.setData(Uri.parse("package:" + getPackageName())); + startActivity(batteryIntent); + } + super.onCreate(savedInstanceState); @@ -88,6 +102,18 @@ public boolean onConsoleMessage(ConsoleMessage consoleMessage) { // for now until i make a proper app webView.loadUrl("https://chat.network-z.com/serverlist"); + + QRScanner.scan(this).thenAccept(result -> { + runOnUiThread(() -> { + if (result instanceof JSONObject) { + JSONObject json = (JSONObject) result; + Log.d("QRCODE", json.toString()); + } else { + String raw = (String) result; + Log.d("QRCODE", raw); + } + }); + }); } private void startFetcher() { diff --git a/app/src/main/java/community/dcts/app/QRScanner.java b/app/src/main/java/community/dcts/app/QRScanner.java new file mode 100644 index 0000000..51b0fe3 --- /dev/null +++ b/app/src/main/java/community/dcts/app/QRScanner.java @@ -0,0 +1,265 @@ +package community.dcts.app; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.os.Bundle; +import android.util.Size; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.ExperimentalGetImage; +import androidx.camera.core.ImageAnalysis; +import androidx.camera.core.ImageProxy; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.camera.view.PreviewView; +import androidx.activity.OnBackPressedCallback; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.mlkit.vision.barcode.BarcodeScanner; +import com.google.mlkit.vision.barcode.BarcodeScannerOptions; +import com.google.mlkit.vision.barcode.BarcodeScanning; +import com.google.mlkit.vision.barcode.common.Barcode; +import com.google.mlkit.vision.common.InputImage; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class QRScanner { + + private static CompletableFuture pendingResult; + + public static CompletableFuture scan(Activity activity) { + if (pendingResult != null && !pendingResult.isDone()) { + pendingResult.cancel(true); + } + pendingResult = new CompletableFuture<>(); + + Intent intent = new Intent(activity, ScanActivity.class); + activity.startActivity(intent); + + return pendingResult; + } + + static void deliverResult(Object result) { + if (pendingResult != null && !pendingResult.isDone()) { + pendingResult.complete(result); + } + } + + static void deliverError(Exception e) { + if (pendingResult != null && !pendingResult.isDone()) { + pendingResult.completeExceptionally(e); + } + } + + static void deliverCancel() { + if (pendingResult != null && !pendingResult.isDone()) { + pendingResult.cancel(true); + } + } + + static Object parse(String raw) { + try { + Object value = new JSONTokener(raw).nextValue(); + if (value instanceof JSONObject || value instanceof JSONArray) { + return value; + } + } catch (JSONException ignored) {} + return raw; + } + + public static class ScanActivity extends AppCompatActivity { + + private static final int PERMISSION_REQUEST = 1001; + + private PreviewView previewView; + private ExecutorService executor; + private BarcodeScanner barcodeScanner; + private ProcessCameraProvider cameraProvider; + private boolean found = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + executor = Executors.newSingleThreadExecutor(); + + BarcodeScannerOptions options = new BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build(); + barcodeScanner = BarcodeScanning.getClient(options); + + buildLayout(); + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + startCamera(); + } else { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST); + } + } + + private void buildLayout() { + FrameLayout root = new FrameLayout(this); + root.setBackgroundColor(Color.BLACK); + + previewView = new PreviewView(this); + root.addView(previewView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + TextView hint = new TextView(this); + hint.setText("QR Code scannen"); + hint.setTextColor(Color.WHITE); + hint.setTextSize(16); + hint.setShadowLayer(4, 0, 0, Color.BLACK); + FrameLayout.LayoutParams hintParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + hintParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + hintParams.bottomMargin = dpToPx(80); + root.addView(hint, hintParams); + + ImageButton closeBtn = new ImageButton(this); + closeBtn.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + closeBtn.setBackgroundColor(Color.TRANSPARENT); + closeBtn.setColorFilter(Color.WHITE); + closeBtn.setOnClickListener(v -> { + QRScanner.deliverCancel(); + finish(); + }); + FrameLayout.LayoutParams closeParams = new FrameLayout.LayoutParams( + dpToPx(48), dpToPx(48) + ); + closeParams.gravity = Gravity.TOP | Gravity.END; + closeParams.topMargin = dpToPx(16); + closeParams.rightMargin = dpToPx(16); + root.addView(closeBtn, closeParams); + + setContentView(root); + + getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + QRScanner.deliverCancel(); + finish(); + } + }); + } + + private void startCamera() { + ListenableFuture future = + ProcessCameraProvider.getInstance(this); + + future.addListener(() -> { + try { + cameraProvider = future.get(); + bindCamera(); + } catch (Exception e) { + QRScanner.deliverError(e); + finish(); + } + }, ContextCompat.getMainExecutor(this)); + } + + private void bindCamera() { + cameraProvider.unbindAll(); + + CameraSelector selector = new CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build(); + + Preview preview = new Preview.Builder().build(); + preview.setSurfaceProvider(previewView.getSurfaceProvider()); + + ImageAnalysis imageAnalysis = new ImageAnalysis.Builder() + .setTargetResolution(new Size(1280, 720)) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build(); + + imageAnalysis.setAnalyzer(executor, this::analyzeFrame); + + cameraProvider.bindToLifecycle(this, selector, preview, imageAnalysis); + } + + @OptIn(markerClass = ExperimentalGetImage.class) + private void analyzeFrame(@NonNull ImageProxy imageProxy) { + if (found || imageProxy.getImage() == null) { + imageProxy.close(); + return; + } + + InputImage inputImage = InputImage.fromMediaImage( + imageProxy.getImage(), + imageProxy.getImageInfo().getRotationDegrees() + ); + + barcodeScanner.process(inputImage) + .addOnSuccessListener(barcodes -> { + if (found) return; + for (Barcode barcode : barcodes) { + String raw = barcode.getRawValue(); + if (raw == null || raw.isEmpty()) continue; + + found = true; + QRScanner.deliverResult(QRScanner.parse(raw)); + finish(); + break; + } + }) + .addOnFailureListener(e -> {}) + .addOnCompleteListener(task -> imageProxy.close()); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_REQUEST) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startCamera(); + } else { + QRScanner.deliverError(new SecurityException("camera permission denied")); + finish(); + } + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (cameraProvider != null) cameraProvider.unbindAll(); + barcodeScanner.close(); + executor.shutdown(); + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1ada668..68c28a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,9 @@ appcompat = "1.6.1" material = "1.10.0" activity = "1.8.0" constraintlayout = "2.1.4" +playServicesMlkitBarcodeScanning = "18.3.1" +cameraView = "1.5.3" +cameraLifecycle = "1.5.3" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -16,6 +19,10 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a material = { group = "com.google.android.material", name = "material", version.ref = "material" } activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +play-services-mlkit-barcode-scanning = { group = "com.google.android.gms", name = "play-services-mlkit-barcode-scanning", version.ref = "playServicesMlkitBarcodeScanning" } +camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraView" } +camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycle" } +camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraLifecycle" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 4ab7588d6ddd92124ce07174027821e69646db66 Mon Sep 17 00:00:00 2001 From: Marcel <40896559+hackthedev@users.noreply.github.com> Date: Sat, 14 Mar 2026 06:26:02 +0100 Subject: [PATCH 2/2] Login Wip working? --- .../java/community/dcts/app/Accounts.java | 351 ++++++++++++++++++ .../java/community/dcts/app/JSBridge.java | 80 +++- .../java/community/dcts/app/MainActivity.java | 57 ++- 3 files changed, 481 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/community/dcts/app/Accounts.java diff --git a/app/src/main/java/community/dcts/app/Accounts.java b/app/src/main/java/community/dcts/app/Accounts.java new file mode 100644 index 0000000..d58d257 --- /dev/null +++ b/app/src/main/java/community/dcts/app/Accounts.java @@ -0,0 +1,351 @@ +package community.dcts.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Accounts { + + private static final String PREFS = "dcts_accounts"; + private final SharedPreferences prefs; + private final Context context; + + public interface OnPick { void onAccount(JSONObject account); } + public interface OnScan { void onScanRequested(); } + + public Accounts(Context context) { + this.context = context; + this.prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } + + private String norm(String url) { + if (url == null) return ""; + url = url.toLowerCase().trim(); + url = url.replaceFirst("^https?://", ""); + while (url.endsWith("/")) url = url.substring(0, url.length() - 1); + return url; + } + + private JSONObject loadAll() { + try { + return new JSONObject(prefs.getString("data", "{}")); + } catch (Exception e) { + return new JSONObject(); + } + } + + private JSONArray load(String url) { + JSONArray arr = loadAll().optJSONArray(url); + return arr != null ? arr : new JSONArray(); + } + + private void write(String url, JSONArray list) { + try { + JSONObject all = loadAll(); + + if (list.length() == 0) all.remove(url); + else all.put(url, list); + + prefs.edit().putString("data", all.toString()).apply(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private int findIndex(JSONArray list, String id) { + for (int i = 0; i < list.length(); i++) { + try { + if (id.equals(list.getJSONObject(i).optString("id"))) return i; + } catch (Exception ignored) {} + } + return -1; + } + + public void save(String url, JSONObject account) { + url = norm(url); + + try { + JSONArray list = load(url); + String id = account.getString("id"); + int idx = findIndex(list, id); + + if (idx >= 0) list.put(idx, account); + else list.put(account); + + write(url, list); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void delete(String url, String id) { + url = norm(url); + JSONArray list = load(url); + JSONArray filtered = new JSONArray(); + + for (int i = 0; i < list.length(); i++) { + try { + if (!id.equals(list.getJSONObject(i).optString("id"))) + filtered.put(list.getJSONObject(i)); + } catch (Exception ignored) {} + } + + write(url, filtered); + } + + public JSONObject get(String url, String id) { + url = norm(url); + JSONArray list = load(url); + int idx = findIndex(list, id); + + if (idx >= 0) { + try { return list.getJSONObject(idx); } + catch (Exception ignored) {} + } + + return null; + } + + public List getAll(String url) { + url = norm(url); + JSONArray list = load(url); + List result = new ArrayList<>(); + + for (int i = 0; i < list.length(); i++) { + try { result.add(list.getJSONObject(i)); } + catch (Exception ignored) {} + } + + return result; + } + + private List collectAllAccounts(List origins) { + List items = new ArrayList<>(); + List seenIds = new ArrayList<>(); + + try { + JSONObject all = loadAll(); + Iterator keys = all.keys(); + + while (keys.hasNext()) { + String url = keys.next(); + JSONArray list = all.getJSONArray(url); + + for (int i = 0; i < list.length(); i++) { + JSONObject acc = list.getJSONObject(i); + String accId = acc.optString("id"); + + if (seenIds.contains(accId)) continue; + + seenIds.add(accId); + items.add(acc); + origins.add(url); + } + } + } catch (Exception ignored) {} + + return items; + } + + private View buildAvatar(String icon, String name) { + GradientDrawable circle = new GradientDrawable(); + circle.setShape(GradientDrawable.OVAL); + circle.setColor(Color.parseColor("#000000")); + circle.setSize(96, 96); + + if (icon != null && !icon.isEmpty() && icon.startsWith("https")) { + ImageView img = new ImageView(context); + img.setScaleType(ImageView.ScaleType.CENTER_CROP); + img.setLayoutParams(new LinearLayout.LayoutParams(96, 96)); + img.setBackground(circle); + img.setClipToOutline(true); + + new Thread(() -> { + try { + java.io.InputStream in = new java.net.URL(icon).openStream(); + android.graphics.Bitmap bmp = android.graphics.BitmapFactory.decodeStream(in); + ((Activity) context).runOnUiThread(() -> img.setImageBitmap(bmp)); + } catch (Exception ignored) {} + }).start(); + + return img; + } + + TextView letter = new TextView(context); + letter.setBackground(circle); + letter.setText(name.substring(0, 1).toUpperCase()); + letter.setTextColor(Color.WHITE); + letter.setTextSize(18); + letter.setGravity(Gravity.CENTER); + letter.setWidth(96); + letter.setHeight(96); + + return letter; + } + + private LinearLayout buildInfoColumn(String name, String status) { + LinearLayout info = new LinearLayout(context); + info.setOrientation(LinearLayout.VERTICAL); + info.setPadding(24, 0, 0, 0); + + TextView nameView = new TextView(context); + nameView.setText(name); + nameView.setTextSize(16); + nameView.setTypeface(null, Typeface.BOLD); + info.addView(nameView); + + if (status != null && !status.isEmpty()) { + TextView statusView = new TextView(context); + statusView.setText(status); + statusView.setTextSize(13); + statusView.setAlpha(0.6f); + info.addView(statusView); + } + + return info; + } + + private TextView buildDeleteButton(String origin, String id, String name, + String currentUrl, AlertDialog[] dialogRef, + OnPick onPick, OnScan onScan) { + + TextView btn = new TextView(context); + btn.setText("\u2715"); + btn.setTextSize(18); + btn.setTextColor(Color.parseColor("#FF4444")); + btn.setPadding(24, 0, 0, 0); + + btn.setOnClickListener(v -> { + new AlertDialog.Builder(context) + .setMessage("Remove " + name + "?") + .setPositiveButton("Remove", (d, w) -> { + delete(origin, id); + if (dialogRef[0] != null) dialogRef[0].dismiss(); + pick(currentUrl, onPick, onScan); + }) + .setNegativeButton("Cancel", null) + .show(); + }); + + return btn; + } + + private LinearLayout buildScanButton() { + LinearLayout scanBtn = new LinearLayout(context); + scanBtn.setOrientation(LinearLayout.HORIZONTAL); + scanBtn.setGravity(Gravity.CENTER_VERTICAL); + scanBtn.setPadding(48, 24, 48, 24); + + TextView icon = new TextView(context); + icon.setText("\uD83D\uDCF7"); + icon.setTextSize(20); + scanBtn.addView(icon); + + TextView label = new TextView(context); + label.setText("Scan QR Code"); + label.setTextSize(16); + label.setPadding(24, 0, 0, 0); + scanBtn.addView(label); + + return scanBtn; + } + + private LinearLayout buildAccountRow(JSONObject account, String origin, String currentUrl, + AlertDialog[] dialogRef, OnPick onPick, OnScan onScan) { + + String id = account.optString("id"); + String icon = account.optString("icon"); + String name = account.optString("name", account.optString("loginName", "?")); + String status = account.optString("status", ""); + boolean foreign = !origin.equals(currentUrl); + + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setPadding(48, 28, 48, 28); + + if (foreign) row.setAlpha(0.6f); + + row.addView(buildAvatar(icon, name)); + + String displayName = name; + LinearLayout info = buildInfoColumn(displayName, status); + row.addView(info, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); + + row.addView(buildDeleteButton(origin, id, name, currentUrl, dialogRef, onPick, onScan)); + + row.setOnClickListener(v -> { + if (dialogRef[0] != null) dialogRef[0].dismiss(); + + try { + JSONObject safe = new JSONObject(account.toString()); + if (foreign) safe.remove("token"); + onPick.onAccount(safe); + } catch (Exception ignored) {} + }); + + return row; + } + + public void pick(String currentUrl, OnPick onPick, OnScan onScan) { + currentUrl = norm(currentUrl); + String finalUrl = currentUrl; + + List origins = new ArrayList<>(); + List items = collectAllAccounts(origins); + + LinearLayout root = new LinearLayout(context); + root.setOrientation(LinearLayout.VERTICAL); + root.setPadding(0, 24, 0, 0); + + LinearLayout scanBtn = buildScanButton(); + root.addView(scanBtn); + + View divider = new View(context); + divider.setBackgroundColor(Color.parseColor("#33888888")); + root.addView(divider, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 2)); + + AlertDialog[] dialogRef = new AlertDialog[1]; + + for (int i = 0; i < items.size(); i++) { + root.addView(buildAccountRow(items.get(i), origins.get(i), finalUrl, dialogRef, onPick, onScan)); + } + + if (items.isEmpty()) { + TextView empty = new TextView(context); + empty.setText("No accounts yet"); + empty.setGravity(Gravity.CENTER); + empty.setPadding(48, 64, 48, 64); + empty.setAlpha(0.5f); + root.addView(empty); + } + + scanBtn.setOnClickListener(v -> { + if (dialogRef[0] != null) dialogRef[0].dismiss(); + onScan.onScanRequested(); + }); + + dialogRef[0] = new AlertDialog.Builder(context) + .setTitle("Pick Account") + .setView(root) + .setNegativeButton("Cancel", null) + .show(); + } +} \ No newline at end of file diff --git a/app/src/main/java/community/dcts/app/JSBridge.java b/app/src/main/java/community/dcts/app/JSBridge.java index 5bb24e6..4e56752 100644 --- a/app/src/main/java/community/dcts/app/JSBridge.java +++ b/app/src/main/java/community/dcts/app/JSBridge.java @@ -1,6 +1,8 @@ package community.dcts.app; +import android.app.Activity; import android.content.Context; +import android.util.Log; import android.webkit.JavascriptInterface; import android.webkit.WebView; @@ -9,10 +11,17 @@ public class JSBridge { private final WebView webView; private final dSyncSign signer; + private final Activity activity; + private volatile String currentUrl = ""; - public JSBridge(WebView webView, Context context) { + public void updateUrl(String url) { + this.currentUrl = url; + } + + public JSBridge(WebView webView, Activity activity) { this.webView = webView; - this.signer = new dSyncSign(context); + this.activity = activity; + this.signer = new dSyncSign(activity); } @JavascriptInterface @@ -25,8 +34,75 @@ public void notify(String text) { android.util.Log.d("WebClient", text); } + @JavascriptInterface + public String saveAccount(String json) { + try { + JSONObject account = new JSONObject(json); + Accounts accounts = new Accounts(webView.getContext()); + accounts.save(currentUrl, account); + return "ok"; + } catch (Exception e) { + Log.e("WEBVIEW_JS", "saveAccount failed", e); + return null; + } + } + + @JavascriptInterface + public void pickAccount() { + activity.runOnUiThread(() -> { + Accounts accounts = new Accounts(activity); + accounts.pick(currentUrl, account -> { + + // build some strings lol. kinda wacky ngl + String js = "CookieManager.setCookie('token', " + escapeJs(account.optString("token")) + ");"; + js += "CookieManager.setCookie('id', " + escapeJs(account.optString("id")) + ");"; + js += "CookieManager.setCookie('username', " + escapeJs(account.optString("name")) + ");"; + + // some more wacky shit lol + String pow = account.optString("pow", ""); + String[] parts = pow.split("-", 2); + String challenge = parts.length > 0 ? parts[0] : ""; + String solution = parts.length > 1 ? parts[1] : ""; + + js += "CookieManager.setCookie('pow_challenge', " + escapeJs(challenge) + ");"; + js += "CookieManager.setCookie('pow_solution', " + escapeJs(solution) + ");"; + + js += "location.reload();"; + + // idk why, ide suggested it + String finalJs = js; + + webView.post(() -> webView.evaluateJavascript(finalJs, null)); + }, this::scanAccountCode); + }); + } + + private String escapeJs(String s) { + return "'" + s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n") + "'"; + } + + @JavascriptInterface + public void scanAccountCode() { + try { + Accounts accounts = new Accounts(activity); + QRScanner.scan(activity).thenAccept(result -> { + activity.runOnUiThread(() -> { + if (result instanceof JSONObject) { + JSONObject account = (JSONObject) result; + Log.d("QRCODE", account.toString()); + accounts.save(currentUrl, account); + } + }); + }); + } catch (Exception e) { + Log.e("WEBVIEW_JS", "scanAccountCode failed", e); + } + } + @JavascriptInterface public String setAccountCredentials(String identifier, String id, String token) { + // should clarify. + // this is used for the message inbox fetching only so notifications work. try { android.content.SharedPreferences prefs = webView.getContext() .getSharedPreferences("dcts_accounts", Context.MODE_PRIVATE); diff --git a/app/src/main/java/community/dcts/app/MainActivity.java b/app/src/main/java/community/dcts/app/MainActivity.java index 2f8ea77..4c724b4 100644 --- a/app/src/main/java/community/dcts/app/MainActivity.java +++ b/app/src/main/java/community/dcts/app/MainActivity.java @@ -38,6 +38,7 @@ protected void onDestroy() { } } + @Override protected void onCreate(Bundle savedInstanceState) { /* @@ -80,7 +81,17 @@ protected void onCreate(Bundle savedInstanceState) { webSettings.setJavaScriptEnabled(true); webSettings.setMediaPlaybackRequiresUserGesture(false); - webView.setWebViewClient(new WebViewClient()); + // for context bla bla + JSBridge bridge = new JSBridge(webView, this); + webView.addJavascriptInterface(bridge, "dcts"); + + webView.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + bridge.updateUrl(url); + } + }); // error logging for debugging lol webView.setWebChromeClient(new WebChromeClient() { @@ -96,24 +107,60 @@ public boolean onConsoleMessage(ConsoleMessage consoleMessage) { } }); - - // key feature - webView.addJavascriptInterface(new JSBridge(webView, this), "dcts"); - // for now until i make a proper app webView.loadUrl("https://chat.network-z.com/serverlist"); + /* QRScanner.scan(this).thenAccept(result -> { runOnUiThread(() -> { if (result instanceof JSONObject) { JSONObject json = (JSONObject) result; + Log.d("QRCODE", "From JSON"); Log.d("QRCODE", json.toString()); } else { String raw = (String) result; + Log.d("QRCODE", "From String"); Log.d("QRCODE", raw); } }); }); + */ + + + /* + Accounts accounts = new Accounts(this); + accounts.pick("chat.network-z.com", + account -> { + // user hat account gewählt + String name = account.optString("name"); + String id = account.optString("id"); + String token = account.optString("token"); + String pow = account.optString("pow"); + + Log.d("QRCODE", "Picked account"); + Log.d("QRCODE", name); + Log.d("QRCODE", id); + Log.d("QRCODE", token); + Log.d("QRCODE", pow); + }, + () -> { + // user will qr scannen + QRScanner.scan(this).thenAccept(result -> { + runOnUiThread(() -> { + if (result instanceof JSONObject) { + JSONObject json = (JSONObject) result; + Log.d("QRCODE", "From JSON"); + Log.d("QRCODE", json.toString()); + + accounts.save("chat.network-z.com", json); + } + }); + }); + } + ); + + */ + } private void startFetcher() {