Tuesday, January 13, 2026

Android java: input events processing (collection)

There 2 ways to handle input events from user input

  1. Immediate: using listener
  2. At process: at the end to process data, usually user fire "process" button 

Button

    @Override
    protected void onCreate(Bundle savedInstanceState) {
...
        // to process
        AppCompatButton buttonTest = findViewById(R.id.button_test); // change to your button id from layout
        buttonTest.setOnClickListener(myTest);
    }

    View.OnClickListener myButton = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // todo

        }
    };

TimePicker

1. Immediate 

...
        //on view created
        // date picker
        TimePicker myTimePicker = view.findViewById(R.id.my_time_picker);
        myTimePicker.setOnTimeChangedListener(timeChangeListener);
...
    // TimePicker Listeneer
    private final TimePicker.OnTimeChangedListener timeChangeListener =
            new TimePicker.OnTimeChangedListener() {
        @Override
        public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
            // TODO
        }
    };

2.  At process

    // Pull the values exactly as they are right now
    int hour = timePicker.getHour();
    int minute = timePicker.getMinute();

RadioGroup

1. Immediate

...
        //on view created
        // date picker
        RadioGroup radioSelectModeBackup = view.findViewById(R.id.radio_select_mode_backup);
        radioSelectModeBackup.setOnCheckedChangeListener(radioListener);
...
    // RadioGroup Listener
    RadioGroup.OnCheckedChangeListener radioListener = new RadioGroup.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(@NonNull RadioGroup radioGroup, int idSelected) {
            if (idSelected==R.id.radio_weekly) {
                // weekly
            } else if (idSelected==R.id.radio_moonthly) {
                // monthly
            }
        }
    };

2. At process 

    // Inside your "Save" or "Submit" button click listener
    int selectedId = radioSelectModeBackup.getCheckedRadioButtonId();

    if (selectedId == R.id.radio_weekly) {
        // Logic for weekly backup
    } else if (selectedId == R.id.radio_moonthly) {
        // Logic for monthly backup
    } else {
        // Nothing is selected (returns -1 if no default is set in XML)
    }

RecyclerView

RecyclerView layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="64dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="horizontal"
    android:background="#FFFFFF"
    android:gravity="center_vertical">

    <HorizontalScrollView
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:scrollbars="none"
        android:fillViewport="true">

        <TextView
            android:id="@+id/my_item_view"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center_vertical"
            android:paddingStart="12dp"
            android:paddingEnd="12dp"
            android:textSize="20sp"
            android:maxLines="1"
            android:textColor="#333333" />
    </HorizontalScrollView>

    <ImageButton
        android:id="@+id/btn_add_phrase"
        android:layout_width="64dp"
        android:layout_height="match_parent"
        android:src="@android:drawable/ic_input_add"
        android:background="?android:attr/selectableItemBackground"
        android:contentDescription="Add phrase"
        android:scaleType="centerInside"
        app:tint="#4CAF50" />

</LinearLayout>

RecyclerView Adapter and Holder

public class MyRecyclerView extends RecyclerView.Adapter<MyRecyclerView.ViewHolder> {

    String[] stringsTmp = {"Hello", "Test", "How are you?", "Help me write", "The quick brown fox jumps over the lazy dog\n The quick brown fox jumps over the lazy dog"};
    OnMyItemClickListener onMyItemClickListener;

    public MyRecyclerView(OnMyItemClickListener listener) {
        // Use the parameter 'listener', not the uninitialized global variable
        this.onMyItemClickListener = listener;
    }

    @NonNull
    @Override
    public MyRecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.layout_view_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyRecyclerView.ViewHolder holder, int position) {
        // Get the string from your array and put it in the TextView
        String text = stringsTmp[position];
        holder.textView.setText(text);

        // Add a click listener for the item NOT WORKINGN HERE
        // Set the click here. It can see 'onMyItemClickListener' and 'text'
        holder.addButton.setOnClickListener(v -> {
            Log.e("dedetok", "onBindViewHolder Clicked : " + text);
            if (onMyItemClickListener != null) {
                onMyItemClickListener.getString(text);
            }
        });

    }

    @Override
    public int getItemCount() {
        return stringsTmp.length;
    }


    /*
     * View Holder
     */
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView textView;
        public ImageButton addButton; // Changed from Button

        public ViewHolder(View view) {
            super(view);
            // Matches the ID in your item_phrase.xml
            textView = view.findViewById(R.id.my_item_view);
            addButton = view.findViewById(R.id.btn_add_phrase);

        }
    }

    public interface OnMyItemClickListener {
        void getString(String stringSelect);
    }
}

