Tuesday, December 26, 2023

Privacy Policy for Purwakaning (com.dedetok.purwakaning)

 

 

Privacy Statement

Your privacy is important to us. This privacy statement explains what Purwakaning (com.dedetok.purwakaning) application does, regarding your personal data.

Personal data we collect

Purwakaning (com.dedetok.purwakaning) application does not requesting any information about your personal data.

Network & Internet Connection:

Purwakaning (com.dedetok.purwakaning) application does not mandatory to use internet connection. You can use Purwakaning (com.dedetok.purwakaning) application without Internet connection.

If Internet connection exist, Purwakaning (com.dedetok.purwakaning) application will use it to serving for Google AdMob only.

Contact Us:

If You need to contact Us, here is My email address:

dedetoke2021@gmail.com

Android java source code com.dedetok.wargasari

This is base project for com.dedetok.wargasari, com.dedetok.purwakaning, and com.dedetok.pitrapuja listed on Google Play. I tested on Android 7, 8, 9, 10, 11 and 13, I believe no bug or no any code that can caused your privacy data leak. No guarantee if you use apk or other package installer other then google play.

For com.dedetok.turuntirta and com.dedetok.bramara, I keep using MediaPlayer legacy. Source code: https://dedetoknotes.blogspot.com/2024/04/android-java-source-code.html

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

build.gradle.kts (Module: app)

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

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

    defaultConfig {
        applicationId = "com.dedetok.wargasari"
        minSdk = 24
        targetSdk = 34
        versionCode = 202404
        versionName = "2024.04"

        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")
    implementation("com.google.android.gms:play-services-ads-lite:23.0.0")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    // For exposing and controlling media sessions
    implementation("androidx.media3:media3-session:1.3.0")
    // For media playback using ExoPlayer
    implementation("androidx.media3:media3-exoplayer:1.3.0")
    // For HLS playback support with ExoPlayer
    implementation("androidx.media3:media3-exoplayer-hls:1.3.0")
    // UI unstable
    //implementation("androidx.media3:media3-ui:1.3.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"/>

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />


    <!-- 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/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Wargasari"
        tools:targetApi="31"
        android:enableOnBackInvokedCallback="true"
        >

        <!-- android:configChanges="orientation|screenSize" keep state UI -->
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|screenSize"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name=".myservice.DedetokMediaSessionService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="true"
            >
            <intent-filter>
                <action android:name="androidx.media3.session.MediaSessionService"/>
                <action android:name="android.media.browse.MediaBrowserService"/>
            </intent-filter>
        </service>

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

res -> values -> strings.xml

<resources>
    <string name="app_name">Wargasari</string>

    <string-array name="kidung">
        <item>Ida ratu saking luhur |\nKawula nunas lugrane |\nMangda sampun titiang tandruh |\nMengayat bhatara mangkin |\nTitiang ngaturang pejati |\nCanang suci lan daksina |\nSami sampun puput |\nPretingkahing saji ||</item>
        <item>Asep menyan majegau |\nCendana nuhur dewane |\nMangda ida gelis rawuh |\nMijil saking luhuring langit |\nSampun medabdaban sami |\nMaring giri meru reko |\nAncangan sadulur |\nSami pada ngiring ||</item>
        <item>Bhetarane saking luhur |\nNgegenah ring ambarane |\nPenganggene sarwa murub |\nParekan sami mengiring |\nWidyadara widyadari |\nPada medudon dudonan |\nPrabhawa kumetug |\nAngliwer ring langit ||</item>
        <item>Di bale manike luwung |\nMapanyengker ring telagane |\nKedangingin tunjung putih |\nTunjung abang tunjung putih |\nRing madyaning bale alit |\nIda bhatara mebawos |\nNganggit sekar jepun |\nSekar sane wangi ||</item>
        <item>Ring bale emase parum |\nLinggih ida bhetarane |\nBale emas ngranyab murub |\nUpacara sarwa luwih |\nLeluhure sutra putih |\nIda bhatara mabawos |\nBawose di luhur |\nPacang turun gelis ||</item>
        <item>Asep pejati wus katur |\nMendak ida bhetarane |\nPeneteg lan canang harum |\nCanang gantal canang sari |\nParekan pada menangkil |\nPedak sami nunas ica |\nNyadpada menyungsung |\nNgaturang pelinggih ||</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"
        android:padding="10sp"
        />

    <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/play_button"
            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/5649213488 : layout -->
    <com.google.android.gms.ads.AdView
        android:id="@+id/adView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:adSize="BANNER"
        app:adUnitId="ca-app-pub-3940256099942544/6300978111"
        android:layout_gravity="end|center"
        />
</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 -> com.dedetok.wargasari.myservice -> DedetokMediaSessionService.java

package com.dedetok.wargasari.myservice;

import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaSession;
import androidx.media3.session.MediaSessionService;

/*
 * MediaSessionService part - status ok
 * ExoPlayer inside MediaSession
 */

public class DedetokMediaSessionService extends MediaSessionService {

    /*
     * MediaSession creates a default implementation of MediaSession.Callback that
     * automatically handles all commands a MediaController sends to your player
     */
    MediaSession myMediaSession = null;

    /*
     * Create your Player and MediaSession in the onCreate lifecycle event
     *
     */
    @Override
    public void onCreate() {
        super.onCreate();
        ExoPlayer player = new ExoPlayer.Builder(this).build();
        // Let system handle focus audio for incoming or outgoing call
        player.setAudioAttributes(AudioAttributes.DEFAULT, true);
        myMediaSession = new MediaSession.Builder(this, player).build();
        // Log.e("dedetok","DedetokMediaSessionService onCreate"); // debug

    }

    /*
     * The user dismissed the app from the recent tasks
     */
    @Override
    public void onTaskRemoved(@Nullable Intent rootIntent) {
        Player player = myMediaSession.getPlayer();
        if (!player.getPlayWhenReady() || player.getMediaItemCount() == 0 ) {
            /*
             * stop player if not ready || no media playing
             * else play on background
             */
            player.stop();
            stopSelf();
            // Log.e("dedetok", "onTaskRemoved stop and release resources"); // debug
        }
    }

    /*
     * Remember to release the player and media session in onDestroy
     */
    @Override
    public void onDestroy() {
        // https://developer.android.com/media/media3/session/background-playback
        myMediaSession.getPlayer().release();
        myMediaSession.release();
        myMediaSession = null;
        super.onDestroy();
    }

    /*
     * extends MediaSessionService
     */
    @Nullable
    @Override
    public MediaSession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) {
        return myMediaSession;
    }

}

