Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Monday, February 16, 2026

Android java: local webview from local html e.g help page

Create directory and html files

Switch to Project View (not Android view)

  1. Go to: [your_project]
  2. app/src/main/
  3. Right click on main
  4. → New
  5. → Directory
  6. → select or type "assets"
  7. right click "assets" 
  8. → New
  9. → Directory
  10. → type "webhelp"
  11. right click "webhelp" 
  12. → New
  13. → File
  14. → type "htmlhelp.html"
  15. Repeat step 11 for your language e.g. id for Indonesia e.g. "htmlhelp_id.html" 

You can copy paste html code into htmlhelp.html" and "htmlhelp_id.html"

here is layout to show the webview

    ...
    <WebView
        android:id="@+id/my_web_help"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
    ... 

here is code to show the webview

...
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
        WebView webView = view.findViewById(R.id.my_web_help);

        // Safe defaults
        webView.getSettings().setJavaScriptEnabled(false);
        webView.getSettings().setAllowFileAccess(true);
        webView.getSettings().setDomStorageEnabled(false);

        loadHelpPage(webView);
...
    }

    /*
     * file html helper
     */
    private void loadHelpPage(WebView webView) {

        String lang = Locale.getDefault().getLanguage();
        String fileName;

        if ("id".equals(lang)) {
            fileName = "htmlhelp_id.html";
        } else {
            fileName = "htmlhelp.html";
        }

        webView.loadUrl("file:///android_asset/webhelp/" + fileName);
    }
... 

Optional configuration webview when your application got trouble when submit to application store e.g google play store

WebSettings settings = webView.getSettings();

settings.setJavaScriptEnabled(false);
settings.setDomStorageEnabled(false);
settings.setAllowFileAccess(true);          // needed for assets
settings.setAllowContentAccess(false);
settings.setAllowFileAccessFromFileURLs(false);
settings.setAllowUniversalAccessFromFileURLs(false);
settings.setSupportZoom(false);
settings.setBuiltInZoomControls(false);

  

Friday, February 13, 2026

java: comparison between database api in java android and jdbc in java standard edition

Here is comparison between database api in java android and jdbc in java standard edition  

Operation

Java Android

Java SE (JDBC)

INSERT

Returns new row ID (long)

Returns affected rows (int)

UPDATE

Returns affected rows (int)

Same

DELETE

Returns affected rows (int)

Same                      

In java SE (JDBC) id can be retrive using jse ps.getGeneratedKeys() return long where ps is PreparedStatement.

 

Android java: notification pattern code

add permission in project 

AndroidManifest.xml

<manifest ...>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

MainActivity.java 

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        checkPermissions();
    ....
    }



    /*
     * check permission to read contact and write shared folder Download
     */
    private void checkPermissions() {
        List<String> permissions = new ArrayList<>();

        // 1. Android 13+ Notification Permission
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            permissions.add(Manifest.permission.POST_NOTIFICATIONS);
        }

        // API require string convert List<String> to String[];
        requestPermissionLauncher.launch(permissions.toArray(new String[0]));
    }

    /*
     * callback permission request
     */
    private final ActivityResultLauncher<String[]> requestPermissionLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
                // Handle Notification permission (Android 13+)
                boolean notificationsGranted = true;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    notificationsGranted = result.getOrDefault(Manifest.permission.POST_NOTIFICATIONS, false);
                }

            }); 

create MyNotificationService.java class

public class MyNotificationService {

    public static final String CHANNEL_ID = "id_string_channel"; // todo
    public static final int NOTIF_ID = 123456;  // todo

    /*
     * to send notification
     */
    public static void sendNotification(Context context, String title, String message) {
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        NotificationCompat.Builder myNotifBuilder = getNotificationBuilder(context);
        myNotifBuilder
                .setContentTitle(title)
                .setContentText(message);

        notificationManager.notify(NOTIF_ID, myNotifBuilder.build());
    }

    /*
     * get notification for foreground
     */
    public static Notification getForegroundNotification(Context context) {
        Notification myNotif = getNotificationBuilder(context).build();
        return myNotif;
    }
    
    /*
     * Helper that returns the BUILDER so you can customize it before building
     */
    private static NotificationCompat.Builder getNotificationBuilder(Context context) {
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) {
                NotificationChannel notifChannel = new NotificationChannel(
                        CHANNEL_ID,
                        "Background Sync Service",
                        NotificationManager.IMPORTANCE_LOW
                );
                notificationManager.createNotificationChannel(notifChannel);
            }
        }

        return new NotificationCompat.Builder(context, CHANNEL_ID)
                .setSmallIcon(android.R.drawable.stat_notify_sync)
                .setPriority(NotificationCompat.PRIORITY_LOW)
                .setCategory(Notification.CATEGORY_SERVICE);
        //.setOngoing(true); // can not swap away
    }

 

 

