Showing posts with label android. Show all posts
Showing posts with label android. Show all posts

Friday, September 27, 2024

android java: google admob policy collected data checklist

This checklist may be in google play console: App content -> Data Safety!

User property Type Description
Age Text Identifies users by six categories: 18-24, 25-34, 35-44, 45-54, 55-64, and 65+.
App Store Text The store from which the app was downloaded and installed.
App Version Text The versionName (Android) or the Bundle version (iOS).
Country Text The country the user resides in.
Device Brand Text The brand name of the mobile device (e.g., Motorola, LG, or Samsung).
Device Category Text The category of the mobile device (e.g., mobile or tablet).
Device Model Text The mobile device model name (e.g., iPhone 5s or SM-J500M).
First Open Time Number The time (in milliseconds, UTC) at which the user first opened the app, rounded up to the next hour.
Gender Text Identifies users as either male or female.
Interests Text Lists the interests of the user (e.g., "Arts & Entertainment, Games, Sports").
Language Text The language setting of the device OS (e.g., en-us or pt-br).
New/Established N/A New: First opened the app within the last 7 days.
Established: First opened the app more than 7 days ago.
OS Version Text The version of the device OS (e.g., 9.3.2 or 5.1.1).

NOTE: this checklist may change without notice! 

Reference: https://support.google.com/admob/answer/9755590?hl=en

Monday, June 3, 2024

Android java: request multiple permission READ_EXTERNAL_STORAGE & WRITE_EXTERNAL_STORAGE

Minimum API 24 (Android 7.0) 

Note:

  • Beginning with Android 4.4 (API level 19) it's no longer necessary for your app to request the WRITE_EXTERNAL_STORAGE permission to write to its own application-specific directories on external storage, which are provided by getExternalFilesDir(). However, the permission is required for API level 18 and lower. Example:
    File extDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
  • If the permission is a runtime permission or special permission, and if your app is installed on a device that runs Android 6.0 (API level 23) or higher, you must request the runtime permission or special permission yourself.
  • On devices that run Android 4.4 (API level 19) and higher, your app can interact with a documents provider, including external storage volumes and cloud-based storage, using the Storage Access Framework. This framework allows users to interact with a system picker to choose a documents provider and select specific documents and other files for your app to create, open, or modify.
  • To support media file access on devices that run Android 9 (API level 28) or lower, declare the READ_EXTERNAL_STORAGE permission and set the maxSdkVersion to 28. See reference 1.
  • If your app targets Android 10 (API level 29) or lower, you can temporarily opt out of scoped storage in your production app. If you target Android 10, however, you need to set the value of requestLegacyExternalStorage to true in your app's manifest file.

AndroidManifest.xml

...
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"
        />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"
        />
...
    <!-- This attribute is "false" by default on apps targeting Android 10. -->
    <!-- android:requestLegacyExternalStorage="true" -->
    <application
        android:requestLegacyExternalStorage="true"
...

MainActivity.java

...
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);

        // android:maxSdkVersion="28" =  Build.VERSION_CODES.P
        if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
            checkMyPermission();
        }