java -> com.dedetok.wargasari.myservice -> MyMediaController.java

package com.dedetok.wargasari.myservice;

import android.content.ComponentName;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionToken;
import com.dedetok.wargasari.R;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.ExecutionException;

/*
 * MediaController client part
 */
public class MyMediaController {
    String kidungName = "Wargasari";
    Context appContext;
    MediaController myMediaController = null;
    // myMediaController.getApplicationLooper() Returns the Looper associated
    // with the application thread that's used to access the player and on which
    // player events are received.
    MediaItem myMediaItem;
    MyMediaControllerCallback myMediaControllerCallback;

    public MyMediaController(Context appContext, MyMediaControllerCallback myMediaControllerCallback) {
        this.appContext = appContext;
        this.myMediaControllerCallback = myMediaControllerCallback;
        createMediaController(appContext);
    }

    // user send play media
    public void play() {
        if (myMediaController!=null) {
            // pause does not state player i.e. Player.STATE_READY
            if (myMediaController.getPlaybackState()!=Player.STATE_READY) {
                MediaMetadata myMediaMetadata = new MediaMetadata.Builder().
                        setTitle(kidungName).
                        setStation(kidungName).
                        setDisplayTitle(kidungName).
                        //setArtworkUri(myUri). // to set background icon may any size
                                build();

                Uri myUri = Uri.parse("android.resource://com.my.package/" + R.raw.wargasari);
                myMediaItem = new MediaItem.Builder().
                        setMediaMetadata(myMediaMetadata).
                        setUri(myUri).
                        build();
                myMediaController.setMediaItem(myMediaItem); // addMediaItem(MediaItem myMediaItem)
                myMediaController.prepare();
                myMediaController.setPlayWhenReady(true);
            } else {
                //Log.e("dedetok", "MyMediaController start() direct play"); // debug
                myMediaController.play();
            }
        }
    }

