summaryrefslogtreecommitdiffstats
path: root/research/find_lineageos_devices.py
blob: c75da3beae8cb3a4afb8fef727472128587028da (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
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#!/bin/env python
# Program to find devices to support in LineageOS
# Copyright (C) 2019 Denis 'GNUtoo' Carikli <GNUtoo@cyberdimension.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import os
import re
import sys
import yaml

basedir = '_data' + os.sep + 'devices'
results = {}
last_lineageos_version = 16.0

def still_supported(document):
    if not last_lineageos_version in document['versions']:
        return False
    if 'discontinued' in document['channels']:
       return False

    return True

# In some cases it might be interesting to look if a given device has a modem in
# the case where the SOC is known not to have any modem, so we can automatically
# know that the is no shared memory between the modem and the device if there is
# no modem.
# Another case would be for SOCs that have optional modems where the device has
# no modem feature and that we are sure that the modem CPU is not running any code.
def device_has_modem(vendor, product):
    if vendor == 'Google' and product == 'Pixel C':
        return False

    # unknown
    return None

def device_has_shared_memory_between_modem_and_soc(vendor, product):
    # In the "Samsung Mobile Modem Driver (SVNET2) V1 for Memory-type Interface"
    # section in lineageos_s5neolte_defconfig in the lineage-17.0 branch of
    # https://github.com/LineageOS/android_kernel_samsung_universal7580 there
    # is the following configuration:
    # CONFIG_LINK_DEVICE_SHMEM=y
    # CONFIG_LINK_DEVICE_HSIC is not set
    if vendor == 'Samsung' and product == 'Galaxy S5 Neo':
        return True

    # unknown
    return None

def soc_has_modem(vendor, product):
    if vendor == 'HiSilicon':
        pass
    elif vendor == 'Intel':
        pass
    elif vendor in ['Nvidia', 'NVIDIA'] and re.search("^Tegra 4", product):
        # The modem could be implemented in software
        pass
    elif vendor in ['Nvidia', 'NVIDIA'] and re.search("^Tegra K1", product):
        pass
    elif vendor in ['Nvidia', 'NVIDIA'] and re.search("^Tegra X1", product):
        pass
    elif vendor == 'Qualcomm' and re.search("^MSM", product):
        return True
    elif vendor == 'Qualcomm' and re.search("^APQ", product) or re.search("^Snapdragon 600", product):
        return False
    elif vendor == 'Qualcomm' and product == "Qualcomm SDM845 Snapdragon 845":
        return True
    elif vendor == 'Qualcomm' and re.search("^SDM", product):
        pass
    elif vendor == 'Qualcomm' and (re.search("^SM", product) or re.search("^sm", product)):
        pass
    elif vendor == 'Qualcomm' and re.search("^Snapdragon", product):
        pass
    elif vendor == 'Samsung' and re.search("^Exynos", product):
        # Some exynos modems do exists but they are either standalone
        # modems or are SOCs where the AP is a Cortex M7 which are very
        # unlikely to be able to run Android for smartphones or tablets
        return False
    elif vendor == 'TI' and product in ['OMAP4430', 'OMAP4460']:
        # The modem could be implemented in software in the DSP but I never saw
        # it on any device. It's used like that on the SysmoBTS which are BTS,
        # not phones or a tablets.
        return False
    else:
        print("<{}|{}>".format(vendor, product))
        sys.exit(1)
    # Unknown
    return None

def parse_soc(soc):
    soc_vendors = [
        "HiSilicon",
        "Intel",
        "NVIDIA",
        "Nvidia",
        "Qualcomm",
        "Samsung",
        "TI"]

    soc_vendors_quirks = {
        # ^Match : Vendor
        'Exynos' : 'Samsung',
    }

    results = {}
    found = False
    for vendor in soc_vendors:
        match = re.search("^{0} (.*)".format(vendor), soc)
        if match:
            found = True
            if vendor == 'NVIDIA':
                vendor = 'Nvidia'
            results['vendor'] = vendor
            results['product'] = match.group(1)
            results['has_modem'] = soc_has_modem(results['vendor'], results['product'])

    if not found:
        for match_data, vendor in soc_vendors_quirks.items():
            match = re.search("^{0} (.*)".format(match_data), soc)
            if match:
                found = True
                results['vendor'] = vendor
                results['product'] = soc
                results['has_modem'] = soc_has_modem(results['vendor'], results['product'])

    if not 'vendor' in results:
        print("TODO: Handle vendor in \"{}\"".format(soc))
        sys.exit(1)

    return results

def battery_is_removable(battery):
    # Example: Set top box
    if battery == "None" or battery == None:
        return None

    if 'removable' not in battery:
        print("TODO: Handle batteries where removable is absent: {}".format(battery))
        sys.exit(1)

    removable_battery = battery.get('removable')
    if removable_battery == True:
        return True
    elif removable_battery == False:
        return False
    else:
        print("TODO: Handle batteries where removable is \"{}\": {}".format(removable, battery))
        sys.exit(1)

def has_removable_battery(document):
    removable_battery = None
    if 'battery' not in document:
        # We don't know the policies reguarding incomplete data so the data
        # may either be incomplete or the device may not have a battery.
        print("TODO: Add support for devices lacking a battery")
        sys.exit(1)
    else:
        battery = document.get('battery')
        if type(battery) is not list:
            return battery_is_removable(battery)
        else:
            for battery_version in battery:
                if battery_version == None:
                    continue

                if len(battery_version.keys()) != 1:
                    print("TODO: Add support for battery versions with multiple keys: {}".format(battery_version))
                    sys.exit(1)

                nr_removable = 0
                nr_not_removable = 0

                (battery_name, battery_data), = battery_version.items()

                removable = battery_is_removable(battery_data)
                if removable == True:
                    nr_removable += 1
                elif removable == False:
                    nr_not_removable += 1

            if nr_removable == 0 and nr_not_removable > 0:
                return False
            elif nr_not_removable == 0 and nr_removable > 0:
                return True
            else:
                print("TODO: The data has removable and non-removable batteries"
                      "versions for the same device: {}".format(battery))
                print("      Add support for it in the parsing code")
                sys.exit(1)

def interesting_for_replicant(document):
    # For smartphones or tablets with a modem, since the modem is in
    # the same SOC, we would need to do some extensive review of the SOC
    # architecture to understand if the modem can be isolated with an IOMMU
    # We would also need to make sure that the IOMMU is correctly setup (hard)
    # and that the setup happens before the modem core are booted.
    # For devices without a modem, we would still need to make sure that the
    # sound card and the microphone is completely under the control of free
    # software and that there aren't too much proprietary libraries to replace
    soc = parse_soc(document['soc'])
    if soc_has_modem(soc['vendor'], soc['product']) == True:
        return False

    if device_has_shared_memory_between_modem_and_soc(document['vendor'],
                                                      document['name']):
        return False

    # Non replaceable batteries causes too much issues for both users
    # and developers as once you buy a device second hand, the battery
    # doesn't last as much as when the device is new. On some devices,
    # replicing non-removable batteries can be very difficult and damage
    # the device along the way
    #
    # There are some devices where the battery is not removable but
    # since the device can be easily opened, and that the battery has
    # a connector, it might be possible for an experienced user or
    # developer, or a repair shop, or a repair café to open the device
    # and replace the battery.
    #
    # However it would take more research to understand if compatible
    # batteries replacement can easily be found. The LineageOS wiki also
    # doesn't have a way to distinguish between devices that can be easily
    # opened and the ones that aren't.
    #
    # So for now we aproximate non-removable batteries to non-replaceable
    # batteries until we find a way to deal with it.
    if document['type'] != 'Set top box' and not has_removable_battery(document):
        return False

    return True

def store_infos(results, document):
    fields = ['vendor', 'name', 'type']

    device_dict = {}
    for field in fields:
        device_dict[field] = document[field]

    device_dict['removable_battery'] = has_removable_battery(document)

    soc = document['soc']
    soc_data = parse_soc(soc)
    soc_vendor = None
    soc_product = None

    if soc_data['vendor'] not in results:
        results[soc_data['vendor']] = {}
    soc_vendor = results[soc_data['vendor']]

    if soc_data['product'] not in soc_vendor:
        soc_vendor[soc_data['product']] = []
    soc_product = soc_vendor[soc_data['product']]

    soc_product.append(device_dict)

def print_results(results):
    soc_vendors = list(results.keys())
    soc_vendors.sort()
    for soc_vendor in soc_vendors:
        print ("{0}:".format(soc_vendor))
        for soc_product in results[soc_vendor]:
            print ("- {0}:".format(soc_product))
            for device in results[soc_vendor][soc_product]:
                print("  * {0}: {1} ({2})".format(
                    device['vendor'], device['name'], device['type']))

def find_devices(path):
    for filename in os.listdir(path + os.sep + basedir):
        filepath = path + os.sep + basedir + os.sep + filename
        if re.search("\.yml$", filepath):
            yaml_file = open(filepath, 'r')
            document = yaml.load(yaml_file)
            if still_supported(document) and interesting_for_replicant(document):
                store_infos(results, document)
    print_results(results)

def usage(argv0):
    progname = os.path.basename(argv0)
    print("{} [path/to/lineage_wiki]".format(progname))
    sys.exit(1)

if len(sys.argv) != 2:
    usage(sys.argv[0])

find_devices(sys.argv[1])