aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/mygram/droid
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/mygram/droid')
-rw-r--r--app/src/main/java/mygram/droid/App.java44
-rw-r--r--app/src/main/java/mygram/droid/FolderActivity.java12
-rw-r--r--app/src/main/java/mygram/droid/LockActivity.java63
-rw-r--r--app/src/main/java/mygram/droid/Prefs.java47
-rw-r--r--app/src/main/java/mygram/droid/SettingsActivity.java55
-rw-r--r--app/src/main/java/mygram/droid/Ui.java14
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));
}