    // user call pause
    // pause does not change onPlaybackStateChanged!
    // Player.STATE_READY 3
    public void pause() {
        if (myMediaController!=null) {
            myMediaController.pause();
        }
    }

    /*
     * user exit application, must stop player
     * after stop() Player.STATE_IDLE / 1
     * Player.STATE_IDLE but not trigger onIsPlayingChanged(boolean isPlaying) to false, BUG?
     */
    public void stopPlayer() {
        myMediaController.stop();
    }

    // 20240422
    public void setRepeat(boolean booleanRepeat) {
        if (myMediaController!=null) {
            if (booleanRepeat) {
                myMediaController.setRepeatMode(Player.REPEAT_MODE_ONE);
            } else {
                myMediaController.setRepeatMode(Player.REPEAT_MODE_OFF);
            }
        }
    }

    // call when exit application
    public void releaseMediaSession() {
        myMediaController.release();
    }

    /*
     * ContextCompat.getMainExecutor() instead of MoreExecutors.directExecutor()
     * in case you run your service in another process than the UI.
     */
    private void createMediaController(Context appContext) {
        //Log.e("dedetok","createMediaController step 1"); // debug
        // https://developer.android.com/media/media3/session/connect-to-media-app#create-controller
        // step 1 create SessionToken
        SessionToken mySessionToken =
                new SessionToken(appContext,
                        new ComponentName(appContext, DedetokMediaSessionService.class));
        //Log.e("dedetok","createMediaController step 2"); // debug
        // step 2 build MediaController using SessionToken
        ListenableFuture<MediaController> myListenableFutureMediaController =
                new MediaController.Builder(appContext, mySessionToken).buildAsync();
        myListenableFutureMediaController.addListener(() -> {
            // MediaController is available here with myListenableFutureMediaController.get()
            try {
                //Log.e("dedetok","try to get Media Controller"); // debug
                myMediaController = myListenableFutureMediaController.get();

                myMediaController.addListener(myPlayerListener);
            } catch(ExecutionException | InterruptedException e) {
                //Log.e("dedetok", e.getMessage()); // debug

            }
        }, MoreExecutors.directExecutor());
    }

    /*
     * Playback listener 20240229
     */
    Player.Listener myPlayerListener = new Player.Listener() {
        @Override
        public void onPlaybackStateChanged(int playbackState) {
            Player.Listener.super.onPlaybackStateChanged(playbackState);
            if (playbackState == Player.STATE_IDLE)
                Log.e("dedetok", "Player.STATE_IDLE 1"); // debug
            else if (playbackState == Player.STATE_BUFFERING)
                Log.e("dedetok", "Player.STATE_BUFFERING 2"); // debug
            else if (playbackState == Player.STATE_READY)
                Log.e("dedetok", "Player.STATE_READY 3"); // debug
            else if (playbackState == Player.STATE_ENDED)
                Log.e("dedetok", "Player.STATE_ENDED 4"); // debug
        }

        @Override
        public void onIsPlayingChanged(boolean isPlaying) {
            Log.e("dedetok", "onIsPlayingChanged change to "+isPlaying); // debug
            myMediaControllerCallback.isPlaying(isPlaying);

        }
    };

    public interface MyMediaControllerCallback {
        // from MyMediaController change player status
        void isPlaying(boolean isPlaying);
    }
}

java -> MyDataProvider.java

package com.dedetok.wargasari;

