-
Notifications
You must be signed in to change notification settings - Fork 891
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
Changes from 3 commits
b2809b6
4a53d7f
435ab83
34b6fd2
bdfa882
aa873a1
4162f19
4fdceae
2880ebf
47727e5
cf84a95
168a044
e85760c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,9 +102,11 @@ | |
android:authorities="@string/file_authority" | ||
android:grantUriPermissions="true" | ||
android:exported="false"> | ||
tools:replace="android:authorities"> | ||
<meta-data | ||
android:name="android.support.FILE_PROVIDER_PATHS" | ||
android:resource="@xml/file_paths"/> | ||
android:resource="@xml/file_paths" | ||
tools:replace="android:resource" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and this |
||
</provider> | ||
|
||
<receiver android:name="androidx.media.session.MediaButtonReceiver"> | ||
|
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; | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
||
/** | ||
|
@@ -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; | ||
|
@@ -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; | ||
|
||
|
@@ -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)); | ||
|
@@ -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 { | ||
|
@@ -1837,6 +1863,122 @@ public void onError(@NonNull Throwable e) { | |
); | ||
} | ||
|
||
public void shareAyahAudio(SuraAyah start, SuraAyah end) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in the new |
||
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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.* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please let's not use star imports There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. constant - also what's relevant about this number here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have okio in the code that might simplify this logic There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit - please avoid m prefix on variables There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
There was a problem hiding this comment.
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?