If you use action listener on item in holder, DO NOT add onItemSelected event into adapter or any else touch event. They will conflict with other event inside the items.

You need to implement interface  MyRecyclerView.OnMyItemClickListener on your activity or fragment.

You can remove all event on holder if you prefered to use onItemSelected event.

 

 

 

 

 

Monday, January 12, 2026

Debian 13: using systemd timesync to update ntp client time

Since Debain Jessie, Debian use systemd to synchronize ntp clinet

To list timezone:

# timedatectl list-timezones | grep Jakarta
Asia/Jakarta

To set timezone:

# timedatectl set-timezone Asia/Jakarta

To show

# timedatectl 
               Local time: Mon 2026-01-12 12:44:27 WIB
           Universal time: Mon 2026-01-12 05:44:27 UTC
                 RTC time: Mon 2026-01-12 05:44:27
                Time zone: Asia/Jakarta (WIB, +0700)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Edit /etc/systemd/timesyncd.conf

...
[Time]
#NTP=
NTP=0.id.pool.ntp.org 1.id.pool.ntp.org 2.id.pool.ntp.org 2.id.pool.ntp.org 3.id.pool.ntp.org
#FallbackNTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org
...

To restart service

# systemctl restart systemd-timesyncd

you can use old way using ntp client, but you need to remove this package

# timedatectl set-ntp false

Now you can install ntp and edit configuration /etc/ntp.conf.

 

 

Wednesday, January 7, 2026

Java: using build tool gradle wrapper to setup project and run

 

  • java application openjdk 17
  • gradle 8.7

install gradle 

# apt-get install gradle 

create folder project example

$ mkdir example
$ cd example

create init project

wrap gradle version for your project 

$ gradle wrapper --gradle-version 8.7
openjdk version "17.0.16" 2025-07-15
OpenJDK Runtime Environment (build 17.0.16+8-Debian-1deb12u1)
OpenJDK 64-Bit Server VM (build 17.0.16+8-Debian-1deb12u1, mixed mode, sharing)

init using gradle 8.7  debian 13 uses old 4.4 

$ ./gradlew init \
  --type java-application \
  --dsl groovy \
  --test-framework junit-jupiter \
  --package com.dedetok \
  --project-name example
openjdk version "17.0.16" 2025-07-15
OpenJDK Runtime Environment (build 17.0.16+8-Debian-1deb12u1)
OpenJDK 64-Bit Server VM (build 17.0.16+8-Debian-1deb12u1, mixed mode, sharing)
Downloading https://services.gradle.org/distributions/gradle-8.7-bin.zip
...............................................................................................................................
Unzipping ~/.gradle/wrapper/dists/gradle-8.7-bin/bhs2wmbdwecv87pi65oeuq5iu/gradle-8.7-bin.zip to ~/.gradle/wrapper/dists/gradle-8.7-bin/bhs2wmbdwecv87pi65oeuq5iu
Set executable permissions for: ~/.gradle/wrapper/dists/gradle-8.7-bin/bhs2wmbdwecv87pi65oeuq5iu/gradle-8.7/bin/gradle

Welcome to Gradle 8.7!

Here are the highlights of this release:
 - Compiling and testing with Java 22
 - Cacheable Groovy script compilation
 - New methods in lazy collection properties

For more details see https://docs.gradle.org/8.7/release-notes.html

Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details

Enter target Java version (min: 7, default: 21): 17

Select application structure:
  1: Single application project
  2: Application and library project
Enter selection (default: Single application project) [1..2] 1

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] no


> Task :init
To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.7/samples/sample_building_java_applications_multi_project.html

BUILD SUCCESSFUL in 5m 12s
1 actionable task: 1 executed

./settings.gradle

plugins {
    // Apply the foojay-resolver plugin to allow automatic download of JDKs
    id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}

rootProject.name = 'example'
include('app')

./app/build.gradle

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'application'
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation libs.junit.jupiter

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // This dependency is used by the application.
    implementation libs.guava
}

// Apply a specific Java toolchain to ease working on different environments.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

