initial release - nightd v1.1
24
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
21
LICENSE
Normal file
|
|
@ -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.
|
||||
109
README.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<p align="center">
|
||||
<img src="assets/banner.png" alt="nightd banner" width="400"/>
|
||||
</p>
|
||||
|
||||
<h1 align="center">nightd</h1>
|
||||
<p align="center">
|
||||
<em>screen dimmer for android tv</em><br>
|
||||
<strong>keep your media playing while the screen goes dark</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/sudoxnym/nightd/releases/latest"><img src="https://img.shields.io/github/v/release/sudoxnym/nightd?style=flat-square&color=00d4ff" alt="latest release"/></a>
|
||||
<a href="https://github.com/sudoxnym/nightd/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-ff69b4?style=flat-square" alt="license"/></a>
|
||||
<img src="https://img.shields.io/badge/android-7.0+-7B68EE?style=flat-square" alt="android 7.0+"/>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>built by <a href="https://github.com/sudoxnym">sudoxnym</a> for the sleep-deprived</sub>
|
||||
</p>
|
||||
33
app/build.gradle
Normal file
|
|
@ -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 {
|
||||
}
|
||||
62
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.sudox.nightd"
|
||||
android:versionCode="2"
|
||||
android:versionName="1.1">
|
||||
|
||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35" />
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="nightd"
|
||||
android:banner="@drawable/banner"
|
||||
android:theme="@style/NightdTheme">
|
||||
|
||||
<service
|
||||
android:name=".NightdService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="screen_overlay"/>
|
||||
<intent-filter>
|
||||
<action android:name="com.sudox.nightd.TOGGLE" />
|
||||
<action android:name="com.sudox.nightd.ON" />
|
||||
<action android:name="com.sudox.nightd.OFF" />
|
||||
<action android:name="com.sudox.nightd.DIM" />
|
||||
<action android:name="com.sudox.nightd.BLACK" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".NightdActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
15
app/src/main/java/com/sudox/nightd/BootReceiver.java
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
42
app/src/main/java/com/sudox/nightd/NightdActivity.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
149
app/src/main/java/com/sudox/nightd/NightdService.java
Normal file
|
|
@ -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<View> 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();
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable/banner.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
7
app/src/main/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="NightdTheme" parent="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
<item name="android:colorBackground">@android:color/black</item>
|
||||
</style>
|
||||
</resources>
|
||||
BIN
assets/banner.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
banner_source.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
16
build.gradle
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
2
gradle.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
org.gradle.jvmargs=-Xmx1536m
|
||||
android.useAndroidX=false
|
||||
BIN
icon_source.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
2
settings.gradle
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
rootProject.name = "nightd"
|
||||
include ':app'
|
||||