...    int MY_CODE_REQUEST = 123;
    /*
     * 1. check permission
     */
    private void checkMyPermission() {
        // Build.VERSION.SDK_INT >= 23
        // https://riptutorial.com/android/example/23932/multiple-runtime-permissions-from-same-permission-groups
        ArrayList<String> sPermissionRequest = new ArrayList<>();
        int myRead = ContextCompat.checkSelfPermission(getApplicationContext()
                , READ_EXTERNAL_STORAGE);
        int myWrite = ContextCompat.checkSelfPermission(getApplicationContext()
                , WRITE_EXTERNAL_STORAGE);
        if (myRead != PackageManager.PERMISSION_GRANTED) {
            sPermissionRequest.add(getString(R.string.s_permission_read_storage));
        }
        if (myWrite != PackageManager.PERMISSION_GRANTED) {
            sPermissionRequest.add(getString(R.string.s_permission_write_storage));
        }

        if (sPermissionRequest.size()>0) {
            // NOT Granted
            String[] sPermissions = new String[sPermissionRequest.size()];
            for (int i=0;i<sPermissionRequest.size();i++) {
                sPermissions[i]=sPermissionRequest.get(i);
            }
            //sLog.e("dedetok", "sPermissionRequest.size()>0 size "+sPermissions.length); // debug
            requestPermissions(sPermissions, MY_CODE_REQUEST);
        }
    }

    /*
     * 2. receive permission status
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode==MY_CODE_REQUEST) {

            if (permissions.length>0) {
                ArrayList<String> myPermission = new ArrayList<>();
                for (int i=0;i<permissions.length;i++) {
                    //Log.e("dedetok", permissions[i]+" "+grantResults[i]); // debug
                    if (permissions[i].equals(getString(R.string.s_permission_read_storage)) &&
                            grantResults[i]!=PackageManager.PERMISSION_GRANTED) {
                        myPermission.add(READ_EXTERNAL_STORAGE);
                    }
                    if (permissions[i].equals(getString(R.string.s_permission_write_storage)) &&
                            grantResults[i]!=PackageManager.PERMISSION_GRANTED) {
                        myPermission.add(WRITE_EXTERNAL_STORAGE);
                    }
                }
                // Not granted, Request permissions on runtime

                String[] sRequestPermission = new String[myPermission.size()];
                for (int i=0;i<myPermission.size();i++) {
                    sRequestPermission[i] = myPermission.get(i);
                }

                //Log.e("dedetok", "requestCode==MY_CODE_REQUEST "+sRequestPermission.length); // debug
                requestPermissionLauncher.launch(sRequestPermission);
            }
        }
    }

    /*
     * 3. callback user permission
     * Register the permissions callback, which handles the user's response to the
     * system permissions dialog. Save the return value, an instance of
     * ActivityResultLauncher, as an instance variable.
     */
    private final ActivityResultLauncher<String[] > requestPermissionLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions()
                    , new ActivityResultCallback() {
                        @Override
                        public void onActivityResult(Object objMap) {
                            //Log.e("dedetok", "onActivityResult "+o.toString()); // debug
                            if (objMap instanceof Map) {
                                StringBuilder mySB = new StringBuilder();
                                Map<String,Boolean> myMapPermission = (Map<String,Boolean>) objMap;
                                myMapPermission.forEach((permissionKey, permissionBoolean)->{
                                    //Log.e("dedetok", permissionKey+" "+READ_EXTERNAL_STORAGE+" "+permissionBoolean); // debug
                                    if (permissionKey.equals(READ_EXTERNAL_STORAGE) && !permissionBoolean) {
                                        mySB.append(READ_EXTERNAL_STORAGE+" ");
                                    }
                                    if (permissionKey.equals(WRITE_EXTERNAL_STORAGE) && !permissionBoolean) {
                                        mySB.append(WRITE_EXTERNAL_STORAGE+" ");
                                    }
                                });
                                //Log.e("dedetok", "mySB "+mySB.toString()); // debug
                                if (mySB.length()>0) {
                                    updateStatus(mySB.toString());
                                }
                            }
                        }
                    });

 References:

  1. https://developer.android.com/training/data-storage/shared/documents-files
  2. https://developer.android.com/training/permissions/declaring
  3. https://developer.android.com/guide/topics/manifest/uses-permission-element
  4. https://stackoverflow.com/questions/3093365/how-can-i-check-the-system-version-of-android
  5. https://developer.android.com/training/data-storage/use-cases

Friday, May 24, 2024

Android Java: spinner in fragment

layout_fragment_me.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment"
        android:textSize="38sp"
        />
    <Spinner
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/my_spinner"
        android:minHeight="50dp"
        />

    <androidx.appcompat.widget.AppCompatButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="HELLO BUTTON"
        />
</androidx.appcompat.widget.LinearLayoutCompat> 

activity_main.xml

<?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"
    >

    <androidx.fragment.app.FragmentContainerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/fragment_view_me"
        />
    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Main Activity"
        android:textSize="30dp"
        />
</androidx.appcompat.widget.LinearLayoutCompat>

MyData.java

package com.dedetok.tutorialcustomdropdown;

public class MyData {
    public String name, phone;

    @Override
    public String toString() {
        return name+" "+phone;
    }
}

FragmentMe.java

package com.dedetok.tutorialcustomdropdown;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import java.util.ArrayList;

public class FragmentMe extends Fragment {

    ArrayList<MyData> arrayList = new ArrayList<>();

    public FragmentMe() {
        super(R.layout.layout_fragment_me);
        Log.e("dedetok", "Create Fragment ME"); // debug
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        init();
        Log.e("dedetok", "size "+arrayList.size()); // debug
        Spinner mySpinner = view.findViewById(R.id.my_spinner);
        ArrayAdapter<MyData> arrayAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, arrayList);
        Log.e("dedetok", "setAdapter"); // debug
        mySpinner.setAdapter(arrayAdapter);
        mySpinner.setOnItemSelectedListener(mySpinnerListener);

    }

    AdapterView.OnItemSelectedListener mySpinnerListener =
            new AdapterView.OnItemSelectedListener() {

                @Override
                public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
                    if (parent.getItemAtPosition(pos) instanceof MyData) {
                        MyData myData = (MyData) parent.getItemAtPosition(pos);
                        // TODO
                    }
                }

                @Override
                public void onNothingSelected(AdapterView<?> adapterView) {
                    // Do nothing
                }
            };
    private void init() {
        MyData myData = new MyData();
        myData.name = "aaaa";
        myData.phone = "1111";
        arrayList.add(myData);
        myData = new MyData();
        myData.name = "bbbb";
        myData.phone = "2222";
        arrayList.add(myData);
        myData = new MyData();
        myData.name = "ccccc";
        myData.phone = "2233322";
        arrayList.add(myData);
        myData = new MyData();
        myData.name = "4ddddd";
        myData.phone = "444444";
        arrayList.add(myData);
    }
}

MainActivity.java

package com.dedetok.tutorialcustomdropdown;