Thursday, February 12, 2026

Android java: fragment pattern code

Fragment

use just portrait for phone and w600 for tablet (bigger screen)   

for portrait (phone) 

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    >
    
    <!-- Top TextView -->
    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/top_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/app_name"
            android:textSize="18sp"
            android:textStyle="bold"
            android:padding="12dp"
            android:gravity="start|center_vertical"
            android:background="#DDDDDD"
            android:layout_weight="1"
            />

        <androidx.appcompat.widget.AppCompatImageButton
            android:id="@+id/button_help"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:background="?android:attr/selectableItemBackground"
            android:src="@android:drawable/ic_menu_help" />
    </androidx.appcompat.widget.LinearLayoutCompat>
    
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/my_fragment_container_main"
        android:name="[Your java main fragment with full package name]"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />

</androidx.appcompat.widget.LinearLayoutCompat>

for w600 tablet

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <!-- Top TextView -->
    <TextView
        android:id="@+id/top_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Your Title Here"
        android:textSize="18sp"
        android:textStyle="bold"
        android:padding="12dp"
        android:gravity="center"
        android:background="#DDDDDD" />

    <!-- Horizontal Content -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="horizontal"
        android:weightSum="3">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/my_fragment_container_left"
        android:name="[Your java left fragment with full package name]"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <View
        android:id="@+id/divider_line"
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:background="#000000" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/my_fragment_container_main"
        android:name="[Your java main fragment with full package name]"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="2" />

    </LinearLayout>

</LinearLayout> 

Guideline for layout LinearLayout

  • orientation android:orientation="vertical" 
    android:layout_width="match_parent" 
    android:layout_height="0dp" 
    android:layout_weight="1"
  • orientation android:orientation="horizontal"
    android:weightSum="3" 
    android:layout_width="0dp" 
    android:layout_height="match_parent" android:layout_weight="1"  

in main activity on onCreate(Bundle savedInstanceState)

    public boolean isDualPane = false; // default is portrait

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        isDualPane = findViewById(R.id.my_fragment_container_left) != null;

        // R.id.main id for root portrait & w600

        AppCompatImageButton buttonHelp = findViewById(R.id.button_help);
        buttonHelp.setOnClickListener(buttonHelpListener);        

        // Load Fragments only if this is a fresh start (savedInstanceState == null)
        // This prevents overlapping fragments during screen rotation
        if (savedInstanceState == null) {

            // If system loaded the w600 layout, load both Fragment
            if (isDualPane) {
                replaceFragment(
                        R.id.my_fragment_container_left,
                        new FragmentLeft(),
                        null);
                replaceFragment(
                        R.id.my_fragment_container_main,
                        new FragmentMain(),
                        null);
            } else {
                // default load main container
                replaceFragment(
                        R.id.my_fragment_container_main,
                        new FragmentMain(),
                        null); // when navigating add "Fragment Main"
            }
        }

        View.OnClickListener buttonHelpListener = new View.OnClickListener() {
   
         @Override
       
     public void onClick(View view) {
           
     Fragment currentFragment =
               
         getSupportFragmentManager()
                   
             .findFragmentById(R.id.my_fragment_container_main);
                if (currentFragment instanceof FragmentMain) {
                    // NOTE: we still use R.id.my_fragment_container_main in portrait
  
                  replaceFragment(
                            R.id.my_fragment_container_main,
                            new FragmentLeft(),
                            "Main Menu");
                } else {
                    getSupportFragmentManager().popBackStack();
                }
            }
        };

        /*
         * fragment helper
         */
        private void replaceFragment(
                         int containerId,
                         Fragment fragment,
                         String addToBackStackString) {
            FragmentTransaction transaction =
                    getSupportFragmentManager().beginTransaction();
            transaction.replace(containerId, fragment);
            if (addToBackStackString!=null) {
                transaction.addToBackStack(addToBackStackString);
            }
            transaction.commit();
        }
        ....
To implement onbackpresseddispatcher pattern code, see this  https://dedetoknotes.blogspot.com/2026/01/android-java-onbackpresseddispatcher.html

 

Thursday, January 29, 2026

Android java: write and delete file in folder shared folder e.g Download pattern code