application {
    // Define the main class for the application.
    mainClass = 'com.dedetok.App'
}

tasks.named('test') {
    // Use JUnit Platform for unit tests.
    useJUnitPlatform()
}

./gradle/libs.versions.toml

[versions]
guava = "32.1.3-jre"
junit-jupiter = "5.10.1"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

To show current gradle version

$ ./gradlew --version

------------------------------------------------------------
Gradle 8.7
------------------------------------------------------------

Build time:   2024-03-22 15:52:46 UTC
Revision:     650af14d7653aa949fce5e886e685efc9cf97c10

Kotlin:       1.9.22
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.16 (Debian 17.0.16+8-Debian-1deb12u1)
OS:           Linux 6.12.57+deb13-amd64 amd64

Test run

$ ./gradlew run
Path for java installation '/usr/lib/jvm/openjdk-17' (Common Linux Locations) does not contain a java executable

> Task :app:run
Hello World!

BUILD SUCCESSFUL in 473ms
2 actionable tasks: 1 executed, 1 up-to-date

or you can run using style $ ./gradlew :app:run

Now add maridb jcoonect into project, using Traditional Groovy Approach. edit ./app/build.gradle

...
dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation libs.junit.jupiter

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // This dependency is used by the application.
    implementation libs.guava

    // mariadb jconnector (single file no need change libs.versions.toml
    implementation 'org.mariadb.jdbc:mariadb-java-client:3.3.3'

    // using libs.versions.toml must editing this file 
    //implementation libs.mariadb.client

}
...

If you wish to useing Version Catalog Approach, use 2nd option for ./app/build.gradle and edit libs.version.toml

[versions]
guava = "32.1.3-jre"
junit-jupiter = "5.10.1"
mariadb = "3.3.3"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
mariadb-client = { group = "org.mariadb.jdbc", name = "mariadb-java-client", version.ref = "mariadb" }

use Version Catalog Approach for complex project and involving multiple developers.

Edit ./app/src/main/java/com/dedetok/App.java

/*
 * This source file was generated by the Gradle 'init' task
 */
package com.dedetok;

import java.sql.*;

public class App {
    public String getGreeting() {
        return "Hello World!";
    }

    public static void main(String[] args) throws SQLException {
        System.out.println(new App().getGreeting());
        
        String dbUrl = "jdbc:mariadb://localhost:3306/mydatabase";
        String dbUName = "myuname";
        String dbPass = "mypass";
        
        Connection conn = DriverManager.getConnection(dbUrl, dbUName, dbPass);

    }
}

To generate single file jar for your project, edit  ./app/build.gradle

...
jar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    // change with yours
    manifest {
        attributes "Main-Class": "com.dedetok.App"
    }

    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}
...

You can run directly using java command

$ ./gradlew build
...
$ java -jar ./app/build/libs/app.jar
Hello World!

Your jar file location is ./app/build/libs/

Now you can edit  ./app/src/main/java/com/dedetok/App.java finish your project. Use any text editor if you wish.

Tuesday, January 6, 2026

Debian 13: bash to clean up unused gradle in folder $HOME/.gradle

stop any gradle daemon (better run this after you start your Debian)

copy paste this bash and change permission to execute

#!/bin/bash

# Set the cutoff date yyyy-mm-dd
CUTOFF_DATE="2025-08-01"

# Gradle folder
GRADLE_DIR="$HOME/.gradle"

# List of folders to clean
FOLDERS=("android" "build-scan-data" "caches" "daemon" "kotilin-profile" "native" "notifications" "undefined-build" "wrapper" ".tmp")

# 1. Stop Gradle Daemons first so files aren't locked
if [ -f "$GRADLE_DIR/daemon" ]; then
    echo "Stopping Gradle daemons..."
    gradle --stop 2>/dev/null || ./gradlew --stop 2>/dev/null
fi

echo "Cleaning Gradle folders in $GRADLE_DIR modified before $CUTOFF_DATE..."

for folder in "${FOLDERS[@]}"; do
    TARGET="$GRADLE_DIR/$folder"
    if [ -d "$TARGET" ]; then
        echo "Processing $TARGET..."
        # Find and delete files modified before the cutoff date
        find "$TARGET" -type f ! -newermt "$CUTOFF_DATE" -print -delete
        # Remove empty directories
        find "$TARGET" -type d -empty -print -delete
    else
        echo "Folder $TARGET does not exist, skipping."
    fi