import android.os.Bundle;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.FragmentTransaction;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);


        setContentView(R.layout.activity_main);
        FragmentTransaction transaction = getSupportFragmentManager()
                .beginTransaction();
        FragmentMe fragment = new FragmentMe();
        transaction.setReorderingAllowed(true);
        transaction.add(R.id.fragment_view_me, fragment);
        transaction.commit();
    }
}

Wednesday, May 15, 2024

Android Java: open activity for result to pick up / choose a file in Shared Folder - Download /storage/emulated/0/Download

Note:

Moving from startActivityForResult to ActivityResultLauncher to pickup file in Shared Folder - Download /storage/emulated/0/Download

Minimum Android 7 API 24 Targeted SDK Android 24 API 34

build.gradle.kts (Module :app)

plugins {
    alias(libs.plugins.android.application)
}

android {
    namespace = "com.dedetok.tutorialsharedfolder"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.dedetok.tutorialsharedfolder"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation(libs.appcompat)
    implementation(libs.material)
    implementation(libs.activity)
    implementation(libs.constraintlayout)
    testImplementation(libs.junit)
    androidTestImplementation(libs.ext.junit)
    androidTestImplementation(libs.espresso.core)
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="29"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="29"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

    <application
        android:requestLegacyExternalStorage="true"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TutorialSharedFolder"
        tools:targetApi="31">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

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.constraintlayout.widget.ConstraintLayout>

MainActivity.java

package com.dedetok.tutorialsharedfolder;

import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;

import android.provider.DocumentsContract;

public class MainActivity extends AppCompatActivity {

    /*
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="29"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="29"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    <application
        android:requestLegacyExternalStorage="true"

    /storage/emulated/0/Download

    MediaStore.Downloads (/storage/emulated/0/Download) Android 10 (API level 29) and higher,
     */


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);

        /*
         * check perrmission
         */
        boolean isPermitted;
        String myPermissionRequired = android.Manifest.permission.READ_EXTERNAL_STORAGE;
        isPermitted = myCheckPermission(this, myPermissionRequired);
        Log.e("dedetok", "READ_EXTERNAL_STORAGE: "+isPermitted);

        myPermissionRequired = android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
        isPermitted = myCheckPermission(this, myPermissionRequired);
        Log.e("dedetok", "WRITE_EXTERNAL_STORAGE: "+isPermitted);
        myPermissionRequired = Manifest.permission.MANAGE_EXTERNAL_STORAGE;
        isPermitted = myCheckPermission(this, myPermissionRequired);
        Log.e("dedetok", "MANAGE_EXTERNAL_STORAGE: "+isPermitted);

        /*
         * root Environment.getExternalStorageDirectory() -> /storage/emulated/0
         */
        // get root directory path NOT USED
        File fExternalStorage = Environment.getExternalStorageDirectory();
        String sFExternalStorage = fExternalStorage.getPath();
        //String fsFExternalStorage = sFExternalStorage+File.separator+sFilename;
        Log.e("dedetok", "Environment.getExternalStorageDirectory() "+sFExternalStorage); //debug

        /*
         * Access file in /storage/emulated/0/Download
         * Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) ->
         *          /storage/emulated/0/Download
         */
        // get Download directory path NOT USED IN PICK FILE BUT USED IN PRINT FILE
        File extDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        String dirPath = extDir.getPath();
        Log.e("dedetok", "getExternalStoragePublicDirectory "+dirPath); //debug

        /*
         * Select file in Download Folder
         */
        //getFileURI(); // OLD WAT Deprecated
        getFileURINew(); // New Way using system dialog to select file

        /*
         * writing to Download Folder
         */
        String sFileOutput = extDir+File.separator+"helloworld.txt";
        File fOutput = new File(sFileOutput);
        try {
            PrintWriter myPW = new PrintWriter(fOutput);
            myPW.println("Hello World");
            myPW.println("this is a test");
            myPW.flush();
            myPW.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace(); // debug
        }


    }

    /*
     * self check permission function
     */
    private boolean myCheckPermission(Context appContext, String sPermission) {
        return (ContextCompat.checkSelfPermission(appContext, sPermission)
                == PackageManager.PERMISSION_GRANTED);
        // or
        // return (PermissionChecker.checkSelfPermission(appContext, sPermission)
        //        == PermissionChecker.PERMISSION_GRANTED);
    }

    ///////////// THIS IS PART OF DEPRECATED SECTION START /////////////
    /*
     * get result
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode,
                                    Intent resultData) {
        super.onActivityResult(requestCode, resultCode, resultData);
        if (requestCode == MY_ID_PICK_FILE
                && resultCode == Activity.RESULT_OK) {
            // The result data contains a URI for the document or directory that
            // the user selected.
            Uri uri;
            if (resultData != null) {
                uri = resultData.getData();
                // Perform operations on the document using its URI.
                Log.e("dedetok", "Result"); // debug
                if (uri!=null) {

                    Log.e("dedetok", "Result uri: "+uri.getPath()); // debug
                    openMyFile(uri);
                }
            }
        }

    }
    private static final int MY_ID_PICK_FILE = 1; // any number
    private static final String mimeTypeFilter = "text/csv";
    /*
     * https://developer.android.com/training/data-storage/shared/documents-files
     * https://stackoverflow.com/questions/71667342/how-to-allow-selection-of-csv-in-a-file-chooser-intent-in-android
     * Deprecated startActivityForResult
     */
    private void getFileURI() {
        String[] myMime = {"*/*",
                "text/plain",
                "application/csv",
                "application/vnd.ms-excel",
                "application/excel",
                "application/x-excel",
                "application/x-msexcel"
        };
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // MUST
        intent.addCategory(Intent.CATEGORY_OPENABLE); // MUST
        intent.setType(mimeTypeFilter);

        // Optionally, specify a URI for the file that should appear in the
        // system file picker when it loads.
        intent.putExtra(Intent.EXTRA_MIME_TYPES, myMime);

        startActivityForResult(intent, MY_ID_PICK_FILE);
    }
    ///////////// THIS IS PART OF DEPRECATED SECTION END /////////////

    ///////////// THIS PART OF NEW WAY START /////////////
    /*
     * https://developer.android.com/training/basics/intents/result
     */
    private void getFileURINew() {
        String[] myMime = {"*/*",
                "text/plain",
                "application/csv",
                "application/vnd.ms-excel",
                "application/excel",
                "application/x-excel",
                "application/x-msexcel"
        };
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // MUST
        intent.addCategory(Intent.CATEGORY_OPENABLE); // MUST
        intent.setType(mimeTypeFilter);

        // Optionally, specify a URI for the file that should appear in the
        // system file picker when it loads.
        intent.putExtra(Intent.EXTRA_MIME_TYPES, myMime);

        mGetContent.launch(intent);
    }
    // ActivityResultCallback
    ActivityResultCallback<ActivityResult> myActivityResult = new ActivityResultCallback<ActivityResult>() {
        @Override
        public void onActivityResult(ActivityResult aResult) {
            if (aResult.getResultCode()==RESULT_OK) {
                Intent iData = aResult.getData();
                Uri uriData = iData.getData();
                Log.e("dedetok", "new "+uriData.getPath());

            }

        }
    };
    // ActivityResultContracts
    ActivityResultContracts.StartActivityForResult myARC = new ActivityResultContracts.StartActivityForResult();
    // ActivityResultLauncher
    ActivityResultLauncher<Intent> mGetContent = registerForActivityResult(
        myARC,
        myActivityResult);
    ///////////// THIS PART OF NEW WAY END /////////////

    ///////////// SUPPORTING FUNCTION /////////////
    /*
     * Open File after receive from system pick up
     */
    private void openMyFile(Uri uriMyFile) {
        //getFileURI(); // crash
        StringBuilder stringBuilder = new StringBuilder();
        if (isVirtualFile(uriMyFile)) {
            Log.e("dedetok","It is Virual"); // debug
            ContentResolver resolver = getContentResolver();

            String[] openableMimeTypes = resolver.getStreamTypes(uriMyFile, mimeTypeFilter);

            if (openableMimeTypes != null ||
                    openableMimeTypes.length > 0) {

                try {
                    InputStream inputStream = resolver
                            .openTypedAssetFileDescriptor(uriMyFile, openableMimeTypes[0], null)
                            .createInputStream();
                    BufferedReader reader = new BufferedReader(
                            new InputStreamReader(inputStream));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        stringBuilder.append(line);
                    }
                } catch (IOException e) {
                    e.printStackTrace(); // debug
                }
            }

        } else {
            try {
                InputStream inputStream =
                         getContentResolver().openInputStream(uriMyFile);
                BufferedReader reader = new BufferedReader(
                         new InputStreamReader(inputStream));
                String line;
                while ((line = reader.readLine()) != null) {
                    stringBuilder.append(line);
                }
            } catch (IOException e) {
                e.printStackTrace(); // debug
            }
        }
        Log.e("dedetok",stringBuilder.toString());
    }

    /*
     * check if file receive is virtual
     * Android 7.0 (API level 24) and higher
     *https://developer.android.com/training/data-storage/shared/documents-files
     */
    private boolean isVirtualFile(Uri uri) {
        if (!DocumentsContract.isDocumentUri(this, uri)) {
            return false;
        }

        Cursor cursor = getContentResolver().query(
                uri,
                new String[] { DocumentsContract.Document.COLUMN_FLAGS },
                null, null, null);

        int flags = 0;
        if (cursor.moveToFirst()) {
            flags = cursor.getInt(0);
        }
        cursor.close();

        return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
    }

}

