aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/mygram/droid/Discovery.java
blob: 56777f37cae2bfb92182f86e1bc71cae3b7c6782 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/*
 * 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.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/** Finds Foldergram instances on the LAN. */
// ponytail: probes only the TV's /24 on the default port; manual URL entry covers proxies and other ports.
final class Discovery {
	static final int PORT = 4141;

	private Discovery() {
	}

	/** True for a Foldergram /api/health reply: {"ok":true,...,"storage":{...}}. */
	static boolean isFoldergram(String body) {
		try {
			JSONObject o = new JSONObject(body);
			return o.optBoolean("ok") && o.optJSONObject("storage") != null;
		} catch (JSONException e) {
			return false;
		}
	}

	/** The TV's own IPv4 address, or null when offline. */
	static String localIpv4(Context c) {
		ConnectivityManager cm = c.getSystemService(ConnectivityManager.class);
		LinkProperties lp = cm.getLinkProperties(cm.getActiveNetwork());
		if (lp != null)
			for (LinkAddress a : lp.getLinkAddresses())
				if (a.getAddress() instanceof Inet4Address)
					return a.getAddress().getHostAddress();
		return null;
	}

	/** Base URLs of instances answering on the /24 around self. Blocks a few seconds. */
	static List<String> scan(String self) throws InterruptedException {
		String prefix = self.substring(0, self.lastIndexOf('.') + 1);
		ExecutorService pool = Executors.newFixedThreadPool(64);
		List<String> found = Collections.synchronizedList(new ArrayList<>());
		for (int i = 1; i < 255; i++) {
			String base = "http://" + prefix + i + ":" + PORT;
			pool.execute(() -> {
				try {
					if (isFoldergram(Api.request(base, "GET", "/health", null, 800)))
						found.add(base);
				} catch (IOException e) {
					// nothing listening there
				}
			});
		}
		pool.shutdown();
		pool.awaitTermination(20, TimeUnit.SECONDS);
		List<String> out = new ArrayList<>(found);
		Collections.sort(out);
		return out;
	}

	/** Reverse-DNS name plus IP when there is a name, else the IP. */
	static String label(String base) {
		String ip = URI.create(base).getHost();
		try {
			String name = InetAddress.getByName(ip).getCanonicalHostName();
			return name.equals(ip) ? ip : name + "  " + ip;
		} catch (UnknownHostException e) {
			return ip;
		}
	}
}