Target device Android 7 (API 24) to Android 16 (API 36)

AndroidManifest.xml permission 

     <uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
    <!-- targeting api 13++ use media store -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

To write contact (vcard) to folder com.dedetok.wredacontactbackup in folder Download

    final static String subFolder = "com.dedetok.wredacontactbackup"; // folder ref
    private static File backupFolder, backupFile; //


    private static boolean writeSingleVcf(Context context, Collection<VCard> vCards) {
        try {

            String date = new SimpleDateFormat("yyyyMMddHHmmSS", Locale.US).format(new Date());
            String fileName = date + ".vcf";


            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                // ✅ API 29-36 (Android 10+): Use MediaStore (Scoped Storage)
                return MyWorker.writeMediaStore(context, fileName, vCards);
            } else {
                // ✅ API 24-28 (Android 7-9): Use traditional File API
                // ✅ API 29-36 (Android 10+): Use MediaStore (Scoped Storage)
                return MyWorker.writeLegacy(context, fileName, vCards);

            }
        } catch (Exception e) {
            //Log.e("dedetok",e.getMessage()); // debug
            return false;
        }
    }


    /*
     * write backup MediaStore
     */
    @RequiresApi(api = Build.VERSION_CODES.Q)
    private static boolean writeMediaStore (Context context, String fileName, Collection<VCard> vCards) throws IOException{
        ContentValues values = new ContentValues();
        //Log.e("dedetok", "writeMediaStore"); // debug
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
        values.put(MediaStore.MediaColumns.MIME_TYPE, "text/x-vcard");
        // Creates 'Download/YourSubFolder/' automatically
        values.put(MediaStore.MediaColumns.RELATIVE_PATH,
                Environment.DIRECTORY_DOWNLOADS + File.separator + subFolder);

        Uri uri = context.getContentResolver().insert(
                MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
        if (uri == null) return false;

        OutputStream os = context.getContentResolver().openOutputStream(uri);
        Ezvcard.write(vCards).version(VCardVersion.V3_0).go(os);
        return true;
    }

    /*
     * write backup in legacy mode
     */
    private static boolean writeLegacy(Context context, String fileName, Collection<VCard> vCards) throws IOException{
        // Setup Directory
        File downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        backupFolder = new File(downloads, MySharePreferences.PREF_NAME);
        // Create folder if missing
        if (!backupFolder.exists()) {
            boolean success = backupFolder.mkdirs();
            if (!success) {
                //Log.e("dedetok", "Failed to create folder. Check permissions!"); // debug
                return false;
            }
        }

        backupFile = new File(backupFolder, fileName);
        try (FileOutputStream fos = new FileOutputStream(backupFile)) {
            Ezvcard.write(vCards).version(VCardVersion.V3_0).go(fos);
        }

        //Log.d("dedetok", "File saved at: " + backupFolder.getName()); // debug

        // Tell Android to scan the file so it shows up in Windows/File Manager
        MediaScannerConnection.scanFile(
                context,
                new String[]{ backupFile.getAbsolutePath() },
                null,
                new MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String path, Uri uri) {
                        //Log.e("dedetok", "Scanned: " + path);
                    }
                }
        );

        //Log.e("dedetok", "File created successfully "+backupFile.getName()); // debug
        return true;

    }

To delete file created by application from folder com.dedetok.wredacontactbackup in folder Download

    /*
     * delete File helper for android 7 yo 16
     */
    static private boolean deleteFile(Context context, File fileToDelete) {
        boolean returnValue = false;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            // Android 7–9
            if (fileToDelete.exists()) {
                returnValue = fileToDelete.delete();
                if (returnValue) {
                    // IMPORTANT: Tell the system the file is gone so Google Files updates
                    MediaScannerConnection.scanFile(context, new String[]{fileToDelete.getAbsolutePath()}, null, null);
                }
            }
        } else {
            // Android 10 to 16
            ContentResolver resolver = context.getContentResolver();
            Uri collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI;

            // IMPORTANT: RELATIVE_PATH must end with a slash /
            String relativePath = Environment.DIRECTORY_DOWNLOADS + "/" + subFolder + "/";

            String selection = MediaStore.MediaColumns.DISPLAY_NAME + "=? AND " +
                    MediaStore.MediaColumns.RELATIVE_PATH + "=?";

            String[] selectionArgs = new String[]{
                    fileToDelete.getName(),
                    relativePath
            };

            int rows = 0;
            try {
                // Directly delete from MediaStore - this usually deletes the physical file too
                rows = resolver.delete(collection, selection, selectionArgs);
            } catch (Exception e) {
                //Log.e("dedetok", "MediaStore delete failed", e);
            }

            if (rows > 0) {
                returnValue = true;
            } else {
                // FALLBACK: Physical delete if MediaStore didn't find it
                if (fileToDelete.exists()) {
                    returnValue = fileToDelete.delete();
                    if (returnValue) {
                        // Sync the index
                        MediaScannerConnection.scanFile(context, new String[]{fileToDelete.getAbsolutePath()}, null, null);
                    }
                }
            }
        }
        return returnValue;
    }

 

