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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
|
#+TITLE: Against the Grain: A Mobile Phone for Hackers and Tinkerers That Doesn't Suck
#+DATE:
#+TAGS: writeup
For the past ten months, I've been using my [[https://www.pine64.org/pinephone/][PinePhone]] as a "daily driver." By
which, I mean it's been in my pocket everywhere I go, and it's the device I use
to make phone calls. Depending on your familiarity with the PinePhone (or the
state of "Linux Phones" more generally) this statement is either delirious, or
vapid (why should I care that you use a "smart" phone just like the rest of us?)
Don't be mistaken: the PinePhone is usable as a little cellular-capable PDA, and
it's in a league of its own. This article is my attempt to document my
experiences and rationale for wanting to use one, as well as my thoughts on
"Linux phones" as a whole.
# I considered a couple of different "clever" titles for this post, but settled
# on the simple "I love my PinePhone", after seeing a [[https://blog.danieljanus.pl/2022/08/18/i-love-my-gpd-micro-pc/][post of a similar name]] by
# Daniel Janus's about his GPD Micro PC. Perhaps coincidentally, a lot of the
# reasons he lists for enjoying the laptop line-up with my reasons for enjoying
# the PinePhone.
# ---
# JLK: "for being able to run my favorite applications on the go" not as
# striking as it could be. I think "being a pinebook pro owner" could also use
# some re-wording. In general, I think this is the section that's going to need
# the most copyediting.
I expect "Linux Phone" to be a readily understood term by readers of mine, but
it is a somewhat imprecise term. So I'll clarify that by "Linux Phone," I mean a
mobile phone that runs not only the Linux kernel, but also the user space and
general experience we all associate with the Linux operating system[fn:8].
Notably, this excludes Android[fn:1], which has existed for several years. Not
long ago, a Linux Phone seemed like a pipe dream: one I've had ever since I
first held a smartphone I could call my own. Perhaps it's impractical for many,
but I would be happy to trade ubiquity for being able to run my favorite
applications on the go. I don't use social media like Instagram, or proprietary
messaging applications like WhatsApp and Snapchat. As long as I can run my usual
Linux software stack, and have a modem that can receive and send phone calls and
text messages, I'll be content. So when the PinePhone was announced in 2019, I
was excited. Not only did it tick many of the boxes for my dream "Linux Phone,"
but it came from [[https://www.pine64.org/][PINE64]], a vendor I'd had great experiences with in the past,
being a [[https://www.pine64.org/pinebook-pro/][Pinebook Pro]] owner.
The idea of Linux phones had been at least somewhat popularized at that point
with the earlier announcement of the Librem 5, but the Pinephoe was far more
affordable, and it would be hitting the market before the Librem 5 was scheduled
to. I got it as a Christmas gift. Unfortunately, this was amidst my hellish time
as an undergrad, so I didn't have the time to fully buy into swapping over my
mobile compute stack, so it waited until I graduated. I actually am somewhat
happy that I waited, because the software situation is much better today than it
was three years ago.
My previous "smart" phone was a [[https://en.wikipedia.org/wiki/Honor_5X][Huawei Honor 5X]], which I purchased for about
$200 well before the Trump administration [[https://en.wikipedia.org/wiki/Entity_List#Huawei][banned domestic sales of Huawei
products]].[fn:2] I flashed CyanogenMod (later LineageOS) the second I removed it
from the box for reasons I expect to be self-evident. Initially, it was a
significant upgrade over my previous 2nd generation [[https://en.wikipedia.org/wiki/Moto_G_(2nd_generation)][Moto G]], but the experience
soon grew unbearable as the LineageOS image for the device grew unmaintained.
The System UI would freeze frequently, rendering the phone inoperable until I
forcefully rebooted it; expanding the usable disk space with an external SD card
resulted in strange errors and often the SD would show up as "corrupted" until I
rebooted the phone enough times; and I would frequently have the phone reboot to
[[https://en.wikipedia.org/wiki/TWRP_(software)][TWRP]] while I was walking around with it in my pocket, a symptom I strongly
suspect to be related to panics in the old, non-mainline Kernel. The battery
also couldn't hold a charge, and I was able to remedy that by replacing it, but
the difficulty I had in finding OEM parts indicated that regularly servicing the
battery probably wasn't sustainable. It was time for a change.
* The First Week
# Carry old phone around anyway for GPS (which Pinephone sucks at) and camera
With that, you now understand the situation I found myself in last October.
Software support for my mobile phone was suddenly non-existent, and I was
growing frustrated with it. I had the option of setting up the experimental
PinePhone I'd been hoarding, or fronting a couple hundred dollars for a new
cellphone. I went with the former.
#+BEGIN_EXPORT html
<header class="article-future-interjection">
#+END_EXPORT
I took some nice photos the day I received the PinePhone, and more on the day I
set it up. Despite my best efforts, I have been unable to locate the SD card
those photos were saved to, so the photos that follow below were taken recently.
I didn't receive it with the visible bumps and scuffs -- the phone's sustained
those over a few months of use.
#+BEGIN_EXPORT html
</header>
#+END_EXPORT
** Unboxing
#+CAPTION: PinePhone in front of original box.
[[./Pinephone 1.JPG]]
The PinePhone's initial presentation is very well-done. Despite the cost, the
box it comes in feels nice and gives me an initial sense of quality. The phone
comes in a protective sleeve, with a USB-C cable and a leaflet with some
information. It isn't a manual, but it does link to the Pine64 wiki, which is
close enough to one.
#+CAPTION: PinePhone, unboxed.
[[./Pinephone 2.JPG]]
#+CAPTION: Somewhat blurry close-up of the leaflet.
[[./Pinephone 3.JPG]]
I've OCR'd the leaflet for those who use a screen reader:
#+BEGIN_EXPORT html
<div class="fold-hidden" data-name="transcription of the leaflet's message">
#+END_EXPORT
#+BEGIN_QUOTE
Dear Piner, Congratulations on receiving your Brave Heart edition PinePhone!
You are one of the very first to have a PinePhone. We hope you'll help us and our partner projects by contributing to development.
Your input is valuable, so it is important that you report whatever problems you encounter. Please, include relevant logs and/or UART outputs.
Join the conversation on whichever platform suits you. You can report non-OS specific (kernel) issues you encounter on gitlab.com/pine64-org. OS specific problems should be reported on the PINE64 Wiki (wiki.pine64.org/PinePhone#Software Support) as well as directly to developers in the PinePhone chats (Forums and Chats tab on pine64 org), on PINE64 forums (forum.pine64.org) or on the relevant partner-project forums (see Partner Projects tab on pine64.org).
Brave Heart phones come preloaded with factory test software and nothing else. So you'll have to seek out the OSs that interest you on your own.
Keep in mind that all the OSs are presently pre-release and vary in functionality, even from one pre-release to another. Most mobile distribution OS images are linked on the PinePhone subsection of the PINE64 Wiki. Obtaining OS builds absent from the Wiki may require talking to their developers directly.
The PinePhone Wiki subsection also contains schematics, instructions, hardware configuration details, and other useful information about your device. You can edit and contribute to the Wiki by logging in with your forum credentials.
Brave Heart is meant for early-adopters — developers and enthusiasts — so we expect and encourage you to experiment with the software and hardware by pushing the envelope. That said, please keep in mind that the device is under standard warranty, so breaking components during disassembly or tampering with eFUSEs will void that warranty.
Now, have fun with your PinePhone!
PINE64 Community Team
#+END_QUOTE
#+BEGIN_EXPORT html
</div>
#+END_EXPORT
I care quite a bit about protecting my gadgets, so I went on Thingiverse and
found a [[https://www.thingiverse.com/thing:4658870][hard case design]] for the PinePhone. I could've spent more time sanding
it down and making it look nice, but I'm still a little inexperienced with
making good 3D-printed parts. This was to my downfall as the back cover has a
few scratches now, but it's certainly saved the PinePhone from damage. The
PinePhone shuts itself off upon impact. I think that's a bug, rather than a
feature, but I'm usually quite careful so it doesn't happen often. (In fact,
it's usually when others are handling my phone that it falls.)
Per [[https://www.reddit.com/r/PinePhoneOfficial/comments/havbcm/pinephone_screen_protector/fv5smk0/][this Reddit comment]], I purchased a pack of cheap tempered glass screen
protectors designed for the iPhone Max XS. I haven't dropped the phone enough to
put it to its limits, but thus far it's done well to keep the front of the phone
free from scratches.
#+CAPTION: Photo of the phone next to the case, horribly doctored to show both sides of the case in the same photo.
[[./Pinephone 4.JPG]]
#+CAPTION: The case makes the phone quite chunky ("thicc" as the kids say these days). Holding it is pleasant.
[[./Pinephone 5.JPG]]
** =factorytest=
#+CAPTION: A PinePhone running the factorytest image. Courtesy PINE64, as I lost the photo I took when it was installed on mine. (https://www.pine64.org/2020/01/15/pinephones-start-shipping-all-you-want-to-know/)
[[./Pinephone 7.JPG]]
The PinePhone arrives flashed with a "factory test image" which is suitable for
verifying that the hardware on the PinePhone is functional before you proceed
with it configuring it. The test for the modem was finicky, and the =motor= test
did not work. The device, at this point, was well past the limited warranty, so
I decided to press regardless.
These issues were non-existent when I did install a proper operating system to
the phone, so I suspect there were actually some bugs in =factorytest=.
Experiencing bugs seems to be [[https://forum.pine64.org/showthread.php?tid=13257&pid=90677][consistent with other users' experiences]].
** Distribution
Now that we've got the phone powered up and sufficiently tested, we've have some
decisions to make. What Linux distribution do we want to install on the phone?
Furthermore, what desktop environment do we want use?
The PINE64 wiki has a [[https://wiki.pine64.org/wiki/PinePhone_Software_Releases][page listing most of the distributions]] that are known to
work on the PinePhone, and the choices are surprisingly diverse. On one end of
the spectrum, there's [[https://github.com/GloDroid/][GloDroid]], which is a port of Android to the PinePhone.
That might seem like it defeats the purpose of using the PinePhone, but I'm sure
it can be used for a use-case similar to dual-booting Windows and Linux. Moving
further from Android, we have distributions like [[https://ubports.com/foundation/sponsors][Ubuntu Touch]] which actually use
parts of Android to interact with the underlying phone, but implement a full
Linux user land and display server on top of that. Personally, I think this is a
/really/ cool approach for making ordinary Android phones more useful, and you can
read more about the approach [[https://halium.org/][here]]. Finally, we've got regular mainline Linux,
with both desktop-oriented and mobile-oriented distributions. You can run
Gentoo, Fedora, Arch Linux ARM, etc. on the Pinephone, or you can opt for
[[http://postmarketos.org/][PostmarketOS]] (Alpine-derivative) or [[http://postmarketos.org/][Mobian]] (Debian-derivative).
There are some options that might not fit into my arbitrary "spectrum" idea,
like [[https://sailfishos.org/][Sailfish OS]]. I don't know enough about it to say where it falls. Hopefully
you're taking away that with an open design, you have lots of options.
One last choice I want to mention is the [[https://syndicate-lang.org/journal/2022/06/03/phone-progress][SqueakPhone]], which appears to be based
on PostmarketOS, but the userland is almost entirely written in Smalltalk. It's
a good time to be hacking on mobile devices. We might not be in the golden age,
but we're certainly marching toward it.
As much as I like running Gentoo on most of my machines, I figured that would be
a bit much for me. It also doesn't seem like a good idea to constantly be
compiling things from source on my phone, which probably doesn't have great
thermals (and I assume it would take a few days to compile e.g. Firefox unless I
took the time to properly set up =distcc=.)
So I went with [[http://postmarketos.org/][PostmarketOS]]. I admire the design of Alpine Linux, and I think
that PostmarketOS is the project making the most progress in the mobile Linux
space. Now, PostmarketOS comes with several options for a desktop environment.
The three I consider to be the "main" options are [[https://wiki.postmarketos.org/wiki/Sxmo][Sxmo]], [[https://wiki.postmarketos.org/wiki/Plasma_Mobile][Plasma Mobile]], and
[[https://wiki.postmarketos.org/wiki/Phosh][Phosh]]. Sxmo is basically a mobile-oriented [[https://en.wikipedia.org/wiki/Dwm][dwm]] fork. I'm a former dwm user and
current AwesomeWM user, but running a tiling window manager on my phone seems a
bit much, even for me. And in the Gnome versus KDE footballing[fn:11], I like
Gnome better, and I prefer GTK+ over Qt, so I went with Phosh.
Once you know what you want to install on your PinePhone, the process is
straightforward. Flash a distribution image to an SD card, pop it into the
phone, and power it on. From there, you can install it to EMMC.
** Storage
The internal EMMC on the PinePhone I have is 16GB (later models have a 32GB
EMMC). My music folder far exceeds 16GB, so I bought a relatively large SD card
to use as extra storage. Unlike Android, a regular Linux distribution gives you
some flexibility with how you split storage up across the various storage
devices. I set up a LUKS-encrypted ext4 filesystem on the SD card and threw a
script into [[https://wiki.gentoo.org/wiki//etc/local.d][local.d]] to decrypt it and mount it on top of =/home=. I haven't had a
single issue with it, so we're already doing much better than Android. I can
store basically whatever the hell I want on my phone without worrying about
space constraints.
#+CAPTION: A readily-noticeable feature of the PinePhone is how easy it is to get to the internals. You don't need to do much to get to the SD/SIM slot; there's a notch in the back cover that you can pry up on and it pops right off.
[[./Pinephone 6.JPG]]
** Mobile Data
Mobile data worked surprisingly well, with minimal tinkering. At the time,
PostmarketOS wasn't able to automatically detect the APN for my carrier, but the
PINE64 wiki has a [[Curiously, I went to icanhazip.com and I was given an IPv6 address in response.][list of APN settings]] for common carriers. Once I set it up to
communicate with =NXTGENPHONE=, I was able to kill the Wi-Fi connection and hit
=icanhazip.com=. I knew it worked because I was given an IPv6 address in response.
First time that's happened to me.
#+BEGIN_EXPORT html
<iframe src="https://nitter.net/0daysfordays/status/1489964439608700936/embed" width="500px" height="500px" scrolling="no" ></iframe>
#+END_EXPORT
I was also able to pull out my PinePhone and pull up a picture of Fred Durst at
the Thanksgiving Dinner Table[fn:9], far away from my house, so I was able to test out
mobile data "in practice" fairly early into my PinePhone usage.
** Software
While we can [[https://waydro.id/][run Android applications on GNU/Linux]], it would defeat the purpose
of using this phone to be using Android applications for the daily tasks that I
use my phone for. So, soon after I'd verified all was working, I put together a
list of the packages that I had installed on my old phone, and drew lines to the
analogs on PostmarketOS.
| Android App | PostmarketOS package | Note |
|--------------------------+-------------------------+---------------------------------------------------------------------------------------|
| andOTP | [[https://git.sr.ht/~martijnbraam/numberstation][numberstation]] | |
| AntennaPod | | Dropped; I'll just use an RSS reader. |
| AnySoftKeyboard | [[https://source.puri.sm/Librem5/squeekboard][squeekboard]] | |
| App Manager | | Android-specific application. |
| AudioFX | | Unused on Android. But there are plenty of [[https://wiki.archlinux.org/title/PipeWire#Audio_post-processing][post-processing applications]] for Pipewire. |
| Aurora Store | | Android-specific application. |
| BackgroundRestrictor | | Android-specific application. |
| Browser | | Unused on Android. |
| AVNC | [[https://wiki.postmarketos.org/wiki/VNC][tigervnc]] | Unused in PostmarketOS. |
| Calculator | [[https://wiki.gnome.org/Apps/Calculator][gnome-calculator]]; [[https://www.gnu.org/software/emacs/manual/html_mono/calc.html][calc]] | |
| Calendar | [[https://orgmode.org/][Org mode]] | |
| Calendar Import-Export | [[https://orgmode.org/][Org mode]] | Unused in PostmarketOS.[fn:3] |
| Camera | [[https://wiki.mobian-project.org/doku.php?id=megapixels][Megapixels]] | |
| Clock | [[https://gitlab.gnome.org/GNOME/gnome-clocks][gnome-clocks]] | |
| Contacts | [[https://gitlab.gnome.org/GNOME/gnome-contacts][gnome-contacts]] | |
| Conversations | [[https://wiki.postmarketos.org/wiki/Dino][dino]] | Unused in PostmarketOS.[fn:4] |
| Discord | [[https://github.com/diamondburned/gtkcord4][gtkcord4]] | Discord sucks and I hate it, but I have friends who prefer it, so I have to settle. |
| Email | | Unused on Android. |
| F-Droid | | Android-specific application |
| FFUpdater | | Android-specific application |
| Files | [[https://github.com/tchx84/Portfolio][Portfolio]], [[https://en.wikipedia.org/wiki/Dired][dired]], ls(1) | |
| Firefox | Firefox | |
| FM Radio | | _Not replaceable._[fn:6] |
| Gallery | [[https://wiki.mobian-project.org/doku.php?id=gnomephotos][gnome-photos]] | |
| K-9 Mail | [[https://en.wikipedia.org/wiki/Geary_(e-mail_client)][Geary]] | |
| Libera PRO | [[https://wiki.gnome.org/Apps/Evince][Evince]] | Could use Calibre, but I actually do most of my e-book reading on a rooted Nook now. |
| Messaging | [[https://source.puri.sm/Librem5/chatty][Chatty]] | |
| MuPDF mini | [[https://wiki.gnome.org/Apps/Evince][Evince]] | |
| Music | [[https://www.musicpd.org/][Music Player Daemon]] | |
| NewPipe | [[https://mpv.io/][mpv]], [[https://github.com/yt-dlp/yt-dlp][yt-dlp]] | |
| Obsqr | [[https://wiki.mobian-project.org/doku.php?id=megapixels][Megapixels]] | |
| Offline Calendar | | Android-specific application. |
| OpenKeychain | gpg(1) | |
| Orbot | [[https://gitweb.torproject.org/torsocks.git][torsocks]] | |
| Orgzly | | Not needed as I can run GNU Emacs natively on PostmarketOS. |
| OsmAnd~ | [[https://sr.ht/~mil/mepo/][mepo]] | |
| Password Store | [[https://www.passwordstore.org/][pass]] | |
| Phone | [[https://wiki.mobian-project.org/doku.php?id=calls][Calls]] | |
| Recorder | [[https://ffmpeg.org/ffmpeg.html][ffmpeg]] | |
| RetroArch | RetroArch | Unused in PostmarketOS.[fn:7] |
| Settings | | Android-specific application. |
| Shattered Pixel Dungeons | | Dropped. |
| Signal | | |
| Slide | | Dropped. |
| Syncthing | Syncthing | |
| Termux | [[https://gitlab.gnome.org/GNOME/console][gnome-console]] | |
| Tiny Tiny RSS | [[https://gfeeds.gabmus.org/][gnome-feeds]] | I don't currently use RSS synchronization. |
| Tusky | [[https://github.com/bleakgrey/tootle][Tootle]] | |
| wallabag | | Dropped. |
| Wikipedia | | Dropped. |
Excluded from this list are two banking applications which are effectively
irreplaceable, as they employ some additional anti-tampering and security
measures. I still keep a burner phone around for this -- even though I'm able to
do a lot from the website, there are a few things like digital check deposit and
paying rent through Zelle that I can't do without the mobile app.
# There were somethings that I overlooked in Andrid land because they were built in:
# wlsunset, grim instead of sct, scrot
I'll get into the specifics of using some of these applications (like GNU Emacs)
later in the article.
* Issues Encountered
As one may expect, there are several issues that come up -- some too fundamental
to be addressed by a mere bug report -- when daily-driving the PinePhone. I
believe that most of these make the PinePhone a non-starter for anyone with a
relatively normal use-case. I'm hopeful they'll be resolved in time, but for
now, I think rescuing old mobile hardware neglected by their vendors is strictly
a hobbyist activity.
** Modem: Frequent disconnects, not receiving calls
The modem has been the single most frustrating part about using the PinePhone.
For background: the PinePhone uses a Quectel EG25-G modem, which is effectively
a SOC of its own, running a little embedded Linux distribution distinct from the
rest of the PinePhone. So if the firmware is dogshit ([[https://www.toomanyatoms.com/computer/pinephone.html#modem][which it is]], if you're
using the firmware from Quectel), it can run hot or draw a stupid amount of
power while the main SOC is in standby and drain the battery.
Fortunately, Biktorgj maintains a [[https://github.com/Biktorgj/pinephone_modem_sdk][free firmware implementation]] for the EG25-G
which is much better. Battery life on standby went from a couple of hours to a
whole day when I made the switch.
Regardless of firmware, I was having an issue where the modem would disconnect
from the phone every couple of minutes, which was very frustrating. This is
resolved by using =udev= to set =ATTR{power/control}= to =on= instead of =auto=, at a
cost in power consumption, but the usability is worth the hit in battery life.
Having a distinct modem daughter card seems to be a design feature, at least [[https://puri.sm/posts/the-design-behind-a-modular-and-secure-mobile-phone/][in
the eyes of Purism]], because it means that "those network components are fully
isolated from the main board and cannot freely access the rest of the system."
Indicating "[t]hat is an important privacy feature." My understanding is that
there's no open (hardware) implementation of 4G, but I haven't been able to find
a convincing argument that it can't be done.
Biktorgj's project only addresses parts of the firmware, and not the baseband
implementation. You still need to install ADSP firmware blobs for that. And,
humorously, Quectel doesn't seem to officially publish them, so the PINE64
community just maintains a collection of four different versions with varying
levels of stability depending on the cellular carrier being used.
One issue that I have yet to solve is that, if the phone is sitting in standby
for a while (say, overnight), I can't receive or make calls. But it's
inconsistent. For example, at the time of writing this, I'd had my phone in
standby without restarting for several nights, but I could make a call just now.
It's hard to gleam what's going on from the logs, too. These are some errors I
was able to find.
#+BEGIN_SRC prog
Jul 30 02:02:34 theta daemon.info [2179]: <info> [modem0/bearer1] verbose call end reason (3,1056): [cm] lrrc-connection-establishment-failure-timer-expired
Jul 30 02:02:34 theta daemon.info [2179]: <info> [modem0] state changed (connected -> registered)
Jul 30 02:02:34 theta daemon.info [2179]: <info> [modem0/bearer1] connection #1 finished: duration 22362s, tx: 285780 bytes, rx: 1471594 bytes
...
Jul 30 06:02:43 theta daemon.info [2179]: <info> [modem0/bearer1] verbose call end reason (3,1034): [cm] esm-sync-up-with-nw
Jul 30 06:02:43 theta daemon.info [2179]: <info> [modem0] state changed (connected -> registered)
Jul 30 06:02:43 theta daemon.info [2179]: <info> [modem0/bearer1] connection #2 finished: duration 14407s, tx: 172 bytes, rx: 555 bytes
#+END_SRC
For me, this isn't a huge problem. 90% of the time I'm getting a phone call,
it's Microsoft Sam asking me if I want to extend my car's warranty. If it's
someone actually trying to get a hold of me, they're likely to leave a
voicemail, which I am alerted to even if the phone's in this unusual state of
being unable to receive calls.
So running custom firmware on the modem is currently the best way to have a
moderately-usable modem. With [[https://github.com/fwupd/fwupd/commit/17854099d0e614c06b5a40d2477477ee3d850fc7#diff-5a375f230ee85cf307402aaabd8da6e6dbc8ad32e0a5e9f6d302a896a8387c4cR557][the news]] that Quectel could potentially be locking
down their hardware and preventing users from flashing their own firmware, I
have the same sentiments as Linus Torvalds holds of NVIDIA.
#+CAPTION: Linus Torvalds commenting on the closed nature of NVIDIA.
[[./Torvalds Nvidia.jpg]]
The sad thing is, this modem seems to be the best supported piece of hardware in
ModemManager now, and I don't think we'll see this much work on other modems for
a long while. This Quectel piece of shit will probably be the only usable option
in e.g. PostmarketOS for the foreseeable future.
** Occasional Non-Wake from Suspend
This hasn't been a problem as of late, but my phone would occasionally refuse to
wake up from standby. That is, when the phone goes to sleep because the screen's
been off for 2 minutes, it suspends. But the power button doesn't wake it, nor
does the phone respond to the [[https://wiki.postmarketos.org/wiki/TTYescape][TTYEscape]] key sequence.
I configured =syslogd= to write to disk instead of shared memory to get some
indication of what might be going on, but since doing the issue hasn't presented
itself. I suspected that =gnome-power-manager= was failing to register ACPI
wake-up events in some cases, but I don't see /any/ messages about ACPI in my
=dmesg= output. Seems like [[https://linux-sunxi.org/PSCI][PSCI]] is what's being used, which tracks since the [[http://acpi.info/DOWNLOADS/ACPIspec50.pdf][first
version of the standard to acknowledge ARM]] was only released a decade ago. I
don't know enough about PSCI to hypothesize about what might have been going on.
What matters is that I haven't been noticing the problem.
** Suspend Prevents Alarm from Going off
Rarely a problem for me since I plug my phone in at night and don't have it
configured to suspend when on AC power, but if the phone is suspended, there's
nothing to wake the phone up to check for alarms you've set in =gnome-clocks=. The
effect is that your alarm isn't going to go off.
Fortunately, the modem is almost always running and able to wake the phone, so
if you're using Biktorgj's firmware, you can send the modem a text message to
[[https://github.com/the-modem-distro/pinephone_modem_sdk/blob/kirkstone/docs/SMS_INTERFACE.md][schedule a wake-up call]]. It's a nice solution to a pretty unfortunate problem.
There are some [[https://static.lwn.net/images/pdf/suspend_blockers.pdf][papers]] on how power management is done in Android-land, which
makes me think that user space alarms /could/ work in the presence of an automatic
suspend framework. In fact, the RTC available on the PinePhone [[https://codeberg.org/Silmathoron/pinephone-autowake][is sufficient]] to
trigger a wake event, but configuring it seems to be quite user-unfriendly. I
hope that we see more libraries and software development kits for Linux that
take advantage of mobile hardware capabilities.
** Battery Life
As stated above, battery life out-of-the-box is awful. It's made much better by
installing Biktorgj's modem firmware, but is still somewhat underwhelming. I've
seen this [[https://amosbbatto.wordpress.com/2021/12/10/comparing-l5-and-pp/][attributed to the phone's design consisting of four separate chips]].
The [[https://amosbbatto.wordpress.com/2021/12/10/comparing-l5-and-pp/][PinePhone Keyboard]] comes with a 6000mAh internal battery to effectively
extend the battery capacity of the PinePhone. I haven't purchased one yet.
What I have done is spend about $40 on a 40000mAh power bank from Anker. That
was a good investment, since I can charge my PineBook and other devices as well.
I just keep that and a spare USB-C cable in my bag (which I bring with me
practically everywhere), and I haven't had any issues.
I'm hopeful that PINE64 eventually releases a back cover that would support a
higher-density battery (maybe 5000mAh). My hesitancy with the keyboard is that
I'm worried it would be a little too chunky.. but I wouldn't expect a slightly
wider battery to make it difficult to fit the phone in my pocket.
** Mobile hotspot not working
Non-issue as of PostmarketOS 21.12. The hotspot works fine, and I use it
extensively to connect my PineBook to the internet while on the go.
Even in 21.06, it wasn't a terrible issue to have to work around. The issue was
that I couldn't connect to the internet directly, but I could still connect to
the PinePhone, so SSH tunneling and a SOCKS5 client were all I needed to browse
the web or check my email. It was [[https://forum.pine64.org/showthread.php?tid=10974][apparently a kernel issue]].
** On-screen Keyboard
This is a difficult issue to put into words, and as such I've had a hard time
looking around for mention of it on the bug tracker or elsewhere.
Sometimes, when typing with [[c9ed5147c6d4af76366ee706cdd3dfee3a7b0e14ac0789b24d2a701f2455d0e8ef3bf377bb07ad4dfb2121e5d62877c014b5bbb3c7e24931cdf75bd702f897c43462eaddd6cf255431367b2956a8cb7b26984ed2de05d37efd32068ec15538f2][Squeekboard]] (the on-screen keyboard that comes with
Phosh), I'll press a key once and two characters will be inserted -- as if the
phone registered it as two taps in quick succession.
A solution I'd like to try is to patch Squeekboard and have it keep a timer for
determining how much time there elapses between key press events. If the pause
is too short, then we'd drop the second key press. Squeekboard seems to be
mostly written in Rust, so I find that to be an enticing quality-of-life
improvement project, but I think I've done enough technical work in this post
already, so I'll do it another time.
** Bluetooth Audio
# TODO: This section needs to be updated.
# Procedure:
# 1. `nice -11 mpd`
# 2. `mpc play`
# 3. `pkill mpd`
# 4. `nice -11 mpd`
# 5. Music starts playing without hiccups.
Bluetooth audio remains a pain point, and an elusive one at that. It works only
when attempting to troubleshoot -- never when I actually want to use my
PinePhone as a Bluetooth audio source. The [[https://wiki.archlinux.org/title/Bluetooth_headset#Connecting_works,_but_there_are_sound_glitches_all_the_time][Arch Linux Wiki has a page]] on
troubleshooting my situation, which is that "[c]onnecting works, but there are
sound glitches all the time." In my case, I have no issues connecting to my
car's stereo system, for example, but 90% of the time I will have audio buffer
overruns that cause the audio to pause every second or so. It is infuriating to
have to listen to.
[[# https://forum.pine64.org/showthread.php?tid=10810][CyberSeb on the PINE64 forum has a post]] for configuring the Bluetooth stack to
work better, and I have some recollection of the second step working well, but
as of late the script I have to run those commands (included below) no longer
works. It tends to fail at =pactl set-port-latency-offset=, either because
=BLUEZCARD= isn't defined, or something else. The error messages are hardly
deterministic.
I've added some debugging output and the good ol' song and dance for making Bash
less shitty, in hopes that I'll be able to get more data the next time I run it.
#+BEGIN_SRC sh
#!/usr/bin/env bash
set -euo pipefail
BLUEZCARD=`pactl list cards short | egrep -o bluez.*[[:space:]]`
echo "Determined BLUEZCARD to be $BLUEZCARD"
pactl set-port-latency-offset $BLUEZCARD headset-output 100000
sudo service bluetooth restart
#+END_SRC
It's a little strange that I mentioned only using the second step. This is
because I was convinced that my phone wasn't running Pulse. I really thought it
was on Pipewire, but it seems my memory failed me.
#+BEGIN_SRC prog
theta:~$ sudo apk add pipewire-pulse
ERROR: unable to select packages:
pipewire-pulse-0.3.51-r1:
breaks: postmarketos-ui-phosh-18-r3[!pipewire-pulse]
satisfies: world[pipewire-pulse] gnome-settings-daemon-42.1-r0[pulseaudio] postmarketos-base-ui-gnome-1-r3[pulseaudio] gnome-session-42.0-r1[pulseaudio-alsa]
#+END_SRC
It's Pulse, and I'm hesitant to screw with it's niceness because it does not
have a reputation of being resourceful or performant. I'm wondering if these
issues would go away if I /did/ switch over to using Pipewire, but the error from
=apk= above makes me think that it would be a hard nut to crack. I've tried
[[https://wiki.archlinux.org/title/PulseAudio/Troubleshooting#Setting_the_default_fragment_number_and_buffer_size_in_PulseAudio][setting a default fragment size]] in Pulse as a more reasonable workaround while I
wait for Pulse to eventually die a slow and painful death.
** Cross Compiling Woes
PostmarketOS maintains a tool for cross-compiling packages (among other things)
called [[https://wiki.postmarketos.org/wiki/Installing_pmbootstrap][pmbootstrap]], which I find to be quite nice. =pmbootstrap init= will set you
up with a chroot jail pinned at a specific version of PostmarketOS (or =edge=) for
a specific device and architecture, and from there you can use =pmbootstrap build=
to cross-compile packages for installation on the PinePhone. Cross-compiling can
be a bit slow (it literally took a day to compile Emacs PGTK) because, in most
cases, the toolchain will be running under [[https://www.qemu.org/docs/master/user/main.html][QEMU's user space emulator]], but it's
probably better than melting your phone trying to compile things on the device.
I've had a few sour experiences with cross-compiling, but the issue always came
down to poor quality control in Alpine's =community= repository rather than the
cross compiling workflow not being good. Before learning about [[https://git.sr.ht/~martijnbraam/numberstation][numberstation]], I
was trying to use =gnome-authenticator=, and the version available in =apk= was
[[https://gitlab.alpinelinux.org/alpine/aports/-/issues/13296][completely unusable]]. I tried to build a newer version, which ended up being
incompatible with the libraries installed in my version of PostmarketOS, and I
tried to build a really old version (back when the application was written in
Python), which didn't work either. I ended up cross-compiling [[https://gitlab.alpinelinux.org/alpine/aports/-/issues/13296][otpclient]] with
little friction.
** Lack of software
A lot of what I want to do is well-supported by existing Linux packages, but
there are a couple of blind spots like Signal. In theory, I can use Pidgin and
[[https://signald.org/][signald]], but I haven't been bothered to try it.
In these cases, the solution is to write your own software.
#+CAPTION: One of the first applications I wrote for my PinePhone: a basic Signal client, in Rust, running on my workstation. I obfuscated my partner's phone number for obvious reasons.
[[./Warp MVP.png]]
Being able to do this without the complexity (and Java requirement) of the
Android SDK is the biggest appeal of running a Linux phone to me. So much so
that I've got an entire section dedicated to it later in this article.
* The Good Parts
I started off talking about the problems tat come with using a device like the
PinePhone, but I've continued to use it because for me, the benefits far
outweigh the issues, which I'll outline below.
** Emacs on Mobile
This is the "killer feature" for me.
You might expect Emacs on mobile to be little more than a novelty, but the only
application I think I use more than it is Firefox. I've now got a friction-less
=org-capture= device in my pocket. If an idea pops into my head, or if someone
tells me to do something, I just pull out the PinePhone, =M-<RET> TODO= and type
it in. That note then makes its way to my other machines by the magic of
[[https://syncthing.net/][Syncthing]]. Another use for mobile Emacs is that, sometimes, I'll cuddle up to my
partner, and they'll fall asleep on me, but I really want to work on a blog
post. If this happens, I can use [[https://www.gnu.org/s/tramp/][TRAMP]] to edit the draft over SSH. In fact, I've
literally edited /this blog post/ from my bed while Oli was asleep on me, using
mobile Emacs.
The other uses are honestly pretty mundane. I like being able to use =dired= to
browse the local filesystem; I can use [[https://github.com/speedenator/malyon][Malyon]] to play [[https://en.wikipedia.org/wiki/Zork][Zork]] & friends on the go;
and if I'm really bored, I can just start hacking on Scheme or Elisp code while
I'm sitting on the train.
I was anticipating wanting to pick up [[https://github.com/emacs-evil/evil][evil-mode]], thinking it would be better for
use with an on-screen keyboard, but the Squeekboard terminal layout is actually
quite good for Emacs-ing. I can whip around a buffer at about a fifth my speed
on my workstation, which is pretty good for only using a fifth of my God-given
fingers. Icons (I don't disable =tool-bar-mode= in my mobile configuration) make
for a slightly nicer touch input experience, too.
#+CAPTION: GNU Emacs on the PinePhone. Not blurry, after the process described below.
[[./Pinephone Rnning Emacs.JPG]]
It was a little difficult to get things running. Emacs is in the PostmarketOS
repos.. except the package sucks because it's the old X11 Emacs, and Phosh is
Wayland, so it has to run through Xwayland and fractional scaling makes it a
blurry mess. To resolve that, I ripped a ton of code out of the [[https://git.alpinelinux.org/aports/tree/community/emacs?h=master][APKBUILD]] and
pointed it at a tarball for Emacs =master= (which has [[https://mail.gnu.org/archive/html/emacs-devel/2021-12/msg00126.html][had the PGTK branch merged]]).
#+BEGIN_EXPORT html
<div class="fold-hidden" data-name="APKBUILD for Emacs (pure GTK branch">
#+END_EXPORT
#+BEGIN_SRC prog
# Maintainer: Natanael Copa <ncopa@alpinelinux.org>
# Contributor: Timo Teräs <timo.teras@iki.fi>
pkgname=emacs
pkgver=29.0
pkgrel=7
pkgdesc="The extensible, customizable, self-documenting real-time display editor"
arch="all"
depends="emacs-nox"
url="https://www.gnu.org/software/emacs/emacs.html"
license="GPL-3.0-or-later"
makedepends="
autoconf
automake
gawk
gmp-dev
gnutls-dev
harfbuzz-dev
jansson-dev
linux-headers
ncurses-dev
ncurses-libs
texinfo
"
subpackages="$pkgname-doc $pkgname-nox"
source="emacs-$pkgver.tar.xz"
case $CARCH in
riscv64|s390x)
# limited by librsvg (rust)
_docdir="nox"
;;
,*)
makedepends="
$makedepends
alsa-lib-dev
fontconfig-dev
giflib-dev
glib-dev
gtk+3.0-dev
libgccjit-dev
libjpeg-turbo-dev
libpng-dev
librsvg-dev
libxaw-dev
libxml2-dev
libxpm-dev
pango-dev
tiff-dev
"
subpackages="
$subpackages
$pkgname-gtk3
"
_docdir="gtk3"
;;
esac
prepare() {
default_prepare
./autogen.sh
}
_build_variant() {
cd "$builddir/$1"
shift
CFLAGS=-fno-pie \
LDFLAGS=-no-pie \
./configure \
--build=$CBUILD \
--host=$CHOST \
--prefix=/usr \
--sysconfdir=/etc \
--libexecdir=/usr/lib \
--localstatedir=/var \
--with-gameuser=:games \
--with-gpm \
--with-harfbuzz \
--with-json \
"${@}"
make $_extra
}
_build_gtk3() {
_build_variant gtk3 \
--with-pgtk \
--with-xft \
--with-jpeg=yes \
--with-tiff=no \
--with-gif=ifavailable \
--with-xpm=ifavailable
}
# --with-x-toolkit=gtk3 \
_build_nox() {
_build_variant nox \
--without-sound \
--without-x \
--without-file-notification
}
build() {
mkdir -p nox
mv ./* nox || true
case "$CARCH" in
riscv64|s390x)
# limited by librsvg (rust)
_build_nox
;;
,*)
cp -a nox gtk3
_build_nox
_build_gtk3
;;
esac
}
package() {
mkdir -p "$pkgdir"
}
doc() {
depends=""
mkdir -p "$subpkgdir"
cd "$builddir"/"$_docdir"
make DESTDIR="$subpkgdir" install
# remove conflict with ctags package
mv "$subpkgdir"/usr/share/man/man1/ctags.1.gz "$subpkgdir"/usr/share/man/man1/ctags.emacs.1.gz
# only keep info and man directories, all other is in the specific package
rm -rf "${subpkgdir:?}"/usr/bin \
"$subpkgdir"/usr/lib \
"$subpkgdir"/usr/share/appdata \
"$subpkgdir"/usr/share/applications \
"$subpkgdir"/usr/share/emacs \
"$subpkgdir"/usr/share/icons \
"${subpkgdir:?}"/var \
"$subpkgdir"/usr/lib/systemd
}
_subpackage() {
cd "$builddir/$1"
make DESTDIR="$subpkgdir" install
# remove conflict with ctags package
mv "$subpkgdir"/usr/bin/ctags "$subpkgdir"/usr/bin/ctags.emacs
rm -rf "$subpkgdir"/usr/share/info \
"$subpkgdir"/usr/share/man
# fix user/root permissions on usr/share files
find "$subpkgdir"/usr/share/emacs/ -exec chown root:root {} \;
find "$subpkgdir"/usr/lib -perm -g+s,g+x ! -type d -exec chmod g-s {} \;
# fix perms on /var/games
chmod 775 "$subpkgdir"/var/games
chmod 775 "$subpkgdir"/var/games/emacs
chmod 664 "$subpkgdir"/var/games/emacs/*
chown -R root:games "$subpkgdir"/var/games
# remove useless systemd user file
rm -rf "$subpkgdir"/usr/lib/systemd
}
nox() {
pkgdesc="$pkgdesc - without X11"
depends="
!emacs-gtk3
!emacs-gtk3-nativecomp
!emacs-x11
!emacs-x11-nativecomp
"
_subpackage nox
}
gtk3() {
pkgdesc="$pkgdesc - with GTK3"
depends="
!emacs-gtk3-nativecomp
!emacs-nox
!emacs-x11
!emacs-x11-nativecomp
desktop-file-utils
hicolor-icon-theme
"
_subpackage gtk3
}
sha512sums="
20c96e4485b9acbc5c9049bca9b4d9675cd5f4062cd04a9abde4fb7088c7dc55e3bf473acce8f447825c0c1fd9a5def23623d0219bc0353b31892a0cc23f7884 emacs-29.0.tar.xz
"
#+END_SRC
#+BEGIN_EXPORT html
</div>
#+END_EXPORT
There is no Emacs =29.0= (yet, at the time of writing this), that's just so =apk=
knows that this is newer than what's in the repositories.
** YouTube on Mobile
I was a [[https://en.wikipedia.org/wiki/NewPipe][NewPipe]] user when I was using Android. I'd frequently find it unusable,
and the times it was usable, I'd still get annoying toasts warning me of errors,
just about every time I watched a video. The F-Droid package didn't keep up with
YouTube cat-and-mouse game as quickly as youtube-dl did. I always thought about
how nice it would be to use =mpv= and =yt-dlp= just like I do on desktop, and that's
now a reality.
#+CAPTION: mpv playing one of Andreas Kling's YouTube videos on SerenityOS, using yt-dlp to resolve the media stream.
[[./Pinephone Running mpv.png]]
I get the video URLs from RSS and invoke =mpv= from the terminal. I find it
convenient. The only issue I had is that the screen blanks automatically even
when a video is playing, but this is easily remedied by prefixing =mpv= with
=gnome-session-inhibit --inhibit idle= in the shell.
** Better Music Player
LineageOS included the old Cyanogenmod Music app [[https://github.com/CyanogenMod/android_packages_apps_Eleven][Eleven]], and that's what I used
when I was on Android. I didn't see a use in using any other music player since
they all seem to use the same Android APIs and, hence, all suck as much as
Eleven does. Among other things, it cuts out frequently (presumably the process
getting killed due to memory pressure), and it can't even load a damned jpeg.
#+CAPTION: Album artwork being mangled by some bug unknown to me.
[[./Music on Zeta.png]]
So I was quite happy to be able to use =mpd= to listen to music on the PinePhone.
My entire library's managed with Syncthing.
** Running scripts, cron, other automation
Another "killer feature" is just being able to automate things with =bash= and
=cron= the way I would on desktop. One pain point I remember particularly when I
was using Android was manually adjusting the screen gamma in settings. Now I can
just use =cron= to run =wlsunset= at a particular hour.
I suppose that's the only example that's worth mentioning. I haven't leveraged
it as much as I could have.
** Convergence
A selling point of the PinePhone is [[https://yewtu.be/watch?v=yBeza4UNOm8][convergence]], enabling you to plug your phone
into a monitor and keyboard (over USB-C), and use it as if it were a desktop
computer. I haven't taken advantage of this yet, but I can SSH into my phone,
which is far better than Android, and enough for me to be happy -- just being
able to pull/push files over rsync, run shell commands over SSH using an actual
keyboard...
The only thing I wish I could do is send SMS over SSH and get notifications from
my phone on my workstation. SMS messages can (theoretically) be sent using
=mmcli=, and I'm not sure about notifications. Perhaps I've made a programming
project for myself.
** Run Linux Desktop Applications
Generally speaking, all of the above points boil down to the PinePhone enabling
me to run Linux desktop applications on mobile.
One consideration is the difference in dotfiles between mobile and desktop. So
far, Emacs is the only place I've had to consider this. Basically, I just drop
the theming and any configuration related to programming language modes. I still
bring in my Org configuration and all my quality-of-life changes.
** Software Development Freedom
Cover this below
* Software Development
This is part of "The Good Parts", but I figured it's big enough to be a section
of its own.
** Software Stack Freedom
If you're at least mildly familiar with Android, you know that the Java
ecosystem is nearly unavoidable if you're doing application development for the
platform.[fn:10] The NDK enables application developers to write code in other
languages (provided they "compile down" to machine code) but it isn't practical
to write an entire application this way, as NDK code can't interact with the
system's APIs. Furthermore, the Android SDK is a pain in the ass to use if
you're not using [[https://en.wikipedia.org/wiki/Android_Studio][Google's IDE]]. It's doable, and I have [[https://git.sr.ht/~jakob/mines][done it in the past]], but
I got frustrated before I could set up an emulator for improving the feedback
loop. I was literally pushing to my device via =adb= on every build if I wanted to
experiment with something. I'm describing this as pain-inducing, but it's easy
to understand why it is this way. Google (and Apple) want to have uniformity
across their platforms' third-party applications, so they impose strong opinions
(you /must/ use our UI framework, you /must/ use our Java APIs).
Comparatively, the applications that run on my PinePhone are literally the same
applications that run on my workstation. I can use any language I want, provided
it supports AArch64. I can develop and test on my workstation, and then push it
to the PinePhone with high confidence that it will work as intended.
I've been writing my applications in Rust with =gtk-rs= and =libhandy=. There's been
a (somewhat recent) distinction between "application programming languages" and
"systems programming languages." Rust falls into the latter. The distinction is
somewhat arbitrary as you can write an application in assembly, but the reason
it's come up in recent years is because people want a way to describe languages
that (1) aren't interpreted or VM languages and (2) don't have a convenient
garbage collector. These sorts of language seem to work quite well for a
resource-constrained environment like the PinePhone, even if it is somewhat more
difficult than using something like Python or Ruby.
Using Rust is perhaps a bit overkill. I'm sure Vala would have been a good
choice, too, since it compiles to C, but I went with Rust because I'm more
comfortable with it and it has a ecosystem of libraries for the sorts of things
I want to do.
I've spoken a lot about the language decision, but there's the decision of UI
toolkit too. I went with GTK3 and [[https://gitlab.gnome.org/GNOME/libhandy][libhandy]]: the classic GNOME UI toolkit and
[[https://puri.sm/][Purism]]'s supporting library for adaptive, mobile-friendly layouts and widgets.
But that isn't the only option available. Still in GNOME land, there is GTK4 and
[[https://blogs.gnome.org/alexm/2021/12/31/libadwaita-1-0/][libadwaita]], which I'll probably be using in the near future. I'm just a little
slow to start using cool new things. There are /many/ more choices on the Plasma
Mobile side of the house: [[https://develop.kde.org/frameworks/kirigami//][Kirigami]], [[https://mauikit.org/][MauiKit]] (built on top of Kirigami), plain
[[https://doc.qt.io/qt-5/qtquick-index.html][QtQuick]], or Sailfish OS's [[https://sailfishos.org/develop/docs/silica/][Silica]]. While GTK and QT are the leading frameworks, I
was keeping a close eye on [[https://github.com/dvdsk/pods][pods]], a PinePhone-oriented application using Rust's
[[https://github.com/iced-rs/iced][iced]], which is neither GTK nor QT. Unfortunately, it looks to have since
stagnated. But [[https://sr.ht/~mil/mepo/][mepo]], a maps application, is a surprisingly pleasant mobile
experience and is written just in SDL.
As an aside, I'd like to experiment with some immediate-mode UI frameworks on
the PinePhone. GTK is relatively performant, but I'm curious about whether
something like [[https://github.com/emilk/egui][egui]] would be "snappier". Hell, maybe it would be interesting to
try and write my own UI framework.
*** "Tunes", an MPD Client for Rust
To demonstrate the GTK3 and libhandy combo, I decided to write the minimum
viable product of an application I want on my PinePhone that, to my knowledge,
doesn't exist yet. A graphical MPD client.
Yes... I've been using =mpc= in the terminal emulator since I got the phone. It's
not as pleasant when you don't have a real keyboard, so this application will
theoretically improve my quality-of-life.
But, because I don't want this post to take any longer than it already has, I'm
just going to write about what I could get done in a few weeknights. It's a
single-file, and fairly self-contained.
*** Show Me The Code!
Hey, okay! Don't have a cow, man! It's a few hundred lines and I've dumped it
here under a fold since it's a few hundred lines. You can find it [[https://git.sr.ht/~jakob/tunes][on SourceHut]]
as well.
#+BEGIN_EXPORT html
<div class="fold-hidden" data-name="source code for Tunes">
#+END_EXPORT
#+BEGIN_SRC rust
// Copyright © 2021-2022 Jakob L. Kreuze <[REDACTED]>
//
// This file is part of Tunes.
//
// Tunes is free software; you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation; either version 3 of the
// License, or (at your option) any later version.
//
// Tunes 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 Affero General
// Public License for more details.
//
// You should have received a copy of the GNU Affero General Public
// License along with Tunes. If not, see <http://www.gnu.org/licenses/>.
use futures::{channel::mpsc, StreamExt};
use glib::clone;
use gtk::prelude::*;
use gtk::subclass::prelude::ObjectSubclassExt;
use gtk::{gdk_pixbuf, gio, glib, pango};
use libhandy::prelude::*;
use libhandy::{ApplicationWindow, HeaderBar};
use mpd::idle::Idle;
use mpd::Client;
const MPD_HOST: &str = "127.0.0.1:6600";
fn main() {
let application = gtk::Application::builder()
.application_id("space.jakob.Tunes")
.build();
// We have to wait until the `activate` signal is fired before we can do our
// setup.
application.connect_activate(|app| {
// Our event-handling code will look a bit like what's common in SDL
// with their `SDLPollEvent` interface, in the sense that we'll have all
// of the different sub-systems of this application notify the main
// event loop by way of a channel.
let (sender, mut receiver) = mpsc::channel(1024);
// Load all of the mobile UI support code from `libhandy`.
libhandy::init();
// `mpd` will notify us of events. Let's spin up a thread to listen for
// those notifications, and shuttle them through a channel as they
// arrive.
std::thread::spawn(clone!(@strong sender => move || {
let mut conn = Client::connect(MPD_HOST).unwrap();
while let Ok(_subsystems) = conn.wait(&[mpd::idle::Subsystem::Player]) {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::MpdEvent)
.expect("Couldn't notify thread");
}
}));
// We'll connect to the MPD daemon here so we can populate the UI with
// some information from the current state.
let mut conn = Client::connect(MPD_HOST).unwrap();
// We'll have two "views" in our application: one for viewing and
// manipulating the current `mpd` queue, and another for searching for
// songs to add to the queue. In GTK, we can handle switching between
// these different views using a Stack.
let stack = gtk::Stack::new();
stack.set_expand(true);
let song_info = SongInfo::new(sender.clone());
stack.add_named(song_info.as_ref(), "current_song");
stack.set_child_title(song_info.as_ref(), Some("Now Playing"));
stack.set_child_icon_name(song_info.as_ref(), Some("audio-speakers-symbolic"));
let query_info = QueryInfo::new(sender.clone());
stack.add_named(query_info.as_ref(), "query_songs");
stack.set_child_title(query_info.as_ref(), Some("Search Database"));
stack.set_child_icon_name(query_info.as_ref(), Some("system-search-symbolic"));
// The `HeaderBar` is a GTK concept that libhandy plays nicely with. On
// desktop, the elements for switching stack views will show up there.
// On mobile, it will show up in a `ViewSwitcherBar` at the bottom.
let header_bar = HeaderBar::builder()
.show_close_button(true)
.title(&header_title(&mut conn).unwrap())
.build();
let view_switcher_title = libhandy::ViewSwitcherTitle::builder()
.title("Tunes")
.stack(&stack)
.build();
header_bar.add(&view_switcher_title);
let view_switcher_bar = libhandy::ViewSwitcherBar::builder()
.visible(true)
.can_focus(false)
.stack(&stack)
.reveal(true)
.build();
// The window needs a single child, so we'll join the header bar, the
// stack, and the view switcher into a single box.
let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
content.set_vexpand(true);
content.add(&header_bar);
content.add(&stack);
content.add(&view_switcher_bar);
// Finally, the window. It's tied to a child, which we made above, and
// the GtkApplication that we declared at the beginning of `main`.
let window = ApplicationWindow::builder()
.default_width(350)
.default_height(70)
.modal(true)
.child(&content)
.build();
window.set_application(Some(app));
window.show_all();
// This isn't perfect (it won't run when the window gets its initial
// size), but this is how we notify that the album art display should be
// resized.
window.connect_configure_event(clone!(@strong sender => move |_, _| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::WindowResizeEvent)
.expect("Couldn't notify thread");
false
}));
// Now that everything's been allocated a window, let's go ahead and
// update the widgets.
song_info
.update(&mut conn)
.expect("Couldn't update song info");
// The following code will fill the search view with every song in the
// database. If you have a music library as big as mine, it will
// negatively impact startup time. This could be done in, for example, a
// worker thread, but I've just omitted it because I don't want this
// example to be more complex than it has to be.
//
// let mut query = mpd::Query::new();
// query.and(mpd::Term::Any, "");
// let songs = conn.search(&query, (0, 65535));
// for song in songs.unwrap() {
// query_info.model.insert(0, &SongObject::new(&song));
// }
// Finally, we'll start the "main event loop" we've been talking about
// in the main context of the application.
let main_context = gtk::glib::MainContext::default();
main_context.spawn_local(async move {
let mut conn = Client::connect(MPD_HOST).unwrap();
while let Some(event_type) = receiver.next().await {
match event_type {
StateUpdateKind::MpdEvent => {
if let Ok(title) = header_title(&mut conn) {
header_bar.set_title(Some(&title));
song_info
.update(&mut conn)
.expect("Couldn't update song info");
}
}
StateUpdateKind::WindowResizeEvent => {
song_info
.update_album_art(&mut conn)
.expect("Couldn't update album art");
}
StateUpdateKind::QueryUpdateEvent(query_string) => {
// Let's not produce massive queries while the user is typing :)
if query_string.len() <= 2 {
continue;
}
// Start from a blank slate.
query_info.model.remove_all();
// Query on all fields, case-insensitively, for the text
// that the user input.
let mut query = mpd::Query::new();
query.and(mpd::Term::Any, &query_string);
let songs = conn.search(&query, (0, 65535));
// Insert them all into the model. This is reversed,
// which I don't consider to be a big deal. It's far
// less complex than adding it in order, which you will
// see below in the code that handles the queue.
for song in songs.unwrap() {
query_info.model.insert(0, &SongObject::new(&song));
}
}
StateUpdateKind::QueueDeleteRequest(index) => {
conn.delete(index).expect("Couldn't dequeue song");
}
StateUpdateKind::QueueAddRequest(filename) => {
conn.push_str(filename).expect("Couldn't queue song");
}
StateUpdateKind::PlaybackStateChange(action) => {
dispatch_playback_state_change(&mut conn, action)
.expect("Couldn't queue action");
}
}
}
});
});
application.run();
}
/// Take action on `conn` based on a `PlaybackStateChange` notification
fn dispatch_playback_state_change(
conn: &mut mpd::Client,
action: PlaybackStateChange,
) -> anyhow::Result<()> {
use PlaybackStateChange::*;
match action {
SkipBackwards => conn.prev()?,
SkipForwards => conn.next()?,
Start => conn.play()?,
Stop => conn.stop()?,
Pause => conn.pause(true)?,
}
Ok(())
}
/// Kind of event we can notify the UI future about
#[derive(Debug)]
enum StateUpdateKind {
MpdEvent,
WindowResizeEvent,
QueryUpdateEvent(String),
QueueAddRequest(String),
QueueDeleteRequest(u32),
PlaybackStateChange(PlaybackStateChange),
}
/// A simple action that affects playback state.
#[derive(Debug)]
enum PlaybackStateChange {
Start,
Stop,
Pause,
SkipBackwards,
SkipForwards,
}
/// Produce a short status line for the current state of `conn`.
fn header_title(conn: &mut mpd::client::Client) -> anyhow::Result<String> {
let status = conn.status();
let state_descriptor = match status?.state {
mpd::status::State::Stop => "[STOPPED]",
mpd::status::State::Pause => "[PAUSED]",
mpd::status::State::Play => "[PLAYING]",
};
if let Some(song) = conn.currentsong()? {
Ok(format!(
"{} {} - {}",
state_descriptor,
song.title.unwrap_or_else(|| "Untitled".into()),
song.artist.unwrap_or_else(|| "Untitled".into()),
))
} else {
Ok("Tunes: No Song".into())
}
}
/// View for information about the currently playing song.
struct SongInfo {
container: gtk::Box,
album_art: gtk::Image,
song_text: gtk::Label,
model: gio::ListStore,
}
impl SongInfo {
fn new(sender: mpsc::Sender<StateUpdateKind>) -> Self {
let container = gtk::Box::new(gtk::Orientation::Vertical, 16);
let album_art = gtk::Image::new();
let song_text = gtk::Label::new(None);
song_text.set_justify(gtk::Justification::Center);
song_text.set_line_wrap(true);
song_text.set_line_wrap_mode(pango::WrapMode::WordChar);
container.add(&album_art);
container.add(&song_text);
let action_bar = gtk::Box::new(gtk::Orientation::Horizontal, 16);
action_bar.set_halign(gtk::Align::Center);
let control_previous_song = gtk::Button::from_icon_name(
Some("media-skip-backward-symbolic"),
gtk::IconSize::SmallToolbar,
);
action_bar.add(&control_previous_song);
control_previous_song.connect_clicked(clone!(@strong sender => move |_| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::PlaybackStateChange(
PlaybackStateChange::SkipBackwards,
))
.expect("Couldn't notify thread");
}));
let control_start_song = gtk::Button::from_icon_name(
Some("media-playback-start-symbolic"),
gtk::IconSize::SmallToolbar,
);
action_bar.add(&control_start_song);
control_start_song.connect_clicked(clone!(@strong sender => move |_| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::PlaybackStateChange(
PlaybackStateChange::Start,
))
.expect("Couldn't notify thread");
}));
let control_pause_song = gtk::Button::from_icon_name(
Some("media-playback-pause-symbolic"),
gtk::IconSize::SmallToolbar,
);
action_bar.add(&control_pause_song);
control_pause_song.connect_clicked(clone!(@strong sender => move |_| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::PlaybackStateChange(
PlaybackStateChange::Pause,
))
.expect("Couldn't notify thread");
}));
let control_stop_song = gtk::Button::from_icon_name(
Some("media-playback-stop-symbolic"),
gtk::IconSize::SmallToolbar,
);
action_bar.add(&control_stop_song);
control_stop_song.connect_clicked(clone!(@strong sender => move |_| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::PlaybackStateChange(
PlaybackStateChange::Stop,
))
.expect("Couldn't notify thread");
}));
let control_next_song = gtk::Button::from_icon_name(
Some("media-skip-forward-symbolic"),
gtk::IconSize::SmallToolbar,
);
action_bar.add(&control_next_song);
control_next_song.connect_clicked(clone!(@strong sender => move |_| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::PlaybackStateChange(
PlaybackStateChange::SkipForwards,
))
.expect("Couldn't notify thread");
}));
let model = gio::ListStore::new(SongObject::static_type());
let listbox = gtk::ListBox::new();
listbox.bind_model(
Some(&model),
clone!(@strong sender => move |item| {
let sender = sender.clone();
let box_ = gtk::ListBoxRow::new();
let item = item
.downcast_ref::<SongObject>()
.expect("Row data is of wrong type");
let grid = gtk::Grid::builder().column_homogeneous(true).build();
let remove_individual_song = gtk::Button::from_icon_name(
Some("list-remove-symbolic"),
gtk::IconSize::SmallToolbar,
);
let index = item.property::<u32>("index");
remove_individual_song.connect_clicked(move |_| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::QueueDeleteRequest(index))
.expect("Couldn't notify thread");
sender
.try_send(StateUpdateKind::MpdEvent)
.expect("Couldn't notify thread");
});
grid.attach(&remove_individual_song, 0, 0, 1, 1);
let title_label = gtk::Label::new(None);
title_label.set_line_wrap(true);
title_label.set_line_wrap_mode(pango::WrapMode::WordChar);
item.bind_property("title", &title_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
grid.attach(&title_label, 1, 0, 1, 1);
let album_label = gtk::Label::new(None);
album_label.set_line_wrap(true);
album_label.set_line_wrap_mode(pango::WrapMode::WordChar);
item.bind_property("album", &album_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
grid.attach(&album_label, 2, 0, 1, 1);
let artist_label = gtk::Label::new(None);
artist_label.set_line_wrap(true);
artist_label.set_line_wrap_mode(pango::WrapMode::WordChar);
item.bind_property("artist", &artist_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
grid.attach(&artist_label, 3, 0, 1, 1);
grid.show_all();
box_.add(&grid);
box_.upcast::<gtk::Widget>()
}),
);
let scrolled_window =
gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE);
scrolled_window.add(&listbox);
scrolled_window.set_vexpand(true);
container.add(&action_bar);
container.add(&scrolled_window);
container.show_all();
SongInfo {
container,
album_art,
song_text,
model,
}
}
fn update_album_art(&self, conn: &mut mpd::Client) -> anyhow::Result<()> {
if let Some(song) = conn.currentsong()? {
// If we've been allocated a window, pick the least dimension (width
// or height) and divide that dimension by two to get the size (in
// pixels) that we'll scale the album art to. Otherwise, we default
// to 128.
let album_art_size = std::cmp::min(
self.container
.window()
.map(|x| x.width() / 2)
.unwrap_or(128),
self.container
.window()
.map(|x| x.height() / 2)
.unwrap_or(128),
);
let image_data = conn.albumart(&song)?;
let image_pixbuf = gdk_pixbuf::Pixbuf::from_stream(
&gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&image_data)),
gio::Cancellable::NONE,
)
.ok()
.and_then(|x| {
x.scale_simple(
album_art_size,
album_art_size,
gtk::gdk_pixbuf::InterpType::Hyper,
)
});
self.album_art.set_pixbuf(image_pixbuf.as_ref());
}
Ok(())
}
fn update(&self, conn: &mut mpd::Client) -> anyhow::Result<()> {
self.update_album_art(conn)?;
if let Some(song) = conn.currentsong()? {
let title = song.title.as_deref().unwrap_or("[Unknown]");
let artist = song.artist.as_deref().unwrap_or("[Unknown]");
let album = song
.tags
.get("Album")
.map(|x| x.as_str())
.unwrap_or("[Unknown]");
let text = format!("{}\n{} - {}", title, artist, album);
self.song_text.set_text(&text);
// We'll use `pango` attributes to make the display look nice and
// pretty. Scale the title of the song the most, and still make the
// other info reasonably large.
let attr_list = gtk::pango::AttrList::new();
let mut attr = gtk::pango::AttrFloat::new_scale(2.0);
attr.set_start_index(0);
attr.set_end_index(title.len() as u32);
attr_list.insert(attr);
let mut attr = gtk::pango::AttrFloat::new_scale(1.5);
attr.set_start_index(title.len() as u32 + 1);
attr_list.insert(attr);
self.song_text.set_attributes(Some(&attr_list));
}
self.model.remove_all();
for (i, song) in conn.queue()?.iter().enumerate() {
let index = i.try_into().unwrap();
let object = SongObject::new(song);
object.set_index(index);
self.model.insert(index, &object)
}
Ok(())
}
}
impl AsRef<gtk::Widget> for SongInfo {
fn as_ref(&self) -> >k::Widget {
self.container.upcast_ref()
}
}
/// View for selecting songs to add to the queue.
struct QueryInfo {
container: gtk::Box,
model: gio::ListStore,
}
impl QueryInfo {
fn new(sender: mpsc::Sender<StateUpdateKind>) -> Self {
let container = gtk::Box::new(gtk::Orientation::Vertical, 2);
let query_input = gtk::Entry::builder().visible(true).build();
query_input.connect_key_press_event(clone!(@strong sender => move |widget, _| {
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::QueryUpdateEvent(widget.text().into()))
.expect("Couldn't notify thread");
gtk::Inhibit(false)
}));
let model = gio::ListStore::new(SongObject::static_type());
let listbox = gtk::ListBox::new();
listbox.bind_model(Some(&model), clone!(@strong sender => move |item| {
let sender = sender.clone();
let box_ = gtk::ListBoxRow::new();
let item = item
.downcast_ref::<SongObject>()
.expect("Row data is of wrong type");
let grid = gtk::Grid::builder().column_homogeneous(true).build();
let add_individual_song =
gtk::Button::from_icon_name(Some("list-add-symbolic"), gtk::IconSize::SmallToolbar);
add_individual_song.set_visible(true);
let filename = item.property::<String>("filename");
add_individual_song.connect_clicked(move |_| {
let filename = filename.clone();
let mut sender = sender.clone();
sender
.try_send(StateUpdateKind::QueueAddRequest(filename))
.expect("Couldn't notify thread");
sender
.try_send(StateUpdateKind::MpdEvent)
.expect("Couldn't notify thread");
});
grid.attach(&add_individual_song, 0, 0, 1, 1);
let title_label = gtk::Label::new(None);
title_label.set_line_wrap(true);
title_label.set_line_wrap_mode(pango::WrapMode::WordChar);
item.bind_property("title", &title_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
grid.attach(&title_label, 1, 0, 1, 1);
let album_label = gtk::Label::new(None);
album_label.set_line_wrap(true);
album_label.set_line_wrap_mode(pango::WrapMode::WordChar);
item.bind_property("album", &album_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
grid.attach(&album_label, 2, 0, 1, 1);
let artist_label = gtk::Label::new(None);
artist_label.set_line_wrap(true);
artist_label.set_line_wrap_mode(pango::WrapMode::WordChar);
item.bind_property("artist", &artist_label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
grid.attach(&artist_label, 3, 0, 1, 1);
grid.show_all();
box_.add(&grid);
box_.upcast::<gtk::Widget>()
}));
let scrolled_window =
gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE);
scrolled_window.add(&listbox);
scrolled_window.set_vexpand(true);
container.add(&query_input);
container.add(&scrolled_window);
QueryInfo { container, model }
}
}
impl AsRef<gtk::Widget> for QueryInfo {
fn as_ref(&self) -> >k::Widget {
self.container.upcast_ref()
}
}
// Unfortunately, to use the `ListStore` interface, we'll need to represent our
// data as an actual `glib` object. This is a little hairy in Rust, involving a
// fair bit of boilerplate, but not too terrible.
glib::wrapper! {
pub struct SongObject(ObjectSubclass<imp::SongObject>);
}
impl SongObject {
pub fn new(song: &mpd::song::Song) -> Self {
glib::Object::new(&[
("filename", &song.file.clone()),
(
"title",
&song
.title
.as_ref()
.cloned()
.unwrap_or_else(|| "[Untitled]".into()),
),
(
"artist",
&song
.artist
.as_ref()
.cloned()
.unwrap_or_else(|| "[No Artist]".into()),
),
(
"album",
&song
.tags
.get("Album")
.cloned()
.unwrap_or_else(|| "[Untitled]".into()),
),
])
.expect("Failed to create `SongObject`.")
}
pub fn set_index(&self, idx: u32) {
let private = imp::SongObject::from_instance(self);
private.index.set(idx);
}
}
// These class "implementations" are typically done in a separate
// file/directory. I wanted to keep the example self-contained.
mod imp {
use std::cell::{Cell, RefCell};
use glib::{ParamSpec, ParamSpecString, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use once_cell::sync::Lazy;
// Object holding the state
#[derive(Default)]
pub struct SongObject {
filename: RefCell<String>,
title: RefCell<String>,
artist: RefCell<String>,
album: RefCell<String>,
pub(crate) index: Cell<u32>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for SongObject {
const NAME: &'static str = "TunesSongObject";
type Type = super::SongObject;
}
// Trait shared by all GObjects
impl ObjectImpl for SongObject {
fn properties() -> &'static [ParamSpec] {
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
vec![
ParamSpecString::builder("filename").build(),
ParamSpecString::builder("title").build(),
ParamSpecString::builder("artist").build(),
ParamSpecString::builder("album").build(),
ParamSpecString::builder("index").build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
match pspec.name() {
"filename" => {
let input = value
.get()
.expect("The value needs to be of type `String`.");
self.filename.replace(input);
}
"title" => {
let input = value
.get()
.expect("The value needs to be of type `String`.");
self.title.replace(input);
}
"artist" => {
let input = value
.get()
.expect("The value needs to be of type `String`.");
self.artist.replace(input);
}
"album" => {
let input = value
.get()
.expect("The value needs to be of type `String`.");
self.album.replace(input);
}
"index" => {
let input = value.get().expect("The value needs to be of type `u32`.");
self.index.replace(input);
}
_ => unimplemented!(),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
match pspec.name() {
"filename" => self.filename.borrow().to_value(),
"title" => self.title.borrow().to_value(),
"artist" => self.artist.borrow().to_value(),
"album" => self.album.borrow().to_value(),
"index" => self.index.get().to_value(),
_ => unimplemented!(),
}
}
}
}
#+END_SRC
#+BEGIN_EXPORT html
</div>
#+END_EXPORT
I did all the development for this in Emacs on my primary workstation, keeping
in mind that I would eventually be putting this on a mobile phone, but otherwise
writing it as I would a desktop application. The feedback loop was much faster
than what I had when I was doing Android development all those years ago, since
I was literally compiling and running the program on my workstation.
#+CAPTION: The primary view of Tunes as it appeared on my workstation
[[./Tunes on Workstation.png]]
The only part that was really affected by the mobile consideration was with
actually using a =ListStore= instead of just adding things into a =ListBox=. I'm
frankly not sure I did it right, but the intent was to have an application that
doesn't create a thousand labels at once, but instead instantiating them as they
come into view. This is by no means a mobile-only consideration, but the
PinePhone has an eighth the memory of my workstation, and I have a big (20G)
music collection. Anyway, the right way to do it is described [[https://gtk-rs.org/gtk4-rs/stable/latest/book/list_widgets.html][here]], but that
book is using GTK4, so I wasn't able to lift it verbatim.
The rest of it is standard Rust, once you realize that everything in GTK land is
basically an =Arc<Mutex<T>>=. Closures are a little funny, too, which is why you
see =let mut sender = sender.clone();= show up so frequently: we can't share the
same mutable reference across multiple invocations of the same closure[fn:12]
I tried to go against the grain and use regular Rust structs (that implement
=AsRef<Widget>=) instead of using subclassing, but you can see that I had to do it
anyway to shoehorn the data we got from =mpd= into the =ListStore=. I think the
struct-based composition works a little bit better.
Once I had the code tested, somewhat optimized, and refactored, I was ready to
try it out on the phone.
*** Building and Installing the Application on PostmarketOS
=pmbootstrap= comes with a nice =hello-world-rust= =APKBUILD= to get you started with
packaging your Rust application.
#+BEGIN_SRC sh
# Maintainer: Oliver Smith <[REDACTED]>
pkgname=hello-world-rust
pkgver="0.1.1"
pkgrel=0
pkgdesc="Small test program for (cross) compiling rust"
url="https://gitlab.com/ollieparanoid/hello-world-rust/"
arch="all"
license="Unlicense"
makedepends="cargo"
source="https://gitlab.com/ollieparanoid/hello-world-rust/-/archive/$pkgver/hello-world-rust-$pkgver.tar.bz2"
build() {
cargo build --release --locked
}
check() {
printf 'Hello, world!\n' > expected
target/release/hello_world_rust > real
diff -q expected real
}
package() {
cargo install --path . --root="$pkgdir/usr"
rm "$pkgdir"/usr/.crates.toml
}
sha512sums="b755b02529e6ad40a969d5d563bc28be1202c8008661b72335c8c9e6f06bc5f0220fa047f5444b552815df5184c3ab86eb2f6a4f70701962fa0d4bc9a25ab259 hello-world-rust-0.1.1.tar.bz2"
#+END_SRC
I copied this over to a new directory under =cache_git= named =tunes=, threw my
source tree into a tarball, and edited the template =APKBUILD= to declare the
dependencies my application would need.
#+BEGIN_SRC sh
# Maintainer: Jakob L. Kreuze <[REDACTED]>
pkgname=tunes
pkgver="0.1.1"
pkgrel=0
pkgdesc="Mobile-friendly MPD client"
url="https://git.sr.ht/~jakob/tunes/"
arch="all"
license="GPL-3.0-or-later"
makedepends="cargo gtk+3.0-dev libhandy1-dev"
source="tunes-$pkgver.tar.gz"
options="!check" # no tests
build() {
cargo build --release --locked
}
package() {
cargo install --path . --root="$pkgdir/usr"
rm "$pkgdir"/usr/.crates.toml
}
sha512sums="561c95dcd8cc9e61c7f2faeaa3ffbd5cbd4fc3383a8fe87825b7367343f89ad088de0dd9ca4305b11d22ec9d9e5c1c8300760f73b9b41a497b39dcd0808eb9f8 tunes-0.1.1.tar.gz"
#+END_SRC
After that it was just =pmbootstrap -t 3600 build --arch=aarch64 tunes=[fn:13],
wait an hour or two, and I had a =tunes-0.1.1-r0.apk= I could work with. I =rsync='d
that over to my PinePhone and ran =apk add --allow-untrusted tunes-0.1.1-r0.apk=,
and it worked on the first try.
#+CAPTION: The primary view of Tunes on the PinePhone
[[./Tunes on PinePhone.png]]
I haven't updated the =APKBUILD= to install it, yet, but I've made a =tunes.desktop=
file so that the application shows up on my home screen.d
#+BEGIN_SRC conf
[Desktop Entry]
Type=Application
Version=1.0
Name=Tunes
Comment=Mobile-friendly MPD client
Icon=mpd
Terminal=false
Exec=/usr/bin/tunes
Categories=Multimedia
#+END_SRC
#+CAPTION: The entry for Tunes shows up on my home screen with the MPD logo. My wallpaper (a picture of my sweetheart) makes the text a little hard to read, so I apologize for that.
[[./Tunes on Home Screen.png]]
Final thoughts? That was much more pleasant than anything I've done in Android
land. I've got an application that's actually useful to me that didn't take me
more than a week -- a week where I was working late most nights, mind you.
It's still a proof-of-concept rather than a battle-tested application, ready for
packaging upstream, but it's enough to go off of. I'm expecting to continue
working on it, but I might pull in [[https://github.com/Relm4/Relm4][Relm4]] or [[https://github.com/bodil/vgtk][vgtk]] to cut down on some of the
boilerplate and event loop spaghetti.
**** Comments on the =mpd= interactions
You may notice that I've vendored the entire =mpd= crate into the the =tunes=
repository. In short: the =mpd= crate is pretty old and a little broken. I ran
into [[https://github.com/kstep/rust-mpd/issues/40][this (two-year old!) issue]] using the query interface, so I cloned =master=
and applied =SimonPersson='s patch. Then I ran into /another/ issue where I was
trying to send a song path across a channel instead of the whole =Song=, and I
wasn't able to use that for the API calls I wanted to make, because =ToSongPath=
isn't implemented for =String= or =&str=. It should be, since there's an =impl
ToSongPath for dyn AsRef<str>=, but there isn't, so I had to add my own =push_str=
method. I also merged in another [[https://github.com/kstep/rust-mpd/pull/43][pull request]] from =SimonPersson= which adds
=albumart= support... so I have a pseudo-fork of the =mpd= crate sitting around,
which I had to bring into version control if anyone was going to reasonably
build Tunes from source.
When I eventually come back to this to make it more than a useful prototype,
I'll probably drop =mpd= for something that's better-maintained. Either [[https://github.com/SimonPersson/mpdrs][mpdrs]] as
it's a plain old fork of =mpd=, or [[https://github.com/elomatreb/mpd_client][mpd_client]] if I decide I want to bring in all
of [[https://tokio.rs/][Tokio]] for this little =mpd= client. Decisions, decisions.
* Community
Despite owning several PINE64 widgets and doodads, my interactions with the
PINE64 community have been somewhat limited. I leverage community maintained
resources like the PINE64 wiki and the PINE64 forums frequently, but I don't
post regularly. But I think I should.
The community of people who use the PinePhone is small, but those within are
very willing to helping others, which I admire. The best example I have of this
was when I was preparing for DEF CON and I emailed Biktorgj to ask about the
FOTA code in the EG25-G modem. I sent this in the morning while I was getting
ready for work and literally /minutes/ later I got a detailed response about how
it's been removed from the firmware. It was at that point I knew that the
PinePhone software stack was in good hands.
But, really, these sorts of things make me want to be more involved in the
community. Maybe I'll do something related to mobile Linux for my master's
thesis.
** Porting Software
What follow from "it's easy to develop for the PinePhone because you're writing
applications as if you were writing them for your workstation" is that it should
be relatively easy to port existing applications as well. And this is indeed the
case. The compile times brought me great pain, but I was successful in
cross-compiling [[https://github.com/diamondburned/gtkcord4][diamondburned's gtkcord4]], which has no existing Alpine package
to my knowledge, to run on the PinePhone.
#+BEGIN_SRC sh
# Contributor: Jakob L. Kreuze <[REDACTED]>
# Maintainer: Jakob L. Kreuze <[REDACTED]>
pkgname=gtkcord4
pkgver=0.0.2
pkgrel=0
pkgdesc="GTK4 Discord client in Go"
url="https://github.com/diamondburned/gtkcord4"
arch="all"
license="GPL-3.0"
makedepends="gtk4.0-dev gobject-introspection-dev libcanberra-dev go"
source="$pkgname-$pkgver.tar.gz::https://github.com/diamondburned/gtkcord4/archive/refs/tags/v${pkgver}.tar.gz"
build() {
go build
}
package() {
install -D -m755 $pkgname "$pkgdir"/usr/bin/$pkgname
}
sha512sums="
1c0465f4c2d54794551811c0a536b610a51d3f795c403af3cf10954a46770b42d1aadef4709818f935aa54e2b413052546bdde5214f44e89d5ad2e2d7cbdf514 gtkcord4-0.0.2.tar.gz
"
#+END_SRC
The above is all it took. I initialized =pmbootstrap=, made a directory named
=gtkcord4= under =cache_git/pmaports/main=, ran =pmbootstrap build --arch,=aarch64
gktcord4=, and a couple hours later and I had a =gtkcord4-0.0.2-r0.apk= sitting
under =packages/v21.12/aarch64=.
I'm not sure diamondburned ever anticipated that gtkcord4 would be running on a
mobile device, but thanks to their choice to use GTK4, I didn't have to make any
changes to the code and it still runs great on my device.
#+CAPTION: gtkcord4 running on the PinePhone, showing a conversation between myself and my friend.
#+attr_html: alt="[10:46 AM] Jakob: If you managed to get enough samples, do you think you could do a TEMPEST-like attack on USB?\n[10:47 AM] Ergodic: I don't see why not\n[10:47 AM] Jakob: Or serial, or any other standard where the connection doesn't have a lot to keep it from being leaky\n[10:47 AM] Ergodic: I think Israel can dump ram from far away right?\n[10:47 AM] Ergodic: So pretty much anything\n[10:47 AM] Ergodic: Well\n[10:48 AM] Ergodic: Actually\n[10:48 AM] Ergodic: Wait\n[10:48 AM] Ergodic: With um\n[10:48 AM] Ergodic: A HdMI it doesn't matter if some data is wrong cuz you can keep resampling, same with ram\n[10:48 AM] Ergodic: But you can't with USB unless they're doing the same thing 40 times in a row 👀\n[10:49 AM] Ergodic: Like depending on the protocol, how accurate do you wanna be"
[[./gtkcord4 on PinePhone.png]]
Some applications might need to be modified to work well on a touchscreen. I
haven't had to do that yet, and even if I did, I would expect it to be a
difficult topic to cover in this (already quite long) article. The part that I
will elaborate on is how we got to that magic code block above. The gtkcord4
example is a little boring because of how little it takes to invoke the Go build
system,[fn:14] so let's port [[https://openxcom.org/][OpenXCOM]] instead. I'll start from scratch and
document my process as I go.
Speaking of process, this is basically what I follow:
1. Determine if the software in question is already packaged in another
source-based distribution (basically Gentoo or the Arch AUR).
1. If so, translate the recipe to APKBUILD. In the case of Gentoo, figure out
what set of =USE= flags "make sense" as a default.
2. Use [[https://pkgs.alpinelinux.org/packages][pkgs.alpinelinux.org]] to map each dependency in the original package
spec to an Alpine dependency.
2. If it isn't...
1. Find a skeleton APKBUILD (like the "hello world" example in the
[[*Building and Installing the Application on PostmarketOS]["Building and Installing the Application on PostmarketOS"]] section).
2. Fill it in with the instructions to compile from upstream. I find you need
to specify =build= and =package= as the bare minimum if you explicitly disable
=check=.
3. Guess-and-check for dependencies. Sometimes upstream will be good about
enumerating them, sometimes not so much.
I know that [[https://packages.gentoo.org/packages/games-engines/openxcom][openxcom is packaged in Gentoo]], so we'll start there.
#+BEGIN_SRC sh
# Copyright 1999-2021 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
EAPI=7
inherit cmake xdg-utils
DESCRIPTION="Open-source reimplementation of the popular UFO: Enemy Unknown"
HOMEPAGE="https://openxcom.org/"
if [[ ${PV} == *9999 ]]; then
inherit git-r3
EGIT_REPO_URI="https://github.com/SupSuper/OpenXcom.git"
else
COMMIT="ea9ac466221f8b4f8974d2db1c42dc4ad6126564"
SRC_URI="https://github.com/SupSuper/OpenXcom/archive/${COMMIT}.tar.gz -> ${P}.tar.gz"
KEYWORDS="~amd64 ~arm64 ~x86"
S="${WORKDIR}/OpenXcom-${COMMIT}"
fi
LICENSE="GPL-3+ CC-BY-SA-4.0"
SLOT="0"
IUSE="doc"
RDEPEND="
>=dev-cpp/yaml-cpp-0.5.1
media-libs/libsdl[opengl,video]
media-libs/sdl-gfx
media-libs/sdl-image[png]
media-libs/sdl-mixer[flac,mikmod,vorbis]"
DEPEND="${RDEPEND}"
BDEPEND="doc? ( app-doc/doxygen )"
DOCS=( README.md )
src_compile() {
cmake_src_compile
use doc && cmake_build doxygen
}
src_install() {
use doc && local HTML_DOCS=( "${BUILD_DIR}"/docs/html/. )
cmake_src_install
}
pkg_postinst() {
xdg_icon_cache_update
elog "In order to play you need copy GEODATA, GEOGRAPH, MAPS, ROUTES, SOUND,"
elog "TERRAIN, UFOGRAPH, UFOINTRO, UNITS folders from original X-COM game to"
elog "/usr/share/${PN}/UFO"
elog
elog "If you want to play the TFTD mod, you need to copy ANIMS, FLOP_INT,"
elog "GEODATA, GEOGRAPH, MAPS, ROUTES, SOUND, TERRAIN, UFOGRAPH, UNITS folders"
elog "from the original Terror from the Deep game to"
elog "/usr/share/${PN}/TFTD"
elog
elog "If you need or want text in some language other than english, download:"
elog "https://openxcom.org/translations/latest.zip and uncompress it in"
elog "/usr/share/${PN}/common/Language"
}
pkg_postrm() {
xdg_icon_cache_update
}
#+END_SRC
Although I probably should, I'm not going to bother with =postinst= or =postrm=
right now. I'm also not going to build the docs. What we can tell immediately is
that this is a CMake project (so we should find an =APKBUILD= for something else
that uses cmake) and the dependencies are the following:
- =yaml-cpp=
- =sdl=
- =sdl_gfx=
- =sdl_image=
- =sdl_mixer=
All of these are packaged in Alpine except =sdl_mixer=, so we'll need to port that
ourselves. I was able to take the =APKBUILD= for =sdl_mixer= and use that as a
skeleton. The packages are packaged very similarly, so I was able to fill in the
blanks with some of the info from the [[https://packages.gentoo.org/packages/media-libs/sdl-gfx][Gentoo package]].
#+BEGIN_SRC sh
# Contributor: Jakob L. Kreuze <[REDACTED]>
# Maintainer: Jakob L. Kreuze <[REDACTED]>
pkgname=sdl_gfx
pkgver=2.0.26
pkgrel=3
pkgdesc="Graphics drawing primitives library for SDL"
url="https://www.ferzkopp.net/wordpress/2016/01/02/sdl_gfx-sdl2_gfx/"
arch="all"
license="zlib"
makedepends="sdl-dev"
subpackages="$pkgname-dev"
source="http://www.ferzkopp.net/Software/SDL_gfx-2.0/SDL_gfx-$pkgver.tar.gz"
builddir="$srcdir"/SDL_gfx-$pkgver
prepare() {
default_prepare
update_config_sub
update_config_guess
}
build() {
./configure \
--build=$CBUILD \
--host=$CHOST \
--prefix=/usr \
--sysconfdir=/etc \
--mandir=/usr/share/man \
--infodir=/usr/share/info
make
}
package() {
make DESTDIR="$pkgdir" install
}
sha512sums="e571caa0d7575683efd4cf8f0a41ab10f4acf913f9ece216ac823af11da22c8734fc2c0ea049009a3e1a53715e49622f5bfcfdbdafb95e5151990d0a4eb69c01 SDL_gfx-2.0.26.tar.gz"
#+END_SRC
It took a little bit of trial and error to arrive at the =APKBUILD= above. I first
ran into an issue with autotools not recognizing the target platform.
#+BEGIN_SRC prog
>>> sdl_gfx: Building pmos/sdl_gfx 2.0.26-r3 (using abuild 3.9.0-r0) started Tue, 23 Aug 2022 01:28:27 +0000
>>> sdl_gfx: Checking sanity of /home/pmos/build/APKBUILD...
>>> sdl_gfx: Cleaning up srcdir
>>> sdl_gfx: Cleaning up pkgdir
>>> sdl_gfx: Fetching http://www.ferzkopp.net/Software/SDL_gfx-2.0/SDL_gfx-2.0.26.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 251 100 251 0 0 1764 0 --:--:-- --:--:-- --:--:-- 2127
100 1729k 100 1729k 0 0 2103k 0 --:--:-- --:--:-- --:--:-- 2103k
>>> sdl_gfx: Fetching http://www.ferzkopp.net/Software/SDL_gfx-2.0/SDL_gfx-2.0.26.tar.gz
>>> sdl_gfx: Checking sha512sums...
SDL_gfx-2.0.26.tar.gz: OK
>>> sdl_gfx: Unpacking /var/cache/distfiles/SDL_gfx-2.0.26.tar.gz...
checking build system type... Invalid configuration `aarch64-alpine-linux-musl': machine `aarch64-alpine-linux' not recognized
configure: error: /bin/sh ./config.sub aarch64-alpine-linux-musl failed
>>> ERROR: sdl_gfx: build failed
(011680) [21:28:30] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(011680) [21:28:30] NOTE: The failed command's output is above the ^^^ line in the log file: /home/jakob/Containers/pmbootstrap/pmbootstrap/log.txt
(011680) [21:28:30] ERROR: Command failed (exit code 1): (buildroot_aarch64) % cd /home/pmos/build; busybox su pmos -c CARCH=aarch64 SUDO_APK='abuild-apk --no-progress' PATH=/native/usr/lib/crossdirect/aarch64:/usr/lib/ccache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOME=/home/pmos abuild -D postmarketOS -d
(011680) [21:28:30] See also: <https://postmarketos.org/troubleshooting>
(011680) [21:28:30] Traceback (most recent call last):
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/__init__.py", line 49, in main
getattr(frontend, args.action)(args)
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/helpers/frontend.py", line 114, in build
if not pmb.build.package(args, package, arch_package, force,
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/build/_package.py", line 520, in package
(output, cmd, env) = run_abuild(args, apkbuild, arch, strict, force, cross,
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/build/_package.py", line 447, in run_abuild
pmb.chroot.user(args, cmd, suffix, "/home/pmos/build", env=env)
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/chroot/user.py", line 26, in user
return pmb.chroot.root(args, cmd, suffix, working_dir, output,
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/chroot/root.py", line 76, in root
return pmb.helpers.run_core.core(args, msg, cmd_sudo, None, output,
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/helpers/run_core.py", line 347, in core
check_return_code(args, code, log_message)
File "/home/jakob/Containers/pmbootstrap/.venv/lib/python3.10/site-packages/pmb/helpers/run_core.py", line 219, in check_return_code
raise RuntimeError(f"Command failed (exit code {str(code)}): " +
RuntimeError: Command failed (exit code 1): (buildroot_aarch64) % cd /home/pmos/build; busybox su pmos -c CARCH=aarch64 SUDO_APK='abuild-apk --no-progress' PATH=/native/usr/lib/crossdirect/aarch64:/usr/lib/ccache/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOME=/home/pmos abuild -D postmarketOS -d
#+END_SRC
Fortunately, it wasn't too difficult to find the issue online. Someone had tried
(unsuccessfully) to [[https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/15673][add sdl_ttf to aports]] and ran into the same issue. The
recommendation in the MR comments was to include the =prepare= block above.
When that was sorted, I had a =sdl_gfx= package that I could use as a dependency
for =openxcom=.
#+BEGIN_SRC sh
# Contributor: Jakob L. Kreuze <[REDACTED]>
# Maintainer: Jakob L. Kreuze <[REDACTED]>
_commit="ea9ac466221f8b4f8974d2db1c42dc4ad6126564"
pkgname=openxcom
pkgver=1.0.0
pkgrel=1
pkgdesc="Open-source reimplementation of the popular UFO: Enemy Unknown"
url="https://openxcom.org/"
arch="all"
license="GPL-3.0-or-later"
makedepends="cmake ninja yaml-cpp-dev sdl-dev sdl_gfx-dev sdl_image-dev sdl_mixer-dev glu-dev libexecinfo-dev"
depends="libexecinfo"
source="openxcom-$pkgver.tar.gz::https://github.com/OpenXcom/OpenXcom/archive/$_commit.tar.gz
0001-Link-execinfo-unconditionally.patch"
builddir="$srcdir"/OpenXcom-$_commit
build() {
if [ "$CBUILD" != "$CHOST" ]; then
CMAKE_CROSSOPTS="-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_HOST_SYSTEM_NAME=Linux"
fi
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DBUILD_SHARED_LIBS=True \
$CMAKE_CROSSOPTS
cmake --build build
}
package() {
DESTDIR="$pkgdir" cmake --install build
}
sha512sums="57ff9a9cbbbf48b8c4f792458edf0590d7d0df9a5805eab13a4c984713311e98587afca00778e82bd66fb2f330b354ca80703b87922a92f9ae48e5bdecf68442 openxcom-1.0.0.tar.gz
de4cc52530200992fef0e723acd59fef1b214f5b12baabec4dcca03820fbbc38c30033c0707f918fccc29e7d0d67ddef0c2a7be56d21b2bba7221899c759c282 0001-Link-execinfo-unconditionally.patch"
#+END_SRC
where =0001-Link-execinfo-unconditionally.patch= is the following:
#+BEGIN_SRC diff
From 2fe3e39c90086c7e3953d83ce75b0686ee4f5813 Mon Sep 17 00:00:00 2001
From: "Jakob L. Kreuze" <[REDACTED]>
Date: Tue, 23 Aug 2022 20:11:16 -0400
Subject: [PATCH] Link execinfo unconditionally
---
src/CMakeLists.txt | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d484380ba..b7d3020bd 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -485,9 +485,7 @@ if ( WIN32 )
endif ()
# backtrace(3) requires libexecinfo on some *BSD systems
-if (${CMAKE_SYSTEM_NAME} MATCHES FreeBSD OR ${CMAKE_SYSTEM_NAME} MATCHES NetBSD OR ${CMAKE_SYSTEM_NAME} MATCHES OpenBSD)
- set ( system_libs -lexecinfo )
-endif ()
+set ( system_libs -lexecinfo )
target_link_libraries ( openxcom ${system_libs} ${SDLIMAGE_LIBRARY} ${SDLMIXER_LIBRARY} ${SDLGFX_LIBRARY} ${SDL_LIBRARY} ${OPENGL_LIBRARIES} debug ${YAMLCPP_LIBRARY_DEBUG} optimized ${YAMLCPP_LIBRARY} )
--
2.37.2
#+END_SRC
The first error I got was about a missing =mmintrin.h=. I opened up the source
code and found that was under an =IFDEF= for =MMX= support, so I did a =./configure
--help= to figure out how to disable that. After that, I was getting a message
about a missing =glu.h=.
#+BEGIN_SRC prog
[63/313] Building CXX object src/CMakeFiles/openxcom.dir/Mod/RuleVideo.cpp.o
ninja: job failed: /native/usr/lib/crossdirect/aarch64/g++ -DDATADIR=\"/usr/share/openxcom/\" -DGIT_BUILD=1 -I/usr/include/SDL -I/usr/include/yaml-cpp -I/home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/build -Os -fomit-frame-pointer -O3 -DNDEBUG -std=gnu++11 -MD -MT src/CMakeFiles/openxcom.dir/Mod/RuleVideo.cpp.o -MF src/CMakeFiles/openxcom.dir/Mod/RuleVideo.cpp.o.d -o src/CMakeFiles/openxcom.dir/Mod/RuleVideo.cpp.o -c /home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/src/Mod/RuleVideo.cpp
In file included from /home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/src/Mod/../Engine/OpenGL.h:15,
from /home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/src/Mod/../Engine/Screen.h:22,
from /home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/src/Mod/RuleVideo.cpp:21:
/usr/include/SDL/SDL_opengl.h:47:10: fatal error: GL/glu.h: No such file or directory
47 | #include <GL/glu.h> /* Header File For The GLU Library */
| ^~~~~~~~~~
compilation terminated.
ninja: subcommand failed
>>> ERROR: openxcom: build failed
#+END_SRC
I went ahead and added the =glu-dev= dependency, after which point I was getting
some warnings about redefinitions. So I have a feeling this might have been
another thing that was behind an =IFDEF=, but that's a problem for later.
#+BEGIN_SRC prog
[218/313] Building CXX object src/CMakeFiles/openxcom.dir/Engine/AdlibMusic.cpp.o
ninja: job failed: /native/usr/lib/crossdirect/aarch64/g++ -DDATADIR=\"/usr/share/openxcom/\" -DGIT_BUILD=1 -I/usr/include/SDL -I/usr/include/yaml-cpp -I/home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/build -Os -fomit-frame-pointer -O3 -DNDEBUG -std=gnu++11 -MD -MT src/CMakeFiles/openxcom.dir/Engine/CrossPlatform.cpp.o -MF src/CMakeFiles/openxcom.dir/Engine/CrossPlatform.cpp.o.d -o src/CMakeFiles/openxcom.dir/Engine/CrossPlatform.cpp.o -c /home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/src/Engine/CrossPlatform.cpp
/home/pmos/build/src/OpenXcom-ea9ac466221f8b4f8974d2db1c42dc4ad6126564/src/Engine/CrossPlatform.cpp:68:10: fatal error: execinfo.h: No such file or directory
68 | #include <execinfo.h>
| ^~~~~~~~~~~~
compilation terminated.
ninja: subcommand failed
>>> ERROR: openxcom: build failed
#+END_SRC
The last errors I got were related to =execinfo=. This is, to my knowledge, a
=glibc= thing. Fortunately, Alpine being a popular base image in Docker land means
[[https://github.com/ddopson/node-segfault-handler/issues/70][the workarounds]] are easy to find on the 'net. The missing header file was one,
thing, but then I was getting some linker errors about a missing symbol for
=backtrace=. Searching came up with an [[https://discuss.pytorch.org/t/compiling-master-from-source-on-alpine-fails-with-undefined-reference-to-backtrace/64676][issue in PyTorch]] which gave me some
insight, and then I found a [[https://github.com/OpenXcom/OpenXcom/pull/1123][pull request upstream]] related to it. My patch above
just makes that unconditional; we get all the =backtrace= symbols from =execinfo=,
but we also need to make sure it's actually linked into the binary.
Then.. shit. It built correctly, but my =pmbootstrap= setup was a version behind
the PostmarketOS on my phone (=v21.12= vs =v22.06=), so I was getting some
dependency resolution errors. I re-initialized =pmbootstrap= and then learned that
=sdl-dev= is no longer supported, so I had to backport it from =edge/testing=. It
was at least smooth sailing after that.
#+CAPTION: OpenXcom running on the PinePhone. It performs surprisingly well.
[[./OpenXcom on PinePhone.png]]
So porting software to the PinePhone is relatively easy.
You don't even have to go through half of the mess that I did if you don't care
about cross-compiling or having things tracked by the package manager. You could
probably just install the =gcc= toolchain and do a =make && sudo make install= on
your phone; Alpine/PostmarketOS have [[https://wiki.alpinelinux.org/wiki/Running_glibc_programs][glibc compatibility]]. Or, hell, use a
Flatpak/AppImage/Snap if you want to.
However you do it, the end result is the same. You get to use the same Linux
applications on your phone that you would on your desktop, and I think that's
great.
** Malware
An unrelated aside: the only time I've heard of a trojan for Linux circulating
in the wild was a [[https://hackaday.com/2021/12/16/pinephone-malware-surprises-users-raises-questions/][snake game for the PinePhone]], but I don't think this says
terribly much about the PINE64 community.
* Social Implications
A few weeks ago I had a party at my place, and some chick was talking about how
owning an Android phone is some sort of red flag. I turned to my friend to say
that I hoped my weird-ass Linux phone wasn't a red flag. I thought I was funny,
but in reality the difference doesn't matter to non-technical folk. To them it's
just a "green bubble," or whatever. I don't understand why it's a red flag, nor
do I particularly care, I just wanted to lead with an anecdote about why your
choice of mobile phone somehow carries stigma, at least in my (doomed)
generation.
It hasn't been a problem for me because I don't frequently surround myself with
these types of people who care about what kind of cell phone you have. There
have been a couple of rough spots because of literal technical limitations with
the PinePhone -- for example, PostmarketOS 21.06 wasn't MMS-capable, so I missed
out on some group texts and photos that my parents were sending. But my parents,
my partner, and my friends haven't complained about my weird ass phone. They've
put up with it, and for that I'm appreciative. But it's been a while since I've
had one of those annoying technical problems, so I don't think they've really
noticed.
All-in-all, the people I do tell about how I use a phone running mainline Linux
(mainly coworkers) find it cool but also very characteristic of who I am as a
person. I think that's a fair way to conclude this section.
* Surveying Other's Opinions
I hinted at this in the introduction, but I'll say it again: the PinePhone is
not a popular choice. I know precisely two people who own one (both of whom seem
quite happy with theirs.) I appreciate the PinePhone, and there are others who
appreciate it as well, but the overwhelming opinion is that it isn't ready for
most "real life" use-cases. Come on down to the PINE64 mobile shop. We've got
[[https://xnux.eu/log/#017][(hypothetical) exploding phones]] and [[https://www.pine64.org/2022/08/18/a-response-to-martijns-blog/][core contributors leaving in protest of
bureaucracy]].
The PinePhone is quite unique in that it's backed by hobbyists rather than big
companies, and the effects of that are enough to make it a non-starter for many
people. I find those that own a PinePhone (especially myself) tend to be
dogmatic about software freedom and privacy, and get by without a lot of what
typical smartphones offer.
I don't want to spend too many cycles summarizing what other people have written
about the PinePhone. I would recommend reading Amos B. Batto's article [[https://amosbbatto.wordpress.com/2021/12/10/comparing-l5-and-pp/][Comparing
the Librem 5 USA and PinePhone Beta]] for some more articulate thoughts about
the PinePhone's hardware and how it compares to its main "competitor," the
Librem 5. The [[https://www.toomanyatoms.com/computer/pinephone.html][PinePhone page on toomanyatoms.com]] is also quite good.
* Conclusions
- I enjoy the PinePhone because it's more like my workstation than some strange
alien device that ends up being a pain in the ass to develop for.
---
[fn:1] In practice, Android typically uses an outdated kernel with vendor-specific blobs and modifications, and it notably does _not_ use the GNU/Linux userland. [[https://en.wikipedia.org/wiki/Bionic_(software)][Bionic]] is the libc, [[https://source.android.com/devices/graphics/surfaceflinger-windowmanager][SurfaceFlinger]] is the display server, ... In general, there is very little semblance between Android and the Linux distributions one may be familiar with. You cannot, in general, run a regular Linux application on Android.
[fn:2] If we take a minute to consider the policy /without/ associating it with partisan decisionmaking, I think this was a [[https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/790270/HCSEC_OversightBoardReport-2019.pdf][good call]] (albeit poorly implemented).
[fn:3] Using the Android calendar was so painful that I wrote some scripts to generate ICS files for my college classes, recurring meetings at work, etc. I am so thankful that I don't need to use this anymore.
[fn:4] Folks message me on XMPP so infrequently that I can get by just using it on desktop.
[fn:6] Which is a bit of a shame. It wasn't a feature I used often on my old phone, but I was happy it was there. I have some really fond memories of sitting in the car when I was 16 and using the FM radio app on my phone to scan the airwaves as we passed through Maine during the winter.
[fn:7] It was a bit of a pain to set up when I first tried it, so I gave up.
[fn:8] While I tend to use "GNU/Linux" to refer to the kernel + user space, the distribution I'm running on my phone doesn't actually use GNU components. [[https://postmarketos.org/][PostmarketOS]] is based on Alpine, and hence uses [[https://musl.libc.org/][musl]] and [[https://www.busybox.net/][BusyBox]].
# JLK: Early 2000's?
[fn:9] If I recall, my cousin had pulled out a picture of his bedroom back in the late 90's and was commenting on the Limp Bizkit poster, and I mentioned that they'd released an album earlier that week. (And said something about Fred Durst's new appearance.)
[fn:10] The only attempt I've seen at "breaking into" Android land with something that isn't based on Java is David Boddie's [[https://www.boddie.org.uk/david/www-repo/Projects/#DUCK][DUCK]], which I'd experimented with and enjoyed quite a bit. Unfortunately, I had my falling out with Android development around when I discovered it and never made anything of note with it.
[fn:11] I /think/ this term originates from [[https://dustycloud.org/][Christine Lemmer-Webber]]. It's a neologism for arguing about which of some number of choices is the best, when one thing being better than another is not only subjective but also a triviality, and when the arguments tend to be unusually heated. American football teams is a good example. I don't think anyone actually cares about the Gnome versus KDE argument nowadays (it seems to have been more relevant in my dad's time), so maybe it isn't accurate to call it footballing in 2022.
[fn:12] And I can't figure out how to tell the compiler that I want to =move= the mutable =sender= into the closure and use that across all invocations. I don't think it's possible, but someone better than me at Rust is probably going to write me an email and tell me the better way to do this. When that happens, I'll update this post with an addendum.
[fn:13] The =-t 3600= is to tell =pmbootstrap= not to kill itself if it doesn't see any output in half an hour. It's absolutely the most annoying thing in =pmbootstrap= because things just sometimes take a really long time to cross-compile.
[fn:14] I should have included =go= as a build dependency, come to think of it.
|