Comprei um Nokia e72 dois meses atrás. Meu objetivo principal era experimentar um celular com teclado QWERTY e uma bateria grande (não grande em mAh, mas grande no sentido de que só preciso carregar uma vez por semana).

O Nokia e72 que comprei, rodando o meu launcher
O Nokia e72 que comprei, rodando o meu launcher
Navegando neste blog pelo aparelho
Navegando neste blog pelo aparelho

A UI/UX desse celular é muito estranha, porque a Nokia colocou um monte de funcionalidades em uma única tela. Mas o primeiro problema que encontrei foi TLS: o navegador não suportava nenhuma versão moderna de TLS. O aparelho parou de receber atualizações por volta de 2011, então a maioria dos sites que exigem protocolos modernos não carrega. Procurando uma solução, encontrei grupos da comunidade com vários projetos dedicados a manter esses aparelhos usáveis.

Launcher padrão do Nokia e72
Launcher padrão do Nokia e72

/nnproject/

Esse grupo tem muitas soluções para Nokia. Eles têm um cliente de Telegram, um cliente SSH e, o mais importante para mim, um patch que habilita TLS 1.2. Ele usa mbedTLS e funciona muito bem. Também têm alguns patches que melhoram o Java, mas não instalei, já que não pretendo instalar aplicações Java por enquanto.

Um amigo me mostrou outra coisa que é essencial nesse aparelho: o ROM Patcher. Ele me deixou "hackear" o celular e instalar qualquer aplicação que eu quisesse. O processo de instalação em si é bem divertido. Com o ROM Patcher você pode escrever dados arbitrários na memória, então a comunidade construiu bypasses para a proteção de arquivos de sistema, para aplicações não assinadas e mais. Depois de instalar, consegui instalar qualquer aplicação sem cair na verificação de certificados da Nokia.

Então: boas aplicações, uma boa comunidade, mas eu não tinha nenhum ferramental decente para escrever apps novos. Foi aí que pensei: precisamos de um SDK de verdade para aplicações nativas, um que pegue ideias emprestadas da web e de bibliotecas modernas.

E foi assim que nasceu o projeto Epoc. Um SDK para construir aplicações em Rust.

Como o Epoc funciona

Depois que a indústria migrou para outros sistemas operacionais como iOS e Android, a Symbian Foundation publicou o código-fonte do sistema, mas sem os detalhes de firmware específicos da Nokia e de outros fabricantes. Isso me deu acesso a muitas funções e me permitiu escrever uma camada em C++ para conectar meu SDK a elas. O Nokia e72 não tem GPU, então tudo na tela precisa ser desenhado à mão. Comecei por aí e, quando isso funcionou, adicionei uma nova camada de abstração para deixar o processo mais parecido com construir uma aplicação web.

Criando uma aplicação com cpp

void CRecentControl::DrawRowL(const TRect& aRect, TInt aIndex, TBool aSelected)
    {
    if (aSelected)
        {
        iBackGc->SetBrushColor(TRgb(0x2d, 0x5a, 0x8c));
        iBackGc->SetBrushStyle(CGraphicsContext::ESolidBrush);
        iBackGc->Clear(aRect);
        }

    // The row has to know it is selected in order to know what colour it is.
    const TRgb name = aSelected ? TRgb(0xff, 0xff, 0xff) : TRgb(0xf0, 0xf0, 0xf0);
    const TRgb time = aSelected ? TRgb(0xff, 0xff, 0xff) : TRgb(0x8a, 0x8a, 0x94);

    const TRect inner(aRect.iTl.iX + KPad, aRect.iTl.iY,
                      aRect.iBr.iX - KPad, aRect.iBr.iY);
    const CFbsFont* strong = (const CFbsFont*) iCoeEnv->NormalFont();
    const CFbsFont* small  = (const CFbsFont*) CEikonEnv::Static()->AnnotationFont();
    iBackGc->SetBrushStyle(CGraphicsContext::ENullBrush);

    // Right column first, because the left one is whatever it leaves.
    // The baseline offset is measured from the bottom of the box.
    TPtrC timeText(KChats[aIndex].iTime);
    const TInt timeW = small->TextWidthInPixels(timeText);
    const TInt timeBase = inner.Height()
                          - (inner.Height() - small->HeightInPixels()) / 2
                          - small->DescentInPixels();
    iBackGc->UseFont(small);
    iBackGc->SetPenColor(time);
    iBackGc->DrawText(timeText, inner, timeBase, CGraphicsContext::ERight, 0);
    iBackGc->DiscardFont();

    TPtrC nameText(KChats[aIndex].iName);
    const TRect left(inner.iTl.iX, inner.iTl.iY,
                     inner.iBr.iX - timeW - KPad, inner.iBr.iY);
    const TInt nameBase = left.Height()
                          - (left.Height() - strong->HeightInPixels()) / 2
                          - strong->DescentInPixels();
    iBackGc->UseFont(strong);
    iBackGc->SetPenColor(name);
    iBackGc->DrawText(nameText, left, nameBase, CGraphicsContext::ELeft, 0);
    iBackGc->DiscardFont();
    }