Android java: OnBackPressedDispatcher pattern code

AndroidManifest.xml 

    <application
...
        android:enableOnBackInvokedCallback="true" 

Main Activity 

On main activity declare:

        // callback on back key pressed
        OnBackPressedCallback myBackPressedCallback = new OnBackPressedCallback(true) {
            @Override
            public void handleOnBackPressed() {

                FragmentManager fm = getSupportFragmentManager();

                // Pop fragment back stack if possible
                if (fm.getBackStackEntryCount() > 0) {
                    fm.popBackStack();
                    return;
                }

                // Log.e("dedetok", "handleOnBackPressed()"); //debug
                AlertDialog.Builder myAliertDialog = new AlertDialog.Builder(
                        MainActivity.this);
                myAliertDialog.setTitle(R.string.dialog_title).
                        setMessage(R.string.dialog_message).
                        setIcon(android.R.drawable.ic_dialog_alert).
                        setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                // finish() will execute onDestroy()
                                finish();
                            }
                        }).
                        setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                // do nothing
                            }
                        }).
                        show();
            }
        };

onCreate() in main activity

    ...
        // onbackkeypress
        // onBackPressed() deprecated -> OnBackPressedCallback & getOnBackPressedDispatcher()
        // dispatcher
        OnBackPressedDispatcher myOnbackPressedDispatcher = getOnBackPressedDispatcher();

        // add callback to dispatcher
        myOnbackPressedDispatcher.addCallback(this, myBackPressedCallback);
    ...

Fragment

To replace fragment in main activity

               Fragment helpFragment = new FragmentHelp();

                requireActivity()
                        .getSupportFragmentManager()
                        .beginTransaction()
                        .replace(R.id.fragment_container, helpFragment)
                        .addToBackStack("help")
                        .commit();

addToBackStack("help") is the way android system implement on back pressed

If you have button back on your fragment e.g helpFragment, you need to emulate OnBackPressedDispatcher

                requireActivity()
                        .getOnBackPressedDispatcher()
                        .onBackPressed();

Activity B

onCreate in activity B

    // Inside Activity B's onCreate
    getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
        @Override
        public void handleOnBackPressed() {
        // This runs when the physical back button OR 
        // the programmatic onBackPressed() is called.
        finish(); 
        }
    });

    // Inside your Button's onClick (Assuming it's in the Activity)
    buttonExit.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
        // This triggers the callback above
        getOnBackPressedDispatcher().onBackPressed();
        }
    });

 

 

Friday, January 23, 2026

Android java: gradle corrupted, how to fix it

This is error message when gradle corrupted due to accidentally deleted or fail updated.

"The contents of the immutable workspace '~/.gradle/caches/9.1.0/groovy-dsl/52b1078aa736f3877f522d2046a7f837' have been modified" 

To make easier to fix gradle, exit Android Studio, open shell and go to project root directory. You will find file gradlew.

Clean up your gradle

$ ./gradlew --stop
Stopping Daemon(s)
2 Daemons stopped
$ rm -rf ~/.gradle/caches/9.1.0/

Run help to force gradlew download and rebuild library required.

$ ./gradlew help
Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details

> Configure project :app
WARNING: The option setting 'android.usesSdkInManifest.disallowed=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.sdk.defaultTargetSdkToCompileSdkIfUnset=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.enableAppCompileTimeRClass=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.builtInKotlin=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.newDsl=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.r8.optimizedResourceShrinking=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.defaults.buildfeatures.resvalues=true' is deprecated.
The current default is 'false'.
It will be removed in version 10.0 of the Android Gradle plugin.
w: file://~/Workspace/Android/WredaContactBackup/app/build.gradle.kts:5:1: 'fun Project.android(configure: Action<BaseAppModuleExtension>): Unit' is deprecated. Replaced by com.android.build.api.dsl.ApplicationExtension.
This class is not used for the public extensions in AGP when android.newDsl=true, which is the default in AGP 9.0, and will be removed in AGP 10.0.
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties

> Task :help

Welcome to Gradle 9.1.0.

To run a build, run gradlew <task> ...

To see a list of available tasks, run gradlew tasks

To see more detail about a task, run gradlew help --task <task>

To see a list of command-line options, run gradlew --help

For more detail on using Gradle, see https://docs.gradle.org/9.1.0/userguide/command_line_interface.html

For troubleshooting, visit https://help.gradle.org

BUILD SUCCESSFUL in 28s
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html

Open your Android Studio, repair android studio project choose index repair.

If this error occurred

"Ambiguous method call: both 'FragmentActivity.onCreate(Bundle) (In ~/.gradle/caches/9.1.0/transforms/dae60150ff0476652c6b13819aa96f10/transformed/fragment-1.5.4/jars/classes.jar!/androidx/fragment/app/FragmentActivity.class)' and 'ComponentActivity.onCreate(Bundle) (In ~/.gradle/caches/9.1.0/transforms/48c7a661e0aa50fff79a603bd45cc34c/transformed/activity-1.12.2/jars/classes.jar!/androidx/activity/ComponentActivity.class)' match"

Invalidate cache (everything) and restart.

If you find some deprecated gradle option like these:

$ ./gradlew help
Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details

> Configure project :app
WARNING: The option setting 'android.usesSdkInManifest.disallowed=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.sdk.defaultTargetSdkToCompileSdkIfUnset=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.enableAppCompileTimeRClass=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.builtInKotlin=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.newDsl=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.r8.optimizedResourceShrinking=false' is deprecated.
The current default is 'true'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The option setting 'android.defaults.buildfeatures.resvalues=true' is deprecated.
The current default is 'false'.
It will be removed in version 10.0 of the Android Gradle plugin.
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties
WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance.
To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties

> Task :help

Welcome to Gradle 9.1.0.

To run a build, run gradlew <task> ...

To see a list of available tasks, run gradlew tasks

To see more detail about a task, run gradlew help --task <task>

To see a list of command-line options, run gradlew --help

For more detail on using Gradle, see https://docs.gradle.org/9.1.0/userguide/command_line_interface.html

For troubleshooting, visit https://help.gradle.org

BUILD SUCCESSFUL in 5s
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html

Edit file gradle.properties, find those configuration and take some adjustment. You can close Android Studio and back to shell, this is my gradle.properties:

# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
#android.defaults.buildfeatures.resvalues=true #deprecated
#android.sdk.defaultTargetSdkToCompileSdkIfUnset=false #deprecated
#android.enableAppCompileTimeRClass=false #deprecated
#android.usesSdkInManifest.disallowed=false #deprecated
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
#android.r8.optimizedResourceShrinking=false #deprecated
#android.builtInKotlin=false #deprecated
#android.newDsl=false #deprecated
# edited dedetok
#android.enableJetifier=true
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false

Validate gradle is clear

$ ./gradlew help

> Task :help

Welcome to Gradle 9.1.0.

To run a build, run gradlew <task> ...

To see a list of available tasks, run gradlew tasks

To see more detail about a task, run gradlew help --task <task>

To see a list of command-line options, run gradlew --help

For more detail on using Gradle, see https://docs.gradle.org/9.1.0/userguide/command_line_interface.html

For troubleshooting, visit https://help.gradle.org

BUILD SUCCESSFUL in 503ms
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.1.0/userguide/configuration_cache_enabling.html

Now you have clean project.  

Saturday, January 17, 2026

Android java: dealing with Trusted credentials on Android 7.1.1 below for letencrypt

Dealing with connection to https on old android may depend on Trusted credentials on the the device. 

Letsencrypt root certificate does not installed on Trusted credentials prior android 7.1.1.

Some application may throw:

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

Solution 1 add user Trusted credentials 

Install Letsencrypt pem manually on old phone i.e android version 7.1.1 or older. You can download from https://letsencrypt.org/certs/isrgrootx1.pem?hl=en-US .

On your device (mine is evercoss gen pro x pro android 7.0). Go to setting -> Security.

You may find:

  • Trusted Credentials 
  • User Credentials
  • Install from SD Card

Choose "Install from SD Card":

  • Filed Name of Certification e.g ISGR ROOT X1 or Letsecrypt.
  • Credentials Use select VPN and aps

Open your Android Studio project and create res/xml/network_security_config.xml.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="system" />
            <certificates src="user" />
        </trust-anchors>
    </base-config>
