Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] share ayah as audio #2011

Closed
wants to merge 13 commits into from
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@
android:authorities="@string/file_authority"
android:grantUriPermissions="true"
android:exported="false">
tools:replace="android:authorities">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we need to add this?

<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
android:resource="@xml/file_paths"
tools:replace="android:resource" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this

</provider>

<receiver android:name="androidx.media.session.MediaButtonReceiver">
Expand Down
144 changes: 143 additions & 1 deletion app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.quran.labs.androidquran.ui;

import static com.quran.labs.androidquran.database.DatabaseUtils.closeCursor;
import static com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter.AUDIO_PAGE;
import static com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter.TAG_PAGE;
import static com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter.TRANSLATION_PAGE;
Expand All @@ -15,13 +16,17 @@
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.Menu;
Expand Down Expand Up @@ -70,10 +75,12 @@
import com.quran.labs.androidquran.common.LocalTranslationDisplaySort;
import com.quran.labs.androidquran.common.QuranAyahInfo;
import com.quran.labs.androidquran.common.audio.model.QariItem;
import com.quran.labs.androidquran.dao.audio.AudioPathInfo;
import com.quran.labs.androidquran.dao.audio.AudioRequest;
import com.quran.labs.androidquran.data.Constants;
import com.quran.labs.androidquran.data.QuranDataProvider;
import com.quran.labs.androidquran.data.QuranDisplayData;
import com.quran.labs.androidquran.database.SuraTimingDatabaseHandler;
import com.quran.labs.androidquran.database.TranslationsDBAdapter;
import com.quran.labs.androidquran.di.component.activity.PagerActivityComponent;
import com.quran.labs.androidquran.di.module.activity.PagerActivityModule;
Expand Down Expand Up @@ -123,8 +130,10 @@
import com.quran.reading.common.AudioEventPresenter;
import com.quran.reading.common.ReadingEventPresenter;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
Expand All @@ -138,9 +147,12 @@
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.observers.DisposableObserver;
import io.reactivex.rxjava3.observers.DisposableSingleObserver;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.TuplesKt;
import kotlin.jvm.internal.Intrinsics;
import timber.log.Timber;

