aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena/scrcpy/Devices.java
blob: 0ba61a314dae9d0192a03ad465a2f364dfc3323f (plain) (blame)
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package invalid.lena.scrcpy;

import android.content.Context;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;

// Tiny JSON-backed list of paired targets, stored at filesDir/devices.json.
// No SQLite, no Room. Read on demand, written whole-file on add/remove.
//
// parse() / serialize() are pure-java and android-free for unit tests.
// load() / save() add the Context-rooted file I/O.
public final class Devices {

    private static final String FILE = "devices.json";

    public static final class Device {
        public final String host;
        public final int    port;

        public Device(String host, int port) {
            this.host = host;
            this.port = port;
        }

        // Bracket IPv6 literals so the port stays unambiguous. Round-trips
        // through parseAddress().
        @Override
        public String toString() {
            return host.indexOf(':') < 0
                    ? host + ":" + port
                    : "[" + host + "]:" + port;
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Device d)) return false;
            return port == d.port && host.equals(d.host);
        }

        @Override
        public int hashCode() {
            return host.hashCode() * 31 + port;
        }
    }

    private Devices() {}

    // Parse "host:port" exactly as the target's Wireless debugging screen
    // prints it. IPv6 literals must be bracketed ("[fe80::1]:5555") because
    // the address itself contains colons; an unbracketed one is rejected
    // rather than silently split at the wrong colon. Returns null on
    // anything malformed - the only caller is a text field.
    public static Device parseAddress(String s) {
        if (s == null) return null;
        s = s.trim();
        String host, port;
        if (s.startsWith("[")) {
            int end = s.indexOf(']');
            if (end < 0 || !s.startsWith("]:", end)) return null;
            host = s.substring(1, end);
            port = s.substring(end + 2);
        } else {
            int colon = s.indexOf(':');
            if (colon < 0 || colon != s.lastIndexOf(':')) return null;
            host = s.substring(0, colon);
            port = s.substring(colon + 1);
        }
        int p = parsePort(port);
        if (host.isEmpty() || p < 0) return null;
        return new Device(host, p);
    }

    // Digits only, 1-65535. Returns -1 if it is not a usable port.
    // Integer.parseInt() alone would accept "+5555" and " 5555".
    public static int parsePort(String s) {
        if (s == null) return -1;
        s = s.trim();
        if (s.isEmpty() || s.length() > 5) return -1;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) < '0' || s.charAt(i) > '9') return -1;
        }
        int p = Integer.parseInt(s);
        return p >= 1 && p <= 65535 ? p : -1;
    }

    // Pure-java parse: returns whatever rows are well-formed; logs and
    // skips anything malformed instead of nuking the list.
    public static List<Device> parse(String json) {
        List<Device> out = new ArrayList<>();
        if (json == null) return out;
        json = json.trim();
        if (json.isEmpty()) return out;
        JSONArray arr;
        try { arr = new JSONArray(json); }
        catch (Exception e) { Log.e(e, "devices: not a json array"); return out; }

        for (int i = 0; i < arr.length(); i++) {
            try {
                JSONObject o = arr.getJSONObject(i);
                out.add(new Device(o.getString("host"), o.getInt("port")));
            } catch (Exception e) {
                Log.w("devices: skipping malformed row %d: %s", i, e);
            }
        }
        return out;
    }

    public static String serialize(List<Device> devices) {
        try {
            JSONArray arr = new JSONArray();
            for (Device d : devices) {
                JSONObject o = new JSONObject();
                o.put("host", d.host);
                o.put("port", d.port);
                arr.put(o);
            }
            return arr.toString(2);
        } catch (Exception e) {
            Log.e(e, "devices: serialize failed");
            return "[]";
        }
    }

    public static List<Device> load(Context ctx) {
        File f = new File(ctx.getFilesDir(), FILE);
        if (!f.exists()) return new ArrayList<>();
        try {
            return parse(new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8));
        } catch (Exception e) {
            Log.e(e, "devices: load failed");
            return new ArrayList<>();
        }
    }

    public static void save(Context ctx, List<Device> devices) throws IOException {
        File f = new File(ctx.getFilesDir(), FILE);
        AtomicFiles.write(f, serialize(devices).getBytes(StandardCharsets.UTF_8));
    }

    // Add or replace by host+port. Context-rooted; the in-place helper
    // is the test seam.
    public static synchronized List<Device> upsert(Context ctx, Device d) throws IOException {
        List<Device> list = load(ctx);
        list.removeIf(d::equals);
        list.add(d);
        save(ctx, list);
        return list;
    }

    // Remove the matching device (by host+port). Returns the updated list.
    public static synchronized List<Device> remove(Context ctx, Device d) throws IOException {
        List<Device> list = load(ctx);
        list.removeIf(d::equals);
        save(ctx, list);
        return list;
    }
}