</network-security-config>

Edit AndroidManifest.xml

...
   <application
    ... 
   android:networkSecurityConfig="@xml/network_security_config"
   ...
   >
...

If https server ever falls back to http (not https), you should add cleartextTrafficPermitted="true" to the <base-config> tag (Not Recommended except your application need to access http):

...
<base-config 
   ...
   cleartextTrafficPermitted="true"
   ...
>
...

if this XML fix doesn't work, it's not the certificate—it's the Android 7.0 Cipher bug. In that specific case, you will have to use Conscrypt library (Solution 2).

Solution 2 using Conscript Library

Add dependency into gradle.build app

dependencies {
    ...
    implementation 'org.conscrypt:conscrypt-android:2.5.2'
    ...
}

Initialize at application startup or foreground

...
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { 
    // Below Android 7.1.1
    Security.insertProviderAt(Conscrypt.newProvider(), 1);
}
...


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.

 

 

 

 

 

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 

Thursday, December 4, 2025

Android java: improving application screen form factor

In AndroidManifest.xml we can define screen form factor to used (androidmanifest.xml android:screenOrientation).

These are the options

  1. "unspecified" : The default value. The system chooses the orientation. The policy it uses, and therefore the choices made in specific contexts, might differ from device to device.
  2. "behind" : The same orientation as the activity that's immediately beneath it in the activity stack.
  3. "landscape" : Landscape orientation (the display is wider than it is tall).
  4. "portrait" : Portrait orientation (the display is taller than it is wide).
  5. "reverseLandscape" : Landscape orientation in the opposite direction from normal landscape. Added in API level 9.
  6. "reversePortrait" : Portrait orientation in the opposite direction from normal portrait. Added in API level 9.
  7. "sensorLandscape" : Landscape orientation, but can be either normal or reverse landscape based on the device sensor. The sensor is used even if the user has locked sensor-based rotation. Added in API level 9.
  8. "sensorPortrait" : Portrait orientation, but can be either normal or reverse portrait based on the device sensor. The sensor is used even if the user has locked sensor-based rotation. However, depending on the device configuration, upside-down rotation might not be allowed. Added in API level 9.
  9. "userLandscape" : Landscape orientation, but can be either normal or reverse landscape based on the device sensor and the user's preference. Added in API level 18.
  10. "userPortrait" : Portrait orientation, but can be either normal or reverse portrait based on the device sensor and the user's preference. However, depending on the device configuration, upside-down rotation might not be allowed. Added in API level 18.
  11. "sensor" : The device orientation sensor determines the orientation. The orientation of the display depends on how the user is holding the device. It changes when the user rotates the device. Some devices, though, don't rotate to all four possible orientations, by default. To use all four orientations, use "fullSensor". The sensor is used even if the user locked sensor-based rotation.
  12. "fullSensor" : The device orientation sensor determines the orientation for any of the four orientations. This is similar to "sensor", except this allows for any of the four possible screen orientations regardless of what the device normally supports. For example, some devices don't normally use reverse portrait or reverse landscape, but this enables those orientations. Added in API level 9.
  13. "nosensor" : The orientation is determined without reference to a physical orientation sensor. The sensor is ignored, so the display doesn't rotate based on how the user moves the device.
  14. "user" : The user's current preferred orientation.
  15. "fullUser" : If the user has locked sensor-based rotation, this behaves the same as user, otherwise it behaves the same as fullSensor and allows any of the four possible screen orientations. Added in API level 18.
  16. "locked" : Locks the orientation to its current rotation, whatever that is. Added in API level 18. 

Warning: To improve the layout of apps on form factors with smallest width >= 600dp, the system ignores the following values of this attribute for apps that target Android 16 (API level 36):

  1.     portrait
  2.     landscape
  3.     reversePortrait
  4.     reverseLandscape
  5.     sensorPortrait
  6.     sensorLandscape
  7.     userPortrait
  8.     userLandscape 

To create variant for table,  Goto main activity, on your main activity dropdown select "create table qualifier", it will create copy of main activity that require to modify to match tablet out. the layout is on layout -> main_activity -> main_activity.xml (sw600dp).

I use:

  1. main_activity.xml
  2. main_activity.xml (sw600dp)
main_activity.xml will be use for all mobile device, don't create main_activity (land). main_activity.xml (sw600dp) will be use on tablet. 

