commit 704c999dc2aa9f10286b2d7e5a81df8231d8921e Author: sudoxnym Date: Sun Dec 7 12:14:08 2025 +0100 initial release - nightd v1.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48a6e5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Gradle +.gradle/ +build/ +*.class + +# Android +local.properties +*.apk +*.ap_ +*.dex + +# IDE +.idea/ +*.iml +.vscode/ + +# OS +.DS_Store +Thumbs.db + +# Build artifacts +obj/ +*.o +*.so diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0d7dcd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 sudoxnym + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eb8419 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +

+ nightd banner +

+ +

nightd

+

+ screen dimmer for android tv
+ keep your media playing while the screen goes dark +

+ +

+ latest release + license + android 7.0+ +

+ +--- + +## what is this? + +nightd is a tiny android tv app that blacks out your screen while keeping your media playing. perfect for: + +- **falling asleep to music/podcasts** - screen goes dark, audio keeps playing +- **saving power on oled displays** - no burn-in, no wasted pixels +- **background audio** - cast audio without lighting up your room + +## modes + +| mode | what it does | +|------|-------------| +| **dim** | single overlay layer (~40% darker) | +| **black** | 5 stacked layers = pitch black (android limits overlay opacity to 80%, so we stack 'em) | + +cycle order: `off → dim → black → off` + +## installation + +1. download the latest [nightd.apk](https://github.com/sudoxnym/nightd/releases/latest) +2. sideload via adb: `adb install nightd.apk` +3. grant "display over other apps" permission when prompted + +## usage + +### with key mapper (recommended) + +use [key mapper](https://github.com/keymapperorg/KeyMapper) to bind nightd to remote buttons: + +| action | intent | +|--------|--------| +| toggle | `com.sudox.nightd.TOGGLE` | +| dim mode | `com.sudox.nightd.DIM` | +| black mode | `com.sudox.nightd.BLACK` | +| turn off | `com.sudox.nightd.OFF` | + +**package:** `com.sudox.nightd` +**service:** `com.sudox.nightd.NightdService` + +example: bind "zoom in" to toggle, "zoom out" to off. + +### with adb + +```bash +# toggle (cycles through off → dim → black → off) +adb shell am startservice -a com.sudox.nightd.TOGGLE com.sudox.nightd/.NightdService + +# specific modes +adb shell am startservice -a com.sudox.nightd.DIM com.sudox.nightd/.NightdService +adb shell am startservice -a com.sudox.nightd.BLACK com.sudox.nightd/.NightdService +adb shell am startservice -a com.sudox.nightd.OFF com.sudox.nightd/.NightdService +``` + +## how it works + +android limits overlay opacity to 80% per layer. to achieve true black, nightd stacks 5 layers: + +``` +0.8 × 0.8 × 0.8 × 0.8 × 0.8 = 0.032 (96.8% blocked) +``` + +plus it sets screen brightness to 0 for good measure. + +## permissions + +- **SYSTEM_ALERT_WINDOW** - required to draw overlay +- **FOREGROUND_SERVICE** - keeps the overlay running +- **WRITE_SETTINGS** - adjusts screen brightness (optional, gracefully fails) + +## building from source + +```bash +# clone +git clone https://github.com/sudoxnym/nightd.git +cd nightd + +# build (requires android ndk) +./gradlew assembleRelease + +# or use the aapt/d8/apksigner toolchain directly +``` + +## license + +MIT - do whatever you want with it. + +--- + +

+ built by sudoxnym for the sleep-deprived +

diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..0aaf796 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.sudox.nightd' + compileSdk 34 + + defaultConfig { + applicationId "com.sudox.nightd" + minSdk 21 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + } + debug { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9e204b3 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/sudox/nightd/BootReceiver.java b/app/src/main/java/com/sudox/nightd/BootReceiver.java new file mode 100644 index 0000000..f75af54 --- /dev/null +++ b/app/src/main/java/com/sudox/nightd/BootReceiver.java @@ -0,0 +1,15 @@ +package com.sudox.nightd; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + // Service ready to be triggered after boot + // Does not auto-start overlay, just ensures service is available + } + } +} diff --git a/app/src/main/java/com/sudox/nightd/NightdActivity.java b/app/src/main/java/com/sudox/nightd/NightdActivity.java new file mode 100644 index 0000000..12bb9e9 --- /dev/null +++ b/app/src/main/java/com/sudox/nightd/NightdActivity.java @@ -0,0 +1,42 @@ +package com.sudox.nightd; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; + +public class NightdActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Check overlay permission first + if (!Settings.canDrawOverlays(this)) { + Intent intent = new Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + getPackageName()) + ); + startActivityForResult(intent, 100); + } else { + toggleService(); + finish(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == 100 && Settings.canDrawOverlays(this)) { + toggleService(); + } + finish(); + } + + private void toggleService() { + Intent intent = new Intent(this, NightdService.class); + intent.setAction("com.sudox.nightd.TOGGLE"); + startForegroundService(intent); + } +} diff --git a/app/src/main/java/com/sudox/nightd/NightdService.java b/app/src/main/java/com/sudox/nightd/NightdService.java new file mode 100644 index 0000000..8eee1fc --- /dev/null +++ b/app/src/main/java/com/sudox/nightd/NightdService.java @@ -0,0 +1,149 @@ +package com.sudox.nightd; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.IBinder; +import android.provider.Settings; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import java.util.ArrayList; +import java.util.List; + +public class NightdService extends Service { + private static final String CHANNEL_ID = "nightd_channel"; + private static final int NOTIFICATION_ID = 1; + + private WindowManager windowManager; + private List overlayViews = new ArrayList<>(); + private int currentMode = 0; // 0=off, 1=dim, 2=black + private int savedBrightness = 128; + + @Override + public void onCreate() { + super.onCreate(); + windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + createNotificationChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String action = intent == null ? null : intent.getAction(); + + if ("com.sudox.nightd.DIM".equals(action)) { + setMode(1); + } else if ("com.sudox.nightd.BLACK".equals(action)) { + setMode(2); + } else if ("com.sudox.nightd.OFF".equals(action)) { + setMode(0); + } else { + // Toggle: off -> dim -> black -> off + setMode((currentMode + 1) % 3); + } + + return START_STICKY; + } + + private void setMode(int mode) { + // Clear existing overlays + for (View v : overlayViews) { + try { windowManager.removeView(v); } catch (Exception e) {} + } + overlayViews.clear(); + + if (mode == 0) { + // Off + try { + Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, savedBrightness); + } catch (Exception e) {} + currentMode = 0; + stopForeground(true); + stopSelf(); + return; + } + + if (Settings.canDrawOverlays(this) == false) { + Intent permIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + permIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(permIntent); + return; + } + + // Save brightness on first activation + if (currentMode == 0) { + try { + savedBrightness = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS); + } catch (Exception e) {} + } + + try { + Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 0); + } catch (Exception e) {} + + startForeground(NOTIFICATION_ID, buildNotification(mode)); + + int layers = (mode == 1) ? 1 : 5; + for (int i = 0; i < layers; i++) { + FrameLayout layer = new FrameLayout(this); + layer.setBackgroundColor(0xFF000000); + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | + WindowManager.LayoutParams.FLAG_FULLSCREEN, + PixelFormat.TRANSLUCENT + ); + params.gravity = Gravity.TOP | Gravity.START; + params.screenBrightness = 0.0f; + + try { + windowManager.addView(layer, params); + overlayViews.add(layer); + } catch (Exception e) { break; } + } + currentMode = mode; + } + + private void createNotificationChannel() { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, "nightd overlay", NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("nightd screen overlay service"); + getSystemService(NotificationManager.class).createNotificationChannel(channel); + } + + private Notification buildNotification(int mode) { + Intent offIntent = new Intent(this, NightdService.class); + offIntent.setAction("com.sudox.nightd.OFF"); + PendingIntent pendingIntent = PendingIntent.getService(this, 0, offIntent, PendingIntent.FLAG_IMMUTABLE); + + String modeText = (mode == 1) ? "dim" : "black"; + return new Notification.Builder(this, CHANNEL_ID) + .setContentTitle("nightd: " + modeText) + .setContentText("tap to disable") + .setSmallIcon(android.R.drawable.ic_menu_view) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build(); + } + + @Override + public IBinder onBind(Intent intent) { return null; } + + @Override + public void onDestroy() { + setMode(0); + super.onDestroy(); + } +} diff --git a/app/src/main/res/drawable/banner.png b/app/src/main/res/drawable/banner.png new file mode 100644 index 0000000..cdf8235 Binary files /dev/null and b/app/src/main/res/drawable/banner.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..544150e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c461d11 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a8b3ce1 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..44957fd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..a68f30b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d012dd5 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ + + + + diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000..959cd6d Binary files /dev/null and b/assets/banner.png differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..aeec94a Binary files /dev/null and b/assets/icon.png differ diff --git a/banner_source.png b/banner_source.png new file mode 100644 index 0000000..959cd6d Binary files /dev/null and b/banner_source.png differ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9afd82c --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.2.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a5d55cb --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536m +android.useAndroidX=false diff --git a/icon_source.png b/icon_source.png new file mode 100644 index 0000000..aeec94a Binary files /dev/null and b/icon_source.png differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..19470ef --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "nightd" +include ':app'