import static android.util.TypedValue.COMPLEX_UNIT_SP;
import android.content.Context;
import android.content.res.TypedArray;
import com.dedetok.wargasari.myservice.MyMediaController;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyDataProvider implements MyMediaController.MyMediaControllerCallback {
    MyMediaController myMediaController;
    Context appContext;
    MyDataProviderCallback myDataProviderCallback;
    ExecutorService executor;
    MyAdapter myAdapter;
    boolean isPlaying;

    public MyDataProvider(Context appContext) {
        this.appContext = appContext;
        this.myDataProviderCallback = (MyDataProvider.MyDataProviderCallback) appContext;
        myMediaController = new MyMediaController(appContext, this);
    }

    /*
     * async task to create recyclerview.adapter
     * load array text into adapter
     */
    public void loadKidungArrayAdapter(TypedArray availFontSize, int cTextSizeState, float defaultTextSize) {
        if (executor == null) {
            executor = Executors.newSingleThreadExecutor();
        }
        Runnable myRun = new Runnable() {
            @Override
            public void run() {
                myAdapter = new MyAdapter(getTexts(), COMPLEX_UNIT_SP, availFontSize.getFloat(cTextSizeState, defaultTextSize));
                myDataProviderCallback.setKidungArrayAdapter(myAdapter);
                executor.shutdown();
                executor = null;
            }
        };
        executor.execute(myRun);
    }

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

    /*
     * user play media
     * send command play to media controller
     */
    public void play() {
        if (myMediaController!=null) {
            //Log.e("dedetok", "MyDataProvider Playing"); // debug
            // player does not instanly ready
            myMediaController.play();
        }
    }

    // user call pause
    public void pause() {
        if (myMediaController!=null) {
            myMediaController.pause();
        }
    }

    // player exit application must stop player
    public void stopPlayer() {
        myMediaController.stopPlayer();
    }

    // 20240422
    public void setRepeat(boolean booleanRepeat) {
        myMediaController.setRepeat(booleanRepeat);
    }

    // call when application destroy
    public void releaseMediaSession() {
        myMediaController.releaseMediaSession();
    }

    // user change font size
    public void changeFontSize(float fSize) {
        myAdapter.changeFontSize(COMPLEX_UNIT_SP,fSize);
    }

    /*
     * implementation MyMediaControllerCallback START
     */
    // from MyMediaController change player status
    @Override
    public void isPlaying(boolean isPlaying) {
        this.isPlaying=isPlaying;
        myDataProviderCallback.isPlaying(isPlaying);
    }

    /*
     * implementation MyMediaControllerCallback START
     */

    public interface MyDataProviderCallback {
        // from MyDataProvider to update recyclerview
        void setKidungArrayAdapter(MyAdapter myAdapter);
        void isPlaying(boolean isPlaying);
    }
}

java -> MainActivity.java

package com.dedetok.wargasari;

import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatImageButton;
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.TypedArray;
import android.os.Bundle;
import android.view.View;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdView;

