diff options
Diffstat (limited to 'app/src/main/java/mygram/droid')
| -rw-r--r-- | app/src/main/java/mygram/droid/App.java | 44 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/FolderActivity.java | 12 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/LockActivity.java | 63 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/Prefs.java | 47 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/SettingsActivity.java | 55 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/Ui.java | 14 |
6 files changed, 217 insertions, 18 deletions
diff --git a/app/src/main/java/mygram/droid/App.java b/app/src/main/java/mygram/droid/App.java index ffbdd8d..1919343 100644 --- a/app/src/main/java/mygram/droid/App.java +++ b/app/src/main/java/mygram/droid/App.java @@ -14,19 +14,61 @@ package mygram.droid; +import android.app.Activity; import android.app.Application; +import android.content.Intent; +import android.os.Bundle; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -public final class App extends Application { +public final class App extends Application implements Application.ActivityLifecycleCallbacks { + /** Until the PIN is entered; set again whenever the app leaves the screen. */ + static boolean locked = true; + private int started; + @Override public void onCreate() { super.onCreate(); Prefs.init(this); + registerActivityLifecycleCallbacks(this); // One cookie jar for the Foldergram session: Api, Glide and ExoPlayer // all use HttpURLConnection, which consults the default handler. CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL)); } + + /** Before any screen draws, cover it with the PIN prompt if the app is locked. */ + @Override + public void onActivityStarted(Activity a) { + started++; + if (locked && Prefs.hasPin() && !(a instanceof LockActivity)) + a.startActivity(new Intent(a, LockActivity.class)); + } + + @Override + public void onActivityStopped(Activity a) { + if (--started == 0) + locked = true; + } + + @Override + public void onActivityCreated(Activity a, Bundle state) { + } + + @Override + public void onActivityResumed(Activity a) { + } + + @Override + public void onActivityPaused(Activity a) { + } + + @Override + public void onActivitySaveInstanceState(Activity a, Bundle state) { + } + + @Override + public void onActivityDestroyed(Activity a) { + } } diff --git a/app/src/main/java/mygram/droid/FolderActivity.java b/app/src/main/java/mygram/droid/FolderActivity.java index dd674c4..65ac596 100644 --- a/app/src/main/java/mygram/droid/FolderActivity.java +++ b/app/src/main/java/mygram/droid/FolderActivity.java @@ -19,11 +19,9 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.view.Gravity; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; -import android.widget.TextView; import android.widget.Toast; import androidx.recyclerview.widget.GridLayoutManager; @@ -52,15 +50,7 @@ public final class FolderActivity extends Activity { super.onCreate(state); LinearLayout column = new LinearLayout(this); column.setOrientation(LinearLayout.VERTICAL); - LinearLayout header = new LinearLayout(this); - header.setGravity(Gravity.CENTER_VERTICAL); - int p = Ui.dp(this, 16); - header.setPadding(p * 2, p, p * 2, 0); - header.addView(Ui.iconButton(this, R.drawable.ic_arrow_back, "Back", v -> finish())); - TextView title = Ui.text(this, getIntent().getStringExtra("name"), 24); - title.setPadding(p, 0, 0, 0); - header.addView(title); - column.addView(header); + column.addView(Ui.header(this, getIntent().getStringExtra("name"))); content = new FrameLayout(this); column.addView(content, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1)); setContentView(column); diff --git a/app/src/main/java/mygram/droid/LockActivity.java b/app/src/main/java/mygram/droid/LockActivity.java new file mode 100644 index 0000000..dd95915 --- /dev/null +++ b/app/src/main/java/mygram/droid/LockActivity.java @@ -0,0 +1,63 @@ +/* + * mygram-droid: Android TV client for Foldergram. + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * + * 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.os.Bundle; +import android.text.InputType; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Toast; + +/** Splash with the logo and a PIN prompt; App shows it while the app is locked. */ +public final class LockActivity extends Activity { + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + LinearLayout box = Ui.column(this); + ImageView logo = new ImageView(this); + logo.setImageResource(R.drawable.ic_launcher); + logo.setContentDescription("mygram"); + int s = Ui.dp(this, 96); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(s, s); + lp.bottomMargin = Ui.dp(this, 24); + box.addView(logo, lp); + EditText pin = Ui.field(this, "PIN", InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + box.addView(pin); + View ok = Ui.iconButton(this, R.drawable.ic_check, "Unlock", v -> { + if (Prefs.checkPin(pin.getText().toString())) { + App.locked = false; + finish(); + } else { + pin.setText(""); + Toast.makeText(this, "Wrong PIN", Toast.LENGTH_SHORT).show(); + } + }); + box.addView(ok); + // The keyboard's done key unlocks; the D-pad cannot leave the keyboard. + pin.setOnEditorActionListener((v, action, event) -> ok.performClick()); + setContentView(box); + pin.requestFocus(); + } + + /** Back leaves the app instead of revealing the screen underneath. */ + @Override + @SuppressWarnings("deprecation") // no predictive back on this TV; onBackPressed still fires + public void onBackPressed() { + finishAffinity(); + } +} diff --git a/app/src/main/java/mygram/droid/Prefs.java b/app/src/main/java/mygram/droid/Prefs.java index 33ce878..0ff295d 100644 --- a/app/src/main/java/mygram/droid/Prefs.java +++ b/app/src/main/java/mygram/droid/Prefs.java @@ -17,6 +17,11 @@ package mygram.droid; import android.content.Context; import android.content.SharedPreferences; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + /** Settings stored on the device. */ final class Prefs { private static SharedPreferences prefs; @@ -57,4 +62,46 @@ final class Prefs { static void setTimes(int photo, int background) { prefs.edit().putInt("photo", photo).putInt("background", background).apply(); } + + static boolean validPin(String pin) { + return pin.matches("[0-9]{4,8}"); + } + + static boolean hasPin() { + return !prefs.getString("pin", "").isEmpty(); + } + + /** Stores "salt:sha256(salt + pin)", never the PIN; null removes it. */ + static void setPin(String pin) { + if (pin == null) { + prefs.edit().remove("pin").apply(); + return; + } + byte[] b = new byte[16]; + new SecureRandom().nextBytes(b); + String salt = hex(b); + prefs.edit().putString("pin", salt + ":" + hash(salt, pin)).apply(); + } + + static boolean checkPin(String pin) { + String[] stored = prefs.getString("pin", "").split(":"); + return stored.length == 2 && MessageDigest.isEqual(stored[1].getBytes(StandardCharsets.UTF_8), + hash(stored[0], pin).getBytes(StandardCharsets.UTF_8)); + } + + // ponytail: one SHA-256 round; a 4-8 digit PIN is a privacy screen, not a secret against someone with the device storage. + static String hash(String salt, String pin) { + try { + return hex(MessageDigest.getInstance("SHA-256").digest((salt + pin).getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); // every Java platform ships SHA-256 + } + } + + private static String hex(byte[] b) { + StringBuilder s = new StringBuilder(); + for (byte x : b) + s.append(String.format("%02x", x)); + return s.toString(); + } } diff --git a/app/src/main/java/mygram/droid/SettingsActivity.java b/app/src/main/java/mygram/droid/SettingsActivity.java index a8839bb..3783058 100644 --- a/app/src/main/java/mygram/droid/SettingsActivity.java +++ b/app/src/main/java/mygram/droid/SettingsActivity.java @@ -18,12 +18,16 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.text.InputType; +import android.view.ViewGroup; import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.ScrollView; import android.widget.Toast; -/** Viewer password, photo and background durations, change instance. */ +/** Viewer password, durations, app PIN, change instance. */ public final class SettingsActivity extends Activity { + private static final int PIN_INPUT = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD; + @Override protected void onCreate(Bundle state) { super.onCreate(state); @@ -41,23 +45,58 @@ public final class SettingsActivity extends Activity { EditText bg = Ui.field(this, "15", InputType.TYPE_CLASS_NUMBER); bg.setText(String.valueOf(Prefs.backgroundSeconds())); box.addView(bg); + box.addView(Ui.text(this, Prefs.hasPin() ? "New app PIN, 4 to 8 digits (empty keeps it)" + : "App PIN, 4 to 8 digits (empty for none)", 14)); + EditText pin = Ui.field(this, "PIN", PIN_INPUT); + box.addView(pin); + EditText again = Ui.field(this, "Repeat PIN", PIN_INPUT); + box.addView(again); LinearLayout row = Ui.row(this); row.addView(Ui.iconButton(this, R.drawable.ic_check, "Save", v -> save(pw.getText().toString(), - photo.getText().toString(), bg.getText().toString()))); + photo.getText().toString(), bg.getText().toString(), pin.getText().toString(), + again.getText().toString()))); + if (Prefs.hasPin()) + row.addView(Ui.iconButton(this, R.drawable.ic_lock_open, "Remove PIN", v -> { + Prefs.setPin(null); + Toast.makeText(this, "PIN removed", Toast.LENGTH_SHORT).show(); + finish(); + })); row.addView(Ui.iconButton(this, R.drawable.ic_swap, "Change instance", v -> startActivity(new Intent(this, SetupActivity.class)))); box.addView(row); - setContentView(box); + + // Taller than the screen with the PIN fields; the scroll view follows focus. + ScrollView scroll = new ScrollView(this); + scroll.addView(box); + LinearLayout column = new LinearLayout(this); + column.setOrientation(LinearLayout.VERTICAL); + column.addView(Ui.header(this, "Settings")); + column.addView(scroll, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1)); + setContentView(column); row.getChildAt(0).requestFocus(); } - private void save(String password, String photo, String background) { + private void save(String password, String photo, String background, String pin, String again) { int p = seconds(photo), b = seconds(background); if (p < 1 || b < 1) { - Toast.makeText(this, "Durations must be 1 to 600 seconds", Toast.LENGTH_LONG).show(); + toast("Durations must be 1 to 600 seconds"); return; } + if (!pin.isEmpty() || !again.isEmpty()) { + if (!Prefs.validPin(pin)) { + toast("The PIN must be 4 to 8 digits"); + return; + } + if (!pin.equals(again)) { + toast("The two PINs differ"); + return; + } + } Prefs.setTimes(p, b); + if (!pin.isEmpty()) { + Prefs.setPin(pin); + App.locked = false; // just typed it; ask from the next time the app leaves the screen + } if (password.equals(Prefs.password())) { finish(); return; @@ -68,7 +107,11 @@ public final class SettingsActivity extends Activity { }, ok -> { Prefs.setPassword(password); finish(); - }, e -> Toast.makeText(this, "Login failed: " + e.getMessage(), Toast.LENGTH_LONG).show()); + }, e -> toast("Login failed: " + e.getMessage())); + } + + private void toast(String s) { + Toast.makeText(this, s, Toast.LENGTH_LONG).show(); } private static int seconds(String s) { diff --git a/app/src/main/java/mygram/droid/Ui.java b/app/src/main/java/mygram/droid/Ui.java index 234dad3..172fbff 100644 --- a/app/src/main/java/mygram/droid/Ui.java +++ b/app/src/main/java/mygram/droid/Ui.java @@ -14,6 +14,7 @@ package mygram.droid; +import android.app.Activity; import android.content.Context; import android.graphics.Color; import android.graphics.RenderEffect; @@ -153,6 +154,19 @@ final class Ui { button.getChildAt(1).setVisibility(current ? View.VISIBLE : View.INVISIBLE); } + /** Back arrow and title, for screens outside the main view. */ + static LinearLayout header(Activity a, String title) { + LinearLayout header = new LinearLayout(a); + header.setGravity(Gravity.CENTER_VERTICAL); + int p = dp(a, 16); + header.setPadding(p * 2, p, p * 2, 0); + header.addView(iconButton(a, R.drawable.ic_arrow_back, "Back", v -> a.finish())); + TextView t = text(a, title, 24); + t.setPadding(p, 0, 0, 0); + header.addView(t); + return header; + } + static void blur(ImageView v, float radius) { v.setRenderEffect(RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)); } |
