WebDroid
İki sekmenin birbirini arayıp mesajlaşabildiği, tarayıcıda çalışan bir Android 15.
Tarayıcıda çalışan bir Android telefon. Görünüşü ve hareketleri Android 15'e yakın, içindeki uygulamalar da gerçekten çalışıyor: iki sekme açıp birinden ötekini arayabilir, mesajlaşabilir, bilgisayarının kamerasıyla fotoğraf çekip galeride görebilirsin.

- Durum
- Tamamlandı
- Kategori
- Tarayıcıda OS
- Güncelleme
- 4 Ekim 2026
- Yığın
- React 19TypeScriptBunTailwindSQLite
- Bağlantılar
- Kaynak kod
Tarayıcıda çalışan bir Android telefon. Görünüşünü ve hareketlerini Android 15'e yakın tuttum, içindeki uygulamalar da gerçekten çalışıyor. İki tarayıcı sekmesi açarsan iki ayrı telefonun oluyor; birbirlerini sesli arayabiliyor, birbirlerine mesaj atabiliyorlar. Bilgisayarının kamerasıyla çektiğin fotoğraf da galeride görünüyor. İlk açılışta seni dil, ad, ekran kilidi ve duvar kâğıdı için kısa bir kurulum sihirbazı karşılıyor.
Telefonda Telefon, Mesajlar, Rehber, Kamera, Galeri, Dosyalar, Tarayıcı, Hesap Makinesi, Takvim, Saat, Notlar, Müzik, Hava Durumu ve Ayarlar hazır geliyor. Haritalar ve Ses Kaydedici'yi ise Uygulama Mağazası'ndan sen kuruyorsun; sistem uygulamalarını kaldıramıyorsun. Harita OpenStreetMap (Leaflet), hava durumu da Open-Meteo kullanıyor, bu yüzden hiçbir servis API anahtarı istemiyor. İstemciyi React 19, Tailwind CSS 4 ve zustand ile yazdım; arkada da Bun ile yazılmış bir sunucu var.
İki sekme, iki telefon
Her cihaz sunucudan kendi telefon numarasını alıyor; numaralar 5550100'den başlayıp sırayla dağıtılıyor. İkinci bir telefon açmak için ekranın yanındaki araç çubuğunun en alttaki düğmesine basabilir ya da adrese ?device=bir-isim ekleyebilirsin. Cihaz kimliği önce bu parametreden okunuyor. Parametre yoksa tarayıcıda kayıtlı değere bakılıyor, ikisi de yoksa rastgele bir kimlik üretiliyor.
Cihazlar arasındaki her şey tek bir push kanalından geçiyor. kernel/push.ts her cihazı WebSocket ile bağlıyor, sunucu da mesajı alıcının numarasına ait konuya yayınlıyor. Alıcı çevrim dışıysa ve gönderen kuyruk istediyse mesaj sunucuda bekliyor, cihaz bağlandığında teslim ediliyor. Gönderen her mesaj için bir onay alıyor; 5 saniye içinde onay gelmezse gönderim başarısız sayılıyor. Bağlantı koparsa 3 saniye sonra yeniden kuruluyor. Uçak modunu açtığında ya da Wi-Fi'ı kapattığında bağlantı gerçekten kapanıyor, diğer cihazlar da bu telefonu ulaşılamaz görüyor. Mesajlar uygulaması teslim edildi ve okundu bilgisini de aynı kanaldan geri alıyor.
Sesli aramalar WebRTC ile yapılıyor (kernel/telecom.ts). Sinyalleşme için ayrı bir sunucu yazmadım; aynı push kanalında OFFER, RINGING, ANSWER, DECLINE, CANCEL, END, BUSY ve HOLD intent'leri dolaşıyor. ICE adaylarını tek tek göndermek yerine, aday toplama bitince tam SDP'yi tek mesajda iletiyorum. Gelen arama, ekranı uyandıran tam ekran bir bildirimle geliyor; 30 saniye içinde cevaplanmayan arama düşüyor ve geride bir cevapsız arama bildirimi kalıyor. Sessize alma, beklemeye alma ve hoparlör de var. Masaüstünde tek bir ses çıkışı olduğu için hoparlörü ses düzeyiyle taklit ettim. Bağlantı STUN ya da TURN sunucusu olmadan kurulduğu için aramalar aynı makinede ya da aynı yerel ağda çalışıyor.
Uygulamalar birbirini tanımıyor
Her uygulama src/apps/ altında tek bir dosya ve bir listeye kayıtlı. Uygulamalar birbirini import etmiyor; yalnızca çekirdeğin genel API'sini ve ortak arayüz bileşenlerini kullanıyor, birbirleriyle de intent'ler üzerinden konuşuyor. Bir uygulamanın tanımında şunlar var: iki dilli adı ve açıklaması, ikon rengi için bir OKLCH ton açısı, istediği izinler, karşıladığı intent filtreleri (Galeri VIEW image/*, Telefon DIAL tel: gibi), önem düzeyi olan bildirim kanalları, varsa ana ekran widget'ı ve diğer uygulamaların okuyabildiği içerik sağlayıcıları (rehber gibi). İşleyen saat ya da bugünün tarihini gösteren takvim gibi canlı ikonları başlatıcının kendisi çiziyor.
kernel/os.ts Android'in görev modelini taklit ediyor. Başlatıcıdan, bildirimden ya da sistemden açılan bir uygulama yeni bir görev başlatıyor; uygulama içinden açılanlar ise çağıranın görevinin üstüne ekleniyor. startActivity bir Promise dönüyor ve açılan activity finish(result) çağırdığında bu Promise sonuçla çözülüyor. Örneğin Rehber, bir kişiye fotoğraf seçerken bir GET_CONTENT intent'i gönderip seçilen görseli bu şekilde bekliyor. Bir intent'i birden fazla uygulama karşılıyorsa bir seçici açılıyor, SEND içinse her zaman paylaşım sayfası çıkıyor. Karşılayan uygulama yoksa "Bu işlemi yapabilecek uygulama yok" uyarısını görüyorsun.
İzinlerin dört durumu var: verildi, bu seferlik, reddedildi ve engellendi. "Yalnızca bu sefer" seçeneği kamera, mikrofon ve konum için çıkıyor ve görev kapanınca geçersiz oluyor. İki kez reddettiğin izin engelleniyor, diyalog da bir daha açılmıyor. Aynı izin için aynı anda gelen istekler ise tek bir diyaloğu paylaşıyor.
Bütün cihazlar tek bir SQLite dosyasında
Bu projede veriler tarayıcıda değil, sunucuda duruyor. src/server.ts bir Bun.serve sunucusu; bun:sqlite ile data/webdroid.sqlite dosyasını kullanıyor. Dört tablo var: cihazlar ve numaraları, cihaz başına anahtar-değer durumu, cihaz başına dosyalar ve çevrim dışı cihazları bekleyen mesajlar. İstemcideki zustand store'ları, bu anahtar-değer ucuna yazan bir depolama adaptörüyle oluşturuluyor. Not otomatik kaydı gibi her tuş vuruşunda oluşan yazmalar 400 ms bekletilip birleştiriliyor. Başarısız yazmalar yeniden deneniyor, sayfa kapanırken bekleyenler de keepalive ile gönderiliyor. Dosya uçları ses ve video için Range isteklerini yanıtlıyor. Yollar .. ya da boş karakter içeremiyor, cihaz kimlikleri de SQL çalışmadan önce doğrulanıyor.
Telefondaki Tarayıcı uygulaması sayfaları sunucudaki bir proxy üzerinden, Android kullanıcı ajanıyla çekiyor. Proxy yalnızca http ve https kabul ediyor; localhost, 10.x, 192.168.x gibi yerel ağ adreslerini reddediyor. HTML yanıtlara bir <base> etiketi ve küçük bir köprü betiği ekleniyor. Sayfa sandbox'lı bir srcdoc iframe'inde çiziliyor, bağlantı tıklamaları ve GET formları da postMessage ile uygulamaya iletiliyor. Yerel ağ kontrolü yalnızca adres adına baktığı için DNS rebinding'e karşı korumuyor; yerel bir simülatör için bunu kabul edilebilir buldum.
Pencere yöneticisini, dosya sistemini ve uygulama kaydını, tarayıcıda yazdığım diğer işletim sistemleriyle ayrı bir yazıda karşılaştırdım; merak edersen oradan devam edebilirsin.