In case you want to lock phone to use portrait, delete main_activity.xml (land), you need to remove android:screenOrientation from AndroidManifest.xml. To make it consistent, this is sample of code in java for your main activity

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 1. Enable edge-to-edge for Java Activities
        // You need to import androidx.activity.EdgeToEdge;
        EdgeToEdge.enable(this);
        super.onCreate(savedInstanceState);
        // Fragment
        // layout portrait or lanscape?
        // portrait my_fragment_container
        // langsacpe fragment_config_container, fragment_main_container
        setContentView(R.layout.activity_m); // general, let system choose between portrait and landscape
        // Check if landscape (two-pane) layout is active
        View config = findViewById(R.id.fragment_config_container);
        View main = findViewById(R.id.fragment_main_container);
        isTwoPane = (config != null && main != null); // already checked not null
        // Conditional Orientation Lock
        if (!isTwoPane) {
            // If single-pane (phone), force portrait before fragment transactions
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }
        if (isTwoPane) {
            // dual-pane
            setFragment(R.id.fragment_config_container, new FragmentConfig());
            setFragment(R.id.fragment_main_container, new FragmentMain());
        } else {
            // single-pane
            // all phone must use this activity_m.xml (land)
            setFragment(R.id.my_fragment_container, new FragmentMain());
        }
        ...
    }

    private void setFragment(int containerId, Fragment fragment) {
        View container = findViewById(containerId);
        if (container == null) {
            //Log.e("dedetok", "Container ID " + containerId + " not found in this layout!");
            return;  // prevent crash
        }
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(containerId, fragment);
        transaction.commit();

    } 

Immediately, lock screen for single pane (phone) to portrait, i.e. setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 

Tuesday, December 2, 2025

Android java: Releasing resource Activity/Fragment <-> AndroidViewModel <-> MyController

Activity/Fragment <-> AndroidViewModel <-> MyController

MyController will hold

  1. Executor
  2. MediaController 
  3. Network access
  4. Database access
  5. Application context for above operation  

Proper way to clear the resource

  1. Executor
            // MyAndroidViewModel onClear() has been reached
            if (myExecutor!=null) {
                myExecutor.shutdown();
                myExecutor = null;
            }
  2. Media Controller
            if (myMediaController!=null) {
                myMediaController.releaseMediaSession(); // MUST BE CLEAR
                myMediaController = null;
            }
  3. Network access
            myGetRadioLogo=null;
  4. Database access  
            myDBAccess=null;
  5. Application context
            appContext=null; // MUST SET NULL AT ONCLEAR

Fail to release these resources may lead to memory leak

Fail to release resource with not properly sequence may lead application crashed, this crash can be found on logcat.  

Tuesday, November 11, 2025

Android java: using DataLive for orientation change

I test it in Android 10 xiaomi mia2

What will happen when orientation change e.g. screen change or language change?

the object in activity will be destroyed. here is the sequence:

  • after orientation change
    V  onPause
    V  onStop
    V  onDestrouy
  • after flusing all data
    V  onCreate
    V  onCreate myClass :null
    V  MyClass constructor
    V  onCreateView myClass.getRandom
    V  onStart
    V  onResume 

At xiomi mia2, override public void onConfigurationChanged(Configuration newConfig) method did not called at all during screen orientation change, not appearred in logcat. except, you put android:configChanges in your layout.

Base on the android behave, these are the solution can be used to keep the object during configuration change:

  1. Local persistence to handle process death for complex or large data. Persistent local storage includes databases or DataStore.
  2. Retained objects such as ViewModel instances to handle UI-related state in memory while the user is actively using the app.
  3. Saved instance state to handle system-initiated process death and keep transient state that depends on user input or navigation. 

ViewModel is recommended used for today an future, These are some options:

  1. ViewModel    Holds UI data & logic    Core architecture component
  2. ComputableLiveData (Deprecated)
  3. MediatorLiveData    Combines multiple LiveData sources    Merging streams
  4. MutableLiveData    Observable, mutable data    UI updates
  5. SavingStateLiveData    LiveData with saved-state persistence    Restore after process death 

Comparison between AndroidViewModel vss ViewModel

AndroidViewModel require Context for DB, prefs, etc. gemini: Avoid if possible. Only use when you absolutely need Context. violates a core principle of good architecture: separation of concerns and testability.
ViewModel UI logic without needing Context Network requirement may use this, it does not need context

NOTE: Don’t store Activity or Fragment in VieModel nor AndroidViewModel! 

These are the java code for education, first text using direct access to object in activity, and second text using live data and AndroidViewModel. this code used AndroidViewModel, because I need application's context to access sqlite.

