/* * mygram-droid: Android TV client for Foldergram. * Copyright (C) 2026 Danilo M. * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ package mygram.droid; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.text.InputType; import android.view.View; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import java.util.LinkedHashMap; import java.util.Map; /** Instance picker: LAN scan, manual URL, then the password when the instance asks for one. */ public final class SetupActivity extends Activity { private LinearLayout box; @Override protected void onCreate(Bundle state) { super.onCreate(state); box = Ui.column(this); setContentView(box); scan(); } private void scan() { reset("Searching the network for Foldergram…"); Api.async(() -> { Map found = new LinkedHashMap<>(); String self = Discovery.localIpv4(this); if (self != null) for (String base : Discovery.scan(self)) found.put(base, Discovery.label(base)); return found; }, this::found, e -> { reset("Scan failed: " + e.getMessage()); actions(); }); } private void found(Map instances) { reset(instances.isEmpty() ? "No Foldergram found on port " + Discovery.PORT : "Choose an instance"); for (Map.Entry e : instances.entrySet()) { TextView t = Ui.text(this, e.getValue(), 20); int p = Ui.dp(this, 12); t.setPadding(p * 2, p, p * 2, p); Ui.focusable(t); t.setOnClickListener(v -> choose(e.getKey())); box.addView(t, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); // full width the zoom clips the text } actions(); box.getChildAt(1).requestFocus(); } private void actions() { LinearLayout row = Ui.row(this); row.addView(Ui.iconButton(this, R.drawable.ic_refresh, "Rescan", v -> scan())); row.addView(Ui.iconButton(this, R.drawable.ic_edit, "Enter URL", v -> manual())); box.addView(row); row.getChildAt(0).requestFocus(); } private void manual() { reset("Instance URL"); EditText url = Ui.field(this, "http://host:" + Discovery.PORT, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); box.addView(url); box.addView(Ui.iconButton(this, R.drawable.ic_check, "Connect", v -> { String s = url.getText().toString().trim(); if (!s.isEmpty()) choose(Api.normalize(s)); })); url.requestFocus(); } private void choose(String base) { reset("Connecting to " + base + "…"); Api.async(() -> Api.authStatus(base), status -> { if ("public".equals(status.optString("accessMode"))) save(base, ""); else password(base); }, e -> { reset("Cannot reach " + base + ": " + e.getMessage()); actions(); }); } private void password(String base) { reset("Viewer password for " + base); EditText pw = Ui.field(this, "Password", Ui.password()); box.addView(pw); View login = Ui.iconButton(this, R.drawable.ic_check, "Log in", v -> { String s = pw.getText().toString(); Api.async(() -> { Api.login(base, s); return true; }, ok -> save(base, s), e -> Toast.makeText(this, e instanceof Api.HttpError && ((Api.HttpError) e).code == 401 ? "Wrong password" : e.getMessage(), Toast.LENGTH_LONG).show()); }); box.addView(login); // The keyboard's done key logs in; the D-pad cannot leave the keyboard. pw.setOnEditorActionListener((v, action, event) -> login.performClick()); pw.requestFocus(); } private void save(String base, String password) { Prefs.setServer(base, password); startActivity(new Intent(this, MainActivity.class) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)); } private void reset(String title) { box.removeAllViews(); box.addView(Ui.text(this, title, 24)); } }