References:

  • https://developer.android.com/training/basics/intents/result
  • https://devofandroid.blogspot.com/2022/09/get-result-from-another-activity-using.html
  • https://medium.com/@steves2001/moving-from-android-startactivityforresult-to-registerforactivityresult-76ca04044ff1
  • https://www.geeksforgeeks.org/how-to-use-activityforresultluncher-as-startactivityforresult-is-deprecated-in-android/


Friday, April 26, 2024

Android java: using SQLiteOpenHelper instead Room Library

Using additional Room may increase size of application, means more lines required to executed. I preferred to implement SQLiteOpenHelper rather then using Room Library.

Here is code for extending SQLiteOpenHelper:

package com.dedetok.radiowalkman.mydb;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

/*
 * added 20240319
 * Async task will be done in DB Repository to access database (RUD)
 */

public class MyDBHelper extends SQLiteOpenHelper {

    final static String dbName = "myradiolist"; // db name must final static
    final static int dbversion = 2; // db version must final static

    public MyDBHelper(@Nullable Context appContext) {
        //
        super(appContext, dbName, null, dbversion);
    }

    /*
     * Only called once after creating a new database
     * Will not called if database exist (upgrade version)
     */
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        //Log.e("dedetok","onCreate"); //debug
        createMyDB(sqLiteDatabase);
    }

    /*
     * Only called once if database exist and version number increase (upgrade)
     */
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        // upgrade from 1 to 2
        //Log.e("dedetok","onUpgrade "+oldVersion+" to "+newVersion); //debug
        if (oldVersion==1) {
            upgradeDBV1toV2(sqLiteDatabase);
        }

    }

    /*
     * version 1
     */
    private void createMyDB(SQLiteDatabase sqLiteDatabase) {
        //Log.e("dedetok","createMyDB"); //debug
        // Main table radiolist
        String sqlCreateRadioList = "CREATE TABLE IF NOT EXISTS "+
                "radiolist ("+
                "countryname TEXT NOT NULL,"+
                "cityname TEXT NOT NULL,"+
                "radiostation TEXT NOT NULL,"+
                "logourl TEXT NOT NULL,"+
                "streamurl TEXT NOT NULL,"+
                "PRIMARY KEY(countryname, cityname, radiostation));";
        sqLiteDatabase.execSQL(sqlCreateRadioList);
        // table radiolistversion only has 1 record and one column
        String sqlCreateRadioListVersion = "CREATE TABLE IF NOT EXISTS "+
                "radiolistversion ("+
                "listversion TEXT NOT NULL,"+
                "PRIMARY KEY(listversion));";
        sqLiteDatabase.execSQL(sqlCreateRadioListVersion);

        //Log.e("dedetok", "create table app_preferences"); //debug
        // 20240422
        upgradeDBV1toV2(sqLiteDatabase);

    }

    /*
     * version 1 to version 2
     */
    private void upgradeDBV1toV2(SQLiteDatabase sqLiteDatabase) {
        //Log.e("dedetok", "upgradeDBV1toV2"); //debug
        // 20240422
        String sqlCreatePreferences = "CREATE TABLE IF NOT EXISTS "+
                "app_preferences ("+
                "pref_key TEXT NOT NULL,"+
                "pref_value TEXT NOT NULL,"+
                "PRIMARY KEY(pref_key));";
        sqLiteDatabase.execSQL(sqlCreatePreferences);
    }

}

 This is java class in Radio Walkman application.