Primeira versão com a minha symbian-ui (já escrita em Rust)

fn draw_row(c: &mut Canvas<'_>, rect: Rect, theme: &Theme<'_>, index: usize, selected: bool) {
    let (name, time) = CHATS[index];
    let pad = theme.metrics.pad;
    let inner = rect.inset_xy(pad, 0);

    let (fg, dim) = if selected {
        (theme.palette.selection_text, theme.palette.selection_text)
    } else {
        (theme.palette.text, theme.palette.dim)
    };

    // Right column first, because the left one is whatever it leaves.
    let time_w = theme.fonts.small.measure(time);
    let (right, left) = inner.split_right(time_w);
    c.draw_text_in(right, time, theme.fonts.small, dim, Align::End);
    c.draw_text_in(Rect { x1: left.x1 - pad, ..left },
                   name, theme.fonts.strong, fg, Align::Start);
}

Versão atual (usando symbian-decl-ui)

fn declared_row(index: usize) -> Group {
    let (name, time) = CHATS[index];
    // No `if selected`. `Ink::Text` and `Ink::Dim` resolve against the ground
    // the list put them on, so the row says what it means and the highlight is
    // handled where the highlight is painted.
    Row::new()
        .align(CrossAlign::Stretch)
        .padding(Edges { left: 5, right: 5, top: 0, bottom: 0 })
        .gap(5)
        .child(Text::new(name).font(FontRole::Strong).ink(Ink::Text).flex(1))
        .child(Text::new(time).font(FontRole::Small).ink(Ink::Dim).align(Align::End))
}
O novo launcher no tema claro, com os apps Web, Telegram, Maps e Recent
O novo launcher no tema claro, com os apps Web, Telegram, Maps e Recent

Fazendo engenharia reversa das aplicações do sistema

Depois que rodei meu primeiro exemplo em Rust no Nokia, comecei uma aplicação para navegar mais facilmente dentro do sistema e listar aplicações, ler e escrever. Então, inspirado pelo ecossistema Android, construí o meu próprio ADB para Symbian (ADBian). Com esse programa eu consigo plugá-lo no Claude Code ou em outro harness de código e ler muitos binários para entender como isso funciona. Depois de algumas semanas descobri muitas funções e comecei o processo de criar algumas aplicações usando esse SDK.

Linhas: uma linha de conversa com avatar, uma linha de ícone e uma linha de valor
Linhas: uma linha de conversa com avatar, uma linha de ícone e uma linha de valor
Controles: switch, checkbox e radio buttons
Controles: switch, checkbox e radio buttons
  1. Biblioteca de componentes;
  2. Tg, um cliente de Telegram (a comunidade já tem várias soluções para isso, mas construir um eu mesmo parecia divertido, mesmo que termine sendo slop);
  3. Cal, uma solução de calendário integrada com iCal e com suporte a notificações MTM;
  4. Epoc OS, uma ideia doida de mudar o processo de boot e colocar o meu launcher controlando o ciclo de vida das aplicações e montar o meu servidor de notificações dentro dele;
  5. Store, para tornar fácil instalar a versão mais recente de um app usando a mesma abordagem do ObtainX.
Cal na visão de mês, desenhado na tela de 320x240
Cal na visão de mês, desenhado na tela de 320x240
Maps resolvendo a posição por torre de celular, sem fix de GPS
Maps resolvendo a posição por torre de celular, sem fix de GPS

Resultados

Vou tornar tudo isso público, e tentei documentar tudo para ajudar as pessoas a construir novas aplicações. Comecei com IA porque meu objetivo principal era provar que isso podia ser viável de usar, e agora posso começar a codar no meu SDK. Portar todas as funções à mão, sem IA, teria sido lento demais, e isso é só um projeto paralelo para mim.

Links

A Rust SDK for Symbian S60 3rd Edition