/**
Expand Down Expand Up @@ -231,7 +243,7 @@ public class PagerActivity extends AppCompatActivity implements
@Inject ShareUtil shareUtil;
@Inject AudioUtils audioUtils;
@Inject QuranDisplayData quranDisplayData;
@Inject QuranInfo quranInfo;
@Inject QuranInfo quranInfo;
@Inject QuranFileUtils quranFileUtils;
@Inject AudioPresenter audioPresenter;
@Inject QuranEventLogger quranEventLogger;
Expand All @@ -249,6 +261,13 @@ public class PagerActivity extends AppCompatActivity implements

private final PagerHandler handler = new PagerHandler(this);

private Disposable timingDisposable;
private int gaplessSura;
private SparseIntArray gaplessSuraData = new SparseIntArray();
public static final File audioCacheDirectory= new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath() +
File.separator +"quran_android_cache");


private static class PagerHandler extends Handler {
private final WeakReference<PagerActivity> activity;

Expand Down Expand Up @@ -335,6 +354,11 @@ public void onCreate(Bundle savedInstanceState) {
compositeDisposable = new CompositeDisposable();

setContentView(R.layout.quran_page_activity_slider);
if (!audioCacheDirectory.exists()) {
if (!audioCacheDirectory.mkdirs()){
Toast.makeText(PagerActivity.this, "could not create directory", Toast.LENGTH_SHORT).show();
}
}
audioStatusBar = findViewById(R.id.audio_area);
audioStatusBar.setIsDualPageMode(quranScreenInfo.isDualPageMode());
audioStatusBar.setQariList(audioUtils.getQariList(this));
Expand Down Expand Up @@ -1760,6 +1784,8 @@ public boolean onMenuItemClick(MenuItem item) {
shareAyahLink(startSuraAyah, endSuraAyah);
} else if (itemId == com.quran.labs.androidquran.common.toolbar.R.id.cab_share_ayah_text) {
shareAyah(startSuraAyah, endSuraAyah, false);
}else if (itemId == com.quran.labs.androidquran.common.toolbar.R.id.cab_share_ayah_audio) {
shareAyahAudio(startSuraAyah, endSuraAyah);
} else if (itemId == com.quran.labs.androidquran.common.toolbar.R.id.cab_copy_ayah) {
shareAyah(startSuraAyah, endSuraAyah, true);
} else {
Expand Down Expand Up @@ -1837,6 +1863,122 @@ public void onError(@NonNull Throwable e) {
);
}

public void shareAyahAudio(SuraAyah start, SuraAyah end) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move all this code out of here - somewhere in that feature/audiosharing module you'll make instead so we don't add more lines to this already massive class

SuraAyah actualStart,actualEnd;
if (start == null || end == null) {
return;
}else {
kotlin.Pair pair;
if (start.compareTo(end) <= 0) {
pair = TuplesKt.to(start, end);
} else {
Timber.Forest.e(new IllegalStateException("End isn't larger than the start: " + start + " to " + end));
pair = TuplesKt.to(end, start);
}

kotlin.Pair pair2 = pair;
actualStart = (SuraAyah) pair2.component1();
actualEnd = (SuraAyah) pair2.component2();
}

final QariItem qari = audioStatusBar.getAudioInfo();
AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(this,qari);

assert audioPathInfo != null;
if (audioPathInfo.getGaplessDatabase() != null) {
createAndShareAudio(actualStart,actualEnd,audioPathInfo);
}
}

private void createAndShareAudio(SuraAyah start, SuraAyah end, AudioPathInfo audioPathInfo) {
showProgressDialog();
String databasePath = audioPathInfo.getGaplessDatabase();
compositeDisposable.add(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's prefer coroutines to Rx - you'll be able to do that easier when we move out of this class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the new AudioShareUtils class in the feature:audio

Single.fromCallable(() -> {
assert databasePath != null;
SuraTimingDatabaseHandler db = SuraTimingDatabaseHandler.Companion.getDatabaseHandler(databasePath);
SparseIntArray firstSurahMap = new SparseIntArray();
SparseIntArray lastSurahMap = new SparseIntArray();
Cursor firstSurahCursor;
Cursor lastSurahCursor = null;

try {
firstSurahCursor = db.getAyahTimings(start.sura);
Timber.Forest.d("got cursor of data");
if (firstSurahCursor != null && firstSurahCursor.moveToFirst()) {
do {
int ayah = firstSurahCursor.getInt(1);
int time = firstSurahCursor.getInt(2);
firstSurahMap.put(ayah, time);
} while (firstSurahCursor.moveToNext());
}

lastSurahCursor = db.getAyahTimings(end.sura);
Timber.Forest.d("got cursor of data");
if (lastSurahCursor != null && lastSurahCursor.moveToFirst()) {
do {
int ayah = lastSurahCursor.getInt(1);
int time = lastSurahCursor.getInt(2);
lastSurahMap.put(ayah, time);
} while (lastSurahCursor.moveToNext());
}
} catch (SQLException sqlException) {
Timber.Forest.e(sqlException);
} finally {
closeCursor(lastSurahCursor);
}
ArrayList<SparseIntArray> mapArray = new ArrayList<>(Arrays.asList(firstSurahMap, lastSurahMap));
return mapArray;
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableSingleObserver<ArrayList<SparseIntArray>>() {
@Override
public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList<SparseIntArray> sparseIntArrayList) {
Intrinsics.checkNotNullExpressionValue(sparseIntArrayList, "mapArray");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can't be null - if it were null it'd crash, rx2+ doesn't support null emissions


int startAyah = start.ayah;
int endAyah = end.ayah;
int startAyahTime = startAyah == 1?sparseIntArrayList.get(0).get(0):sparseIntArrayList.get(0).get(startAyah);
int endAyahTime = startAyah == 1?sparseIntArrayList.get(1).get(0):sparseIntArrayList.get(1).get(endAyah+1)==0?audioUtils.getSurahDuration(PagerActivity.this,audioUtils.getSurahAudioPath(audioPathInfo,end.sura)):sparseIntArrayList.get(1).get(endAyah+1);

if (start.sura == end.sura){
shareUtil.shareAudioFileIntent(PagerActivity.this,new File(audioUtils.getSurahSegment(audioUtils.getSurahAudioPath(audioPathInfo,start.sura),startAyahTime,endAyahTime)));
}else {
ArrayList<String> paths = new ArrayList<>();
String path1 = audioUtils.getSurahAudioPath(audioPathInfo,start.sura);
int upperCut = audioUtils.getSurahDuration(PagerActivity.this,path1);
String firstSegment = audioUtils.getSurahSegment(path1,startAyahTime,upperCut);
String path2 = audioUtils.getSurahAudioPath(audioPathInfo,end.sura);
String lastSegment = audioUtils.getSurahSegment(path2,0,endAyahTime);

for (int surahIndex = start.sura; surahIndex<=end.sura; surahIndex++){
if (surahIndex == start.sura){
paths.add(firstSegment);
continue;
}
if (surahIndex != end.sura){
paths.add(audioUtils.getSurahAudioPath(audioPathInfo,surahIndex));
continue;
}
paths.add(lastSegment);
}
if (!paths.isEmpty()){
File sharableAudioFile = audioUtils.getMergedAudioFromSegments(paths);
shareUtil.shareAudioFileIntent(PagerActivity.this,sharableAudioFile);
}
}
dismissProgressDialog();
}

@Override
public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) {
dismissProgressDialog();

}
})
);
}

private void showProgressDialog() {
if (progressDialog == null) {
progressDialog = new ProgressDialog(this);
Expand Down
107 changes: 105 additions & 2 deletions app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@ package com.quran.labs.androidquran.util

import android.content.Context
import android.content.Intent
import android.media.MediaMetadataRetriever
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat.startActivity
import androidx.core.content.FileProvider
import com.quran.data.core.QuranInfo
import com.quran.data.model.SuraAyah
import com.quran.labs.androidquran.common.audio.model.AudioConfiguration
import com.quran.labs.androidquran.common.audio.model.QariItem
import com.quran.labs.androidquran.common.audio.util.QariUtil
import com.quran.labs.androidquran.dao.audio.AudioPathInfo
import com.quran.labs.androidquran.service.AudioService
import com.quran.labs.androidquran.ui.PagerActivity
import com.quran.labs.androidquran.util.audioConversionUtils.CheapSoundFile
import timber.log.Timber
import java.io.File
import java.util.Locale
import java.io.*
import java.util.*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please let's not use star imports

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have fixed this in the latest commit

import javax.inject.Inject
import kotlin.math.roundToInt

class AudioUtils @Inject constructor(
private val quranInfo: QuranInfo,
Expand Down Expand Up @@ -283,6 +291,101 @@ class AudioUtils @Inject constructor(
}
}

fun getLocalAudioPathInfo(context: Context,qari: QariItem): AudioPathInfo? {
val localPath = getLocalQariUri(qari)
if (localPath != null) {
val databasePath = getQariDatabasePathIfGapless(qari)
val urlFormat = if (databasePath.isNullOrEmpty()) {
localPath + File.separator + "%d" + File.separator +
"%d" + AUDIO_EXTENSION
} else {
localPath + File.separator + "%03d" + AUDIO_EXTENSION
}
return AudioPathInfo(urlFormat, localPath, databasePath)
}
return null
}

fun getMergedAudioFromSegments(segments: ArrayList<String>): File {
var mergedAudioPath = segments[0]
if (segments.size > 1) {
for (i in 1 until segments.size) {
mergedAudioPath = mergeAudios(mergedAudioPath, segments[i])!!
}
}
return File(mergedAudioPath)
}

private fun mergeAudios(path1: String, path2: String): String? {
val tempAudioName = UUID.randomUUID().toString() + ".mp3"
val destFile = File(PagerActivity.audioCacheDirectory.path + File.separator + tempAudioName)
try {
val fileInputStream = FileInputStream(path1)
val bArr = ByteArray(1048576)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constant - also what's relevant about this number here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 1 MB (1024*1024 bytes), but I have removed this in favor of "Okio"

val fileOutputStream = FileOutputStream(destFile)
while (true) {
val read = fileInputStream.read(bArr)
if (read == -1) {
break
}
fileOutputStream.write(bArr, 0, read)
fileOutputStream.flush()
}
fileInputStream.close()
val fileInputStream2 = FileInputStream(path2)
while (true) {
val read2 = fileInputStream2.read(bArr)
if (read2 == -1) {
break
}
fileOutputStream.write(bArr, 0, read2)
fileOutputStream.flush()
}
fileInputStream2.close()
fileOutputStream.close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have okio in the code that might simplify this logic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

return destFile.path
} catch (e2: FileNotFoundException) {
e2.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return null
}

fun getSurahSegment(path: String, lowerCut: Int, upperCut: Int): String? {
val tempAudioName = UUID.randomUUID().toString() + ".mp3"
val destFile = File(PagerActivity.audioCacheDirectory.path + File.separator + tempAudioName)
val mSoundFile = arrayOfNulls<CheapSoundFile>(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - please avoid m prefix on variables

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

try {
mSoundFile[0] = CheapSoundFile.create(path, null)
if (lowerCut == 0 && upperCut == 0) {
return null
}
val startTime = lowerCut.toFloat() / 1000
val endTime = upperCut.toFloat() / 1000
val samplesPerFrame = mSoundFile[0]?.samplesPerFrame
val sampleRate = mSoundFile[0]?.sampleRate
val avg = sampleRate?.div(samplesPerFrame!!)
val startFrames = (startTime * avg!!).roundToInt()
val endFrames = (endTime * avg!!).roundToInt()
mSoundFile[0]?.WriteFile(destFile, startFrames, endFrames - startFrames)
} catch (e: IOException) {
e.printStackTrace()
}
return destFile.path
}

fun getSurahDuration(context: Context,path: String): Int {
val mmr = MediaMetadataRetriever()
mmr.setDataSource(context, Uri.parse(path))
val durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
return durationStr!!.toInt()
}

fun getSurahAudioPath(audioPathInfo: AudioPathInfo, surah: Int): String? {
return String.format(Locale.US, audioPathInfo.localDirectory, surah)
}

companion object {
const val ZIP_EXTENSION = ".zip"
const val AUDIO_EXTENSION = ".mp3"
Expand Down
Loading