done

echo "Cleanup complete."

Note:

  • change CUTOFF_DATE for any desire date
  • add or remove FOLDERS depends on your folder structure 

termux android: ffmpeg split video, rotate, convert h26h video, merge video

Requirement

Install termux from 

  • play store 
  • github.com/termux/termux-app latest but require allow install from unknown source

open termux and update

$ pkg upgrade

install ffmpeg

$ pkg install ffmpeg nano termux-api

Allow termux to manage files

$ termux-setup-storage

Finding the correct path 

Find location of your file, and open termux

You can get file info from file manager provided by your phone. This is information from Google File -> File info

/emulated/0/DCIM/Camera/Thumbnail/ebd64812dbf4ded1ce79xa 

Open your termux

 ~ $ cd ~/storage
~/storage $ pwd
/data/data/com.termux/files/home/storage
~/storage $ ls
audiobooks  downloads   movies    podcasts
dcim        external-0  music     shared
documents   media-0     pictures

In android it is DCIM but in termux it is become dcim 

Note these:

  • From file manager: /emulated/0/DCIM/Camera/Thumbnail/ebd64812dbf4ded1ce79xa -> to broadcast the files
  • From termux: /data/data/com.termux/files/home/storage/dcim/Camera/Thumbnail/ebd64812dbf4ded1ce79xa -> to browse in termux terminal

Split a video by time

Target:

  • every piece has 1 minutes 58 seconds
  • resolution low 480
  • output h264
  • sound mono

command to convert '172967815750944 (1).mp4'

$ ffmpeg -i 172967815750944\ \(1\).mp4 -vf scale=-2:480 -r 25 -c:v libx264 -preset veryfast -crf 28 -ac 1 -c:a aac -b:a 96k -f segment -segment_time 118 -reset_timestamps 1 out_%03d.mp4

this command will create files with name starting with name out_[number].mp4

This is fastest way to split video into smaller size, but you need to write the long command. This is bash sh to split file to avoid write long command. Open nano and copy paste it, name it with mysplitvid.sh. Make it execute $ chmod u+x mysplitvid.sh.

#!/data/data/com.termux/files/usr/bin/bash

# Check input
if [ -z "$1" ]; then
  echo "Usage: $0 inputvideo.mp4"
  exit 1
fi

INPUT="$1"
BASENAME=$(basename "$INPUT" .mp4)

# Output directory (public, visible to other apps)
OUTDIR="/sdcard/Download/${BASENAME}_split"
mkdir -p "$OUTDIR"

# FFmpeg split + encode
ffmpeg -i "$INPUT" \
  -vf scale=-2:480 \
  -r 25 \
  -c:v libx264 \
  -preset veryfast \
  -crf 28 \
  -ac 1 \
  -c:a aac \
  -b:a 96k \
  -f segment \
  -segment_time 118 \
  -reset_timestamps 1 \
  "$OUTDIR/${BASENAME}_out%02d.mp4"