Monday, April 1, 2024

Android java source code com.dedetok.turuntirta

This is source code for com.dedetok.turuntirta and com.dedetok.bramara. I keep using legacy MediaPlayer.

Put turuntirta.mp3 in folder [Workspace]/Wargasari/app/src/main/res/raw/

build.gradle.kts (Module: app)

plugins {
    id("com.android.application")
}

android {
    namespace = "com.dedetok.turuntirta"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.dedetok.turuntirta"
        minSdk = 24
        targetSdk = 34
        versionCode = 202402
        versionName = "2024.02"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    implementation("com.google.android.gms:play-services-ads-lite:23.0.0")
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- For apps targeting Android 13 or higher & GMA SDK version 20.3.0 or lower -->
    <uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <!-- permission for media player -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <!-- android:enableOnBackInvokedCallback="true" Back Pressed Target API level>33 -->
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/my_ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/my_ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TurunTirta"
        tools:targetApi="31"
        android:enableOnBackInvokedCallback="true"
        >
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:configChanges="orientation|screenSize"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
        <!-- AD App ID: ca-app-pub-0220748109202708~7061470683 : AndroidManifest.xml -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-0220748109202708~7061470683"
            />
    </application>

</manifest>

res -> values -> strings.xml

<resources>
    <string name="app_name" translatable="false">Kidung Turun Tirta</string>

    <string-array name="kidung">
        <item>Turun tirta saking luhur|\nMenyiratan pemangkune|\nMekalangan muncrat mumbul|\nMapan tirtha merta jati|\nPaican Bhatara sami|\nPanglukatan dasa mala|\nSami pada lebur|\nMalene ring gumi||</item>
        <item>Meketis ping tiga sampun|\nPabahan Siwa dwarane|\nWasuhane raris kinum|\nPing tiga lantas mesugi|\nRing waktra megentos genti|\nToya amertha Hyang Widhine|\nSami sampun puput|\nMengalangin hati||</item>
    </string-array>

    <array name="my_font_size">
        <item>18</item>
        <item>24</item>
        <item>32</item>
        <item>42</item>
    </array>
    <string name="dialog_title">Exit Application</string>
    <string name="dialog_message">Do you want to exit application?</string>
    <string name="play_pause">Play/Pause</string>
    <string name="zoom_in">Zoom In</string>
    <string name="zoom_out">Zoom Out</string>
    <string name="bait">Stanza</string>
    <string name="play_repeat">Repeat play</string>

</resources>

res -> values -> ic_launcher_background.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="ic_launcher_background">#FFFFFF</color>
</resources>

res -> Layout -> layout_stanza.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/stanza_title"
        android:textColor="#9C27B0"
        />
    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/stanza_content"
        />

</androidx.appcompat.widget.LinearLayoutCompat>

res -> Layout -> activity_main.xml

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    >

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >

        <androidx.appcompat.widget.AppCompatButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/b_zoom_in"
            android:text="@string/zoom_in"
            />
        <androidx.appcompat.widget.AppCompatButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/b_zoom_out"
            android:text="@string/zoom_out"
            />
    </androidx.appcompat.widget.LinearLayoutCompat>

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/my_recycler_view"
        android:scrollbars="vertical"
        android:layout_weight="1"
        />

    <androidx.appcompat.widget.AppCompatSeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/my_seekbar"
        />

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_gravity="center">
        <androidx.appcompat.widget.AppCompatImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/my_play"
            android:src="@android:drawable/ic_media_play"
            android:layout_gravity="center_horizontal"
            android:contentDescription="@string/play_pause"
            />
        <androidx.appcompat.widget.SwitchCompat
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/switch_repeat"
            android:layout_gravity="center_horizontal"
            android:contentDescription="@string/play_repeat"
            android:text="@string/play_repeat"
            />
    </androidx.appcompat.widget.LinearLayoutCompat>

    <!-- Sample AdMob unit ID: ca-app-pub-3940256099942544/6300978111 -->
    <!-- AD Unit ID: ca-app-pub-0220748109202708/8538203883 : layout -->
    <com.google.android.gms.ads.AdView
        android:id="@+id/adView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:adSize="BANNER"
        android:layout_gravity="end|center"
        app:adUnitId="ca-app-pub-0220748109202708/8538203883"
        />
</androidx.appcompat.widget.LinearLayoutCompat>

java -> StanzaContainer.java

package package com.dedetok.wargasari;;

public class StanzaContainer {

    public String stanzaTitle;
    public String stanzaContent;

    public StanzaContainer(String stanzaTitle, String stanzaContent) {
        this.stanzaTitle = stanzaTitle;
        this.stanzaContent = stanzaContent;

    }
}

java -> MyAdapter.java

package com.dedetok.wargasari;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;

public class MyAdapter extends RecyclerView.Adapter  {

    ArrayList<StanzaContainer>  myStanza;

    int myUnitTextSize;
    float myTextSize;

    public MyAdapter(ArrayList<StanzaContainer> myStanza, int unit, float size) {
        this.myStanza = myStanza;
        this.myUnitTextSize = unit;
        this.myTextSize = size;

    }

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

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        StanzaContainer myTmpStanza = myStanza.get(position);
        ((MyHolder) holder).stanzaTitle.setText(myTmpStanza.stanzaTitle);
        ((MyHolder) holder).stanzaTitle.setTextSize(myUnitTextSize, myTextSize);
        ((MyHolder) holder).stanzaContent.setText(myTmpStanza.stanzaContent);
        ((MyHolder) holder).stanzaContent.setTextSize(myUnitTextSize, myTextSize);
    }

    @Override
    public int getItemCount() {
        return myStanza.size();
    }

    public void changeFontSize(int unit, float size) {
        this.myUnitTextSize = unit;
        this.myTextSize = size;
        notifyDataSetChanged();
    }

    private class MyHolder extends RecyclerView.ViewHolder {

        public AppCompatTextView stanzaTitle, stanzaContent;

        public MyHolder(@NonNull View itemView) {
            super(itemView);

            stanzaTitle = itemView.findViewById(R.id.stanza_title);
            stanzaContent = itemView.findViewById(R.id.stanza_content);
        }
    }
}

