From d463d4bd7ecaaab3bf33da649c2457e13b8f1ea6 Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Sun, 21 Aug 2022 17:29:03 -0400 Subject: [org] Initial commit for "I Love My PinePhone" --- org/I Love My PinePhone/brainstorming.org | 242 +++ org/I Love My PinePhone/i-love-my-pinephone.org | 1896 +++++++++++++++++++++++ 2 files changed, 2138 insertions(+) create mode 100644 org/I Love My PinePhone/brainstorming.org create mode 100644 org/I Love My PinePhone/i-love-my-pinephone.org diff --git a/org/I Love My PinePhone/brainstorming.org b/org/I Love My PinePhone/brainstorming.org new file mode 100644 index 0000000..9030c4d --- /dev/null +++ b/org/I Love My PinePhone/brainstorming.org @@ -0,0 +1,242 @@ +* Outline + +** Introduction + +*** What is a Linux Phone? + +*** Why did I want one? + +**** Enter: my old phones + +**** Android Grievances + +- System UI freezes very frequently, makes phone inoperable +- Poor integration with external SD +- Random kernel panics.. as to be expected on a (3.10.108-lineageos-g6f027fb1e3f non-mainline kernel)? +- SD Card problem. Hard to capture as screenshots since it doesn't let me take them when it happens + - Explain how you're using SD's + +**** Could be solved by getting a better-supported phone, but that's expensive! + +**** Software Development Freedom + +**** Run Linux Desktop Apps + +=openxcom= is one of a few examples I care about + +*** Enter: Pinephone + +*** PostmarketOS (with mention of some other distros for the Pinephone) + +I am a Gentoo fan, but I haven't had a great time with + +https://wiki.gentoo.org/wiki/PinePhone + +I like Alpine a lot, though. It strikes me as similar to Gentoo in a lot of the +ways I care about (fairly easy to bootstrap, ...) + +*** Some Other options (Halium, etc.) + +- Shout out SqueakPhone. + - "I think we're in the golden age of open hacking on mobile devices." + +** The Initial Transition + +*** Factorytest + +- =motor= test did not work + - However, it worked in PMOS. +- Some finicky-ness with the modem :( + +*** 3D Printed Case + +*** Screen Protector + +- Got some for the iPhone X + +*** Mobile Data + +https://wiki.pine64.org/wiki/PinePhone_APN_Settings#AT.26T_.28USA.29 + +Curiously, I went to icanhazip.com and I was given an IPv6 address in response. +First time that's happened to me, ever. + +*** Storage + +Start off by installing to EMMC, then mount 256G SD as OverlayFS on /home, /opt + +*** Mostly worked Out-of-the-Box + +*** Litmus: Being Able to Show a Picture of Fred Durst at the Thanksgiving Dinner Table + +*** Roles of Applications + +# Obviously, I'm not going to be using Android applications for the daily tasks that I use my phone for. + +**** Raw List + ++andOTP+ ++AntennaPod+ +AnySoftKeyboard ++App Manager+ +AudioFX ++Aurora Store+ ++BackgroundRestrictor+ +Browser|Not used| +aAVNC +Calculator +Calendar +Calendar Import-Export ++Camera+ +Clock +Contacts +Conversations +DCU +Discord +Email|Not used +F-Droid +FFUpdater +Files +Firefox +FM Radio ++Gallery+ +K-9 Mail +Libera PRO +Messaging +MuPDF mini +Music +NewPipe +Obsqr +Offline Calendar +OpenKeychain +Orbot +Orgzly +OsmAnd~ +Password Store ++Phone+ +Recorder +RetroArch +Settings +Shattered Pixel Dungeons +Signal +Slide +Snapchat +Syncthing +Termux +Tiny Tiny RSS +Tusky +USAA +wallabag +Wikipedia + +**** Table + +| andOTP | gnome-authenticator | https://wiki.mobian-project.org/doku.php?id=authenticator | +| Camera | Megapixels | https://wiki.mobian-project.org/doku.php?id=megapixels | +| Gallery | gnome-photos | https://wiki.mobian-project.org/doku.php?id=gnomephotos | +| Phone | Calls | https://wiki.mobian-project.org/doku.php?id=calls | + +Dropping: + +- AntennaPod: I'll just use an RSS reader. +- App Manager: Android-specific +- Aurora Store: Android-specific +- BackgroundRestrictor: Android-specific + +** Issues Encountered + +*** Modem: Frequent Disconnects, not receiving calls + +Contingency: Custom modem firmware +- Disabled automatic suspends + +Modem has been the single most frustrating part about using the PP. +- I find it funny that the ADSP firmwares are blobs we just found somewhere; Quectel doesn't officially publish them. + +With the news that Quectel could potentially be locking down their hardware and preventing users from flashing their own firmware, all I have to say further about the modem is... + +# [Linus Torvalds FUCK YOU NVIDIA] + +See (Toomanyatoms) for further evidence of Quectel's absolute incompetence. + +The sad thing is, there probably won't be this much work on other modems for a while. This Quectel piece of shit will probably be the best supported modem in e.g. PostmarketOS for the foreseeable future. + +# Unless... + +*** Occasional Non-Wake from Suspend + +- Configured syslogd to write to disk, waiting to debug. [TODO]. + +*** Suspend Prevents Alarm from Going off + +Contingency: Modem firmware can do a "wake-up call" + +*** Battery Life + +- Power draw has been attributed to four separate chips (https://amosbbatto.wordpress.com/2021/12/10/comparing-l5-and-pp/) + +*** Mobile hotspot not working + +- Laptop can connect, but internet is not passed through. + - Apparently a kernel issue: https://forum.pine64.org/showthread.php?tid=10974 + - Easier solution for the time being has been to use [[https://github.com/rofl0r/microsocks][microsocks]], since I can + connect to my phone via hotspot at least. + +*** Lack of software + +- Write your own! + - I had to write my own Signal client... + +*** Cross Compiling Woes + +- Literally could not get gnome-authenticator to work + - Quality control for packages seems to be much less than Gentoo +- Spending literally a day trying to compile Emacs PGTK +- It ain't Gentoo + +** Quality-of-Life + +*** Being able to use Emacs on mobile + +- Any particular use-cases that are well supported? + +# Necessary talking point: I've literally edited this blog post using Emacs from my bed while the gf was asleep on me. + +# "You might expect Emacs on mobile to be a bit of a Gimmick, but it's the application I use the second most, right after Firefox" +# - Attribute this to Syncthing or rsync making it easy to get config over, not the casse in Termux +# - Also icons make it a little bit nicer for touch input + + +*** Being able to use yt-dlp and mpv instead of New Pipe + +*** Running scripts, cron, other automation + +*** Contacting phone over SSH + +*** Considerations: dotfiles on mobile vs desktop + +*** Convergence + +** Software Development; Porting + +#In non-QT land, you have two options: +#- gtk3 and libhandy +#- gtk4 and libadwaita +#There used to be anti-GTK sentiment, but these days it rocks + +** Community + +*** Malware + +** Social Implications + +# I appreciate my partner's willingness to put up with me using a weird phone +# that can't do anything. + +# Can't use Uber.. but I didn't really use Uber anyway with my non-gApps smartphone. + +** Surveying Others' Opinions on the Pinephone + +** My Dream Cellphone + +** Conclusion diff --git a/org/I Love My PinePhone/i-love-my-pinephone.org b/org/I Love My PinePhone/i-love-my-pinephone.org new file mode 100644 index 0000000..957aea2 --- /dev/null +++ b/org/I Love My PinePhone/i-love-my-pinephone.org @@ -0,0 +1,1896 @@ +#+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 +
+#+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 +
+#+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: + +# TODO: Fold. + +#+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 + +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 + +#+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]] | currently unused.[fn:5] | +| 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]: [modem0/bearer1] verbose call end reason (3,1056): [cm] lrrc-connection-establishment-failure-timer-expired +Jul 30 02:02:34 theta daemon.info [2179]: [modem0] state changed (connected -> registered) +Jul 30 02:02:34 theta daemon.info [2179]: [modem0/bearer1] connection #1 finished: duration 22362s, tx: 285780 bytes, rx: 1471594 bytes +... +Jul 30 06:02:43 theta daemon.info [2179]: [modem0/bearer1] verbose call end reason (3,1034): [cm] esm-sync-up-with-nw +Jul 30 06:02:43 theta daemon.info [2179]: [modem0] state changed (connected -> registered) +Jul 30 06:02:43 theta daemon.info [2179]: [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 internet. + +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. + +# TODO: Attempt to find mention of it in the bug tracker. + +** Bluetooth sucks + +# TODO + +# https://forum.pine64.org/showthread.php?tid=10810 +Switch to pulseaudio :( +#+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 + + +** 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- 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]]). + +# TODO: Fold. + +#+BEGIN_SRC prog +# Maintainer: Natanael Copa +# Contributor: Timo Teräs +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 + +There is no Emacs =29.0=, 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 + +AOSP has a [[https://android.googlesource.com/platform/packages/apps/Music/][Music]] app, 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 Music 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. + +# TODO: Picture + +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. + +# TODO: Bring this down there and comment on boilerplate. +# I'd also like to try using [[https://github.com/Relm4/Relm4][Relm4]] and [[https://github.com/bodil/vgtk][vgtk]]. It's fun to experiment with these sorts of things. + +** "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. + +# TODO: Fold. This is mad long. + +#+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 . + +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 { + 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) -> 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::() + .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::("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::() + }), + ); + + 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 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) -> 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::() + .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::("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::() + })); + + 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 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); +} + +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, + title: RefCell, + artist: RefCell, + album: RefCell, + pub(crate) index: Cell, + } + + // 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> = 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 + +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>=. 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=) 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 prog +# 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 prog +# 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. + +*** 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=, 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. + +** 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 + +https://amosbbatto.wordpress.com/2021/12/10/comparing-l5-and-pp/ +https://news.ycombinator.com/item?id=24596248 + +* 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:5] I fucking hate Discord. This package is broken on mobile anyway. #TODO: Elaborate + +[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. -- cgit v1.3