-- MyClass.java

package com.dedetok.testdataorientation;

import android.content.Context;
import android.util.Log;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import java.util.Random;

public class MyClass {
    // dummy
    Context context;

    boolean isCreated = false;
    int randomNumber=-1;

    public MyClass(Context context) {
        this.context = context;
        Log.v("deddetok", "MyClass constructor"); // debug

    }

    public String getRandom() {
        if (!isCreated) {
            Random random = new Random();
            randomNumber = random.nextInt(100);
            isCreated = true;
        }
        return String.valueOf(randomNumber);
    }

    public LiveData<String> getRandomLive() {
        if (!isCreated) {
            Random random = new Random();
            randomNumber = random.nextInt(100);
            isCreated = true;
        }
        MutableLiveData<String> returnValue = new MutableLiveData<>();
        returnValue.setValue(String.valueOf(randomNumber));

        return returnValue;
    }
}

-- MyAndroidViewModel

package com.dedetok.testdataorientation;

import android.app.Application;

import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;

public class MyAndroidViewModel extends AndroidViewModel {
    MyClass myClass;

    public MyAndroidViewModel(@NonNull Application application) {
        super(application);
        myClass = new MyClass(application);
    }

    public LiveData<String> getData() {
        return myClass.getRandomLive();
    }
}

-- MainActivity.java

package com.dedetok.testdataorientation;

import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.ViewModelProvider;

public class MainActivity extends AppCompatActivity {

    MyClass myClass = null;

    AppCompatTextView myTextView1, myTextView2;

    MyAndroidViewModel myAndroidViewModel;

    /* to preserve myclass
     * 1. Local persistence to handle process death for complex or large data. Persistent local storage includes databases or DataStore.
     * 2. Retained objects such as ViewModel instances to handle UI-related state in memory while the user is actively using the app.
     * 3. Saved instance state to handle system-initiated process death and keep transient state that depends on user input or navigation.
     */

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        Log.v("dedetok", "onCreate"); // debug


        myTextView1 = findViewById(R.id.textView1);
        myTextView2 = findViewById(R.id.textView2);

        Log.v("dedetok", "onCreate myClass :"+myClass); //
        // myTextView1
        if (myClass==null) {
            myClass = new MyClass(this);
        }
        myTextView1.setText(myClass.getRandom()); // work, created in memory

        // ✅ Get ViewModel (it survives rotation)
        // myTextView2
        myAndroidViewModel = new ViewModelProvider(this).get(MyAndroidViewModel.class);
        myAndroidViewModel.getData().observe(this, value -> {
            // 'value' is a plain String here
            myTextView2.setText(value);
        }); //

    }

    @Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        Log.v("dedetok", "onCreateView"); // debug
        //Log.v("dedetok", "onCreateView myClass.getRandom"); // debug
        //myTextView.setText(myClass.getRandom()); // crash textview not ready
        return super.onCreateView(parent, name, context, attrs);
    }

    /*
     * ## activity life cycle 2 ##
     */
    @Override
    protected void onStart() {
        super.onStart();
        Log.v("dedetok", "onCreateView myClass.getRandom"); // debug
        //myTextView1.setText(myClass.getRandom()); // work, view has been created
        myAndroidViewModel.getData().observe(this, value -> {
            // 'value' is a plain String here
            myTextView2.setText(value);
        }); // this is fine place

        Log.v("dedetok", "onStart"); // debug

    }

    /*
     * ## activity life cycle 3 ##
     */
    @Override
    protected void onResume() {
        super.onResume();

        Log.v("dedetok", "onResume"); // debug


    }

    /*
     * ## activity life cycle 4 ##
     */
    @Override
    protected void onPause() {
        Log.v("dedetok", "onPause"); // debug
        super.onPause();

    }

    /*
     * ## activity life cycle 5 ##
     */
    @Override
    protected void onStop() {
        Log.v("dedetok", "onStop"); // debug
        super.onStop();

    }

    /*
     * ## activity life cycle 6 ##
     */
    @Override
    protected void onDestroy() {
        Log.v("dedetok", "onDestrouy"); // debug

        super.onDestroy();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        Log.v("dedetok", "onConfigurationChanged"); // debug
    }
}

-- activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:minHeight="50dp"
        android:textSize="24sp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        />
    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView2"
        android:minHeight="50dp"
        android:textSize="24sp"
        app:layout_constraintTop_toBottomOf="@+id/textView1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

Some of parts of this content generated by ChatGPT and Gemini with some modification.