java -> MainActivity.java

package com.dedetok.turuntirta;

import static android.util.TypedValue.COMPLEX_UNIT_SP;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.appcompat.widget.AppCompatSeekBar;
import androidx.appcompat.widget.SwitchCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.SeekBar;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdView;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    MediaPlayer myMediaPlayer = null;
    AppCompatSeekBar mySeekBar;
    int cTextSizeState = 0;
    TypedArray availFontSize;
    float defaultTextSize = 18;
    boolean myPlayerStatus = false;
    private Bundle myBundle;
    private int myCurrentPos =0;
    static final String STATE_MY_PLAYER_STATUS = "currentStatus";
    static final String STATE_MY_PLAYER_POSITION = "currentPosition";
    AppCompatImageButton myPlayButton; //20240302
    // handler to update seekbar
    final Handler myHandler = new Handler(Looper.getMainLooper()); // new Handler() deprecated 20240302

    //20240423
    PowerManager.WakeLock wakeLock;

    /*
     * ## activity life cycle 1 ##
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // add version number from gradle into activity title
        try {
            PackageInfo myPackage = getPackageManager().getPackageInfo(getPackageName(),0);
            String myTitle =  getString(R.string.app_name)+" "+myPackage.versionName;
            setTitle(myTitle);
        } catch (PackageManager.NameNotFoundException e) {
            // Do Nothing, pass process
        }

        // ads:adUnitId ca-app-pub-0220748109202708/8538203883 : AdView in Layout
        // app id ca-app-pub-0220748109202708~7061470683    : AndroidManifest.xml
        AdView mAdView = findViewById(R.id.adView);
        AdRequest adRequest = new AdRequest.Builder().build();
        mAdView.loadAd(adRequest);

        // Button Play 20240302
        myPlayButton = findViewById(R.id.my_play);
        myPlayButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (myPlayerStatus) {
                    // pause
                    pauseMedia();
                } else {
                    // play
                    playMedia();
                }
            }
        });

        // seekbar
        mySeekBar = findViewById(R.id.my_seekbar);
        mySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int myProgress, boolean isFromUser) {

                if (isFromUser) {
                    if (myMediaPlayer!=null) {
                        myMediaPlayer.seekTo(myProgress);
                    }
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // DO NOTHING
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // DO NOTHING
            }
        });

        // recyclerview
        availFontSize = getResources().obtainTypedArray(R.array.my_font_size);
        RecyclerView myRecyclerView = findViewById(R.id.my_recycler_view);
        LinearLayoutManager myLinearLayout = new LinearLayoutManager(getApplicationContext());
        myLinearLayout.setOrientation(LinearLayoutManager.VERTICAL);
        myRecyclerView.setLayoutManager(myLinearLayout);
        MyAdapter myAdapter = new MyAdapter(getTexts(), COMPLEX_UNIT_SP, availFontSize.getFloat(cTextSizeState, defaultTextSize));
        myRecyclerView.setAdapter(myAdapter);

        // button zoom text
        AppCompatButton bZoomOut = findViewById(R.id.b_zoom_out);
        bZoomOut.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (cTextSizeState==0) {
                    //Log.i("Font Change", "Zoom out do nothing");
                }  else {
                    cTextSizeState--;
                    myAdapter.changeFontSize(COMPLEX_UNIT_SP,availFontSize.getFloat(cTextSizeState, defaultTextSize));
                    //Log.i("Font Change", Float.toString(availFontSize.getFloat(cTextSizeState,defaultTextSize)));
                }
            }
        });
        AppCompatButton bZoomIn = findViewById(R.id.b_zoom_in);
        bZoomIn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (cTextSizeState+1==availFontSize.length()) {
                    //Log.i("Font Change", "Zoom in do nothing");
                }  else {
                    cTextSizeState++;
                    myAdapter.changeFontSize(COMPLEX_UNIT_SP,availFontSize.getFloat(cTextSizeState, defaultTextSize));
                    //Log.i("Font Change", Float.toString(availFontSize.getFloat(cTextSizeState,defaultTextSize)));
                }
            }
        });

        // to save state if user switch to / from other application
        myBundle = savedInstanceState; // added multimedia

        // onBackPressed() deprecated -> OnBackPressedCallback & getOnBackPressedDispatcher()
        // dispatcher
        OnBackPressedDispatcher myOnbackPressedDispatcher = getOnBackPressedDispatcher();
        // callback on back key pressed
        OnBackPressedCallback myBackPressedCallback = new OnBackPressedCallback(true) {
            @Override
            public void handleOnBackPressed() {
                // Log.e("dedetok", "handleOnBackPressed()"); //debug
                AlertDialog.Builder myAlertDialog = new AlertDialog.Builder(
                        MainActivity.this);
                myAlertDialog.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();

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

        //20240422
        SwitchCompat sRepeat = findViewById(R.id.switch_repeat);
        sRepeat.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean booleanRepeat) {
                //myDataProvider.setRepeat(booleanRepeat);
                if (myMediaPlayer!=null) {
                    myMediaPlayer.setLooping(booleanRepeat);
                }
            }
        });

    }

    // ## activity life cycle 2 ##
    // onStart() 20240302
    @Override
    protected void onStart() {
        super.onStart();
        // 20240423
        // Android N (7.0) or latter MediaPlayer should create in OnStart
        // Media Player
        // preparing mediaplayer
        // no need to call prepare(); create() does that for you
        if (myMediaPlayer==null) {
            myMediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.turuntirta);
        }
        myMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mediaPlayer) {
                // seekbar at start and stop thread
                myPlayerStatus = mediaPlayer.isPlaying();
                myCurrentPos=0;
                mySeekBar.setProgress(myCurrentPos);
                if (!myMediaPlayer.isLooping()) {
                    pauseMedia();
                }
            }
        });
    }

    /*
     * ## activity life cycle 3 ## 20240302
     *    activity process 3
     *    onResume()
     *    @Override onRestoreInstanceState(Bundle savedInstanceState) ?
     */
    @Override
    public void onResume() {
        super.onResume();

        loadMyState();
        // link mySeekbar to myPlayer
        mySeekBar.setMax(myMediaPlayer.getDuration());
        if (myPlayerStatus && !myMediaPlayer.isPlaying()) {
            playMedia();
        }
        if (myMediaPlayer.isPlaying()) {
            myMediaPlayer.seekTo(myCurrentPos);
        }
    }

    /*
     * ## activity life cycle 4 ## 20240302
     * onPause()
     * @Override onSaveInstanceState(Bundle outState) ?
     */
    @Override
    public void onPause() {
        super.onPause();
    }

    // ## activity life cycle 5 ## 20240302
    @Override
    protected void onStop() {
        saveMyState();

        // 20240213
        // Android N (7.0) or latter MediaPlayer should stop and release in OnStop
        // we meed to keep play media even if screen does not on top
        // move release resource to onDestroy()
        super.onStop();

    }

    /*
     * ## activity life cycle 5 ## 20240302
     * clean up onDestroy()
     */
    @Override
    public void onDestroy() {
        //Log.e("dedetok","onDestroy Activity"); //debug

        // 20240423
        // release all media resources
        // release lock and executor include in funcion pauseMedia()
        pauseMedia();
        if (myMediaPlayer != null) {
            myMediaPlayer.release();
            myMediaPlayer = null;
        }

        super.onDestroy();

    }

    /* 20240302
     * onConfigurationChanged Called:
     * - after orientation change
     */
    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        // DO NOTHING
    }

    /* 20240302
     * onSaveInstanceState Called:
     * - after switch to other application
     * - after onConfigurationChanged
     */
    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        //Log.e("dedetok", "onSaveInstanceState"); // debug
        // TODO save something
    }

    /* 20240302
     * onRestoreInstanceState Called:
     * - after back to application
     * - after onConfigurationChanged
     */
    @Override
    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        //Log.e("dedetok", "onRestoreInstanceState"); // debug
        // TODO save something

    }

    private ArrayList<StanzaContainer> getTexts() {
        String[] kidung = getResources().getStringArray(R.array.kidung);
        int numKidung = kidung.length;
        ArrayList<StanzaContainer> myStanza = new ArrayList<StanzaContainer>();
        for (int i=0;i<numKidung; i++) {
            int j=i+1;
            String stanzaTitle = getString(R.string.bait)+" "+j;
            StanzaContainer myTmpStanza = new StanzaContainer(stanzaTitle, kidung[i]);
            myStanza.add(myTmpStanza);
        }
        //Log.i("array",Integer.toString(kidunglist.length));
        return myStanza;
    }

    /*
     * to load state
     */
    void loadMyState() {
        if (myBundle != null) {
            super.onRestoreInstanceState(myBundle);
            // Save the user's current media player state
            myCurrentPos = myBundle.getInt(STATE_MY_PLAYER_POSITION);
            myPlayerStatus = myBundle.getBoolean(STATE_MY_PLAYER_STATUS);
        }
    }


    /*
     * to save state
     */
    void saveMyState() {
        if (myBundle != null) {
            myBundle.putInt(STATE_MY_PLAYER_POSITION, myCurrentPos);
            myBundle.putBoolean(STATE_MY_PLAYER_STATUS, myPlayerStatus);
            super.onSaveInstanceState(myBundle);
        }
    }

    // 20240302
    void playMedia() {
        //Log.e("dedetok", "playMedia create execcutor"); // debugg
        executor = Executors.newSingleThreadExecutor();

        //Log.e("dedetok", "playMedia wakelock and start"); // debugg

        // 20240423
        // try to get WAKE_LOCK
        if (wakeLock==null) {
            PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
            wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                    "MyApp::com.dedetok.turuntirta");
        }
        wakeLock.acquire();
        myMediaPlayer.setWakeMode(this, PowerManager.PARTIAL_WAKE_LOCK);
        myMediaPlayer.start();

        // set icon to pause
        myPlayButton.setImageResource(android.R.drawable.ic_media_pause);
        myPlayerStatus = myMediaPlayer.isPlaying();

        //Log.e("dedetok", "playMedia seekbar"); // debugg

        // update seekbar TODO CRASH
        executor.submit(mySeekbarUpdater);
    }

    // 20240302
    void pauseMedia() {
        //Log.e("dedetok", "pauseMedia pause"); // debugg
        myMediaPlayer.pause();
        // set icon to play
        myPlayButton.setImageResource(android.R.drawable.ic_media_play);
        myPlayerStatus = myMediaPlayer.isPlaying();

        //Log.e("dedetok", "pauseMedia waklock release"); // debugg

        // 20240423
        // try to release WAKE_LOCK
        if (wakeLock!=null) {
            wakeLock.release();
        }
        //Log.e("dedetok", "pauseMedia executor release"); // debugg
        if (executor!=null) {
            executor.shutdown();
        }
    }

    /* 20240302
     * Thread to update seekbar during mediaplayer run
     */
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Runnable mySeekbarUpdater = new Runnable() {
        @Override
        public void run() {
            if (myMediaPlayer!=null) {
                if (myMediaPlayer.isPlaying()) {
                    myCurrentPos = myMediaPlayer.getCurrentPosition();
                    mySeekBar.setProgress(myCurrentPos);
                    myHandler.postDelayed(this, 500); // 500 millisecond
                }
            }
        }
    };
}