public class MainActivity extends AppCompatActivity implements
        MyDataProvider.MyDataProviderCallback {

    int cTextSizeState = 0;
    TypedArray availFontSize;
    float defaultTextSize = 18;
    RecyclerView myRecyclerView;
    MyDataProvider myDataProvider;
    AppCompatImageButton bPlayPause;

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

        myDataProvider = new MyDataProvider(this);

        // 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/5649213488 : AdView in Layout
        // app id ca-app-pub-0220748109202708~2695747083 : AndroidManifest.xml
        AdView mAdView = findViewById(R.id.adView);
        AdRequest adRequest = new AdRequest.Builder().build();
        mAdView.loadAd(adRequest);

        // recyclerview
        availFontSize = getResources().obtainTypedArray(R.array.my_font_size);
        myRecyclerView = findViewById(R.id.my_recycler_view);
        LinearLayoutManager myLinearLayout = new LinearLayoutManager(getApplicationContext());
        myLinearLayout.setOrientation(LinearLayoutManager.VERTICAL);
        myRecyclerView.setLayoutManager(myLinearLayout);
        // adapter will created on async task

        // 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) {
                    cTextSizeState--;
                    if (myDataProvider!=null) {
                        myDataProvider.changeFontSize(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()) {
                    cTextSizeState++;
                    if (myDataProvider!=null) {
                        myDataProvider.changeFontSize(availFontSize.getFloat(cTextSizeState, defaultTextSize));
                    }
                }
            }
        });

        // 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 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();
            }
        };
        // add callback to dispatcher
        myOnbackPressedDispatcher.addCallback(this, myBackPressedCallback);

        // 20240328
        bPlayPause = findViewById(R.id.play_button);
        bPlayPause.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (myDataProvider!=null) {
                    if (myDataProvider.isPlaying) {
                        myDataProvider.pause();
                    } else {
                        myDataProvider.play();
                    }
                }
            }
        });

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

    }

    // ## activity life cycle 2 ##
    // onStart()
    @Override
    protected void onStart() {
        super.onStart();
        // 20240329
        // Android N (7.0) or latter MediaPlayer should create in OnStart
        // TypedArray availFontSize, int cTextSizeState, float defaultTextSize)
        // create Adapter in async task
        myDataProvider.loadKidungArrayAdapter(availFontSize, cTextSizeState, defaultTextSize);
    }

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

    /*
     * ## activity life cycle 4 ##
     * onPause()
     * @Override onSaveInstanceState(Bundle outState) ?
     */
    @Override
    public void onPause() {
        // do something before pause
        //Log.e("dedetok","onPause"); // debug
        super.onPause();
    }

    // ## activity life cycle 5 ##
    @Override
    protected void onStop() {
        // 20240329
        // Android N (7.0) or latter MediaPlayer should stop and release in OnStop
        // do something before stop
        //Log.e("dedetok","onStop"); // debug
        super.onStop();
    }

    /*
     * ## activity life cycle 6 ##
     * clean up onDestroy()
     */
    @Override
    public void onDestroy() {
        // Log.e("dedetok","onDestroy Activity"); // debug
        // do something before destroy
        //Log.e("dedetok","onDestroy"); // debug
        // fix android 8, 9, 10, 11 to stop media and release resource
        myDataProvider.stopPlayer();
        myDataProvider.releaseMediaSession();
        super.onDestroy();
    }

    /*
     * impelementation MyDataProvider START
     */
    // from MyDataProvider to update recyclerview
    @Override
    public void setKidungArrayAdapter(MyAdapter myAdapter) {
        // can not direct update
        myRecyclerView.post(new Runnable() {
            @Override
            public void run() {
                myRecyclerView.setAdapter(myAdapter);
            }
        });
    }

    public void isPlaying(boolean isPlaying) {
        bPlayPause.post(new Runnable() {
            @Override
            public void run() {
                if (isPlaying) {
                    bPlayPause.setImageDrawable(getDrawable(android.R.drawable.ic_media_pause));
                } else {
                    bPlayPause.setImageDrawable(getDrawable(android.R.drawable.ic_media_play));
                }
            }
        });
    }

    /*
     * impelementation MyDataProvider END
     */

}


Wednesday, December 20, 2023

Android Studio Hedgehog | 2023.1.1: import java library problem

Causing: Unknown

Symptoms: Can not import any java library

Option:

  • File > Invalidate Caches... -> clear all (Working on my project)
  • File > Sync Project with Gradle Files (not tested)

https://stackoverflow.com/questions/45982092/android-studio-not-recognizing-import-statements

Android Studio Hedgehog | 2023.1.1: manual backup android project

Manual backup android studio 2023 project 

Root project files:

  • build.gradle.kts
  • gradle.properties
  • gradlew
  • gradle.bat
  • local.properties
  • settings.gradle.kts
  • *.jks (java keystore files if you publish your application with signed)

folders root project -> app:

  • build.gradle.kts
  • proguard-rules.pro
  • src (all)

Tuesday, December 19, 2023

Privacy Policy for Kramaning (com.dedetok.kramaning)

Privacy Statement

Your privacy is important to us. This privacy statement explains what Kramaning (com.dedetok.kramaning) application does, regarding your personal data.

Personal data we collect

Kramaning (com.dedetok.kramaning) application does not requesting any information about your personal data.

Network & Internet Connection:

Kramaning (com.dedetok.kramaning) application does not mandatory to use internet connection. You can use Kramaning (com.dedetok.kramaning) application without Internet connection.

If Internet connection exist, Kramaning (com.dedetok.kramaning) application will use it to serving for Google AdMob only.

Contact Us:

If You need to contact Us, here is My email address:

dedetoke2021@gmail.com