# Notify Android media scanner for all outputs
for file in "$OUTDIR"/*.mp4; do
  am broadcast \
    -a android.intent.action.MEDIA_SCANNER_SCAN_FILE \
    -d "file://$file" >/dev/null
done

echo "✅ Done!"
echo "📂 Output folder: $OUTDIR”

Convert video h256 to social media

Target:

  • Video Codec: H.264 (libx264)
  • Audio Codec: AAC
  • Pixel Format: yuv420p (Required for maximum compatibility)
  • Audio Sample Rate: 48kHz or 44.1kHz
  • Container: MP4  

To convert an H.265 MP4 to a Twitter-compatible, highly compressed, lower-resolution MP4 using FFmpeg, you need to re-encode the video to H.264 video codec, AAC audio codec, a maximum resolution of 1280x720, and a lower bitrate/higher CRF value.

Assume your file is inputfile.mp4 and located at Download folder in your Android.  Here are command to convert your video to make compatible for social media. Currently social media do not support high compression like h265.

~/ $ cd ~/storage
~/storage $ ls
audiobooks downloads movies podcasts
dcim external-0 music shared
documents media-0 pictures
~/storage $ cd downloads
~/storage/downloads $ ls *.mp4
inputfile.mp4
~/storage/downloads $ ffmpeg -i inputfile.mp4 -c:v libx264 -crf 28 -preset medium -vf "scale=1280:720,format=yuv420p" -c:a aac -b:a 128k -movflags faststart output.mp4
...

This is bash sh to split file to avoid write long command. Open nano and copy paste it, name it with myconverth265.sh. Make it execute $ chmod u+x myconverth265.sh.

#!/data/data/com.termux/files/usr/bin/bash
set -e
if [ $# -ne 2 ]; then
    echo "Usage: $0 input.mp4 output.mp4"
    exit 1
fi

INPUT="$1"
OUTPUT="$2"

ffmpeg -i "$INPUT" \
  -c:v libx264 -crf 28 -preset medium \
  -vf "scale=1280:720,format=yuv420p" \
  -c:a aac -b:a 128k \
  -movflags faststart \
  "$OUTPUT"

MEDIA_PATH="$(realpath "$OUTPUT")"

am broadcast \
  -a android.intent.action.MEDIA_SCANNER_SCAN_FILE \
  -d "file://$MEDIA_PATH"

echo "Scanned:"
echo "$MEDIA_PATH"

Rotate

ffmpeg -i inputvid.mp4 -vf "transpose=1" outputvid.mp4
Parameters:
-i inputvid.mp4: Specifies the input video file.
-vf "transpose=1": This filter rotates the video 90 degrees clockwise.
0 = 90° counterclockwise and vertical flip (default) 
1 = 90° clockwise 
2 = 90° counterclockwise 
3 = 90° clockwise and vertical flip

Example use

$ cd storage/dcim/Camera
ffmpeg -i input.mp4 -vf "transpose=2" out.mp4
$ ls ~/storage/dcim/Camera/out.mp4 
/data/data/com.termux/files/home/storage/dcim/Camera/out.mp4
am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///storage/emulated/0/DCIM/Camera/out.mp4

Merge file with the same video properties

list file to merge

~/.../Thumbnail/ebd64812dbf4ded1ce79xa $ ls
out_001.mp4  out_004.mp4  out_007.mp4
out_002.mp4  out_005.mp4
out_003.mp4  out_006.mp4 

Create file list.txt

~/.../Thumbnail/ebd64812dbf4ded1ce79xa $ nano list.txt
E.g
# comment list of files
file 'out_001.mp4'
file 'out_002.mp4'
file 'out_003.mp4'
file 'out_004.mp4'
file 'out_005.mp4'
file 'out_006.mp4'
file 'out_007.mp4’ 

Merge

~/.../Thumbnail/ebd64812dbf4ded1ce79xa $ ffmpeg -f concat -safe 0 -i list.txt -c copy merged.mp4 

broadcast the new video

~/.../Thumbnail/ebd64812dbf4ded1ce79xa $ am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/DCIM/Camera/Thumbnail/ebd64812dbf4ded1ce79xa

Make your output video accessible by other application 

Your output file owner is Termux. To make it accessible by other applications e.g. photos, files, etc, you can try these option

• Force android system to scan your output

$ am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/[real_location]

From file location above

Note these:

  • From file manager: /emulated/0/DCIM/Camera/Thumbnail/ebd64812dbf4ded1ce79xa
  • From termux: /data/data/com.termux/files/home/storage/dcim/Camera/Thumbnail/ebd64812dbf4ded1ce79xa 

Replace [real_location] becomes file:///sdcard/DCIM/Camera/Thumbnail/ebd64812dbf4ded1ce79xa

It will scan all files in drive, or you can specify for specific file only.

Using termux-api (termux-media-scan) is an alternative way for am.  On newer Android versions (Android 10+), this may not work due to scoped storage restrictions. termux-media-scan is the recommended way. 

$ termux-media-scan /sdcard/[real_location] 

I test am and termux-media-scan on android 13 infinix Note 12, they work properly without issue. except for multimedia store in Thumbnail folder, only show in file manager, e.g google files.

Using bash script

To use bash script, at ~/ create bash script e.g. convert.sh and you can paste the bash code above under nano editor

~/ $ nano convert.sh
~/ $ chmod +x convert.sh

Goto directory e.g Download and run the script using ~/[filename].sh e.g 

~/.../Thumbnail/ebd64812dbf4ded1ce79xa $ ~/convert.sh vidIn.mp4 vinOut.mp3