From b2809b6cea01a58931951ee98122c19d0f8fc42c Mon Sep 17 00:00:00 2001 From: Doozy <98877504+DoozyDoz@users.noreply.github.com> Date: Tue, 5 Jul 2022 08:24:53 -0700 Subject: [PATCH 01/18] Feat share ayah as audio (#1) * attempt 1 * attempt 2 * attempt 3 * clean up 1 * attempt 3 * attempt 4 * cleanup * cleanup 2 --- app/src/main/AndroidManifest.xml | 15 +- .../labs/androidquran/ui/PagerActivity.java | 143 +++++++- .../labs/androidquran/util/AudioUtils.kt | 106 +++++- .../quran/labs/androidquran/util/ShareUtil.kt | 15 + .../util/audioConversionUtils/CheapMP3.java | 323 ++++++++++++++++++ .../audioConversionUtils/CheapSoundFile.java | 229 +++++++++++++ app/src/main/res/xml/file_paths.xml | 9 + .../src/main/res/drawable/ic_audio.xml | 5 + .../toolbar/src/main/res/drawable/ic_text.xml | 5 + .../toolbar/src/main/res/menu/ayah_menu.xml | 77 +++-- .../toolbar/src/main/res/values/strings.xml | 1 + 11 files changed, 891 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapMP3.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapSoundFile.java create mode 100644 common/toolbar/src/main/res/drawable/ic_audio.xml create mode 100644 common/toolbar/src/main/res/drawable/ic_text.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e61449217..7beb6dd9c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,14 +97,25 @@ android:exported="false" android:authorities="@string/authority"/> + + + + + + + + + + tools:replace="android:authorities"> + android:resource="@xml/file_paths" + tools:replace="android:resource" /> diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 3f042e521b..0786bce808 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -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; @@ -74,6 +79,7 @@ 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 +129,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 +146,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 +242,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 +260,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 activity; @@ -335,6 +353,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 +1783,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 +1862,122 @@ public void onError(@NonNull Throwable e) { ); } + public void shareAyahAudio(SuraAyah start, SuraAyah end) { + 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( + 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 mapArray = new ArrayList<>(Arrays.asList(firstSurahMap, lastSurahMap)); + return mapArray; + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith(new DisposableSingleObserver>() { + @Override + public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList sparseIntArrayList) { + Intrinsics.checkNotNullExpressionValue(sparseIntArrayList, "mapArray"); + + 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 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); diff --git a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt index 5cde50f940..25c843a7b3 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt @@ -2,17 +2,24 @@ 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.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.* import javax.inject.Inject +import kotlin.math.roundToInt class AudioUtils @Inject constructor( private val quranInfo: QuranInfo, @@ -283,6 +290,101 @@ class AudioUtils @Inject constructor( } } + fun getLocalAudioPathInfo(context: Context,qari: QariItem): AudioPathInfo? { + val localPath = getLocalQariUri(context, qari) + if (localPath != null) { + val databasePath = getQariDatabasePathIfGapless(context, 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): 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) + 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() + 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(1) + 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" diff --git a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt index a2f0450851..06b8661605 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt @@ -7,7 +7,10 @@ import android.content.Context import android.content.Intent import android.widget.Toast import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import com.quran.data.model.QuranText +import com.quran.labs.androidquran.BuildConfig import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo @@ -15,6 +18,7 @@ import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils import com.quran.labs.androidquran.ui.util.ToastCompat import dagger.Reusable +import java.io.File import java.text.NumberFormat import java.util.Locale import javax.inject.Inject @@ -124,4 +128,15 @@ class ShareUtil @Inject internal constructor(private val quranDisplayData: Quran append("]") } } + + + fun shareAudioFileIntent(activity: Activity, file: File) { + val authorities = BuildConfig.APPLICATION_ID + ".fileprovider" + val uri = FileProvider.getUriForFile(activity, authorities, file) + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.putExtra(Intent.EXTRA_STREAM, uri) + shareIntent.type = "audio/mp3" + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + activity.startActivity(Intent.createChooser(shareIntent, "Share")) + } } diff --git a/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapMP3.java b/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapMP3.java new file mode 100644 index 0000000000..191a256093 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapMP3.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.quran.labs.androidquran.util.audioConversionUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; + +/** + * CheapMP3 represents an MP3 file by doing a "cheap" scan of the file, + * parsing the frame headers only and getting an extremely rough estimate + * of the volume level of each frame. + * + * TODO: Useful unit tests might be to look for sync in various places: + * FF FA + * FF FB + * 00 FF FA + * FF FF FA + * ([ 00 ] * 12) FF FA + * ([ 00 ] * 13) FF FA + */ +public class CheapMP3 extends CheapSoundFile { + public static Factory getFactory() { + return new Factory() { + public CheapSoundFile create() { + return new CheapMP3(); + } + public String[] getSupportedExtensions() { + return new String[] { "mp3" }; + } + }; + } + + // Member variables representing frame data + private int mNumFrames; + private int[] mFrameOffsets; + private int[] mFrameLens; + private int[] mFrameGains; + private int mFileSize; + private int mAvgBitRate; + private int mGlobalSampleRate; + private int mGlobalChannels; + + // Member variables used during initialization + private int mMaxFrames; + private int mBitrateSum; + private int mMinGain; + private int mMaxGain; + + public CheapMP3() { + } + + public int getNumFrames() { + return mNumFrames; + } + + public int[] getFrameOffsets() { + return mFrameOffsets; + } + + public int getSamplesPerFrame() { + return 1152; + } + + public int[] getFrameLens() { + return mFrameLens; + } + + public int[] getFrameGains() { + return mFrameGains; + } + + public int getFileSizeBytes() { + return mFileSize; + } + + public int getAvgBitrateKbps() { + return mAvgBitRate; + } + + public int getSampleRate() { + return mGlobalSampleRate; + } + + public int getChannels() { + return mGlobalChannels; + } + + public String getFiletype() { + return "MP3"; + } + + /** + * MP3 supports seeking into the middle of the file, no header needed, + * so this method is supported to hear exactly what a "cut" of the file + * sounds like without needing to actually save a file to disk first. + */ + public int getSeekableFrameOffset(int frame) { + if (frame <= 0) { + return 0; + } else if (frame >= mNumFrames) { + return mFileSize; + } else { + return mFrameOffsets[frame]; + } + } + + public void ReadFile(File inputFile) + throws java.io.FileNotFoundException, + java.io.IOException { + super.ReadFile(inputFile); + mNumFrames = 0; + mMaxFrames = 64; // This will grow as needed + mFrameOffsets = new int[mMaxFrames]; + mFrameLens = new int[mMaxFrames]; + mFrameGains = new int[mMaxFrames]; + mBitrateSum = 0; + mMinGain = 255; + mMaxGain = 0; + + // No need to handle filesizes larger than can fit in a 32-bit int + mFileSize = (int)mInputFile.length(); + + FileInputStream stream = new FileInputStream(mInputFile); + + int pos = 0; + int offset = 0; + byte[] buffer = new byte[12]; + while (pos < mFileSize - 12) { + // Read 12 bytes at a time and look for a sync code (0xFF) + while (offset < 12) { + offset += stream.read(buffer, offset, 12 - offset); + } + int bufferOffset = 0; + while (bufferOffset < 12 && + buffer[bufferOffset] != -1) + bufferOffset++; + + if (mProgressListener != null) { + boolean keepGoing = mProgressListener.reportProgress( + pos * 1.0 / mFileSize); + if (!keepGoing) { + break; + } + } + + if (bufferOffset > 0) { + // We didn't find a sync code (0xFF) at position 0; + // shift the buffer over and try again + for (int i = 0; i < 12 - bufferOffset; i++) + buffer[i] = buffer[bufferOffset + i]; + pos += bufferOffset; + offset = 12 - bufferOffset; + continue; + } + + // Check for MPEG 1 Layer III or MPEG 2 Layer III codes + int mpgVersion = 0; + if (buffer[1] == -6 || buffer[1] == -5) { + mpgVersion = 1; + } else if (buffer[1] == -14 || buffer[1] == -13) { + mpgVersion = 2; + } else { + bufferOffset = 1; + for (int i = 0; i < 12 - bufferOffset; i++) + buffer[i] = buffer[bufferOffset + i]; + pos += bufferOffset; + offset = 12 - bufferOffset; + continue; + } + + // The third byte has the bitrate and samplerate + int bitRate; + int sampleRate; + if (mpgVersion == 1) { + // MPEG 1 Layer III + bitRate = BITRATES_MPEG1_L3[(buffer[2] & 0xF0) >> 4]; + sampleRate = SAMPLERATES_MPEG1_L3[(buffer[2] & 0x0C) >> 2]; + } else { + // MPEG 2 Layer III + bitRate = BITRATES_MPEG2_L3[(buffer[2] & 0xF0) >> 4]; + sampleRate = SAMPLERATES_MPEG2_L3[(buffer[2] & 0x0C) >> 2]; + } + + if (bitRate == 0 || sampleRate == 0) { + bufferOffset = 2; + for (int i = 0; i < 12 - bufferOffset; i++) + buffer[i] = buffer[bufferOffset + i]; + pos += bufferOffset; + offset = 12 - bufferOffset; + continue; + } + + // From here on we assume the frame is good + mGlobalSampleRate = sampleRate; + int padding = (buffer[2] & 2) >> 1; + int frameLen = 144 * bitRate * 1000 / sampleRate + padding; + + int gain; + if ((buffer[3] & 0xC0) == 0xC0) { + // 1 channel + mGlobalChannels = 1; + if (mpgVersion == 1) { + gain = ((buffer[10] & 0x01) << 7) + + ((buffer[11] & 0xFE) >> 1); + } else { + gain = ((buffer[9] & 0x03) << 6) + + ((buffer[10] & 0xFC) >> 2); + } + } else { + // 2 channels + mGlobalChannels = 2; + if (mpgVersion == 1) { + gain = ((buffer[9] & 0x7F) << 1) + + ((buffer[10] & 0x80) >> 7); + } else { + gain = 0; // ??? + } + } + + mBitrateSum += bitRate; + + mFrameOffsets[mNumFrames] = pos; + mFrameLens[mNumFrames] = frameLen; + mFrameGains[mNumFrames] = gain; + if (gain < mMinGain) + mMinGain = gain; + if (gain > mMaxGain) + mMaxGain = gain; + + mNumFrames++; + if (mNumFrames == mMaxFrames) { + // We need to grow our arrays. Rather than naively + // doubling the array each time, we estimate the exact + // number of frames we need and add 10% padding. In + // practice this seems to work quite well, only one + // resize is ever needed, however to avoid pathological + // cases we make sure to always double the size at a minimum. + + mAvgBitRate = mBitrateSum / mNumFrames; + int totalFramesGuess = + ((mFileSize / mAvgBitRate) * sampleRate) / 144000; + int newMaxFrames = totalFramesGuess * 11 / 10; + if (newMaxFrames < mMaxFrames * 2) + newMaxFrames = mMaxFrames * 2; + + int[] newOffsets = new int[newMaxFrames]; + int[] newLens = new int[newMaxFrames]; + int[] newGains = new int[newMaxFrames]; + for (int i = 0; i < mNumFrames; i++) { + newOffsets[i] = mFrameOffsets[i]; + newLens[i] = mFrameLens[i]; + newGains[i] = mFrameGains[i]; + } + mFrameOffsets = newOffsets; + mFrameLens = newLens; + mFrameGains = newGains; + mMaxFrames = newMaxFrames; + } + + stream.skip(frameLen - 12); + pos += frameLen; + offset = 0; + } + + // We're done reading the file, do some postprocessing + if (mNumFrames > 0) + mAvgBitRate = mBitrateSum / mNumFrames; + else + mAvgBitRate = 0; + } + + public void WriteFile(File outputFile, int startFrame, int numFrames) + throws java.io.IOException { + outputFile.createNewFile(); + FileInputStream in = new FileInputStream(mInputFile); + FileOutputStream out = new FileOutputStream(outputFile); + int maxFrameLen = 0; + for (int i = 0; i < numFrames; i++) { + if (mFrameLens[startFrame + i] > maxFrameLen) + maxFrameLen = mFrameLens[startFrame + i]; + } + byte[] buffer = new byte[maxFrameLen]; + int pos = 0; + for (int i = 0; i < numFrames; i++) { + int skip = mFrameOffsets[startFrame + i] - pos; + int len = mFrameLens[startFrame + i]; + if (skip > 0) { + in.skip(skip); + pos += skip; + } + in.read(buffer, 0, len); + out.write(buffer, 0, len); + pos += len; + } + in.close(); + out.close(); + } + + static private int BITRATES_MPEG1_L3[] = { + 0, 32, 40, 48, 56, 64, 80, 96, + 112, 128, 160, 192, 224, 256, 320, 0 }; + static private int BITRATES_MPEG2_L3[] = { + 0, 8, 16, 24, 32, 40, 48, 56, + 64, 80, 96, 112, 128, 144, 160, 0 }; + static private int SAMPLERATES_MPEG1_L3[] = { + 44100, 48000, 32000, 0 }; + static private int SAMPLERATES_MPEG2_L3[] = { + 22050, 24000, 16000, 0 }; +} diff --git a/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapSoundFile.java b/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapSoundFile.java new file mode 100644 index 0000000000..9a8dc9b368 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapSoundFile.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.quran.labs.androidquran.util.audioConversionUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.HashMap; +/** + * CheapSoundFile is the parent class of several subclasses that each + * do a "cheap" scan of various sound file formats, parsing as little + * as possible in order to understand the high-level frame structure + * and get a rough estimate of the volume level of each frame. Each + * subclass is able to: + * - open a sound file + * - return the sample rate and number of frames + * - return an approximation of the volume level of each frame + * - write a new sound file with a subset of the frames + * + * A frame should represent no less than 1 ms and no more than 100 ms of + * audio. This is compatible with the native frame sizes of most audio + * file formats already, but if not, this class should expose virtual + * frames in that size range. + */ +public class CheapSoundFile { + public interface ProgressListener { + /** + * Will be called by the CheapSoundFile subclass periodically + * with values between 0.0 and 1.0. Return true to continue + * loading the file, and false to cancel. + */ + boolean reportProgress(double fractionComplete); + } + + public interface Factory { + public CheapSoundFile create(); + public String[] getSupportedExtensions(); + } + + static Factory[] sSubclassFactories = new Factory[] { + CheapMP3.getFactory() + }; + + static ArrayList sSupportedExtensions = new ArrayList(); + static HashMap sExtensionMap = + new HashMap(); + + static { + for (Factory f : sSubclassFactories) { + for (String extension : f.getSupportedExtensions()) { + sSupportedExtensions.add(extension); + sExtensionMap.put(extension, f); + } + } + } + + /** + * Static method to create the appropriate CheapSoundFile subclass + * given a filename. + * + * TODO: make this more modular rather than hardcoding the logic + */ + public static CheapSoundFile create(String fileName, + ProgressListener progressListener) + throws java.io.FileNotFoundException, + java.io.IOException { + File f = new File(fileName); + if (!f.exists()) { + throw new java.io.FileNotFoundException(fileName); + } + String name = f.getName().toLowerCase(); + String[] components = name.split("\\."); + if (components.length < 2) { + return null; + } + Factory factory = sExtensionMap.get(components[components.length - 1]); + if (factory == null) { + return null; + } + CheapSoundFile soundFile = factory.create(); + soundFile.setProgressListener(progressListener); + soundFile.ReadFile(f); + return soundFile; + } + + public static boolean isFilenameSupported(String filename) { + String[] components = filename.toLowerCase().split("\\."); + if (components.length < 2) { + return false; + } + return sExtensionMap.containsKey(components[components.length - 1]); + } + + /** + * Return the filename extensions that are recognized by one of + * our subclasses. + */ + public static String[] getSupportedExtensions() { + return sSupportedExtensions.toArray( + new String[sSupportedExtensions.size()]); + } + + protected ProgressListener mProgressListener = null; + protected File mInputFile = null; + + protected CheapSoundFile() { + } + + public void ReadFile(File inputFile) + throws java.io.FileNotFoundException, + java.io.IOException { + mInputFile = inputFile; + } + + public void setProgressListener(ProgressListener progressListener) { + mProgressListener = progressListener; + } + + public int getNumFrames() { + return 0; + } + + public int getSamplesPerFrame() { + return 0; + } + + public int[] getFrameOffsets() { + return null; + } + + public int[] getFrameLens() { + return null; + } + + public int[] getFrameGains() { + return null; + } + + public int getFileSizeBytes() { + return 0; + } + + public int getAvgBitrateKbps() { + return 0; + } + + public int getSampleRate() { + return 0; + } + + public int getChannels() { + return 0; + } + + public String getFiletype() { + return "Unknown"; + } + + /** + * If and only if this particular file format supports seeking + * directly into the middle of the file without reading the rest of + * the header, this returns the byte offset of the given frame, + * otherwise returns -1. + */ + public int getSeekableFrameOffset(int frame) { + return -1; + } + + private static final char[] HEX_CHARS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + public static String bytesToHex (byte hash[]) { + char buf[] = new char[hash.length * 2]; + for (int i = 0, x = 0; i < hash.length; i++) { + buf[x++] = HEX_CHARS[(hash[i] >>> 4) & 0xf]; + buf[x++] = HEX_CHARS[hash[i] & 0xf]; + } + return new String(buf); + } + + public String computeMd5OfFirst10Frames() + throws java.io.FileNotFoundException, + java.io.IOException, + java.security.NoSuchAlgorithmException { + int[] frameOffsets = getFrameOffsets(); + int[] frameLens = getFrameLens(); + int numFrames = frameLens.length; + if (numFrames > 10) { + numFrames = 10; + } + + MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); + FileInputStream in = new FileInputStream(mInputFile); + int pos = 0; + for (int i = 0; i < numFrames; i++) { + int skip = frameOffsets[i] - pos; + int len = frameLens[i]; + if (skip > 0) { + in.skip(skip); + pos += skip; + } + byte[] buffer = new byte[len]; + in.read(buffer, 0, len); + digest.update(buffer); + pos += len; + } + in.close(); + byte[] hash = digest.digest(); + return bytesToHex(hash); + } + + public void WriteFile(File outputFile, int startFrame, int numFrames) + throws java.io.IOException { + } +} diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 783bd79240..15ebff8dcc 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -3,4 +3,13 @@ + + + + + + diff --git a/common/toolbar/src/main/res/drawable/ic_audio.xml b/common/toolbar/src/main/res/drawable/ic_audio.xml new file mode 100644 index 0000000000..c0d2d8d87b --- /dev/null +++ b/common/toolbar/src/main/res/drawable/ic_audio.xml @@ -0,0 +1,5 @@ + + + diff --git a/common/toolbar/src/main/res/drawable/ic_text.xml b/common/toolbar/src/main/res/drawable/ic_text.xml new file mode 100644 index 0000000000..772216cef6 --- /dev/null +++ b/common/toolbar/src/main/res/drawable/ic_text.xml @@ -0,0 +1,5 @@ + + + diff --git a/common/toolbar/src/main/res/menu/ayah_menu.xml b/common/toolbar/src/main/res/menu/ayah_menu.xml index bd95395f6a..20750b1d80 100644 --- a/common/toolbar/src/main/res/menu/ayah_menu.xml +++ b/common/toolbar/src/main/res/menu/ayah_menu.xml @@ -1,34 +1,47 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/common/toolbar/src/main/res/values/strings.xml b/common/toolbar/src/main/res/values/strings.xml index 5a85f64de3..d66437b049 100644 --- a/common/toolbar/src/main/res/values/strings.xml +++ b/common/toolbar/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ Tag this Ayah Share Ayah Link Share Ayah Text + Share Ayah Audio Ayah Translation/Tafseer Play from Here Recite from here From 4a53d7fc49157ae4cd2232b83281752fbbe8551a Mon Sep 17 00:00:00 2001 From: Doozy Date: Tue, 5 Jul 2022 18:54:02 +0300 Subject: [PATCH 02/18] manifest cleanup --- app/src/main/AndroidManifest.xml | 15 ++------------- app/src/main/res/xml/file_paths.xml | 9 --------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7beb6dd9c1..7e61449217 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,25 +97,14 @@ android:exported="false" android:authorities="@string/authority"/> - - - - - - - - - + android:exported="false"> + android:resource="@xml/file_paths"/> diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 15ebff8dcc..783bd79240 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -3,13 +3,4 @@ - - - - - - From 435ab83c956ba9d49268378c8ea5b27618757982 Mon Sep 17 00:00:00 2001 From: Doozy Date: Tue, 5 Jul 2022 19:09:24 +0300 Subject: [PATCH 03/18] cleanup --- app/src/main/AndroidManifest.xml | 4 +++- .../com/quran/labs/androidquran/ui/PagerActivity.java | 5 +++-- .../java/com/quran/labs/androidquran/util/AudioUtils.kt | 5 +++-- app/src/main/res/xml/file_paths.xml | 9 +++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e61449217..8b1d95b069 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -102,9 +102,11 @@ android:authorities="@string/file_authority" android:grantUriPermissions="true" android:exported="false"> + tools:replace="android:authorities"> + android:resource="@xml/file_paths" + tools:replace="android:resource" /> diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 0786bce808..dd2ed639fb 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -75,6 +75,7 @@ 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; @@ -1880,8 +1881,8 @@ public void shareAyahAudio(SuraAyah start, SuraAyah end) { actualEnd = (SuraAyah) pair2.component2(); } - final QariItem qari = audioStatusBar.getAudioInfo(); - AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(this,qari); + final QariItem qari = audioStatusBar.getAudioInfo(); + AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(this,qari); assert audioPathInfo != null; if (audioPathInfo.getGaplessDatabase() != null) { diff --git a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt index 25c843a7b3..3b3922bc7b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt @@ -12,6 +12,7 @@ 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 @@ -291,9 +292,9 @@ class AudioUtils @Inject constructor( } fun getLocalAudioPathInfo(context: Context,qari: QariItem): AudioPathInfo? { - val localPath = getLocalQariUri(context, qari) + val localPath = getLocalQariUri(qari) if (localPath != null) { - val databasePath = getQariDatabasePathIfGapless(context, qari) + val databasePath = getQariDatabasePathIfGapless(qari) val urlFormat = if (databasePath.isNullOrEmpty()) { localPath + File.separator + "%d" + File.separator + "%d" + AUDIO_EXTENSION diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 783bd79240..15ebff8dcc 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -3,4 +3,13 @@ + + + + + + From 34b6fd2485e2bcfec8d80309b1b237904007423d Mon Sep 17 00:00:00 2001 From: Doozy Date: Thu, 7 Jul 2022 13:29:07 +0300 Subject: [PATCH 04/18] Icons changed --- .../src/main/res/drawable-hdpi/ic_qaf.png | Bin 0 -> 1181 bytes .../src/main/res/drawable-hdpi/ic_speaker.png | Bin 0 -> 1150 bytes .../src/main/res/drawable-mdpi/ic_qaf.png | Bin 0 -> 640 bytes .../src/main/res/drawable-mdpi/ic_speaker.png | Bin 0 -> 640 bytes .../src/main/res/drawable-xhdpi/ic_qaf.png | Bin 0 -> 1443 bytes .../src/main/res/drawable-xhdpi/ic_speaker.png | Bin 0 -> 1528 bytes .../src/main/res/drawable-xxhdpi/ic_qaf.png | Bin 0 -> 2614 bytes .../src/main/res/drawable-xxhdpi/ic_speaker.png | Bin 0 -> 2817 bytes .../src/main/res/drawable-xxxhdpi/ic_qaf.png | Bin 0 -> 3202 bytes .../src/main/res/drawable-xxxhdpi/ic_speaker.png | Bin 0 -> 3451 bytes .../toolbar/src/main/res/drawable/ic_audio.xml | 5 ----- common/toolbar/src/main/res/menu/ayah_menu.xml | 4 ++-- 12 files changed, 2 insertions(+), 7 deletions(-) create mode 100644 common/toolbar/src/main/res/drawable-hdpi/ic_qaf.png create mode 100644 common/toolbar/src/main/res/drawable-hdpi/ic_speaker.png create mode 100644 common/toolbar/src/main/res/drawable-mdpi/ic_qaf.png create mode 100644 common/toolbar/src/main/res/drawable-mdpi/ic_speaker.png create mode 100644 common/toolbar/src/main/res/drawable-xhdpi/ic_qaf.png create mode 100644 common/toolbar/src/main/res/drawable-xhdpi/ic_speaker.png create mode 100644 common/toolbar/src/main/res/drawable-xxhdpi/ic_qaf.png create mode 100644 common/toolbar/src/main/res/drawable-xxhdpi/ic_speaker.png create mode 100644 common/toolbar/src/main/res/drawable-xxxhdpi/ic_qaf.png create mode 100644 common/toolbar/src/main/res/drawable-xxxhdpi/ic_speaker.png delete mode 100644 common/toolbar/src/main/res/drawable/ic_audio.xml diff --git a/common/toolbar/src/main/res/drawable-hdpi/ic_qaf.png b/common/toolbar/src/main/res/drawable-hdpi/ic_qaf.png new file mode 100644 index 0000000000000000000000000000000000000000..efeb74fc25bcbb5049ad1eaae902fb3a4d0d0086 GIT binary patch literal 1181 zcmV;O1Y-M%P)Px(S4l)cRA@u(nQ7>zV-&`J7n7YdiDaj&rNkF&_I(*N;e#(e$R5Iwl6`Dr9a}`% z#)P6|lwDDFrR|y z0rmx6vXQ_lz${=-U@72N;CA3_N#E5*ViW+T0Ji{-0CRw!fb)Q@;O{p618@~EUDEvj z6o81Zhu;7;2mS;;1#ZgJF|dPxxxiFO@7Egd^Fxh5MC=MY(J<_L;Dk(})qxFwRe?t( z8DO;x8^CheZLS2i0B!@$l=O9@peJ=H-rn~^1<){ZMc`myzNGuA-MokoMgm|W4er-9 z1~7A|K2*!d0Yt=sz=^;Pz_K|T9t19y^kprAkphT_ z&2sAQku;)buj$0)38HEmj|_nG9XK7h05Cu5m)AYyBt2IXg;4_761W!F7We@87FY?` zxB+OI&q(^M6htQg$#FIUK9}@!$z>672yh4RybD;;+=%d`z8^5Xwzw8fmh@4HPM_%t zU>(z6=d&bp`c{dE!+^Uog=QFdrj_}UL6{SnZcmZ)VynVDPz@lxe>AYayNM*HL#sr@ z5x_mb%fPcq@-_svw$TZ|j~+AA2U-;l0ATIx7_R~|CAovNN&|q;E&!Y+lk>0X$kQav zXjM1>fW0$)&X@F9=|vGS6}U5d|DrzgJaBpzNmU9(#PTk(973l^dbj8nAR;!&8L&^w zH`hq=9YsrUn`EC59q~l5z*G9nodM0qjL_bU-!cn=}b7GhPjs;Eyoc|A( zda2}GMC_FjczT!s&IOJ%2uTl>7*={FB96-TJWG-v{zX!oLM~um

r(Oo&9prhp&* zj>2V<&MH-03*gdZl-{gOljJ?mKoSvaWnCSRoyk1u=2E4#0NkJ5DLj3r=f-VbbMIMl zagsQ1Ds}B32dLaynE($8xYsWTlpTa z(QoDt8>ZKtq!WPNP04Fqq%KgiO?Q(uvRIvN&Ta2|<|Y6CFpJ#{U_sZ7^gt=p$xTE2 vXrKrT(XL|EV*tgj8q&dW1cv0;Vzqw(+|Xh`^N!J|00000NkvXXu0mjf)y^n2 literal 0 HcmV?d00001 diff --git a/common/toolbar/src/main/res/drawable-hdpi/ic_speaker.png b/common/toolbar/src/main/res/drawable-hdpi/ic_speaker.png new file mode 100644 index 0000000000000000000000000000000000000000..22d33dd552b195a4a16b663b85f1a0427452fa85 GIT binary patch literal 1150 zcmV-^1cCdBP)Px(I7vi7RA@u(m|3iyV;F^>rDkFlW9X0w<-*K`;l@0RDG?!2lqhN*f|wFBHPjFi z7eZRxxG+SD8%-oGL<~_N>Epg#-R6LSGhlmr z;4I)~U{~NJN#`{IupL|W2L1%Tl9b=ZH822Wz;xg$;2dDby8U%Yr!`q(dp7WBy_es2 zB>mO2Z;$}?2Ob6v0yb;8xLW}G#b{s~;6vbAN%`%#`UYToJ>XQ}86e^F?=#y1u)Q%b zP15hS*9YbU_XF|#s|Di6P6*lpsE}v^xH)hga09SWn~Np{U^|}v6j%ma0em6Jw&RG4 zfi=MO!1I#+Xa}MbfPH{HJD`uplB_2I8+LfF9e|x;G_V!$GH|1$mA1D8-Ubc@-UsGN z`nneY@xRuwuEcoxVz7XpjgRi#c0Jnf(ShM$)&oX8=zF#{!FiImwfWE?SoW64&u?67(+M z-ZJQ9;BjDgV4kFfwiA+9);qrfvnBmJYydk0PXMO_Yk^skezKjC`&fm_a$t{2=2Hsr zF2&4}p#sR|w$BD00bd6A&wF;wOfEkij+dd9>Dt`kfN_x|Fq(~Sz z19(AFs?nriV9Em+522OCKtd?8I}#!xbhxAss+5L-6C&~?T{CnBY^QT486&grk#ySx zNjzQBceW!610cyfUs5X7@IuITDo@I2q;+coYK$FYj#SA3vW`cGA zwk-f(*8a~cdBZ}4Tz>;i7ojositTAN#4iB0>vp89i7ypMEhA;*dz7h5Mu(N&M#^ph zv}l||fQNtsDt5;obvxAWTSI-ASz7b(M@@D#g-Fx>f1Z~|XVqYyi7FK)t6Kocw}ekV z62uWp5>ibD3=BXE6EQphZmK5qqNJ3`j;kE7SB1~AZoUcSz<~p(yvV1;d?%VyQuvc5 z0KGo{UjX!KOfUOJ0DAG-Aoq+I7^F9QIcvl~FJ2quo)H6s^ky$-O?d|X2BpK+K-#iZ QtpET307*qoM6N<$f_?P?egFUf literal 0 HcmV?d00001 diff --git a/common/toolbar/src/main/res/drawable-mdpi/ic_qaf.png b/common/toolbar/src/main/res/drawable-mdpi/ic_qaf.png new file mode 100644 index 0000000000000000000000000000000000000000..51bed2d502f70542236fd8c3b421133d30292317 GIT binary patch literal 640 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 literal 0 HcmV?d00001 diff --git a/common/toolbar/src/main/res/drawable-xhdpi/ic_speaker.png b/common/toolbar/src/main/res/drawable-xhdpi/ic_speaker.png new file mode 100644 index 0000000000000000000000000000000000000000..3784a535a1e9b28f9377b2016bd0c0448113e40e GIT binary patch literal 1528 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 literal 0 HcmV?d00001 diff --git a/common/toolbar/src/main/res/drawable-xxxhdpi/ic_qaf.png b/common/toolbar/src/main/res/drawable-xxxhdpi/ic_qaf.png new file mode 100644 index 0000000000000000000000000000000000000000..81ff927acfa729b483ef5d6e9cefc67248b197ee GIT binary patch literal 3202 zcmb7H={wZ@7yitQCCezu*oG#1)*|a5S(^D86ynKFktJkb!`G87MGOzw*P5hIlBaA_ zq^w!OkY!|FlOe-y0O4m!&;jLL*m(fhH6Xo9*MjYqZP`{S{k#!EK}GSbG3i%Lr60C;>FMjXViLpn z6btz|6U=#Q9#%h5dfl1ODyH}*i%d%Ue)XE%ZC7q`Wy}{(o1)dn6wSuWuY*5*8^)*c z`sy2_Tjj>0!W_snYQ*q7XPR0b(d8{Ae)iUtX#^ zuon+mXYZ^rtv(u@C+%{;pOajFNPs*J2gTOIj_yvKG7K<0YX`+4wk6^3iEbYg8c$KJ zi4g&9CziKQGUvTV+|8OAf58n^FswE(@$d4$pOgQX+*5za-6;gf zE63>hT*`iV@9kOXCqWRRl7o5%tY8XDQ;7{~MK)w%M$l^jW(mY`fp=wyIy7|%2rZ3j zf_$dSDT^{9^J2xRX-Yse^RNuias)_=#L(hkX~+seR|U+7f6ZQ|n2QcJE9Vs1tbkx6 z+R~@CvDwg{Gm$10xcOf|i-FGRsP-jG2N1+2RQ%5L>(VGp*Kg1giA#pFpt#=+UtzrRFv#R<( zC4;+mCIjFYu9Ds3o0^u#Ol+%DRR$}$X2W0)ZI~H8876s=9LV;Rp7E_~`*Z4sVGq77 zf{FL338Xby!C09;=nJ+a+-{oudRdvFsm z;zq3Uj8!$j>Ew`eP;;{Yi_wy{<-Ea&pRm6UP81qLcfe|Se9V>zXEz_(( zSWkSu1_Z`S@>@5*Vyq>rC7B1k@-qwO&|h7Y+7Gv^Y$ut@V=6g-RCmV=}ZEm$XR$sd!O z-c;Yr%emiNcq_@+_%b7yM0g9sK?4cFqY?ZNC0@XQoW$V08(qRxo~=PIsRFAT+3Gz- zqSAYLO0xNeYn!QBme4FRcp%!BC;4tm$y?0MmK=(Xa2b*g2< zo4mPHP~)aUmW>_HI=9lOE~zx#?;~;KArT+~%Pwy`_;w!$nOSQmPsy+Q>F!!XYgMT& zoWc(p(DWXzHm%gD$_Vj*^BIjn9AF}EA6Zcq)zk+y+>jelcGc@`2RY|U4E%?ApyTWq zy|~Q8qIIHH?T79et2YmTw-xv4`nwE!wdixA`*+Gj=jSR@%C^FC{a8QAkPsclGSd!y zCQe-IUZubBwhUBeT?1vdrA6@D$j2gmO7AKqu{+DGJy>LW$1#EJe(_zyjg;=UQ9c`C z-}#8@C}{E9*&~Z{C4N^p(cB|CSR?n3tjn$s;AL;k)~k(B3rTZ!6Y2I7Ns2U2ulrD~ zNmss48K&3X2Hxd%E-MNge797x6P+cRV7!5aNSY-FxiqZvesy3=_eR7vrA-}bU zLns*?$b`_Y?>{02uyab)A4^6l}LF(swj z@p$5Vil#+HTaQGeM`9lui|jl<_3-V75xoXcO-T)^L(pwAiIXx{f=}KOwnww(81+(O z2TG;EGp&XxC&RHnA#u5BqRXdF>fv>_h39q1913o^?~(ArBuN<|f5n-%)E(zx=(!a+ z@wbv6U#n$ZB1Oo~7Z8V)oR);n`CwXku5|X~NHd+FVScTtsn~jYakXk!oq8BS4$5IQFUj-3I!GzWm~4Mc zjZM3+U4X!~VGH7XuVc^Dq$QSLB@1X$zc&h&X-@|DNZacr(K9vr!tG5fVi zpz7429NFGSAlz{f)L4)|Fd#PyvFrA(Of;Jb4sD7uD_p0>6I3!!0dn2Sex*WPwW$5~ z=L}XWG9AK!45eDCnX=%|OwwpMvp++tqV)i@I12gNPy_!-T1RQquJ3IBmH0;U@sx|z zAhBK0V9n1zY=#3^?=PLSwcWEw-mtE%7l$%p(iBiyRXGT(dw}!u6OK62rm|v?v&i+x z01Xn@ekJ~synkvD4ipXvuef+9+a(@&u)0qeP=p5~B3b%VzXX=r6b~b9kE!^nS}OTU zw)4?LfR-g!T?L#<=#V?fPWHI?EbP#U1vFCjerHwgUueIBDTTjzlGeiXPM0U%RD^1i zYm8gjFOlzl_`&;LY8mQHI#VwODYNZWuSwZndL1g`k$9K}crkb$?ts25|Cx~#QG7Ui z`*DBH)=RjKoG`(08v~%aI|~o5{uxyA;b*DX+ zC8J)oW7d>3@GKTK=L>p~BjTL?M>mn|vE(aM;3J>5cdPr)u2o%t0Sov%P#?zdc?_ps zSftyOfsK*2Ct&J}Z_Q&{9%R&Onj*BsfX03pAFn+d;HO_wofgNM0Ij{XA?dt7 zvbFy_Baeyr(UzcZrk%oETON;AwJ=873j?0oZH?telfHy0~ zo_Rdz?}B9wpOdhn*%S+}%3C^rAkkakbRBOmgUW4Uxx1<3m~$$E`jF^57S47qcIj#e z7A9fUaz6T8BfCh4t*PFR$<;XSwOTzGvXL9Njts^SFP9?do4qY5G@tD+P9KI`sFZm0 zgFJ4X^hfu=_L@5*kVtyVH7DbQk5i0^a7({DW1?cQx;}T?Re;>ZT)iGl4=NBg7_`-YK?~RBnbbP49v)9#LmfOqN-hM;1=EDB5MRpNe+IN2h z2t)-*@y;?>F@z|(6>QJSf5iuEP1~~`;JJ6wbig5HijH0Ac8%l?r&K4&E$EC)YInCH z=uyOBR#+u^vo@*$d=%2Y!%*2kYOC?aBVd5c&uv7~q41ti`XqloFLC&3kJ}GdElZCM{lIXKcG3>8mf3l$Jcl!aJVS zcOe|8|A=N^+b{1LL$9xN>Yp>a;ERGEH2qo`KD!`M+G`1-hp1|S;X*YpsRK1~WZ950 z`cTu((^Hm4E}RAw#MMl6-6j74&uqKgi2eeizVZA5I@fksId=7@*y4vbqTp)3f29B| zcuJ400sd7b$D!!=VnSo*l~@l(|H^XUfcHXBzUk73#c!4M1tF|20-_ksJ!U@i=-rfg zH6o;ZX6_r0%6BHS3Qx4S^w$~3#tHnks@k7IZ@xX<6i>L`3tT_C zp2gcB89}Bcs^B literal 0 HcmV?d00001 diff --git a/common/toolbar/src/main/res/drawable-xxxhdpi/ic_speaker.png b/common/toolbar/src/main/res/drawable-xxxhdpi/ic_speaker.png new file mode 100644 index 0000000000000000000000000000000000000000..8f09e81ad5f53ff4a9ee4cf7e42ced8ab08ec95d GIT binary patch literal 3451 zcmb_f`8U-47k_^~#yXa%?2IO6B#cP5td(tsJSs(Dh%BYhU~GdA5!x_3wuZ=*rwEA$ z*(1y&OC-rQB2r_C&{N^-`yYIN`2KM3>)dnhx#ynOJ?C}qE7{)8N<>Ij2ml~LvNk`p zgUEkFfNv+anOx}J0VwE{)iF@kD?h)}q(d@4>JUa*aP{d|w~#RIOp*~)RXiD&l*EF0 zPGTJFg;6#Zt?EKayniJXn%QG`6bn3{2hy4aiVp<5iPf*OG41Nv0<&SItDl#Knl@iV zdedf|M;4k)LtjMBPDHNvF33eBku7Wxiy*fgD_*lH++W}?pyC22Ja;3S?bN$fn46a& zRUnjQHpO*B+e7xXC?TKb^N0}gl3=g@YY27#;u^6pN6IA&;DSIw@!sfaAOR9=3If+l zc>kAC`e+>6D1g`fAP9v5HQ41@H?KbcxOrVbfjOU)gtm2%YSLD*IYyiZkZMqYGRQ)# z1@T&a`tOUpEljf601ZrtxR0Z5C8p7*nU(jw{K|+3%Nc|TAvK^j#B#S)Dt+NFgq!b$ zQnyioI_wzR)h+Z!t~0Q%eAx#r$4kcLsRI1CSc@YVGcg)Xfvy~1xgptiibc zx2^^ZQ-6~ZeU@~B{Q>6hus;4}S+M@2zW*>(p@Wors}D*+MsYKDT&1&vxh}wL01LSY zbB+>EY}GbA5(M32<;aOcw0r355>_Z2v_^{ctr2RfaIO zTO^QgoFtAM1(=VbB{zoqMNULUk`m#--&1D=O`^Z#3A3Ap+vgs5d5Z$ZBE(@-ypkhp zH16P$8ASF!9I&XED}>J!sYgG|d89`r>4&>+>) zqB?%uf#ulZdjRhSDYaZdjahFW7wqH+mwfL=9?rbK+5z8@s%MWpnuQbbG zjD%Y{_uzi&AtBaBLX1^JU_j!B!y-D0^1c^t(l<~Q?^x(6<}eyGC9?Vw9D=x+a3IVs z(guNxAG-^k2)f+F)_w9-{)KuV?zH#CbxO&@%1n><81P>4tz0QHti=?1pFE>?2atQt7X z(qqgGx^@@-0Ddh?1;^BycUQLbEJcF#eSkj7{4DXBnX&b=&dFM?sKINa+ zF`CFhe3=`WI;@QOyO9E=0)!O_{Wy2g#}nfezF+UkSDL8c{p{O~UcrRQr>%-Y;i8=) z4UO`}&5%>V%;c!5fnFJm69F3{bo@S}rMV5|`6opgGNJZeg;Sk`8}gGCM7jc^N0?Qx zUf~KM`|znXfYx#96{{Il;nlk@y-ES;wz|A+dH)AL(6w1pvZkJ5*Ow%`Xnj+6AF4o; zoQo(9L^Bv{onx$u)?FPQ!DCT2=VB8m@mNJ3=Hi+kPF7L09Fn?wMLNfC`KtLq`E|+; zL;Q%u=hsD(b6S<~*}jK=(xDPk?JqTS@5Ma!7 zO-SSj&-{z;e2%DnwqW!`m3pwfI3;Ppr7^oml{EpN!?(o6;Tci$32&DZ!n==5&E8lA zi75i&T30r;c~(}c`k!@d9k%VN&2eofb-kr$8doh7z<&Nr@g0F8Fcj?HO1T) zcV|>(fIQ~InCEfbee`O7}r#uSNIQ-)qsBFwA7xH{4p7P+cL zrD=S^{k+el!sR#_^mq08M3xi?bN!eXr25~cD>q2zfCAUa^V47zzbfs+L_ev&lII;OdfFEyPR># zqs-Pw=Cxo{T6Cj6(a7$4MxrA)3~+B}>k5>H7|~knY_|%R3yh}epUp|TT}SGvs)b7I z#$)b9KQ;RuF){NMeg?Y~?^Vk39w(leSP*2m2!7u*pIx(MJm%ZoXKZ{; zh)fjp6ZuBtDH=5+6~p@a&)d)8Pe;+u7TB~k1|@x3U1$u*e#}R8WDu>mho7An|6o`C z)QSk3EswcM<{xS9eC(E$@XI~vZT(~q;qFi;oqwL-=kN?G@7bj}N_+d-6^WCEnE|qAqL6s8{(jk6 zq5MJW9hs@5h$VTC&W>8bm7a_>fl1xK)7GVNT|tgdpI&fvpD@z>#PdV=Wt-(A!=JQ6 zNReFb@y!o`6wbb6=le%j402+&b*J=4-lzVNI#!hPkf!v6*V+ke%QN+Trd`r~ihFJb znW@+?PQBWjKNmtjOsAsvv%P#YOmDp@#E(}S37i2Jfo5|2@&2y8g=DN#lsbFnk`Lp` z`qeGZRc!DIw5$34%!o&jD&RvQ_^jLi9>ZnSX8(3!9jLBeS*PPfP`;nMa=tb(hQgps|N<=Ak({3YzBM12R%3 zz_JI!|;HbCOa{TuXNNf_s%&LX>@n1`umYV`|G&ZS-E~s!!D~z1ORLy+Nmmvv&M) zW{*AIlENvv0m|wvDv1o{*^s1n+qs3l0zlt8fInc$&%IZPo82ve0=4Jx8(x?ggZ2Wq z-MIz;RalqyvU=rs&Z`L^(Dx~?|5$qG+KD^&7&O6=m!ry6Ul~h1U-^|32hw)r$Ethm z%C3^^`IQtI6ma#IZj!rrEA3QM%^efD_k;^JmfDw~4vJukXy@ZKgOl_pAa%y6Ih60& z;b$Yyhhy|f!%FH0dW=Bad`Mm9AG6FVGVL-DAbi|%Oo=fFc6c+FO8>&RY@%)=1q5)* z;SsdVG1B0l<(J?VyHd5NQ!{P*H~Rj}5fQvy7ALzhVN;ojAD(M&HCPqZ%T@+1AM+y0 z>(|Ei=Dv8;_IyM3NGK3^*Afye7618yoI)`lvP_>0b_%r(@;BUwHvRSIGs=g%w*E@Y z!X<-0*N&aoO#F?Vkkn)90{OPUu*kwQuea%HG3+L>Y)0!CjQc$(?-)8#E#_(`5gPG% z=L^j5nev>|V~*8P|Jq$H>F#!+%Z0~1_B5&NYbYV5xnoz+aqCBRvx};OIkIjjU|AYF z_|&8}yun^OvSrZrQq^>uBo6{$pRe>U?|ixA$+COD>H6k37Ab56wB3TuKy3JgWQ%88 zC_?|I_!jXd>m#sdWop|x0`a>^LjOOX@IP7(6?w4DQ+B_&(XY(4bjM8rl7*dl*)jLH F{{Wh2AKCx_ literal 0 HcmV?d00001 diff --git a/common/toolbar/src/main/res/drawable/ic_audio.xml b/common/toolbar/src/main/res/drawable/ic_audio.xml deleted file mode 100644 index c0d2d8d87b..0000000000 --- a/common/toolbar/src/main/res/drawable/ic_audio.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/common/toolbar/src/main/res/menu/ayah_menu.xml b/common/toolbar/src/main/res/menu/ayah_menu.xml index 20750b1d80..7dffef4cb9 100644 --- a/common/toolbar/src/main/res/menu/ayah_menu.xml +++ b/common/toolbar/src/main/res/menu/ayah_menu.xml @@ -19,11 +19,11 @@ android:title="@string/share_ayah" /> Date: Thu, 7 Jul 2022 14:34:50 +0300 Subject: [PATCH 05/18] proper "sharable audio" name --- .../labs/androidquran/ui/PagerActivity.java | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index dd2ed639fb..e925927471 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -261,11 +261,12 @@ 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 ArrayList audioCacheFilePaths = new ArrayList<>(); + private SuraAyah selectedStartSuraAyah = null; + private SuraAyah selectedEndSuraAyah = null; + private QariItem selectedQari = null; private static class PagerHandler extends Handler { @@ -1864,7 +1865,7 @@ public void onError(@NonNull Throwable e) { } public void shareAyahAudio(SuraAyah start, SuraAyah end) { - SuraAyah actualStart,actualEnd; + audioCacheFilePaths.clear(); if (start == null || end == null) { return; }else { @@ -1877,16 +1878,16 @@ public void shareAyahAudio(SuraAyah start, SuraAyah end) { } kotlin.Pair pair2 = pair; - actualStart = (SuraAyah) pair2.component1(); - actualEnd = (SuraAyah) pair2.component2(); + selectedStartSuraAyah = (SuraAyah) pair2.component1(); + selectedEndSuraAyah = (SuraAyah) pair2.component2(); } - final QariItem qari = audioStatusBar.getAudioInfo(); - AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(this,qari); + selectedQari = audioStatusBar.getAudioInfo(); + AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(this,selectedQari); assert audioPathInfo != null; if (audioPathInfo.getGaplessDatabase() != null) { - createAndShareAudio(actualStart,actualEnd,audioPathInfo); + createAndShareAudio(selectedStartSuraAyah,selectedEndSuraAyah,audioPathInfo); } } @@ -1942,7 +1943,10 @@ public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList paths = new ArrayList<>(); String path1 = audioUtils.getSurahAudioPath(audioPathInfo,start.sura); @@ -1963,7 +1967,9 @@ public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList Date: Thu, 7 Jul 2022 14:57:20 +0300 Subject: [PATCH 06/18] share intent title --- app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt | 2 +- app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-az/strings.xml | 1 + app/src/main/res/values-bs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-id/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-kk/strings.xml | 1 + app/src/main/res/values-ku/strings.xml | 1 + app/src/main/res/values-ms/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sq/strings.xml | 1 + app/src/main/res/values-sr/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-th/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-ug/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-uz/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ 29 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt index 06b8661605..7680397dcf 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt @@ -137,6 +137,6 @@ class ShareUtil @Inject internal constructor(private val quranDisplayData: Quran shareIntent.putExtra(Intent.EXTRA_STREAM, uri) shareIntent.type = "audio/mp3" shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - activity.startActivity(Intent.createChooser(shareIntent, "Share")) + activity.startActivity(Intent.createChooser(shareIntent, activity.getString(R.string.share_audio_file_title))) } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 73bc2d4901..5b205754d8 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -240,6 +240,7 @@ تم نسخ الآية + Share Audio File تصنيف المرجعية diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 9ec44ee78d..11195285fa 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -286,6 +286,7 @@ Âyet kopyalandı + Share Audio File Sayfa işareti etiketi diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index e338fbdf8b..0f34787ecd 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -305,6 +305,7 @@ Ajet kopiran + Share Audio File Označi zabilješku diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bab66e2b91..ae8670b660 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -311,6 +311,7 @@ Vers kopiert + Share Audio File Ein Schlagwort zum Lesezeichen hinzufügen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 137dbd1829..339b705e93 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -170,6 +170,7 @@ Versículo copiado + Share Audio File Etiquetar Favorito Borrar Etiqueta diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 3834c97f48..649ecb3d82 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -167,6 +167,7 @@ آیه کپی شد + Share Audio File تعیین برچسب برای نشانک diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b1172bd431..05d65e6d42 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -258,6 +258,7 @@ Aya copiée + Share Audio File Étiqueter le favori diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 86a3318dcc..4a1fa3d20d 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -305,6 +305,7 @@ Ajet kopiran + Share Audio File Označi zabilješku diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ad25141740..228bcf8c5b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -234,6 +234,7 @@ Ája kimásolva + Share Audio File Könyvelző címkézése diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index eda7bd57b3..23ac5275cf 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -290,6 +290,7 @@ Ayat telah tersalin + Share Audio File Labeli Penanda diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b7bee30bae..66b23401ea 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -123,6 +123,7 @@ Avviare la riproduzione da: Inizio della pagina Ayah Copiato + Share Audio File Segnalibro Cancellare il tag Modifica tag diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 77500bfc18..f030f479ab 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -291,6 +291,7 @@ Аят көшірілді + Share Audio File Бетбелгіні белгілеу diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index c34e4e6625..0eebc3becd 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -187,6 +187,7 @@ ئایەت لەبەریگیراوە + Share Audio File بڕگە نیشانەکرا diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index ebf680ce38..749c5f8da2 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -260,6 +260,7 @@ Ayat telah disalin + Share Audio File Beri Label pada Penanda diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6545623e91..b182bbc86c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -282,6 +282,7 @@ Voor de vertaalde pagina\'s (/voor vertalingen): Ga naar instellingen en wijzig Ayah Gekopieerd + Share Audio File Label Bladwijzer diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 0945ece736..d2423a99b6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -295,6 +295,7 @@ Ayah Skopiowany + Share Audio File Tag Zakładka diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 286e311cad..3d9ddaf84d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -205,6 +205,7 @@ escolher um leitor-qari diferente. Clique play para baixar e reproduzir a págin Ayah Copiada + Share Audio File Tagear Marcador diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5af0673500..d3da9addd8 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -344,6 +344,7 @@ Аят скопирован + Share Audio File Отметить закладку diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 51def7be8c..13d074e440 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -293,6 +293,7 @@ Ajahu kopjon + Share Audio File Tag Libërshënues diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 05009fcd2a..d37de82a85 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -305,6 +305,7 @@ Ajet kopiran + Share Audio File Označi zabelešku diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 43c1d06432..6e12fc4138 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -293,6 +293,7 @@ Ayah Kopierad + Share Audio File Tag Bookmark diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ee9dacdc34..a38f9f2193 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -284,6 +284,7 @@ อายะห์ คัดลอก + Share Audio File ที่คั่นหน้าแท็ก diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 9ec44ee78d..11195285fa 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -286,6 +286,7 @@ Âyet kopyalandı + Share Audio File Sayfa işareti etiketi diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index eb2bf74b2a..497a034ad0 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -160,6 +160,7 @@ ئايەت كۆچۈرۈلدى + Share Audio File خەتكۈچكە بەلگە قوش diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4de0b3641e..519ed660b8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -296,6 +296,7 @@ Ая скопійований + Share Audio File Закладка тега diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index f6a801f835..2187c221b0 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -319,6 +319,7 @@ Oyat xotiraga koʻchirildi + Share Audio File Xatchoʻpni teglash diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 61d3a72da6..672ee8cf84 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -175,6 +175,7 @@ 复制成功 + Share Audio File 为收藏加标签 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af568b9731..7aded67319 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -365,6 +365,7 @@ Ayah Copied + Share Audio File Tag Bookmark @@ -411,6 +412,7 @@ Download selection Delete selection + Quran Audio files Update Several Quran audio files have been updated. Quran has removed From 4162f19436b4ae59bc69bf99f1cff55966225b71 Mon Sep 17 00:00:00 2001 From: Doozy Date: Thu, 7 Jul 2022 15:12:00 +0300 Subject: [PATCH 07/18] [bugfix] - app was crushing when full surah is selected --- .../quran/labs/androidquran/ui/PagerActivity.java | 12 +++++++++--- .../com/quran/labs/androidquran/util/AudioUtils.kt | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index e925927471..6b4e7cb1e7 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -1883,7 +1883,7 @@ public void shareAyahAudio(SuraAyah start, SuraAyah end) { } selectedQari = audioStatusBar.getAudioInfo(); - AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(this,selectedQari); + AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(selectedQari); assert audioPathInfo != null; if (audioPathInfo.getGaplessDatabase() != null) { @@ -1939,8 +1939,14 @@ public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList Date: Thu, 7 Jul 2022 15:23:47 +0300 Subject: [PATCH 08/18] [bugfix] - app was crushing when full surah is selected 2 --- .../java/com/quran/labs/androidquran/ui/PagerActivity.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 6b4e7cb1e7..6e7483fd3c 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -1940,10 +1940,11 @@ public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList Date: Thu, 7 Jul 2022 16:53:13 +0300 Subject: [PATCH 09/18] request user to download files if not present --- .../presenter/audio/AudioPresenter.kt | 2 +- .../labs/androidquran/ui/PagerActivity.java | 31 +++++++++++++------ .../labs/androidquran/util/AudioUtils.kt | 4 +-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt index 3f9736d8da..9bc73b3ec1 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt @@ -87,7 +87,7 @@ constructor(private val quranDisplayData: QuranDisplayData, lastAudioRequest?.let { play(it) } } - private fun getDownloadIntent(context: Context, request: AudioRequest): Intent? { + fun getDownloadIntent(context: Context, request: AudioRequest): Intent? { val qari = request.qari val audioPathInfo = request.audioPathInfo val path = audioPathInfo.localDirectory diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 6e7483fd3c..8f9d48c3b9 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -1866,6 +1866,7 @@ public void onError(@NonNull Throwable e) { public void shareAyahAudio(SuraAyah start, SuraAyah end) { audioCacheFilePaths.clear(); + if (start == null || end == null) { return; }else { @@ -1887,7 +1888,16 @@ public void shareAyahAudio(SuraAyah start, SuraAyah end) { assert audioPathInfo != null; if (audioPathInfo.getGaplessDatabase() != null) { - createAndShareAudio(selectedStartSuraAyah,selectedEndSuraAyah,audioPathInfo); + if (!audioUtils.haveAllFiles(audioPathInfo.getUrlFormat(),audioPathInfo.getLocalDirectory(),selectedStartSuraAyah,selectedEndSuraAyah,true)){ + AudioRequest audioRequest = new AudioRequest( + selectedStartSuraAyah, selectedEndSuraAyah, selectedQari, 0, 0, true, false, audioPathInfo); + Intent downloadIntent = audioPresenter.getDownloadIntent(this, audioRequest); + if (downloadIntent != null) { + handleRequiredDownload(downloadIntent); + } + }else{ + createAndShareAudio(selectedStartSuraAyah,selectedEndSuraAyah,audioPathInfo); + } } } @@ -1939,15 +1949,16 @@ public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList Date: Mon, 11 Jul 2022 13:47:16 +0300 Subject: [PATCH 10/18] clean up --- .../labs/androidquran/ui/PagerActivity.java | 328 +++++++++++------- .../labs/androidquran/util/AudioUtils.kt | 14 +- 2 files changed, 213 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 8f9d48c3b9..bbe2a8ab35 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -147,7 +147,6 @@ 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; @@ -228,8 +227,10 @@ public class PagerActivity extends AppCompatActivity implements private int defaultNavigationBarColor; private boolean isSplitScreen = false; - @Nullable private QuranAyahInfo lastSelectedTranslationAyah; - @Nullable private LocalTranslation[] lastActivatedLocalTranslations; + @Nullable + private QuranAyahInfo lastSelectedTranslationAyah; + @Nullable + private LocalTranslation[] lastActivatedLocalTranslations; private PagerActivityComponent pagerActivityComponent; @@ -243,7 +244,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; @@ -261,8 +262,9 @@ public class PagerActivity extends AppCompatActivity implements private final PagerHandler handler = new PagerHandler(this); - public static final File audioCacheDirectory= new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath() + - File.separator +"quran_android_cache"); + public static final File audioCacheDirectory = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath() + + File.separator + "quran_android_cache"); private ArrayList audioCacheFilePaths = new ArrayList<>(); private SuraAyah selectedStartSuraAyah = null; private SuraAyah selectedEndSuraAyah = null; @@ -305,12 +307,21 @@ public void onCreate(Bundle savedInstanceState) { isSplitScreen = quranSettings.isQuranSplitWithTranslation(); audioEventPresenterBridge = new AudioEventPresenterBridge( audioEventPresenter, - suraAyah -> { onAudioPlaybackAyahChanged(suraAyah); return null; } + suraAyah -> { + onAudioPlaybackAyahChanged(suraAyah); + return null; + } ); readingEventPresenterBridge = new ReadingEventPresenterBridge( readingEventPresenter, - () -> { onPageClicked(); return null; }, - ayahSelection -> { onAyahSelectionChanged(ayahSelection); return null; } + () -> { + onPageClicked(); + return null; + }, + ayahSelection -> { + onAyahSelectionChanged(ayahSelection); + return null; + } ); // remove the window background to avoid overdraw. note that, per Romain's blog, this is @@ -356,7 +367,7 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.quran_page_activity_slider); if (!audioCacheDirectory.exists()) { - if (!audioCacheDirectory.mkdirs()){ + if (!audioCacheDirectory.mkdirs()) { Toast.makeText(PagerActivity.this, "could not create directory", Toast.LENGTH_SHORT).show(); } } @@ -443,7 +454,8 @@ public void onPageScrolled(int position, float positionOffset, int positionOffse } else if (position == barPos - 1 || position == barPos + 1) { // Swiping to previous or next ViewPager page (i.e. next or previous quran page) final SelectionIndicator updatedSelectionIndicator = - SelectionIndicatorKt.withXScroll(selectionIndicator, viewPager.getWidth() - positionOffsetPixels); + SelectionIndicatorKt.withXScroll(selectionIndicator, + viewPager.getWidth() - positionOffsetPixels); readingEventPresenterBridge.withSelectionIndicator(updatedSelectionIndicator); } else { readingEventPresenterBridge.clearSelectedAyah(); @@ -563,8 +575,14 @@ public void onPageSelected(int position) { this::getCurrentPage, () -> audioStatusBar, () -> ayahToolBar, - ayah -> { ensurePage(ayah.sura, ayah.ayah); return null; }, - sliderPage -> { showSlider(slidingPagerAdapter.getPagePosition(sliderPage)); return null; } + ayah -> { + ensurePage(ayah.sura, ayah.ayah); + return null; + }, + sliderPage -> { + showSlider(slidingPagerAdapter.getPagePosition(sliderPage)); + return null; + } )); } @@ -1393,16 +1411,16 @@ private void ensurePage(int sura, int ayah) { private void requestTranslationsList() { compositeDisposable.add( Single.fromCallable(() -> - translationsDBAdapter.getTranslations()) + translationsDBAdapter.getTranslations()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(new DisposableSingleObserver>() { @Override public void onSuccess(@NonNull List translationList) { final List sortedTranslations = new ArrayList<>(translationList); - Collections.sort(sortedTranslations, new LocalTranslationDisplaySort()); + Collections.sort(sortedTranslations, new LocalTranslationDisplaySort()); - int items = sortedTranslations.size(); + int items = sortedTranslations.size(); String[] titles = new String[items]; for (int i = 0; i < items; i++) { LocalTranslation item = sortedTranslations.get(i); @@ -1419,7 +1437,8 @@ public void onSuccess(@NonNull List translationList) { if (currentActiveTranslationsFilesNames.isEmpty() && items > 0) { currentActiveTranslationsFilesNames = new HashSet<>(); for (int i = 0; i < items; i++) { - currentActiveTranslationsFilesNames.add(sortedTranslations.get(i).getFilename()); + currentActiveTranslationsFilesNames.add( + sortedTranslations.get(i).getFilename()); } } activeTranslationsFilesNames = currentActiveTranslationsFilesNames; @@ -1452,7 +1471,8 @@ public void onSuccess(@NonNull Boolean isBookmarked) { if (sura == null || ayah == null) { // page bookmark bookmarksCache.put(page, isBookmarked); - bookmarksMenuItem.setIcon(isBookmarked ? com.quran.labs.androidquran.common.toolbar.R.drawable.ic_favorite : com.quran.labs.androidquran.common.toolbar.R.drawable.ic_not_favorite); + bookmarksMenuItem.setIcon( + isBookmarked ? com.quran.labs.androidquran.common.toolbar.R.drawable.ic_favorite : com.quran.labs.androidquran.common.toolbar.R.drawable.ic_not_favorite); } else { // ayah bookmark SuraAyah suraAyah = new SuraAyah(sura, ayah); @@ -1501,7 +1521,8 @@ private void refreshBookmarksMenu() { bookmarked = bookmarksCache.get(page - 1); } - menuItem.setIcon(bookmarked ? com.quran.labs.androidquran.common.toolbar.R.drawable.ic_favorite : com.quran.labs.androidquran.common.toolbar.R.drawable.ic_not_favorite); + menuItem.setIcon( + bookmarked ? com.quran.labs.androidquran.common.toolbar.R.drawable.ic_favorite : com.quran.labs.androidquran.common.toolbar.R.drawable.ic_not_favorite); } else { supportInvalidateOptionsMenu(); } @@ -1542,7 +1563,7 @@ private void playFromAyah(int page, int startSura, int startAyah) { final SuraAyah start = new SuraAyah(startSura, startAyah); final SuraAyah end = getSelectionEnd(); // handle the case of multiple ayat being selected and play them as a range if so - final SuraAyah ending = (end == null || start.equals(end) || start.after(end))? null : end; + final SuraAyah ending = (end == null || start.equals(end) || start.after(end)) ? null : end; playFromAyah(start, ending, page, 0, 0, ending != null); } @@ -1785,7 +1806,7 @@ 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) { + } 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); @@ -1823,7 +1844,8 @@ private void shareAyah(SuraAyah start, SuraAyah end, final boolean isCopy) { if (isCopy) { shareUtil.copyToClipboard(this, shareText); } else { - shareUtil.shareViaIntent(this, shareText, com.quran.labs.androidquran.common.toolbar.R.string.share_ayah_text); + shareUtil.shareViaIntent(this, shareText, + com.quran.labs.androidquran.common.toolbar.R.string.share_ayah_text); } } @@ -1852,7 +1874,8 @@ public void shareAyahLink(SuraAyah start, SuraAyah end) { .subscribeWith(new DisposableSingleObserver() { @Override public void onSuccess(@NonNull String url) { - shareUtil.shareViaIntent(PagerActivity.this, url, com.quran.labs.androidquran.common.toolbar.R.string.share_ayah); + shareUtil.shareViaIntent(PagerActivity.this, url, + com.quran.labs.androidquran.common.toolbar.R.string.share_ayah); dismissProgressDialog(); } @@ -1867,128 +1890,117 @@ public void onError(@NonNull Throwable e) { public void shareAyahAudio(SuraAyah start, SuraAyah end) { audioCacheFilePaths.clear(); - 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; - selectedStartSuraAyah = (SuraAyah) pair2.component1(); - selectedEndSuraAyah = (SuraAyah) pair2.component2(); - } + kotlin.Pair pair2 = getReorderedAyatPair(start, end); + selectedStartSuraAyah = (SuraAyah) pair2.component1(); + selectedEndSuraAyah = (SuraAyah) pair2.component2(); - selectedQari = audioStatusBar.getAudioInfo(); - AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(selectedQari); + selectedQari = audioStatusBar.getAudioInfo(); + AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(selectedQari); assert audioPathInfo != null; - if (audioPathInfo.getGaplessDatabase() != null) { - if (!audioUtils.haveAllFiles(audioPathInfo.getUrlFormat(),audioPathInfo.getLocalDirectory(),selectedStartSuraAyah,selectedEndSuraAyah,true)){ - AudioRequest audioRequest = new AudioRequest( - selectedStartSuraAyah, selectedEndSuraAyah, selectedQari, 0, 0, true, false, audioPathInfo); - Intent downloadIntent = audioPresenter.getDownloadIntent(this, audioRequest); - if (downloadIntent != null) { - handleRequiredDownload(downloadIntent); - } - }else{ - createAndShareAudio(selectedStartSuraAyah,selectedEndSuraAyah,audioPathInfo); + boolean gaplessDatabaseExists = audioPathInfo.getGaplessDatabase() != null; + + if (gaplessDatabaseExists) { + if (audioFilesExist(audioPathInfo)) { + createAndShareAudio(selectedStartSuraAyah, selectedEndSuraAyah, audioPathInfo); + } else { + requestDownload(audioPathInfo); } } } + private kotlin.Pair getReorderedAyatPair(SuraAyah start, SuraAyah end) { + 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); + } + return pair; + } + + private boolean audioFilesExist(AudioPathInfo audioPathInfo) { + return audioUtils.haveAllFiles(audioPathInfo.getUrlFormat(), audioPathInfo.getLocalDirectory(), + selectedStartSuraAyah, selectedEndSuraAyah, true); + } + private void createAndShareAudio(SuraAyah start, SuraAyah end, AudioPathInfo audioPathInfo) { showProgressDialog(); - String databasePath = audioPathInfo.getGaplessDatabase(); compositeDisposable.add( - 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 mapArray = new ArrayList<>(Arrays.asList(firstSurahMap, lastSurahMap)); - return mapArray; - }).subscribeOn(Schedulers.io()) + Single.fromCallable(() -> getTimingData(start, end, audioPathInfo)) + .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(new DisposableSingleObserver>() { @Override - public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList sparseIntArrayList) { + public void onSuccess( + @io.reactivex.rxjava3.annotations.NonNull ArrayList sparseIntArrayList) { Intrinsics.checkNotNullExpressionValue(sparseIntArrayList, "mapArray"); int startAyah = start.ayah; int endAyah = end.ayah; - int startAyahTime = 0; - int endAyahTime = sparseIntArrayList.get(1).get(endAyah+1); - - if (startAyah!=1){ - startAyahTime = sparseIntArrayList.get(0).get(startAyah); + SparseIntArray startSurahTimingDataArray = sparseIntArrayList.get(0); + SparseIntArray endSurahTimingDataArray = sparseIntArrayList.get(1); + int startAyahTime; + int endAyahTime; + + boolean isFirstAyahInSurah = startAyah == 1; + int startTimeOfAyahAfterEndAyah = endSurahTimingDataArray.get(endAyah + 1); + boolean isLastAyahInSurah = startTimeOfAyahAfterEndAyah == 0; + + if (isFirstAyahInSurah) { + startAyahTime = 0; + } else { + startAyahTime = startSurahTimingDataArray.get(startAyah); } - if (endAyahTime == 0){ - endAyahTime = audioUtils.getSurahDuration(PagerActivity.this,audioUtils.getSurahAudioPath(audioPathInfo,end.sura)); + + if (isLastAyahInSurah) { + endAyahTime = audioUtils.getSurahDuration(PagerActivity.this, + audioUtils.getSurahAudioPath(audioPathInfo, end.sura)); + } else { + endAyahTime = startTimeOfAyahAfterEndAyah; } + boolean startAndEndAyahAreInSameSurah = start.sura == end.sura; - if (start.sura == end.sura){ - String audioSegmentPath = audioUtils.getSurahSegment(audioUtils.getSurahAudioPath(audioPathInfo,start.sura),startAyahTime,endAyahTime); + if (startAndEndAyahAreInSameSurah) { + String audioSegmentPath = audioUtils.getSurahSegment( + audioUtils.getSurahAudioPath(audioPathInfo, start.sura), startAyahTime, + endAyahTime); audioCacheFilePaths.add(audioSegmentPath); - renameSharableAudioFile(audioSegmentPath); - shareUtil.shareAudioFileIntent(PagerActivity.this,new File(audioSegmentPath)); - }else { - ArrayList 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); + shareAudioSegment(renameSharableAudioFile(audioSegmentPath)); + } else { + ArrayList segmentPaths = new ArrayList<>(); + int endOfSurah = -1; + int startOfSurah = 0; + String startSegmentPath = getSurahSegmentPath(audioPathInfo, start.sura, + startAyahTime, endOfSurah); + String lastSegmentPath = getSurahSegmentPath(audioPathInfo, end.sura, + startOfSurah, endAyahTime); + + for (int surahIndex = start.sura; surahIndex <= end.sura; surahIndex++) { + boolean isTheFirstSurah = surahIndex == start.sura; + boolean isMiddleSurah = (surahIndex != start.sura) && (surahIndex != end.sura); + + if (isTheFirstSurah) { + segmentPaths.add(startSegmentPath); continue; } - if (surahIndex != end.sura){ - paths.add(audioUtils.getSurahAudioPath(audioPathInfo,surahIndex)); + if (isMiddleSurah) { + segmentPaths.add(audioUtils.getSurahAudioPath(audioPathInfo, surahIndex)); continue; } - paths.add(lastSegment); + segmentPaths.add(lastSegmentPath); } - if (!paths.isEmpty()){ - audioCacheFilePaths.addAll(paths); - File sharableAudioFile = audioUtils.getMergedAudioFromSegments(paths); - renameSharableAudioFile(sharableAudioFile.getPath()); - shareUtil.shareAudioFileIntent(PagerActivity.this,sharableAudioFile); + + boolean audioSegmentsWereCreated = !segmentPaths.isEmpty(); + + if (audioSegmentsWereCreated) { + audioCacheFilePaths.addAll(segmentPaths); + String sharableAudioFilePath = audioUtils.getMergedAudioFromSegments( + segmentPaths); + shareAudioSegment(renameSharableAudioFile(sharableAudioFilePath)); } } dismissProgressDialog(); @@ -1997,21 +2009,93 @@ public void onSuccess(@io.reactivex.rxjava3.annotations.NonNull ArrayList getTimingData(SuraAyah start, SuraAyah end, + AudioPathInfo audioPathInfo) { + String databasePath = audioPathInfo.getGaplessDatabase(); + + assert databasePath != null; + SuraTimingDatabaseHandler db = SuraTimingDatabaseHandler.Companion.getDatabaseHandler( + databasePath); + SparseIntArray firstSurahMap = new SparseIntArray(); + SparseIntArray lastSurahMap = new SparseIntArray(); + Cursor firstSurahCursor = null; + Cursor lastSurahCursor = null; + + try { + firstSurahCursor = db.getAyahTimings(start.sura); + firstSurahMap = populateArrayFromCursor(firstSurahCursor); + + lastSurahCursor = db.getAyahTimings(end.sura); + lastSurahMap = populateArrayFromCursor(lastSurahCursor); + + } catch (SQLException sqlException) { + Timber.Forest.e(sqlException); + } finally { + closeCursor(firstSurahCursor); + closeCursor(lastSurahCursor); + } + + ArrayList mapArray = new ArrayList<>( + Arrays.asList(firstSurahMap, lastSurahMap)); + return mapArray; + } + + private SparseIntArray populateArrayFromCursor(Cursor cursor) { + SparseIntArray sparseIntArray = new SparseIntArray(); + if (cursor != null && cursor.moveToFirst()) { + do { + int ayah = cursor.getInt(1); + int time = cursor.getInt(2); + sparseIntArray.put(ayah, time); + } while (cursor.moveToNext()); + } + return sparseIntArray; + } + + private String renameSharableAudioFile(String audioSegmentPath) { String newAudioFileName = selectedQari.getPath() + "_" + selectedStartSuraAyah.sura + "-" + selectedStartSuraAyah.ayah + "_" + selectedEndSuraAyah.sura + "-" + selectedEndSuraAyah.ayah; - String newAudioFilePath = audioCacheDirectory + File.separator+ newAudioFileName + ".mp3"; + String newAudioFilePath = audioCacheDirectory + File.separator + newAudioFileName + ".mp3"; new File(audioSegmentPath).renameTo(new File(newAudioFilePath)); audioCacheFilePaths.remove(audioSegmentPath); - for (String path : audioCacheFilePaths){ + for (String path : audioCacheFilePaths) { new File(path).delete(); } audioCacheFilePaths.clear(); + return newAudioFilePath; + } + + private void shareAudioSegment(String path) { + shareUtil.shareAudioFileIntent(PagerActivity.this, new File(path)); + } + + private String getSurahSegmentPath(AudioPathInfo audioPathInfo, int surah, + int startAyahTime, int endAyahTime) { + int lowerBoundTime = startAyahTime; + int upperBoundTime = endAyahTime; + + String audioFilePath = audioUtils.getSurahAudioPath(audioPathInfo, surah); + boolean isFirstSegment = endAyahTime < 0; + + if (isFirstSegment) { + upperBoundTime = audioUtils.getSurahDuration(PagerActivity.this, audioFilePath); + } + + return audioUtils.getSurahSegment(audioFilePath, lowerBoundTime, upperBoundTime); + } + + private void requestDownload(AudioPathInfo audioPathInfo) { + AudioRequest audioRequest = new AudioRequest( + selectedStartSuraAyah, selectedEndSuraAyah, selectedQari, 0, 0, true, false, audioPathInfo); + + Intent downloadIntent = audioPresenter.getDownloadIntent(this, audioRequest); + if (downloadIntent != null) { + handleRequiredDownload(downloadIntent); + } } private void showProgressDialog() { diff --git a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt index e41e4e364f..b9bd9f6be8 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt @@ -306,14 +306,14 @@ class AudioUtils @Inject constructor( return null } - fun getMergedAudioFromSegments(segments: ArrayList): File { + fun getMergedAudioFromSegments(segments: ArrayList): String { var mergedAudioPath = segments[0] if (segments.size > 1) { for (i in 1 until segments.size) { mergedAudioPath = mergeAudios(mergedAudioPath, segments[i])!! } } - return File(mergedAudioPath) + return mergedAudioPath } private fun mergeAudios(path1: String, path2: String): String? { @@ -353,26 +353,26 @@ class AudioUtils @Inject constructor( } fun getSurahSegment(path: String, lowerCut: Int, upperCut: Int): String? { + if (lowerCut == 0 && upperCut == 0) { + return null + } val tempAudioName = UUID.randomUUID().toString() + ".mp3" val destFile = File(PagerActivity.audioCacheDirectory.path + File.separator + tempAudioName) val mSoundFile = arrayOfNulls(1) 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() + val endFrames = (endTime * avg).roundToInt() mSoundFile[0]?.WriteFile(destFile, startFrames, endFrames - startFrames) } catch (e: IOException) { e.printStackTrace() } - return destFile.path + return destFile.absolutePath } fun getSurahDuration(context: Context,path: String): Int { From cf84a9571ec6dd647ee252f5b2d3204100e99870 Mon Sep 17 00:00:00 2001 From: Doozy Date: Sat, 30 Jul 2022 11:21:24 +0300 Subject: [PATCH 11/18] transffered to audio feature --- .../labs/androidquran/ui/PagerActivity.java | 165 +---------- .../labs/androidquran/util/AudioUtils.kt | 91 +----- feature/audio/build.gradle | 3 + .../database/SuraTimingDatabaseHandler.kt | 126 +++++++++ .../feature/audio/util/AudioShareUtils.kt | 259 ++++++++++++++++++ .../audio/util/soundfile}/CheapMP3.java | 2 +- .../audio/util/soundfile}/CheapSoundFile.java | 5 +- 7 files changed, 400 insertions(+), 251 deletions(-) create mode 100644 feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt create mode 100644 feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt rename {app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils => feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile}/CheapMP3.java (99%) rename {app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils => feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile}/CheapSoundFile.java (97%) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index bbe2a8ab35..891ab2c90d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -85,6 +85,7 @@ import com.quran.labs.androidquran.di.component.activity.PagerActivityComponent; import com.quran.labs.androidquran.di.module.activity.PagerActivityModule; import com.quran.labs.androidquran.di.module.fragment.QuranPageModule; +import com.quran.labs.androidquran.feature.audio.util.AudioShareUtils; import com.quran.labs.androidquran.model.bookmark.BookmarkModel; import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils; import com.quran.labs.androidquran.presenter.audio.AudioPresenter; @@ -1902,7 +1903,11 @@ public void shareAyahAudio(SuraAyah start, SuraAyah end) { if (gaplessDatabaseExists) { if (audioFilesExist(audioPathInfo)) { - createAndShareAudio(selectedStartSuraAyah, selectedEndSuraAyah, audioPathInfo); + AudioShareUtils audioShareUtils = new AudioShareUtils(); + String path = audioShareUtils.createSharableAudioFile(this, selectedStartSuraAyah, + selectedEndSuraAyah, selectedQari, audioPathInfo.getUrlFormat(), + audioPathInfo.getGaplessDatabase()); + shareAudioSegment(path); } else { requestDownload(audioPathInfo); } @@ -1926,168 +1931,10 @@ private boolean audioFilesExist(AudioPathInfo audioPathInfo) { selectedStartSuraAyah, selectedEndSuraAyah, true); } - private void createAndShareAudio(SuraAyah start, SuraAyah end, AudioPathInfo audioPathInfo) { - showProgressDialog(); - compositeDisposable.add( - Single.fromCallable(() -> getTimingData(start, end, audioPathInfo)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(new DisposableSingleObserver>() { - @Override - public void onSuccess( - @io.reactivex.rxjava3.annotations.NonNull ArrayList sparseIntArrayList) { - Intrinsics.checkNotNullExpressionValue(sparseIntArrayList, "mapArray"); - - int startAyah = start.ayah; - int endAyah = end.ayah; - SparseIntArray startSurahTimingDataArray = sparseIntArrayList.get(0); - SparseIntArray endSurahTimingDataArray = sparseIntArrayList.get(1); - int startAyahTime; - int endAyahTime; - - boolean isFirstAyahInSurah = startAyah == 1; - int startTimeOfAyahAfterEndAyah = endSurahTimingDataArray.get(endAyah + 1); - boolean isLastAyahInSurah = startTimeOfAyahAfterEndAyah == 0; - - if (isFirstAyahInSurah) { - startAyahTime = 0; - } else { - startAyahTime = startSurahTimingDataArray.get(startAyah); - } - - if (isLastAyahInSurah) { - endAyahTime = audioUtils.getSurahDuration(PagerActivity.this, - audioUtils.getSurahAudioPath(audioPathInfo, end.sura)); - } else { - endAyahTime = startTimeOfAyahAfterEndAyah; - } - - boolean startAndEndAyahAreInSameSurah = start.sura == end.sura; - - if (startAndEndAyahAreInSameSurah) { - String audioSegmentPath = audioUtils.getSurahSegment( - audioUtils.getSurahAudioPath(audioPathInfo, start.sura), startAyahTime, - endAyahTime); - audioCacheFilePaths.add(audioSegmentPath); - shareAudioSegment(renameSharableAudioFile(audioSegmentPath)); - } else { - ArrayList segmentPaths = new ArrayList<>(); - int endOfSurah = -1; - int startOfSurah = 0; - String startSegmentPath = getSurahSegmentPath(audioPathInfo, start.sura, - startAyahTime, endOfSurah); - String lastSegmentPath = getSurahSegmentPath(audioPathInfo, end.sura, - startOfSurah, endAyahTime); - - for (int surahIndex = start.sura; surahIndex <= end.sura; surahIndex++) { - boolean isTheFirstSurah = surahIndex == start.sura; - boolean isMiddleSurah = (surahIndex != start.sura) && (surahIndex != end.sura); - - if (isTheFirstSurah) { - segmentPaths.add(startSegmentPath); - continue; - } - if (isMiddleSurah) { - segmentPaths.add(audioUtils.getSurahAudioPath(audioPathInfo, surahIndex)); - continue; - } - segmentPaths.add(lastSegmentPath); - } - - boolean audioSegmentsWereCreated = !segmentPaths.isEmpty(); - - if (audioSegmentsWereCreated) { - audioCacheFilePaths.addAll(segmentPaths); - String sharableAudioFilePath = audioUtils.getMergedAudioFromSegments( - segmentPaths); - shareAudioSegment(renameSharableAudioFile(sharableAudioFilePath)); - } - } - dismissProgressDialog(); - } - - @Override - public void onError(@io.reactivex.rxjava3.annotations.NonNull Throwable e) { - dismissProgressDialog(); - } - }) - ); - } - - private ArrayList getTimingData(SuraAyah start, SuraAyah end, - AudioPathInfo audioPathInfo) { - String databasePath = audioPathInfo.getGaplessDatabase(); - - assert databasePath != null; - SuraTimingDatabaseHandler db = SuraTimingDatabaseHandler.Companion.getDatabaseHandler( - databasePath); - SparseIntArray firstSurahMap = new SparseIntArray(); - SparseIntArray lastSurahMap = new SparseIntArray(); - Cursor firstSurahCursor = null; - Cursor lastSurahCursor = null; - - try { - firstSurahCursor = db.getAyahTimings(start.sura); - firstSurahMap = populateArrayFromCursor(firstSurahCursor); - - lastSurahCursor = db.getAyahTimings(end.sura); - lastSurahMap = populateArrayFromCursor(lastSurahCursor); - - } catch (SQLException sqlException) { - Timber.Forest.e(sqlException); - } finally { - closeCursor(firstSurahCursor); - closeCursor(lastSurahCursor); - } - - ArrayList mapArray = new ArrayList<>( - Arrays.asList(firstSurahMap, lastSurahMap)); - return mapArray; - } - - private SparseIntArray populateArrayFromCursor(Cursor cursor) { - SparseIntArray sparseIntArray = new SparseIntArray(); - if (cursor != null && cursor.moveToFirst()) { - do { - int ayah = cursor.getInt(1); - int time = cursor.getInt(2); - sparseIntArray.put(ayah, time); - } while (cursor.moveToNext()); - } - return sparseIntArray; - } - - private String renameSharableAudioFile(String audioSegmentPath) { - String newAudioFileName = selectedQari.getPath() + "_" + selectedStartSuraAyah.sura + "-" + selectedStartSuraAyah.ayah + "_" + selectedEndSuraAyah.sura + "-" + selectedEndSuraAyah.ayah; - String newAudioFilePath = audioCacheDirectory + File.separator + newAudioFileName + ".mp3"; - new File(audioSegmentPath).renameTo(new File(newAudioFilePath)); - audioCacheFilePaths.remove(audioSegmentPath); - for (String path : audioCacheFilePaths) { - new File(path).delete(); - } - audioCacheFilePaths.clear(); - return newAudioFilePath; - } - private void shareAudioSegment(String path) { shareUtil.shareAudioFileIntent(PagerActivity.this, new File(path)); } - private String getSurahSegmentPath(AudioPathInfo audioPathInfo, int surah, - int startAyahTime, int endAyahTime) { - int lowerBoundTime = startAyahTime; - int upperBoundTime = endAyahTime; - - String audioFilePath = audioUtils.getSurahAudioPath(audioPathInfo, surah); - boolean isFirstSegment = endAyahTime < 0; - - if (isFirstSegment) { - upperBoundTime = audioUtils.getSurahDuration(PagerActivity.this, audioFilePath); - } - - return audioUtils.getSurahSegment(audioFilePath, lowerBoundTime, upperBoundTime); - } - private void requestDownload(AudioPathInfo audioPathInfo) { AudioRequest audioRequest = new AudioRequest( selectedStartSuraAyah, selectedEndSuraAyah, selectedQari, 0, 0, true, false, audioPathInfo); diff --git a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt index b9bd9f6be8..4df8b728cf 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt @@ -2,11 +2,7 @@ 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 @@ -14,13 +10,10 @@ 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.* -import java.util.* +import java.io.File +import java.util.Locale import javax.inject.Inject -import kotlin.math.roundToInt class AudioUtils @Inject constructor( private val quranInfo: QuranInfo, @@ -306,86 +299,6 @@ class AudioUtils @Inject constructor( return null } - fun getMergedAudioFromSegments(segments: ArrayList): String { - var mergedAudioPath = segments[0] - if (segments.size > 1) { - for (i in 1 until segments.size) { - mergedAudioPath = mergeAudios(mergedAudioPath, segments[i])!! - } - } - return 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) - 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() - return destFile.path - } catch (e2: FileNotFoundException) { - e2.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } - return null - } - - fun getSurahSegment(path: String, lowerCut: Int, upperCut: Int): String? { - if (lowerCut == 0 && upperCut == 0) { - return null - } - val tempAudioName = UUID.randomUUID().toString() + ".mp3" - val destFile = File(PagerActivity.audioCacheDirectory.path + File.separator + tempAudioName) - val mSoundFile = arrayOfNulls(1) - try { - mSoundFile[0] = CheapSoundFile.create(path, 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.absolutePath - } - - 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.urlFormat, surah) - } - companion object { const val ZIP_EXTENSION = ".zip" const val AUDIO_EXTENSION = ".mp3" diff --git a/feature/audio/build.gradle b/feature/audio/build.gradle index 43eeea22b8..b04dd3534e 100644 --- a/feature/audio/build.gradle +++ b/feature/audio/build.gradle @@ -23,6 +23,7 @@ android { dependencies { implementation project(path: ':common:audio') + implementation project(path: ':common:data') implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" @@ -36,4 +37,6 @@ dependencies { testImplementation "junit:junit:${junitVersion}" testImplementation "com.google.truth:truth:${truthVersion}" + + implementation 'com.jakewharton.timber:timber:5.0.1' } diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt new file mode 100644 index 0000000000..70db297573 --- /dev/null +++ b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt @@ -0,0 +1,126 @@ +package com.quran.labs.androidquran.feature.audio.database + +import android.database.Cursor +import android.database.DefaultDatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteDatabaseCorruptException +import timber.log.Timber +import java.io.File +import java.lang.Exception +import java.sql.SQLException +import java.util.HashMap + +class SuraTimingDatabaseHandler private constructor(path: String) { + private var database: SQLiteDatabase? = null + + object TimingsTable { + const val TABLE_NAME = "timings" + const val COL_SURA = "sura" + const val COL_AYAH = "ayah" + const val COL_TIME = "time" + } + + object PropertiesTable { + const val TABLE_NAME = "properties" + const val COL_PROPERTY = "property" + const val COL_VALUE = "value" + } + + companion object { + private val databaseMap: MutableMap = HashMap() + + @JvmStatic + @Synchronized + fun getDatabaseHandler(path: String): SuraTimingDatabaseHandler { + var handler = databaseMap[path] + if (handler == null) { + handler = SuraTimingDatabaseHandler(path) + databaseMap[path] = handler + } + return handler + } + + @Synchronized + fun clearDatabaseHandlerIfExists(databasePath: String) { + try { + val handler = databaseMap.remove(databasePath) + if (handler != null) { + handler.database?.close() + databaseMap.remove(databasePath) + } + } catch (e: Exception) { + Timber.e(e) + } + } + } + + init { + Timber.d("opening gapless data file, %s", path) + database = try { + SQLiteDatabase.openDatabase( + path, null, + SQLiteDatabase.NO_LOCALIZED_COLLATORS, DefaultDatabaseErrorHandler() + ) + } catch (sce: SQLiteDatabaseCorruptException) { + Timber.d("database corrupted: %s", path) + null + } catch (se: SQLException) { + Timber.d("database at $path ${if (File(path).exists()) "exists " else "doesn 't exist"}") + Timber.e(se) + null + } + } + + private fun validDatabase(): Boolean = database?.isOpen ?: false + + fun getAyahTimings(sura: Int): Cursor? { + if (!validDatabase()) return null + + return try { + database?.query( + TimingsTable.TABLE_NAME, arrayOf( + TimingsTable.COL_SURA, + TimingsTable.COL_AYAH, TimingsTable.COL_TIME + ), + TimingsTable.COL_SURA + "=" + sura, + null, null, null, TimingsTable.COL_AYAH + " ASC" + ) + } catch (e: Exception) { + null + } + } + + fun getVersion(): Int { + if (!validDatabase()) { + return -1 + } + var cursor: Cursor? = null + return try { + cursor = database?.query( + PropertiesTable.TABLE_NAME, arrayOf(PropertiesTable.COL_VALUE), + PropertiesTable.COL_PROPERTY + "= 'version'", null, null, null, null + ) + if (cursor != null && cursor.moveToFirst()) { + cursor.getInt(0) + } else { + 1 + } + } catch (e: Exception) { + 1 + } finally { + DatabaseUtils.closeCursor(cursor) + } + } + + object DatabaseUtils { + @JvmStatic + fun closeCursor(cursor: Cursor?) { + try { + cursor?.close() + } catch (e: Exception) { + // no op + } + } + } +} + diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt new file mode 100644 index 0000000000..95c7d8062f --- /dev/null +++ b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt @@ -0,0 +1,259 @@ +package com.quran.labs.androidquran.feature.audio.util + +import android.content.Context +import android.database.Cursor +import android.database.SQLException +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Environment +import android.util.SparseIntArray +import com.quran.data.model.SuraAyah +import com.quran.labs.androidquran.common.audio.model.QariItem +import com.quran.labs.androidquran.feature.audio.database.SuraTimingDatabaseHandler +import com.quran.labs.androidquran.feature.audio.database.SuraTimingDatabaseHandler.DatabaseUtils.closeCursor +import com.quran.labs.androidquran.feature.audio.util.soundfile.CheapSoundFile +import kotlinx.coroutines.* +import okio.BufferedSink +import okio.buffer +import okio.sink +import okio.source +import timber.log.Timber +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.Locale +import java.util.UUID +import kotlin.math.roundToInt + +class AudioShareUtils { + + val audioCacheDirectory = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).path + + File.separator + "quran_android_cache") + var audioCacheFilePaths: MutableList = ArrayList() + var selectedQari: QariItem? = null + var selectedStartSuraAyah: SuraAyah? = null + var selectedEndSuraAyah: SuraAyah? = null + + + data class ProperAyahOrder(val start: SuraAyah, val end: SuraAyah) + + private fun getReorderedAyahPair(start: SuraAyah, end: SuraAyah): ProperAyahOrder { + val (actualStart, actualEnd) = if (start <= end) { + start to end + } else { + end to start + } + return ProperAyahOrder(actualStart, actualEnd) + } + + + fun createSharableAudioFile(context: Context, start: SuraAyah, end: SuraAyah, qari: QariItem, urlFormat: String, gaplessDatabase: String): String? { + selectedStartSuraAyah = getReorderedAyahPair(start, end).start + selectedEndSuraAyah = getReorderedAyahPair(start, end).end + selectedQari = qari + + var sharablePath: String? = null + + return runBlocking { + GlobalScope.launch(Dispatchers.IO) { + val mapArray = async { + getTimingData(start, end, gaplessDatabase) + }.await() + + val startAyah = start.ayah + val endAyah = end.ayah + val startSurahTimingDataArray: SparseIntArray = mapArray[0] + val endSurahTimingDataArray: SparseIntArray = mapArray[1] + + val isFirstAyahInSurah = startAyah == 1 + val startTimeOfAyahAfterEndAyah = endSurahTimingDataArray[endAyah + 1] + val isLastAyahInSurah = startTimeOfAyahAfterEndAyah == 0 + + val startAyahTime = if (isFirstAyahInSurah) { + 0 + } else { + startSurahTimingDataArray[startAyah] + } + + val endAyahTime = if (isLastAyahInSurah) { + getSurahDuration(context, + getSurahAudioPath(urlFormat, end.sura)!!) + } else { + startTimeOfAyahAfterEndAyah + } + + val startAndEndAyahAreInSameSurah = start.sura == end.sura + + if (startAndEndAyahAreInSameSurah) { + val audioSegmentPath: String = getSurahSegment( + getSurahAudioPath(urlFormat, start.sura)!!, startAyahTime, + endAyahTime)!! + audioCacheFilePaths.add(audioSegmentPath) + sharablePath = getRenamedSharableAudioFile(audioSegmentPath) + } else { + val segmentPaths = java.util.ArrayList() + val endOfSurah = -1 + val startOfSurah = 0 + val startSegmentPath: String = getSurahSegmentPath(context, urlFormat, start.sura, + startAyahTime, endOfSurah) + val lastSegmentPath: String = getSurahSegmentPath(context, urlFormat, end.sura, + startOfSurah, endAyahTime) + + for (surahIndex in start.sura..end.sura) { + val isTheFirstSurah = surahIndex == start.sura + val isMiddleSurah = surahIndex != start.sura && surahIndex != end.sura + if (isTheFirstSurah) { + segmentPaths.add(startSegmentPath) + audioCacheFilePaths.add(startSegmentPath) + continue + } + if (isMiddleSurah) { + segmentPaths.add(getSurahAudioPath(urlFormat, surahIndex)!!) + continue + } + segmentPaths.add(lastSegmentPath) + audioCacheFilePaths.add(lastSegmentPath) + } + + val audioSegmentsWereCreated = segmentPaths.isNotEmpty() + + if (audioSegmentsWereCreated) { + val sharableAudioFilePath: String = getMergedAudioFromSegments( + segmentPaths) + sharablePath = getRenamedSharableAudioFile(sharableAudioFilePath) + } else { + sharablePath = null + } + } + }.join() + return@runBlocking sharablePath + } + + } + + private fun getSurahSegmentPath(context: Context, urlFormat: String, surah: Int, + startAyahTime: Int, endAyahTime: Int): String { + var upperBoundTime = endAyahTime + val audioFilePath: String = getSurahAudioPath(urlFormat, surah)!! + val isFirstSegment = endAyahTime < 0 + if (isFirstSegment) { + upperBoundTime = getSurahDuration(context, audioFilePath) + } + return getSurahSegment(audioFilePath, startAyahTime, upperBoundTime)!! + } + + private fun getRenamedSharableAudioFile(audioSegmentPath: String): String { + val newAudioFileName: String = selectedQari!!.path + "_" + selectedStartSuraAyah!!.sura + "-" + selectedStartSuraAyah!!.ayah + "_" + selectedEndSuraAyah!!.sura + "-" + selectedEndSuraAyah!!.ayah + val newAudioFilePath: String = audioCacheDirectory.toString() + File.separator + newAudioFileName + ".mp3" + File(audioSegmentPath).renameTo(File(newAudioFilePath)) + audioCacheFilePaths.remove(audioSegmentPath) + for (path in audioCacheFilePaths) { + File(path).delete() + } + audioCacheFilePaths.clear() + return newAudioFilePath + } + + private 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() + } + + private fun getSurahAudioPath(urlFormat: String, surah: Int): String? { + return String.format(Locale.US, urlFormat, surah) + } + + private fun getTimingData(start: SuraAyah, end: SuraAyah, gaplessDatabase: String): ArrayList { + val databasePath = gaplessDatabase!! + + val db: SuraTimingDatabaseHandler = SuraTimingDatabaseHandler.getDatabaseHandler( + databasePath) + var firstSurahMap = SparseIntArray() + var lastSurahMap = SparseIntArray() + var firstSurahCursor: Cursor? = null + var lastSurahCursor: Cursor? = null + + try { + firstSurahCursor = db.getAyahTimings(start.sura) + firstSurahMap = populateArrayFromCursor(firstSurahCursor)!! + lastSurahCursor = db.getAyahTimings(end.sura) + lastSurahMap = populateArrayFromCursor(lastSurahCursor) + } catch (sqlException: SQLException) { + Timber.e(sqlException) + } finally { + closeCursor(firstSurahCursor) + closeCursor(lastSurahCursor) + } + + return ArrayList( + listOf(firstSurahMap, lastSurahMap)) + } + + private fun populateArrayFromCursor(cursor: Cursor?): SparseIntArray { + val sparseIntArray = SparseIntArray() + if (cursor != null && cursor.moveToFirst()) { + do { + val ayah = cursor.getInt(1) + val time = cursor.getInt(2) + sparseIntArray.put(ayah, time) + } while (cursor.moveToNext()) + } + return sparseIntArray + } + + private fun getSurahSegment(path: String, lowerCut: Int, upperCut: Int): String? { + if (lowerCut == 0 && upperCut == 0) { + return null + } + val tempAudioName = UUID.randomUUID().toString() + ".mp3" + val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) + val mSoundFile = arrayOfNulls(1) + try { + mSoundFile[0] = CheapSoundFile.create(path, 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.absolutePath + } + + private fun getMergedAudioFromSegments(segments: ArrayList): String { + var mergedAudioPath = segments[0] + if (segments.size > 1) { + for (i in 1 until segments.size) { + mergedAudioPath = mergeAudios(mergedAudioPath, segments[i])!! + audioCacheFilePaths.add(mergedAudioPath) + } + } + return mergedAudioPath + } + + private fun mergeAudios(path1: String, path2: String): String? { + val tempAudioName = UUID.randomUUID().toString() + ".mp3" + val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) + try { + val bufferedSink: BufferedSink = destFile.sink().buffer() + bufferedSink.writeAll(File(path1).source()) + bufferedSink.writeAll(File(path2).source()) + bufferedSink.close() + return destFile.path + } catch (e2: FileNotFoundException) { + e2.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + return null + } + + +} diff --git a/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapMP3.java b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapMP3.java similarity index 99% rename from app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapMP3.java rename to feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapMP3.java index 191a256093..bedbc1af1c 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapMP3.java +++ b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapMP3.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.quran.labs.androidquran.util.audioConversionUtils; +package com.quran.labs.androidquran.feature.audio.util.soundfile; import java.io.File; import java.io.FileInputStream; diff --git a/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapSoundFile.java b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapSoundFile.java similarity index 97% rename from app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapSoundFile.java rename to feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapSoundFile.java index 9a8dc9b368..9ea9dd27fa 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/audioConversionUtils/CheapSoundFile.java +++ b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapSoundFile.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.quran.labs.androidquran.util.audioConversionUtils; +package com.quran.labs.androidquran.feature.audio.util.soundfile; import java.io.File; import java.io.FileInputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.HashMap; + /** * CheapSoundFile is the parent class of several subclasses that each * do a "cheap" scan of various sound file formats, parsing as little @@ -203,7 +204,7 @@ public String computeMd5OfFirst10Frames() numFrames = 10; } - MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); + MessageDigest digest = MessageDigest.getInstance("MD5"); FileInputStream in = new FileInputStream(mInputFile); int pos = 0; for (int i = 0; i < numFrames; i++) { From 168a044da317320dcb87fb9120e4a97d77660497 Mon Sep 17 00:00:00 2001 From: Doozy Date: Sat, 30 Jul 2022 11:30:01 +0300 Subject: [PATCH 12/18] removed greek prefix --- .../androidquran/feature/audio/util/AudioShareUtils.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt index 95c7d8062f..e28d0633cd 100644 --- a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt +++ b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt @@ -210,17 +210,17 @@ class AudioShareUtils { } val tempAudioName = UUID.randomUUID().toString() + ".mp3" val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) - val mSoundFile = arrayOfNulls(1) + val soundFile = arrayOfNulls(1) try { - mSoundFile[0] = CheapSoundFile.create(path, null) + soundFile[0] = CheapSoundFile.create(path, null) val startTime = lowerCut.toFloat() / 1000 val endTime = upperCut.toFloat() / 1000 - val samplesPerFrame = mSoundFile[0]?.samplesPerFrame - val sampleRate = mSoundFile[0]?.sampleRate + val samplesPerFrame = soundFile[0]?.samplesPerFrame + val sampleRate = soundFile[0]?.sampleRate val avg = sampleRate?.div(samplesPerFrame!!) val startFrames = (startTime * avg!!).roundToInt() val endFrames = (endTime * avg).roundToInt() - mSoundFile[0]?.WriteFile(destFile, startFrames, endFrames - startFrames) + soundFile[0]?.WriteFile(destFile, startFrames, endFrames - startFrames) } catch (e: IOException) { e.printStackTrace() } From e85760ce3938571bff2be8f325d8bfc3eee8b366 Mon Sep 17 00:00:00 2001 From: Doozy Date: Sat, 30 Jul 2022 11:48:31 +0300 Subject: [PATCH 13/18] check cache directory existance at sharing --- .../quran/labs/androidquran/ui/PagerActivity.java | 14 +++++--------- .../feature/audio/util/AudioShareUtils.kt | 8 ++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 891ab2c90d..28c6b982b5 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -263,9 +263,6 @@ public class PagerActivity extends AppCompatActivity implements private final PagerHandler handler = new PagerHandler(this); - public static final File audioCacheDirectory = new File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath() + - File.separator + "quran_android_cache"); private ArrayList audioCacheFilePaths = new ArrayList<>(); private SuraAyah selectedStartSuraAyah = null; private SuraAyah selectedEndSuraAyah = null; @@ -367,11 +364,6 @@ 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)); @@ -1907,7 +1899,11 @@ public void shareAyahAudio(SuraAyah start, SuraAyah end) { String path = audioShareUtils.createSharableAudioFile(this, selectedStartSuraAyah, selectedEndSuraAyah, selectedQari, audioPathInfo.getUrlFormat(), audioPathInfo.getGaplessDatabase()); - shareAudioSegment(path); + if(path != null && !path.isEmpty()){ + shareAudioSegment(path); + }else{ + Toast.makeText(this, "could not share audio ayah", Toast.LENGTH_SHORT).show(); + } } else { requestDownload(audioPathInfo); } diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt index e28d0633cd..f36310be79 100644 --- a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt +++ b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt @@ -7,6 +7,7 @@ import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Environment import android.util.SparseIntArray +import android.widget.Toast import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.common.audio.model.QariItem import com.quran.labs.androidquran.feature.audio.database.SuraTimingDatabaseHandler @@ -53,6 +54,13 @@ class AudioShareUtils { selectedEndSuraAyah = getReorderedAyahPair(start, end).end selectedQari = qari + if (!audioCacheDirectory.exists()) { + if (!audioCacheDirectory.mkdirs()) { + Toast.makeText(context, "could not create directory", Toast.LENGTH_SHORT).show() + return null; + } + } + var sharablePath: String? = null return runBlocking { From d83cfbd73023ab119c830356894df578bcb83bdd Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 11 Sep 2022 16:47:03 +0400 Subject: [PATCH 14/18] Move audio sharing to a separate feature module --- app/build.gradle | 4 +- .../data/AyahInfoDatabaseHandler.java | 2 +- .../androidquran/data/QuranDataProvider.java | 2 +- .../database/AudioDatabaseVersionChecker.kt | 1 + .../androidquran/database/DatabaseHandler.kt | 1 + .../translation/ArabicDatabaseUtils.java | 2 +- .../labs/androidquran/service/AudioService.kt | 6 +- .../labs/androidquran/ui/PagerActivity.java | 4 +- .../androidquran/worker/AudioUpdateWorker.kt | 2 +- build.gradle | 1 + common/audio/build.gradle | 2 + .../timing}/SuraTimingDatabaseHandler.kt | 5 +- common/util/build.gradle | 13 + common/util/src/main/AndroidManifest.xml | 1 + .../common/util}/database/DatabaseUtils.kt | 2 +- feature/audio/build.gradle | 2 - .../database/SuraTimingDatabaseHandler.kt | 126 -------- .../feature/audio/util/AudioShareUtils.kt | 267 ---------------- feature/audioshare/build.gradle | 43 +++ .../audioshare/src/main/AndroidManifest.xml | 1 + .../feature/audioshare/AudioShareUtils.kt | 302 ++++++++++++++++++ .../audioshare}/soundfile/CheapMP3.java | 2 +- .../audioshare}/soundfile/CheapSoundFile.java | 2 +- settings.gradle | 2 + 24 files changed, 384 insertions(+), 411 deletions(-) rename {app/src/main/java/com/quran/labs/androidquran/database => common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing}/SuraTimingDatabaseHandler.kt (96%) create mode 100644 common/util/build.gradle create mode 100644 common/util/src/main/AndroidManifest.xml rename {app/src/main/java/com/quran/labs/androidquran => common/util/src/main/java/com/quran/common/util}/database/DatabaseUtils.kt (83%) delete mode 100644 feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt delete mode 100644 feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt create mode 100644 feature/audioshare/build.gradle create mode 100644 feature/audioshare/src/main/AndroidManifest.xml create mode 100644 feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt rename feature/{audio/src/main/java/com/quran/labs/androidquran/feature/audio/util => audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare}/soundfile/CheapMP3.java (99%) rename feature/{audio/src/main/java/com/quran/labs/androidquran/feature/audio/util => audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare}/soundfile/CheapSoundFile.java (99%) diff --git a/app/build.gradle b/app/build.gradle index 0662fe0044..ff023ad3fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,8 +129,10 @@ dependencies { implementation project(path: ':common:toolbar') implementation project(path: ':common:upgrade') implementation project(path: ':common:ui:core') + implementation project(path: ':common:util') implementation project(path: ':feature:audio') + implementation project(path: ':feature:audioshare') implementation project(path: ':feature:downloadmanager') implementation project(path: ':feature:qarilist') @@ -170,7 +172,7 @@ dependencies { kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}") implementation "dev.chrisbanes:insetter-ktx:0.3.1" - implementation 'com.jakewharton.timber:timber:5.0.1' + implementation "com.jakewharton.timber:timber:${timberVersion}" debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' implementation 'com.google.firebase:firebase-crashlytics:18.2.13' diff --git a/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseHandler.java b/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseHandler.java index 79040def61..be1715f33d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseHandler.java +++ b/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseHandler.java @@ -7,7 +7,7 @@ import android.graphics.RectF; import androidx.annotation.NonNull; -import com.quran.labs.androidquran.database.DatabaseUtils; +import com.quran.common.util.database.DatabaseUtils; import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.page.common.data.AyahBounds; import com.quran.page.common.data.AyahCoordinates; diff --git a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java index e7da9e8bb6..43e96f1dee 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java +++ b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java @@ -11,13 +11,13 @@ import android.net.Uri; import android.provider.BaseColumns; +import com.quran.common.util.database.DatabaseUtils; import com.quran.data.core.QuranInfo; import com.quran.labs.androidquran.BuildConfig; import com.quran.labs.androidquran.QuranApplication; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.database.DatabaseHandler; -import com.quran.labs.androidquran.database.DatabaseUtils; import com.quran.labs.androidquran.database.TranslationsDBAdapter; import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.labs.androidquran.util.QuranUtils; diff --git a/app/src/main/java/com/quran/labs/androidquran/database/AudioDatabaseVersionChecker.kt b/app/src/main/java/com/quran/labs/androidquran/database/AudioDatabaseVersionChecker.kt index 1bb60957e8..964cf60d7f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/AudioDatabaseVersionChecker.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/AudioDatabaseVersionChecker.kt @@ -1,5 +1,6 @@ package com.quran.labs.androidquran.database +import com.quran.labs.androidquran.common.audio.timing.SuraTimingDatabaseHandler import com.quran.labs.androidquran.feature.audio.VersionableDatabaseChecker import javax.inject.Inject diff --git a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt b/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt index ccb254ea9b..f1b241ed71 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt @@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat import com.quran.common.search.ArabicSearcher import com.quran.common.search.DefaultSearcher import com.quran.common.search.Searcher +import com.quran.common.util.database.DatabaseUtils import com.quran.data.model.QuranText import com.quran.data.model.VerseRange import com.quran.labs.androidquran.R diff --git a/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java b/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java index 54937c461a..3da020fcd3 100644 --- a/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java +++ b/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java @@ -3,6 +3,7 @@ import android.content.Context; import android.database.Cursor; +import com.quran.common.util.database.DatabaseUtils; import com.quran.data.core.QuranInfo; import com.quran.data.model.QuranText; import com.quran.data.model.bookmark.Bookmark; @@ -10,7 +11,6 @@ import com.quran.labs.androidquran.data.QuranFileConstants; import com.quran.data.model.SuraAyah; import com.quran.labs.androidquran.database.DatabaseHandler; -import com.quran.labs.androidquran.database.DatabaseUtils; import com.quran.labs.androidquran.util.QuranFileUtils; import java.util.ArrayList; diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index 06ac074ba6..d1d9c514d0 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -57,17 +57,17 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.media.session.MediaButtonReceiver +import com.quran.common.util.database.DatabaseUtils import com.quran.data.core.QuranInfo import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.QuranApplication import com.quran.labs.androidquran.R +import com.quran.labs.androidquran.common.audio.timing.SuraTimingDatabaseHandler.Companion.getDatabaseHandler import com.quran.labs.androidquran.dao.audio.AudioPlaybackInfo import com.quran.labs.androidquran.dao.audio.AudioRequest import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.data.QuranFileConstants -import com.quran.labs.androidquran.database.DatabaseUtils.closeCursor -import com.quran.labs.androidquran.database.SuraTimingDatabaseHandler.Companion.getDatabaseHandler import com.quran.labs.androidquran.extension.requiresBasmallah import com.quran.labs.androidquran.presenter.audio.service.AudioQueue import com.quran.labs.androidquran.service.util.AudioFocusHelper @@ -427,7 +427,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, // don't crash the app if the database is corrupt Timber.e(se) } finally { - closeCursor(cursor) + DatabaseUtils.closeCursor(cursor) } map diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 92bc163008..5cd5fa5505 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -80,7 +80,7 @@ import com.quran.labs.androidquran.di.component.activity.PagerActivityComponent; import com.quran.labs.androidquran.di.module.activity.PagerActivityModule; import com.quran.labs.androidquran.di.module.fragment.QuranPageModule; -import com.quran.labs.androidquran.feature.audio.util.AudioShareUtils; +import com.quran.labs.androidquran.feature.audioshare.AudioShareUtils; import com.quran.labs.androidquran.model.bookmark.BookmarkModel; import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils; import com.quran.labs.androidquran.presenter.audio.AudioPresenter; @@ -1917,7 +1917,7 @@ public void shareAyahAudio(SuraAyah start, SuraAyah end) { if (gaplessDatabaseExists) { if (audioFilesExist(audioPathInfo)) { AudioShareUtils audioShareUtils = new AudioShareUtils(); - String path = audioShareUtils.createSharableAudioFile(this, selectedStartSuraAyah, + String path = audioShareUtils.createBlockingSharableAudioFile(this, selectedStartSuraAyah, selectedEndSuraAyah, selectedQari, audioPathInfo.getUrlFormat(), audioPathInfo.getGaplessDatabase()); if(path != null && !path.isEmpty()){ diff --git a/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt b/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt index 2801e3d249..06111025f1 100644 --- a/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt +++ b/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt @@ -11,7 +11,7 @@ import com.quran.labs.androidquran.R import com.quran.labs.androidquran.core.worker.WorkerTaskFactory import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.database.AudioDatabaseVersionChecker -import com.quran.labs.androidquran.database.SuraTimingDatabaseHandler +import com.quran.labs.androidquran.common.audio.timing.SuraTimingDatabaseHandler import com.quran.labs.androidquran.feature.audio.AudioUpdater import com.quran.labs.androidquran.feature.audio.api.AudioUpdateService import com.quran.labs.androidquran.feature.audio.util.AudioFileCheckerImpl diff --git a/build.gradle b/build.gradle index a3af6b15dd..172a41322e 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ buildscript { workManagerVersion = '2.7.1' materialComponentsVersion = '1.6.1' coreKtxVersion = '1.7.0' + timberVersion = '5.0.1' anvilVersion = '2.4.2' moshiVersion = '1.14.0' diff --git a/common/audio/build.gradle b/common/audio/build.gradle index 123f2cdaba..8ea48773cd 100644 --- a/common/audio/build.gradle +++ b/common/audio/build.gradle @@ -16,11 +16,13 @@ android { dependencies { implementation project(":common:data") implementation project(":common:download") + implementation project(path: ':common:util') implementation deps.dagger.runtime implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" implementation "com.squareup.okio:okio:${okioVersion}" + implementation "com.jakewharton.timber:timber:${timberVersion}" testImplementation "junit:junit:${junitVersion}" testImplementation "com.google.truth:truth:${truthVersion}" diff --git a/app/src/main/java/com/quran/labs/androidquran/database/SuraTimingDatabaseHandler.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing/SuraTimingDatabaseHandler.kt similarity index 96% rename from app/src/main/java/com/quran/labs/androidquran/database/SuraTimingDatabaseHandler.kt rename to common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing/SuraTimingDatabaseHandler.kt index 42e06615b2..76e732b385 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/SuraTimingDatabaseHandler.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing/SuraTimingDatabaseHandler.kt @@ -1,14 +1,13 @@ -package com.quran.labs.androidquran.database +package com.quran.labs.androidquran.common.audio.timing import android.database.Cursor import android.database.DefaultDatabaseErrorHandler import android.database.SQLException import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabaseCorruptException +import com.quran.common.util.database.DatabaseUtils import timber.log.Timber import java.io.File -import java.lang.Exception -import java.util.HashMap class SuraTimingDatabaseHandler private constructor(path: String) { private var database: SQLiteDatabase? = null diff --git a/common/util/build.gradle b/common/util/build.gradle new file mode 100644 index 0000000000..a957916758 --- /dev/null +++ b/common/util/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion deps.android.build.compileSdkVersion + defaultConfig { + minSdkVersion deps.android.build.minSdkVersion + targetSdkVersion deps.android.build.targetSdkVersion + } +} + +dependencies { +} diff --git a/common/util/src/main/AndroidManifest.xml b/common/util/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b676f398b --- /dev/null +++ b/common/util/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseUtils.kt b/common/util/src/main/java/com/quran/common/util/database/DatabaseUtils.kt similarity index 83% rename from app/src/main/java/com/quran/labs/androidquran/database/DatabaseUtils.kt rename to common/util/src/main/java/com/quran/common/util/database/DatabaseUtils.kt index c63c2ab132..f7e64b2bd2 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseUtils.kt +++ b/common/util/src/main/java/com/quran/common/util/database/DatabaseUtils.kt @@ -1,4 +1,4 @@ -package com.quran.labs.androidquran.database +package com.quran.common.util.database import android.database.Cursor import java.lang.Exception diff --git a/feature/audio/build.gradle b/feature/audio/build.gradle index b04dd3534e..68c0dead19 100644 --- a/feature/audio/build.gradle +++ b/feature/audio/build.gradle @@ -37,6 +37,4 @@ dependencies { testImplementation "junit:junit:${junitVersion}" testImplementation "com.google.truth:truth:${truthVersion}" - - implementation 'com.jakewharton.timber:timber:5.0.1' } diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt deleted file mode 100644 index 70db297573..0000000000 --- a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/database/SuraTimingDatabaseHandler.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.quran.labs.androidquran.feature.audio.database - -import android.database.Cursor -import android.database.DefaultDatabaseErrorHandler -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteDatabaseCorruptException -import timber.log.Timber -import java.io.File -import java.lang.Exception -import java.sql.SQLException -import java.util.HashMap - -class SuraTimingDatabaseHandler private constructor(path: String) { - private var database: SQLiteDatabase? = null - - object TimingsTable { - const val TABLE_NAME = "timings" - const val COL_SURA = "sura" - const val COL_AYAH = "ayah" - const val COL_TIME = "time" - } - - object PropertiesTable { - const val TABLE_NAME = "properties" - const val COL_PROPERTY = "property" - const val COL_VALUE = "value" - } - - companion object { - private val databaseMap: MutableMap = HashMap() - - @JvmStatic - @Synchronized - fun getDatabaseHandler(path: String): SuraTimingDatabaseHandler { - var handler = databaseMap[path] - if (handler == null) { - handler = SuraTimingDatabaseHandler(path) - databaseMap[path] = handler - } - return handler - } - - @Synchronized - fun clearDatabaseHandlerIfExists(databasePath: String) { - try { - val handler = databaseMap.remove(databasePath) - if (handler != null) { - handler.database?.close() - databaseMap.remove(databasePath) - } - } catch (e: Exception) { - Timber.e(e) - } - } - } - - init { - Timber.d("opening gapless data file, %s", path) - database = try { - SQLiteDatabase.openDatabase( - path, null, - SQLiteDatabase.NO_LOCALIZED_COLLATORS, DefaultDatabaseErrorHandler() - ) - } catch (sce: SQLiteDatabaseCorruptException) { - Timber.d("database corrupted: %s", path) - null - } catch (se: SQLException) { - Timber.d("database at $path ${if (File(path).exists()) "exists " else "doesn 't exist"}") - Timber.e(se) - null - } - } - - private fun validDatabase(): Boolean = database?.isOpen ?: false - - fun getAyahTimings(sura: Int): Cursor? { - if (!validDatabase()) return null - - return try { - database?.query( - TimingsTable.TABLE_NAME, arrayOf( - TimingsTable.COL_SURA, - TimingsTable.COL_AYAH, TimingsTable.COL_TIME - ), - TimingsTable.COL_SURA + "=" + sura, - null, null, null, TimingsTable.COL_AYAH + " ASC" - ) - } catch (e: Exception) { - null - } - } - - fun getVersion(): Int { - if (!validDatabase()) { - return -1 - } - var cursor: Cursor? = null - return try { - cursor = database?.query( - PropertiesTable.TABLE_NAME, arrayOf(PropertiesTable.COL_VALUE), - PropertiesTable.COL_PROPERTY + "= 'version'", null, null, null, null - ) - if (cursor != null && cursor.moveToFirst()) { - cursor.getInt(0) - } else { - 1 - } - } catch (e: Exception) { - 1 - } finally { - DatabaseUtils.closeCursor(cursor) - } - } - - object DatabaseUtils { - @JvmStatic - fun closeCursor(cursor: Cursor?) { - try { - cursor?.close() - } catch (e: Exception) { - // no op - } - } - } -} - diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt b/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt deleted file mode 100644 index f36310be79..0000000000 --- a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/AudioShareUtils.kt +++ /dev/null @@ -1,267 +0,0 @@ -package com.quran.labs.androidquran.feature.audio.util - -import android.content.Context -import android.database.Cursor -import android.database.SQLException -import android.media.MediaMetadataRetriever -import android.net.Uri -import android.os.Environment -import android.util.SparseIntArray -import android.widget.Toast -import com.quran.data.model.SuraAyah -import com.quran.labs.androidquran.common.audio.model.QariItem -import com.quran.labs.androidquran.feature.audio.database.SuraTimingDatabaseHandler -import com.quran.labs.androidquran.feature.audio.database.SuraTimingDatabaseHandler.DatabaseUtils.closeCursor -import com.quran.labs.androidquran.feature.audio.util.soundfile.CheapSoundFile -import kotlinx.coroutines.* -import okio.BufferedSink -import okio.buffer -import okio.sink -import okio.source -import timber.log.Timber -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.util.Locale -import java.util.UUID -import kotlin.math.roundToInt - -class AudioShareUtils { - - val audioCacheDirectory = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).path + - File.separator + "quran_android_cache") - var audioCacheFilePaths: MutableList = ArrayList() - var selectedQari: QariItem? = null - var selectedStartSuraAyah: SuraAyah? = null - var selectedEndSuraAyah: SuraAyah? = null - - - data class ProperAyahOrder(val start: SuraAyah, val end: SuraAyah) - - private fun getReorderedAyahPair(start: SuraAyah, end: SuraAyah): ProperAyahOrder { - val (actualStart, actualEnd) = if (start <= end) { - start to end - } else { - end to start - } - return ProperAyahOrder(actualStart, actualEnd) - } - - - fun createSharableAudioFile(context: Context, start: SuraAyah, end: SuraAyah, qari: QariItem, urlFormat: String, gaplessDatabase: String): String? { - selectedStartSuraAyah = getReorderedAyahPair(start, end).start - selectedEndSuraAyah = getReorderedAyahPair(start, end).end - selectedQari = qari - - if (!audioCacheDirectory.exists()) { - if (!audioCacheDirectory.mkdirs()) { - Toast.makeText(context, "could not create directory", Toast.LENGTH_SHORT).show() - return null; - } - } - - var sharablePath: String? = null - - return runBlocking { - GlobalScope.launch(Dispatchers.IO) { - val mapArray = async { - getTimingData(start, end, gaplessDatabase) - }.await() - - val startAyah = start.ayah - val endAyah = end.ayah - val startSurahTimingDataArray: SparseIntArray = mapArray[0] - val endSurahTimingDataArray: SparseIntArray = mapArray[1] - - val isFirstAyahInSurah = startAyah == 1 - val startTimeOfAyahAfterEndAyah = endSurahTimingDataArray[endAyah + 1] - val isLastAyahInSurah = startTimeOfAyahAfterEndAyah == 0 - - val startAyahTime = if (isFirstAyahInSurah) { - 0 - } else { - startSurahTimingDataArray[startAyah] - } - - val endAyahTime = if (isLastAyahInSurah) { - getSurahDuration(context, - getSurahAudioPath(urlFormat, end.sura)!!) - } else { - startTimeOfAyahAfterEndAyah - } - - val startAndEndAyahAreInSameSurah = start.sura == end.sura - - if (startAndEndAyahAreInSameSurah) { - val audioSegmentPath: String = getSurahSegment( - getSurahAudioPath(urlFormat, start.sura)!!, startAyahTime, - endAyahTime)!! - audioCacheFilePaths.add(audioSegmentPath) - sharablePath = getRenamedSharableAudioFile(audioSegmentPath) - } else { - val segmentPaths = java.util.ArrayList() - val endOfSurah = -1 - val startOfSurah = 0 - val startSegmentPath: String = getSurahSegmentPath(context, urlFormat, start.sura, - startAyahTime, endOfSurah) - val lastSegmentPath: String = getSurahSegmentPath(context, urlFormat, end.sura, - startOfSurah, endAyahTime) - - for (surahIndex in start.sura..end.sura) { - val isTheFirstSurah = surahIndex == start.sura - val isMiddleSurah = surahIndex != start.sura && surahIndex != end.sura - if (isTheFirstSurah) { - segmentPaths.add(startSegmentPath) - audioCacheFilePaths.add(startSegmentPath) - continue - } - if (isMiddleSurah) { - segmentPaths.add(getSurahAudioPath(urlFormat, surahIndex)!!) - continue - } - segmentPaths.add(lastSegmentPath) - audioCacheFilePaths.add(lastSegmentPath) - } - - val audioSegmentsWereCreated = segmentPaths.isNotEmpty() - - if (audioSegmentsWereCreated) { - val sharableAudioFilePath: String = getMergedAudioFromSegments( - segmentPaths) - sharablePath = getRenamedSharableAudioFile(sharableAudioFilePath) - } else { - sharablePath = null - } - } - }.join() - return@runBlocking sharablePath - } - - } - - private fun getSurahSegmentPath(context: Context, urlFormat: String, surah: Int, - startAyahTime: Int, endAyahTime: Int): String { - var upperBoundTime = endAyahTime - val audioFilePath: String = getSurahAudioPath(urlFormat, surah)!! - val isFirstSegment = endAyahTime < 0 - if (isFirstSegment) { - upperBoundTime = getSurahDuration(context, audioFilePath) - } - return getSurahSegment(audioFilePath, startAyahTime, upperBoundTime)!! - } - - private fun getRenamedSharableAudioFile(audioSegmentPath: String): String { - val newAudioFileName: String = selectedQari!!.path + "_" + selectedStartSuraAyah!!.sura + "-" + selectedStartSuraAyah!!.ayah + "_" + selectedEndSuraAyah!!.sura + "-" + selectedEndSuraAyah!!.ayah - val newAudioFilePath: String = audioCacheDirectory.toString() + File.separator + newAudioFileName + ".mp3" - File(audioSegmentPath).renameTo(File(newAudioFilePath)) - audioCacheFilePaths.remove(audioSegmentPath) - for (path in audioCacheFilePaths) { - File(path).delete() - } - audioCacheFilePaths.clear() - return newAudioFilePath - } - - private 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() - } - - private fun getSurahAudioPath(urlFormat: String, surah: Int): String? { - return String.format(Locale.US, urlFormat, surah) - } - - private fun getTimingData(start: SuraAyah, end: SuraAyah, gaplessDatabase: String): ArrayList { - val databasePath = gaplessDatabase!! - - val db: SuraTimingDatabaseHandler = SuraTimingDatabaseHandler.getDatabaseHandler( - databasePath) - var firstSurahMap = SparseIntArray() - var lastSurahMap = SparseIntArray() - var firstSurahCursor: Cursor? = null - var lastSurahCursor: Cursor? = null - - try { - firstSurahCursor = db.getAyahTimings(start.sura) - firstSurahMap = populateArrayFromCursor(firstSurahCursor)!! - lastSurahCursor = db.getAyahTimings(end.sura) - lastSurahMap = populateArrayFromCursor(lastSurahCursor) - } catch (sqlException: SQLException) { - Timber.e(sqlException) - } finally { - closeCursor(firstSurahCursor) - closeCursor(lastSurahCursor) - } - - return ArrayList( - listOf(firstSurahMap, lastSurahMap)) - } - - private fun populateArrayFromCursor(cursor: Cursor?): SparseIntArray { - val sparseIntArray = SparseIntArray() - if (cursor != null && cursor.moveToFirst()) { - do { - val ayah = cursor.getInt(1) - val time = cursor.getInt(2) - sparseIntArray.put(ayah, time) - } while (cursor.moveToNext()) - } - return sparseIntArray - } - - private fun getSurahSegment(path: String, lowerCut: Int, upperCut: Int): String? { - if (lowerCut == 0 && upperCut == 0) { - return null - } - val tempAudioName = UUID.randomUUID().toString() + ".mp3" - val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) - val soundFile = arrayOfNulls(1) - try { - soundFile[0] = CheapSoundFile.create(path, null) - val startTime = lowerCut.toFloat() / 1000 - val endTime = upperCut.toFloat() / 1000 - val samplesPerFrame = soundFile[0]?.samplesPerFrame - val sampleRate = soundFile[0]?.sampleRate - val avg = sampleRate?.div(samplesPerFrame!!) - val startFrames = (startTime * avg!!).roundToInt() - val endFrames = (endTime * avg).roundToInt() - soundFile[0]?.WriteFile(destFile, startFrames, endFrames - startFrames) - } catch (e: IOException) { - e.printStackTrace() - } - return destFile.absolutePath - } - - private fun getMergedAudioFromSegments(segments: ArrayList): String { - var mergedAudioPath = segments[0] - if (segments.size > 1) { - for (i in 1 until segments.size) { - mergedAudioPath = mergeAudios(mergedAudioPath, segments[i])!! - audioCacheFilePaths.add(mergedAudioPath) - } - } - return mergedAudioPath - } - - private fun mergeAudios(path1: String, path2: String): String? { - val tempAudioName = UUID.randomUUID().toString() + ".mp3" - val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) - try { - val bufferedSink: BufferedSink = destFile.sink().buffer() - bufferedSink.writeAll(File(path1).source()) - bufferedSink.writeAll(File(path2).source()) - bufferedSink.close() - return destFile.path - } catch (e2: FileNotFoundException) { - e2.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } - return null - } - - -} diff --git a/feature/audioshare/build.gradle b/feature/audioshare/build.gradle new file mode 100644 index 0000000000..e2d7298566 --- /dev/null +++ b/feature/audioshare/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion deps.android.build.compileSdkVersion + defaultConfig { + minSdkVersion deps.android.build.minSdkVersion + targetSdkVersion deps.android.build.targetSdkVersion + } + + buildTypes { + beta { + consumerProguardFiles 'proguard.cfg' + matchingFallbacks = ['debug'] + } + + release { + consumerProguardFiles 'proguard.cfg' + } + } +} + +dependencies { + implementation project(path: ':common:audio') + implementation project(path: ':common:data') + implementation project(path: ':common:util') + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + + implementation "com.squareup.okio:okio:${okioVersion}" + + implementation "com.squareup.moshi:moshi:${moshiVersion}" + kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}") + + implementation "com.squareup.retrofit2:retrofit:${retrofitVersion}" + + testImplementation "junit:junit:${junitVersion}" + testImplementation "com.google.truth:truth:${truthVersion}" + + implementation "com.jakewharton.timber:timber:${timberVersion}" +} diff --git a/feature/audioshare/src/main/AndroidManifest.xml b/feature/audioshare/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b34d4f3292 --- /dev/null +++ b/feature/audioshare/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt new file mode 100644 index 0000000000..a36bcebd7a --- /dev/null +++ b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt @@ -0,0 +1,302 @@ +package com.quran.labs.androidquran.feature.audioshare + +import android.content.Context +import android.database.Cursor +import android.database.SQLException +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Environment +import android.util.SparseIntArray +import android.widget.Toast +import com.quran.common.util.database.DatabaseUtils +import com.quran.data.model.SuraAyah +import com.quran.labs.androidquran.common.audio.model.QariItem +import com.quran.labs.androidquran.common.audio.timing.SuraTimingDatabaseHandler +import com.quran.labs.androidquran.feature.audioshare.soundfile.CheapSoundFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import okio.BufferedSink +import okio.buffer +import okio.sink +import okio.source +import timber.log.Timber +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.Locale +import java.util.UUID +import kotlin.math.roundToInt + +class AudioShareUtils { + fun createBlockingSharableAudioFile( + context: Context, + start: SuraAyah, + end: SuraAyah, + qari: QariItem, + urlFormat: String, + gaplessDatabase: String + ): String? { + return runBlocking { + createSharableAudioFile(context, start, end, qari, urlFormat, gaplessDatabase) + } + } + + suspend fun createSharableAudioFile( + context: Context, + start: SuraAyah, + end: SuraAyah, + qari: QariItem, + urlFormat: String, + gaplessDatabase: String + ): String? { + assert(end >= start) + + val audioCacheDirectory = File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_MUSIC).path + File.separator + "quran_android_cache") + + if (!audioCacheDirectory.exists()) { + if (!audioCacheDirectory.mkdirs()) { + Toast.makeText(context, "could not create directory", Toast.LENGTH_SHORT).show() + return null; + } + } + + var sharablePath: String? + val audioCacheFilePaths = mutableListOf() + withContext(Dispatchers.IO) { + val mapArray = getTimingData(start, end, gaplessDatabase) + + val startAyah = start.ayah + val endAyah = end.ayah + val startSurahTimingDataArray: SparseIntArray = mapArray[0] + val endSurahTimingDataArray: SparseIntArray = mapArray[1] + + val isFirstAyahInSurah = startAyah == 1 + val startTimeOfAyahAfterEndAyah = endSurahTimingDataArray[endAyah + 1] + val isLastAyahInSurah = startTimeOfAyahAfterEndAyah == 0 + + val startAyahTime = if (isFirstAyahInSurah) { + 0 + } else { + startSurahTimingDataArray[startAyah] + } + + val endAyahTime = if (isLastAyahInSurah) { + getSurahDuration(context, getSurahAudioPath(urlFormat, end.sura)) + } else { + startTimeOfAyahAfterEndAyah + } + + val startAndEndAyahAreInSameSurah = start.sura == end.sura + + if (startAndEndAyahAreInSameSurah) { + val audioSegmentPath: String = getSurahSegment( + audioCacheDirectory, getSurahAudioPath(urlFormat, start.sura), startAyahTime, endAyahTime + )!! + audioCacheFilePaths.add(audioSegmentPath) + sharablePath = getRenamedSharableAudioFile( + qari, + start, + end, + audioSegmentPath, + audioCacheDirectory.toString(), + audioCacheFilePaths + ) + audioCacheFilePaths.clear() + } else { + val segmentPaths = mutableListOf() + val endOfSurah = -1 + val startOfSurah = 0 + val startSegmentPath: String = getSurahSegmentPath( + context, audioCacheDirectory, urlFormat, start.sura, startAyahTime, endOfSurah + ) + val lastSegmentPath: String = getSurahSegmentPath( + context, audioCacheDirectory , urlFormat, end.sura, startOfSurah, endAyahTime + ) + + for (surahIndex in start.sura..end.sura) { + val isTheFirstSurah = surahIndex == start.sura + val isMiddleSurah = surahIndex != start.sura && surahIndex != end.sura + if (isTheFirstSurah) { + segmentPaths.add(startSegmentPath) + audioCacheFilePaths.add(startSegmentPath) + continue + } + if (isMiddleSurah) { + segmentPaths.add(getSurahAudioPath(urlFormat, surahIndex)) + continue + } + segmentPaths.add(lastSegmentPath) + audioCacheFilePaths.add(lastSegmentPath) + } + + val audioSegmentsWereCreated = segmentPaths.isNotEmpty() + + if (audioSegmentsWereCreated) { + val (sharableAudioFilePath, cacheUpdates) = + getMergedAudioFromSegments(audioCacheDirectory, segmentPaths) + audioCacheFilePaths.addAll(cacheUpdates) + sharablePath = getRenamedSharableAudioFile( + qari, + start, + end, + sharableAudioFilePath, + audioCacheDirectory.toString(), + audioCacheFilePaths + ) + audioCacheFilePaths.clear() + } else { + sharablePath = null + } + } + } + return sharablePath + } + + private fun getSurahSegmentPath(context: Context, + audioCacheDirectory: File, + urlFormat: String, + surah: Int, + startAyahTime: Int, + endAyahTime: Int + ): String { + var upperBoundTime = endAyahTime + val audioFilePath: String = getSurahAudioPath(urlFormat, surah) + val isFirstSegment = endAyahTime < 0 + if (isFirstSegment) { + upperBoundTime = getSurahDuration(context, audioFilePath) + } + return getSurahSegment(audioCacheDirectory, audioFilePath, startAyahTime, upperBoundTime)!! + } + + private fun getRenamedSharableAudioFile( + qari: QariItem, + start: SuraAyah, + end: SuraAyah, + audioSegmentPath: String, + audioCacheDirectory: String, + cachedPaths: List + ): String { + val newAudioFileName: String = + qari.path + "_" + start.sura + "-" + start.ayah + "_" + end.sura + "-" + end.ayah + val newAudioFilePath: String = audioCacheDirectory + File.separator + newAudioFileName + ".mp3" + File(audioSegmentPath).renameTo(File(newAudioFilePath)) + cachedPaths + .filter { it != audioSegmentPath } + .onEach { + File(it).delete() + } + return newAudioFilePath + } + + private 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() + } + + private fun getSurahAudioPath(urlFormat: String, surah: Int): String { + return String.format(Locale.US, urlFormat, surah) + } + + private fun getTimingData(start: SuraAyah, end: SuraAyah, gaplessDatabase: String): ArrayList { + val db: SuraTimingDatabaseHandler = SuraTimingDatabaseHandler.getDatabaseHandler(gaplessDatabase) + var firstSurahMap = SparseIntArray() + var lastSurahMap = SparseIntArray() + var firstSurahCursor: Cursor? = null + var lastSurahCursor: Cursor? = null + + try { + firstSurahCursor = db.getAyahTimings(start.sura) + firstSurahMap = populateArrayFromCursor(firstSurahCursor)!! + lastSurahCursor = db.getAyahTimings(end.sura) + lastSurahMap = populateArrayFromCursor(lastSurahCursor) + } catch (sqlException: SQLException) { + Timber.e(sqlException) + } finally { + DatabaseUtils.closeCursor(firstSurahCursor) + DatabaseUtils.closeCursor(lastSurahCursor) + } + + return ArrayList( + listOf(firstSurahMap, lastSurahMap)) + } + + private fun populateArrayFromCursor(cursor: Cursor?): SparseIntArray { + val sparseIntArray = SparseIntArray() + if (cursor != null && cursor.moveToFirst()) { + do { + val ayah = cursor.getInt(1) + val time = cursor.getInt(2) + sparseIntArray.put(ayah, time) + } while (cursor.moveToNext()) + } + return sparseIntArray + } + + private fun getSurahSegment( + audioCacheDirectory: File, + path: String, + lowerCut: Int, + upperCut: Int + ): String? { + if (lowerCut == 0 && upperCut == 0) { + return null + } + val tempAudioName = UUID.randomUUID().toString() + ".mp3" + val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) + val soundFile = arrayOfNulls(1) + try { + soundFile[0] = CheapSoundFile.create(path, null) + val startTime = lowerCut.toFloat() / 1000 + val endTime = upperCut.toFloat() / 1000 + val samplesPerFrame = soundFile[0]?.samplesPerFrame + val sampleRate = soundFile[0]?.sampleRate + val avg = sampleRate?.div(samplesPerFrame!!) + val startFrames = (startTime * avg!!).roundToInt() + val endFrames = (endTime * avg).roundToInt() + soundFile[0]?.WriteFile(destFile, startFrames, endFrames - startFrames) + } catch (e: IOException) { + e.printStackTrace() + } + return destFile.absolutePath + } + + private fun getMergedAudioFromSegments( + audioCacheDirectory: File, + segments: List + ): Pair> { + var mergedAudioPath = segments[0] + val extraCacheFilePaths = mutableListOf() + if (segments.size > 1) { + for (i in 1 until segments.size) { + val path = mergeAudios(audioCacheDirectory, mergedAudioPath, segments[i]) + if (path != null) { + mergedAudioPath = path + extraCacheFilePaths.add(mergedAudioPath) + } + } + } + return mergedAudioPath to extraCacheFilePaths + } + + private fun mergeAudios(audioCacheDirectory: File, path1: String, path2: String): String? { + val tempAudioName = UUID.randomUUID().toString() + ".mp3" + val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) + try { + val bufferedSink: BufferedSink = destFile.sink().buffer() + bufferedSink.writeAll(File(path1).source()) + bufferedSink.writeAll(File(path2).source()) + bufferedSink.close() + return destFile.path + } catch (e2: FileNotFoundException) { + e2.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + return null + } +} diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapMP3.java b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/soundfile/CheapMP3.java similarity index 99% rename from feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapMP3.java rename to feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/soundfile/CheapMP3.java index bedbc1af1c..2562eecfc5 100644 --- a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapMP3.java +++ b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/soundfile/CheapMP3.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.quran.labs.androidquran.feature.audio.util.soundfile; +package com.quran.labs.androidquran.feature.audioshare.soundfile; import java.io.File; import java.io.FileInputStream; diff --git a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapSoundFile.java b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/soundfile/CheapSoundFile.java similarity index 99% rename from feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapSoundFile.java rename to feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/soundfile/CheapSoundFile.java index 9ea9dd27fa..9d34bda407 100644 --- a/feature/audio/src/main/java/com/quran/labs/androidquran/feature/audio/util/soundfile/CheapSoundFile.java +++ b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/soundfile/CheapSoundFile.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.quran.labs.androidquran.feature.audio.util.soundfile; +package com.quran.labs.androidquran.feature.audioshare.soundfile; import java.io.File; import java.io.FileInputStream; diff --git a/settings.gradle b/settings.gradle index 1bd2ed2b15..4198ddcadb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,10 +11,12 @@ include ':common:reading' include ':common:recitation' include ':common:search' include ':common:toolbar' +include ':common:util' include ':common:upgrade' include ':common:ui:core' include ':feature:analytics-noop' include ':feature:audio' +include ':feature:audioshare' include ':feature:downloadmanager' include ':feature:qarilist' include ':feature:recitation' From 6a19f9cbb4c19360c4177887eccee0d642cc05f8 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 11 Sep 2022 16:59:55 +0400 Subject: [PATCH 15/18] Initial clean up to PagerActivity for audio share --- .../presenter/audio/AudioPresenter.kt | 21 +------ .../labs/androidquran/ui/PagerActivity.java | 62 +++++++++---------- .../labs/androidquran/util/AudioUtils.kt | 10 --- 3 files changed, 30 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt index 1fde5313a1..0bb2d69385 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt @@ -4,13 +4,13 @@ import android.content.Context import android.content.Intent import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.R +import com.quran.labs.androidquran.common.audio.model.AudioDownloadMetadata 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.QuranDisplayData import com.quran.labs.androidquran.presenter.Presenter import com.quran.labs.androidquran.service.QuranDownloadService -import com.quran.labs.androidquran.common.audio.model.AudioDownloadMetadata import com.quran.labs.androidquran.service.util.ServiceIntentHelper import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.util.AudioUtils @@ -33,7 +33,7 @@ constructor(private val quranDisplayData: QuranDisplayData, rangeRepeat: Int, enforceRange: Boolean, shouldStream: Boolean) { - val audioPathInfo = getLocalAudioPathInfo(qari) + val audioPathInfo = audioUtil.getLocalAudioPathInfo(qari) if (audioPathInfo != null) { // override streaming if all the files are already downloaded val stream = if (shouldStream) { @@ -136,23 +136,6 @@ constructor(private val quranDisplayData: QuranDisplayData, return ServiceIntentHelper.getAudioDownloadIntent(context, url, destination, title) } - private fun getLocalAudioPathInfo(qari: QariItem): AudioPathInfo? { - pagerActivity?.let { - val localPath = audioUtil.getLocalQariUrl(qari) - if (localPath != null) { - val databasePath = audioUtil.getQariDatabasePathIfGapless(qari) - val urlFormat = if (databasePath.isNullOrEmpty()) { - localPath + File.separator + "%d" + File.separator + - "%d" + AudioUtils.AUDIO_EXTENSION - } else { - localPath + File.separator + "%03d" + AudioUtils.AUDIO_EXTENSION - } - return AudioPathInfo(urlFormat, localPath, databasePath) - } - } - return null - } - private fun haveAllFiles(audioPathInfo: AudioPathInfo, start: SuraAyah, end: SuraAyah): Boolean { return audioUtil.haveAllFiles(audioPathInfo.urlFormat, audioPathInfo.localDirectory, start, end, audioPathInfo.gaplessDatabase != null) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 5cd5fa5505..370e50518c 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -260,11 +260,6 @@ public class PagerActivity extends AppCompatActivity implements private final PagerHandler handler = new PagerHandler(this); - private ArrayList audioCacheFilePaths = new ArrayList<>(); - private SuraAyah selectedStartSuraAyah = null; - private SuraAyah selectedEndSuraAyah = null; - private QariItem selectedQari = null; - private static class PagerHandler extends Handler { private final WeakReference activity; @@ -1902,59 +1897,58 @@ public void onError(@NonNull Throwable e) { } public void shareAyahAudio(SuraAyah start, SuraAyah end) { - audioCacheFilePaths.clear(); - - kotlin.Pair pair2 = getReorderedAyatPair(start, end); - selectedStartSuraAyah = (SuraAyah) pair2.component1(); - selectedEndSuraAyah = (SuraAyah) pair2.component2(); + final SuraAyah selectedStartSuraAyah; + final SuraAyah selectedEndSuraAyah; + if (start.compareTo(end) <= 0) { + selectedStartSuraAyah = start; + selectedEndSuraAyah = end; + } else { + selectedStartSuraAyah = end; + selectedEndSuraAyah = start; + Timber.e(new IllegalStateException("End isn't larger than the start: " + start + " to " + end)); + } - selectedQari = audioStatusBar.getAudioInfo(); + final QariItem selectedQari = audioStatusBar.getAudioInfo(); AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(selectedQari); assert audioPathInfo != null; boolean gaplessDatabaseExists = audioPathInfo.getGaplessDatabase() != null; if (gaplessDatabaseExists) { - if (audioFilesExist(audioPathInfo)) { + if (audioFilesExist(audioPathInfo, selectedStartSuraAyah, selectedEndSuraAyah)) { AudioShareUtils audioShareUtils = new AudioShareUtils(); - String path = audioShareUtils.createBlockingSharableAudioFile(this, selectedStartSuraAyah, - selectedEndSuraAyah, selectedQari, audioPathInfo.getUrlFormat(), - audioPathInfo.getGaplessDatabase()); - if(path != null && !path.isEmpty()){ + String path = audioShareUtils.createBlockingSharableAudioFile( + this, + selectedStartSuraAyah, + selectedEndSuraAyah, + selectedQari, + audioPathInfo.getUrlFormat(), + audioPathInfo.getGaplessDatabase() + ); + + if (path != null && !path.isEmpty()){ shareAudioSegment(path); - }else{ + } else { Toast.makeText(this, "could not share audio ayah", Toast.LENGTH_SHORT).show(); } } else { - requestDownload(audioPathInfo); + requestDownload(audioPathInfo, selectedQari, start, end); } } } - private kotlin.Pair getReorderedAyatPair(SuraAyah start, SuraAyah end) { - 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); - } - return pair; - } - - private boolean audioFilesExist(AudioPathInfo audioPathInfo) { + private boolean audioFilesExist(AudioPathInfo audioPathInfo, SuraAyah start, SuraAyah end) { return audioUtils.haveAllFiles(audioPathInfo.getUrlFormat(), audioPathInfo.getLocalDirectory(), - selectedStartSuraAyah, selectedEndSuraAyah, true); + start, end, true); } private void shareAudioSegment(String path) { shareUtil.shareAudioFileIntent(PagerActivity.this, new File(path)); } - private void requestDownload(AudioPathInfo audioPathInfo) { + private void requestDownload(AudioPathInfo audioPathInfo, QariItem qari, SuraAyah start, SuraAyah end) { AudioRequest audioRequest = new AudioRequest( - selectedStartSuraAyah, selectedEndSuraAyah, selectedQari, 0, 0, true, false, audioPathInfo); + start, end, qari, 0, 0, true, false, audioPathInfo); Intent downloadIntent = audioPresenter.getDownloadIntent(this, audioRequest); if (downloadIntent != null) { diff --git a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt index 19aa4aa45d..6f594059d6 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt @@ -83,16 +83,6 @@ class AudioUtils @Inject constructor( return if (rootDirectory == null) null else rootDirectory + item.path } - fun getLocalQariUri(item: QariItem): String? { - val rootDirectory = quranFileUtils.audioFileDirectory() - return if (rootDirectory == null) null else - rootDirectory + item.path + File.separator + if (item.isGapless) { - "%03d$AUDIO_EXTENSION" - } else { - "%d" + File.separator + "%d" + AUDIO_EXTENSION - } - } - fun getQariDatabasePathIfGapless(item: QariItem): String? { var databaseName = item.databaseName if (databaseName != null) { From 1612b9cf9ee16641d8480b8e9a9e81b0c88880a0 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 11 Sep 2022 17:42:21 +0400 Subject: [PATCH 16/18] Move some methods into audio file utilities --- .../androidquran/dao/audio/AudioRequest.kt | 4 +- .../presenter/audio/AudioPresenter.kt | 6 ++- .../labs/androidquran/ui/PagerActivity.java | 24 +++------ .../labs/androidquran/util/AudioUtils.kt | 36 +------------- .../androidquran/worker/AudioUpdateWorker.kt | 22 ++++++--- .../cache/command/GaplessAudioInfoCommand.kt | 6 +-- .../cache/command/GappedAudioInfoCommand.kt | 4 +- .../common/audio/model}/AudioPathInfo.kt | 2 +- .../common/audio/util/AudioFileTools.kt | 18 +++++++ .../common/audio/util/AudioFileUtil.kt | 49 +++++++++++++++---- 10 files changed, 93 insertions(+), 78 deletions(-) rename {app/src/main/java/com/quran/labs/androidquran/dao/audio => common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model}/AudioPathInfo.kt (81%) create mode 100644 common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileTools.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt b/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt index d24f389fff..02a73133b3 100644 --- a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt +++ b/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt @@ -3,6 +3,7 @@ package com.quran.labs.androidquran.dao.audio import android.os.Parcelable import com.quran.labs.androidquran.common.audio.model.QariItem import com.quran.data.model.SuraAyah +import com.quran.labs.androidquran.common.audio.model.AudioPathInfo import kotlinx.parcelize.Parcelize @Parcelize @@ -13,7 +14,8 @@ data class AudioRequest(val start: SuraAyah, val rangeRepeatInfo: Int = 0, val enforceBounds: Boolean, val shouldStream: Boolean, - val audioPathInfo: AudioPathInfo) : Parcelable { + val audioPathInfo: AudioPathInfo +) : Parcelable { fun isGapless() = qari.isGapless fun needsIsti3athaAudio() = !isGapless() || audioPathInfo.gaplessDatabase?.contains("minshawi_murattal") ?: false diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt index 0bb2d69385..f42fb30e01 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt @@ -5,8 +5,9 @@ import android.content.Intent import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.audio.model.AudioDownloadMetadata +import com.quran.labs.androidquran.common.audio.model.AudioPathInfo import com.quran.labs.androidquran.common.audio.model.QariItem -import com.quran.labs.androidquran.dao.audio.AudioPathInfo +import com.quran.labs.androidquran.common.audio.util.AudioFileUtil import com.quran.labs.androidquran.dao.audio.AudioRequest import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.presenter.Presenter @@ -22,6 +23,7 @@ import javax.inject.Inject class AudioPresenter @Inject constructor(private val quranDisplayData: QuranDisplayData, private val audioUtil: AudioUtils, + private val audioFileUtil: AudioFileUtil, private val quranFileUtils: QuranFileUtils) : Presenter { private var pagerActivity: PagerActivity? = null private var lastAudioRequest: AudioRequest? = null @@ -33,7 +35,7 @@ constructor(private val quranDisplayData: QuranDisplayData, rangeRepeat: Int, enforceRange: Boolean, shouldStream: Boolean) { - val audioPathInfo = audioUtil.getLocalAudioPathInfo(qari) + val audioPathInfo = audioFileUtil.getLocalAudioPathInfo(qari) if (audioPathInfo != null) { // override streaming if all the files are already downloaded val stream = if (shouldStream) { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index 370e50518c..8ba6af7a6a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -71,7 +71,8 @@ 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.common.audio.model.AudioPathInfo; +import com.quran.labs.androidquran.common.audio.util.AudioFileUtil; import com.quran.labs.androidquran.dao.audio.AudioRequest; import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.data.QuranDataProvider; @@ -148,7 +149,6 @@ import io.reactivex.rxjava3.observers.DisposableObserver; import io.reactivex.rxjava3.observers.DisposableSingleObserver; import io.reactivex.rxjava3.schedulers.Schedulers; -import kotlin.TuplesKt; import timber.log.Timber; /** @@ -240,6 +240,7 @@ public class PagerActivity extends AppCompatActivity implements @Inject QuranAppUtils quranAppUtils; @Inject ShareUtil shareUtil; @Inject AudioUtils audioUtils; + @Inject AudioFileUtil audioFileUtil; @Inject QuranDisplayData quranDisplayData; @Inject QuranInfo quranInfo; @Inject QuranFileUtils quranFileUtils; @@ -1897,30 +1898,19 @@ public void onError(@NonNull Throwable e) { } public void shareAyahAudio(SuraAyah start, SuraAyah end) { - final SuraAyah selectedStartSuraAyah; - final SuraAyah selectedEndSuraAyah; - if (start.compareTo(end) <= 0) { - selectedStartSuraAyah = start; - selectedEndSuraAyah = end; - } else { - selectedStartSuraAyah = end; - selectedEndSuraAyah = start; - Timber.e(new IllegalStateException("End isn't larger than the start: " + start + " to " + end)); - } - final QariItem selectedQari = audioStatusBar.getAudioInfo(); - AudioPathInfo audioPathInfo = audioUtils.getLocalAudioPathInfo(selectedQari); + AudioPathInfo audioPathInfo = audioFileUtil.getLocalAudioPathInfo(selectedQari); assert audioPathInfo != null; boolean gaplessDatabaseExists = audioPathInfo.getGaplessDatabase() != null; if (gaplessDatabaseExists) { - if (audioFilesExist(audioPathInfo, selectedStartSuraAyah, selectedEndSuraAyah)) { + if (audioFilesExist(audioPathInfo, start, end)) { AudioShareUtils audioShareUtils = new AudioShareUtils(); String path = audioShareUtils.createBlockingSharableAudioFile( this, - selectedStartSuraAyah, - selectedEndSuraAyah, + start, + end, selectedQari, audioPathInfo.getUrlFormat(), audioPathInfo.getGaplessDatabase() diff --git a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt index 6f594059d6..ab6d7d4485 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/AudioUtils.kt @@ -8,12 +8,11 @@ import com.quran.data.model.SuraAyah import com.quran.data.model.audio.Qari 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 timber.log.Timber import java.io.File import java.util.Locale import javax.inject.Inject -import timber.log.Timber class AudioUtils @Inject constructor( private val quranInfo: QuranInfo, @@ -78,22 +77,6 @@ class AudioUtils @Inject constructor( } } - fun getLocalQariUrl(item: QariItem): String? { - val rootDirectory = quranFileUtils.audioFileDirectory() - return if (rootDirectory == null) null else rootDirectory + item.path - } - - fun getQariDatabasePathIfGapless(item: QariItem): String? { - var databaseName = item.databaseName - if (databaseName != null) { - val path = getLocalQariUrl(item) - if (path != null) { - databaseName = path + File.separator + databaseName + DB_EXTENSION - } - } - return databaseName - } - fun getLastAyahToPlay( startAyah: SuraAyah, currentPage: Int, @@ -281,25 +264,8 @@ class AudioUtils @Inject constructor( } } - fun getLocalAudioPathInfo(qari: QariItem): AudioPathInfo? { - val localPath = getLocalQariUrl(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 - } - companion object { const val ZIP_EXTENSION = ".zip" const val AUDIO_EXTENSION = ".mp3" - - private const val DB_EXTENSION = ".db" } } diff --git a/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt b/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt index 06111025f1..116c7fc325 100644 --- a/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt +++ b/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt @@ -8,10 +8,11 @@ import androidx.work.CoroutineWorker import androidx.work.ListenableWorker import androidx.work.WorkerParameters import com.quran.labs.androidquran.R +import com.quran.labs.androidquran.common.audio.timing.SuraTimingDatabaseHandler +import com.quran.labs.androidquran.common.audio.util.AudioFileUtil import com.quran.labs.androidquran.core.worker.WorkerTaskFactory import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.database.AudioDatabaseVersionChecker -import com.quran.labs.androidquran.common.audio.timing.SuraTimingDatabaseHandler import com.quran.labs.androidquran.feature.audio.AudioUpdater import com.quran.labs.androidquran.feature.audio.api.AudioUpdateService import com.quran.labs.androidquran.feature.audio.util.AudioFileCheckerImpl @@ -31,7 +32,8 @@ class AudioUpdateWorker( private val audioUpdateService: AudioUpdateService, private val audioUtils: AudioUtils, private val quranFileUtils: QuranFileUtils, - private val quranSettings: QuranSettings + private val quranSettings: QuranSettings, + private val audioFileUtil: AudioFileUtil ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result = coroutineScope { @@ -54,14 +56,14 @@ class AudioUpdateWorker( localFilesToDelete.forEach { localUpdate -> if (localUpdate.needsDatabaseUpgrade) { // delete the database - val dbPath = audioUtils.getQariDatabasePathIfGapless(localUpdate.qari) + val dbPath = audioFileUtil.getQariDatabasePathIfGapless(localUpdate.qari) dbPath?.let { SuraTimingDatabaseHandler.clearDatabaseHandlerIfExists(it) } Timber.d("would remove %s", dbPath) File(dbPath).delete() } val qari = localUpdate.qari - val path = audioUtils.getLocalQariUrl(qari) + val path = audioFileUtil.getLocalQariUrl(qari) localUpdate.files.forEach { // delete the file val filePath = if (qari.isGapless) { @@ -116,15 +118,21 @@ class AudioUpdateWorker( private val audioUpdateService: AudioUpdateService, private val audioUtils: AudioUtils, private val quranFileUtils: QuranFileUtils, - private val quranSettings: QuranSettings + private val quranSettings: QuranSettings, + private val audioFileUtil: AudioFileUtil ) : WorkerTaskFactory { override fun makeWorker( appContext: Context, workerParameters: WorkerParameters ): ListenableWorker { return AudioUpdateWorker( - appContext, workerParameters, audioUpdateService, audioUtils, quranFileUtils, - quranSettings + appContext, + workerParameters, + audioUpdateService, + audioUtils, + quranFileUtils, + quranSettings, + audioFileUtil ) } } diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GaplessAudioInfoCommand.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GaplessAudioInfoCommand.kt index 248f36de6b..1be405e6cf 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GaplessAudioInfoCommand.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GaplessAudioInfoCommand.kt @@ -1,6 +1,6 @@ package com.quran.labs.androidquran.common.audio.cache.command -import com.quran.labs.androidquran.common.audio.util.AudioFileUtil +import com.quran.labs.androidquran.common.audio.util.AudioFileTools import okio.FileSystem import okio.Path import javax.inject.Inject @@ -12,7 +12,7 @@ class GaplessAudioInfoCommand @Inject constructor(private val fileSystem: FileSy } private fun fullGaplessDownloads(path: Path): List { - val paths = AudioFileUtil.filesMatchingSuffixWithSuffixRemoved(fileSystem, path, ".mp3") + val paths = AudioFileTools.filesMatchingSuffixWithSuffixRemoved(fileSystem, path, ".mp3") return paths .filter { it.length == 3 } .mapNotNull { it.toIntOrNull() } @@ -20,7 +20,7 @@ class GaplessAudioInfoCommand @Inject constructor(private val fileSystem: FileSy } private fun partialGaplessDownloads(path: Path): List { - val paths = AudioFileUtil.filesMatchingSuffixWithSuffixRemoved(fileSystem, path, ".mp3.part") + val paths = AudioFileTools.filesMatchingSuffixWithSuffixRemoved(fileSystem, path, ".mp3.part") return paths .filter { it.length == 3 } .mapNotNull { it.toIntOrNull() } diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt index 1cad0ca264..fc51123cd3 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt @@ -2,7 +2,7 @@ package com.quran.labs.androidquran.common.audio.cache.command import com.quran.data.core.QuranInfo import com.quran.labs.androidquran.common.audio.model.PartiallyDownloadedSura -import com.quran.labs.androidquran.common.audio.util.AudioFileUtil +import com.quran.labs.androidquran.common.audio.util.AudioFileTools import okio.FileSystem import okio.Path import javax.inject.Inject @@ -17,7 +17,7 @@ class GappedAudioInfoCommand @Inject constructor( .filter { it.name.toIntOrNull() in 1..114 } .associate { directory -> val gappedDownloads = - AudioFileUtil.filesMatchingSuffixWithSuffixRemoved(fileSystem, directory, ".mp3") + AudioFileTools.filesMatchingSuffixWithSuffixRemoved(fileSystem, directory, ".mp3") .mapNotNull { it.toIntOrNull() } .filter { it in 1..286 } directory.toFile().nameWithoutExtension.toInt() to gappedDownloads diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioPathInfo.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/AudioPathInfo.kt similarity index 81% rename from app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioPathInfo.kt rename to common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/AudioPathInfo.kt index 7a1cd0e563..ff9f483d49 100644 --- a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioPathInfo.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/AudioPathInfo.kt @@ -1,4 +1,4 @@ -package com.quran.labs.androidquran.dao.audio +package com.quran.labs.androidquran.common.audio.model import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileTools.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileTools.kt new file mode 100644 index 0000000000..7a5643decb --- /dev/null +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileTools.kt @@ -0,0 +1,18 @@ +package com.quran.labs.androidquran.common.audio.util + +import okio.FileSystem +import okio.Path + +object AudioFileTools { + + fun filesMatchingSuffixWithSuffixRemoved(fileSystem: FileSystem, path: Path, suffix: String): List { + return fileNamesMatchingSuffix(fileSystem, path, suffix) + .map { it.name.removeSuffix(suffix) } + } + + private fun fileNamesMatchingSuffix(fileSystem: FileSystem, path: Path, suffix: String): List { + return fileSystem.listOrNull(path) + ?.filter { it.name.endsWith(suffix) } + ?: emptyList() + } +} diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileUtil.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileUtil.kt index 1ee5f44793..5a40fb24f2 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileUtil.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/AudioFileUtil.kt @@ -1,18 +1,47 @@ package com.quran.labs.androidquran.common.audio.util -import okio.FileSystem -import okio.Path +import com.quran.data.core.QuranFileManager +import com.quran.labs.androidquran.common.audio.model.AudioPathInfo +import com.quran.labs.androidquran.common.audio.model.QariItem +import java.io.File +import javax.inject.Inject -object AudioFileUtil { +class AudioFileUtil @Inject constructor(private val quranFileManager: QuranFileManager) { - fun filesMatchingSuffixWithSuffixRemoved(fileSystem: FileSystem, path: Path, suffix: String): List { - return fileNamesMatchingSuffix(fileSystem, path, suffix) - .map { it.name.removeSuffix(suffix) } + fun getLocalQariUrl(item: QariItem): String? { + val rootDirectory = quranFileManager.audioFileDirectory() + return if (rootDirectory == null) null else rootDirectory + item.path } - private fun fileNamesMatchingSuffix(fileSystem: FileSystem, path: Path, suffix: String): List { - return fileSystem.listOrNull(path) - ?.filter { it.name.endsWith(suffix) } - ?: emptyList() + fun getQariDatabasePathIfGapless(item: QariItem): String? { + var databaseName = item.databaseName + if (databaseName != null) { + val path = getLocalQariUrl(item) + if (path != null) { + databaseName = path + File.separator + databaseName + DB_EXTENSION + } + } + return databaseName + } + + fun getLocalAudioPathInfo(qari: QariItem): AudioPathInfo? { + val localPath = getLocalQariUrl(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 + } + + companion object { + const val AUDIO_EXTENSION = ".mp3" + + private const val DB_EXTENSION = ".db" } } From ddbef2552d3c096dffacad79faa85e2f713e02e9 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 11 Sep 2022 23:04:09 +0400 Subject: [PATCH 17/18] Use normal application cache directory --- app/src/main/AndroidManifest.xml | 4 +--- .../androidquran/feature/audioshare/AudioShareUtils.kt | 9 +++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9bc9d7eee7..22a51739d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -99,11 +99,9 @@ android:authorities="@string/file_authority" android:grantUriPermissions="true" android:exported="false"> - tools:replace="android:authorities"> + android:resource="@xml/file_paths" /> diff --git a/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt index a36bcebd7a..103b8c84c6 100644 --- a/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt +++ b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt @@ -52,14 +52,11 @@ class AudioShareUtils { ): String? { assert(end >= start) - val audioCacheDirectory = File( - Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_MUSIC).path + File.separator + "quran_android_cache") - + val audioCacheDirectory = context.cacheDir if (!audioCacheDirectory.exists()) { if (!audioCacheDirectory.mkdirs()) { Toast.makeText(context, "could not create directory", Toast.LENGTH_SHORT).show() - return null; + return null } } @@ -211,7 +208,7 @@ class AudioShareUtils { try { firstSurahCursor = db.getAyahTimings(start.sura) - firstSurahMap = populateArrayFromCursor(firstSurahCursor)!! + firstSurahMap = populateArrayFromCursor(firstSurahCursor) lastSurahCursor = db.getAyahTimings(end.sura) lastSurahMap = populateArrayFromCursor(lastSurahCursor) } catch (sqlException: SQLException) { From 562aaba7fe8942fceee6d7b6e3d73e4f3aaba8ab Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 12 Sep 2022 00:15:36 +0400 Subject: [PATCH 18/18] Additional cleanup to AudioShareUtils --- .../labs/androidquran/service/AudioService.kt | 22 +-- .../audio/timing/SuraTimingDatabaseHandler.kt | 25 ++- .../feature/audioshare/AudioShareUtils.kt | 177 ++++++++---------- 3 files changed, 103 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index d1d9c514d0..cd49367b2e 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -410,27 +410,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, timingDisposable?.dispose() timingDisposable = Single.fromCallable { val db = getDatabaseHandler(databasePath) - - val map = SparseIntArray() - var cursor: Cursor? = null - try { - cursor = db.getAyahTimings(sura) - Timber.d("got cursor of data") - if (cursor != null && cursor.moveToFirst()) { - do { - val ayah = cursor.getInt(1) - val time = cursor.getInt(2) - map.put(ayah, time) - } while (cursor.moveToNext()) - } - } catch (se: SQLException) { - // don't crash the app if the database is corrupt - Timber.e(se) - } finally { - DatabaseUtils.closeCursor(cursor) - } - - map + db.getAyahTimings(sura) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing/SuraTimingDatabaseHandler.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing/SuraTimingDatabaseHandler.kt index 76e732b385..b56090f113 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing/SuraTimingDatabaseHandler.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/timing/SuraTimingDatabaseHandler.kt @@ -5,6 +5,7 @@ import android.database.DefaultDatabaseErrorHandler import android.database.SQLException import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabaseCorruptException +import android.util.SparseIntArray import com.quran.common.util.database.DatabaseUtils import timber.log.Timber import java.io.File @@ -72,7 +73,7 @@ class SuraTimingDatabaseHandler private constructor(path: String) { private fun validDatabase(): Boolean = database?.isOpen ?: false - fun getAyahTimings(sura: Int): Cursor? { + private fun getAyahTimingsCursor(sura: Int): Cursor? { if (!validDatabase()) return null return try { @@ -89,6 +90,28 @@ class SuraTimingDatabaseHandler private constructor(path: String) { } } + fun getAyahTimings(sura: Int): SparseIntArray { + val map = SparseIntArray() + + var cursor: Cursor? = null + try { + cursor = getAyahTimingsCursor(sura) + if (cursor != null && cursor.moveToFirst()) { + do { + val ayah = cursor.getInt(1) + val time = cursor.getInt(2) + map.put(ayah, time) + } while (cursor.moveToNext()) + } + } catch (exception: SQLException) { + Timber.e(exception) + } finally { + DatabaseUtils.closeCursor(cursor) + } + + return map + } + fun getVersion(): Int { if (!validDatabase()) { return -1 diff --git a/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt index 103b8c84c6..a1d170fb6c 100644 --- a/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt +++ b/feature/audioshare/src/main/java/com/quran/labs/androidquran/feature/audioshare/AudioShareUtils.kt @@ -1,14 +1,9 @@ package com.quran.labs.androidquran.feature.audioshare import android.content.Context -import android.database.Cursor -import android.database.SQLException import android.media.MediaMetadataRetriever import android.net.Uri -import android.os.Environment import android.util.SparseIntArray -import android.widget.Toast -import com.quran.common.util.database.DatabaseUtils import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.common.audio.model.QariItem import com.quran.labs.androidquran.common.audio.timing.SuraTimingDatabaseHandler @@ -20,7 +15,6 @@ import okio.BufferedSink import okio.buffer import okio.sink import okio.source -import timber.log.Timber import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -53,35 +47,32 @@ class AudioShareUtils { assert(end >= start) val audioCacheDirectory = context.cacheDir - if (!audioCacheDirectory.exists()) { - if (!audioCacheDirectory.mkdirs()) { - Toast.makeText(context, "could not create directory", Toast.LENGTH_SHORT).show() - return null - } - } var sharablePath: String? val audioCacheFilePaths = mutableListOf() withContext(Dispatchers.IO) { - val mapArray = getTimingData(start, end, gaplessDatabase) + val (startSurahTimingData, endSurahTimingData) = getTimingData(start, end, gaplessDatabase) val startAyah = start.ayah val endAyah = end.ayah - val startSurahTimingDataArray: SparseIntArray = mapArray[0] - val endSurahTimingDataArray: SparseIntArray = mapArray[1] val isFirstAyahInSurah = startAyah == 1 - val startTimeOfAyahAfterEndAyah = endSurahTimingDataArray[endAyah + 1] + val startTimeOfAyahAfterEndAyah = endSurahTimingData[endAyah + 1] val isLastAyahInSurah = startTimeOfAyahAfterEndAyah == 0 val startAyahTime = if (isFirstAyahInSurah) { 0 } else { - startSurahTimingDataArray[startAyah] + startSurahTimingData[startAyah] } val endAyahTime = if (isLastAyahInSurah) { - getSurahDuration(context, getSurahAudioPath(urlFormat, end.sura)) + val endMarker = endSurahTimingData.get(999, -1) + if (endMarker > 0) { + endMarker + } else { + getSurahDuration(context, getSurahAudioPath(urlFormat, end.sura)) + } } else { startTimeOfAyahAfterEndAyah } @@ -89,61 +80,68 @@ class AudioShareUtils { val startAndEndAyahAreInSameSurah = start.sura == end.sura if (startAndEndAyahAreInSameSurah) { - val audioSegmentPath: String = getSurahSegment( + val audioSegmentPath: String? = getSurahSegment( audioCacheDirectory, getSurahAudioPath(urlFormat, start.sura), startAyahTime, endAyahTime - )!! - audioCacheFilePaths.add(audioSegmentPath) - sharablePath = getRenamedSharableAudioFile( - qari, - start, - end, - audioSegmentPath, - audioCacheDirectory.toString(), - audioCacheFilePaths ) + + sharablePath = if (audioSegmentPath != null) { + audioCacheFilePaths.add(audioSegmentPath) + getRenamedSharableAudioFile( + qari, + start, + end, + audioSegmentPath, + audioCacheDirectory.toString(), + audioCacheFilePaths + ) + } else { + null + } audioCacheFilePaths.clear() } else { val segmentPaths = mutableListOf() val endOfSurah = -1 val startOfSurah = 0 - val startSegmentPath: String = getSurahSegmentPath( + val startSegmentPath: String? = getSurahSegmentPath( context, audioCacheDirectory, urlFormat, start.sura, startAyahTime, endOfSurah ) - val lastSegmentPath: String = getSurahSegmentPath( - context, audioCacheDirectory , urlFormat, end.sura, startOfSurah, endAyahTime + val lastSegmentPath: String? = getSurahSegmentPath( + context, audioCacheDirectory, urlFormat, end.sura, startOfSurah, endAyahTime ) - for (surahIndex in start.sura..end.sura) { - val isTheFirstSurah = surahIndex == start.sura - val isMiddleSurah = surahIndex != start.sura && surahIndex != end.sura - if (isTheFirstSurah) { - segmentPaths.add(startSegmentPath) - audioCacheFilePaths.add(startSegmentPath) - continue - } - if (isMiddleSurah) { - segmentPaths.add(getSurahAudioPath(urlFormat, surahIndex)) - continue + if (startSegmentPath != null && lastSegmentPath != null) { + for (surahIndex in start.sura..end.sura) { + val isTheFirstSurah = surahIndex == start.sura + val isMiddleSurah = surahIndex != start.sura && surahIndex != end.sura + if (isTheFirstSurah) { + segmentPaths.add(startSegmentPath) + audioCacheFilePaths.add(startSegmentPath) + } else if (isMiddleSurah) { + segmentPaths.add(getSurahAudioPath(urlFormat, surahIndex)) + } else { + segmentPaths.add(lastSegmentPath) + audioCacheFilePaths.add(lastSegmentPath) + } } - segmentPaths.add(lastSegmentPath) - audioCacheFilePaths.add(lastSegmentPath) - } - val audioSegmentsWereCreated = segmentPaths.isNotEmpty() + val audioSegmentsWereCreated = segmentPaths.isNotEmpty() - if (audioSegmentsWereCreated) { - val (sharableAudioFilePath, cacheUpdates) = - getMergedAudioFromSegments(audioCacheDirectory, segmentPaths) - audioCacheFilePaths.addAll(cacheUpdates) - sharablePath = getRenamedSharableAudioFile( - qari, - start, - end, - sharableAudioFilePath, - audioCacheDirectory.toString(), - audioCacheFilePaths - ) - audioCacheFilePaths.clear() + if (audioSegmentsWereCreated) { + val (sharableAudioFilePath, cacheUpdates) = + getMergedAudioFromSegments(audioCacheDirectory, segmentPaths) + audioCacheFilePaths.addAll(cacheUpdates) + sharablePath = getRenamedSharableAudioFile( + qari, + start, + end, + sharableAudioFilePath, + audioCacheDirectory.toString(), + audioCacheFilePaths + ) + audioCacheFilePaths.clear() + } else { + sharablePath = null + } } else { sharablePath = null } @@ -158,14 +156,14 @@ class AudioShareUtils { surah: Int, startAyahTime: Int, endAyahTime: Int - ): String { + ): String? { var upperBoundTime = endAyahTime val audioFilePath: String = getSurahAudioPath(urlFormat, surah) val isFirstSegment = endAyahTime < 0 if (isFirstSegment) { upperBoundTime = getSurahDuration(context, audioFilePath) } - return getSurahSegment(audioCacheDirectory, audioFilePath, startAyahTime, upperBoundTime)!! + return getSurahSegment(audioCacheDirectory, audioFilePath, startAyahTime, upperBoundTime) } private fun getRenamedSharableAudioFile( @@ -199,39 +197,21 @@ class AudioShareUtils { return String.format(Locale.US, urlFormat, surah) } - private fun getTimingData(start: SuraAyah, end: SuraAyah, gaplessDatabase: String): ArrayList { - val db: SuraTimingDatabaseHandler = SuraTimingDatabaseHandler.getDatabaseHandler(gaplessDatabase) - var firstSurahMap = SparseIntArray() - var lastSurahMap = SparseIntArray() - var firstSurahCursor: Cursor? = null - var lastSurahCursor: Cursor? = null - - try { - firstSurahCursor = db.getAyahTimings(start.sura) - firstSurahMap = populateArrayFromCursor(firstSurahCursor) - lastSurahCursor = db.getAyahTimings(end.sura) - lastSurahMap = populateArrayFromCursor(lastSurahCursor) - } catch (sqlException: SQLException) { - Timber.e(sqlException) - } finally { - DatabaseUtils.closeCursor(firstSurahCursor) - DatabaseUtils.closeCursor(lastSurahCursor) + private fun getTimingData( + start: SuraAyah, + end: SuraAyah, + gaplessDatabase: String + ): Pair { + val db: SuraTimingDatabaseHandler = + SuraTimingDatabaseHandler.getDatabaseHandler(gaplessDatabase) + val firstSurahMap = db.getAyahTimings(start.sura) + val lastSurahMap = if (start.sura == end.sura) { + firstSurahMap + } else { + db.getAyahTimings(end.sura) } - return ArrayList( - listOf(firstSurahMap, lastSurahMap)) - } - - private fun populateArrayFromCursor(cursor: Cursor?): SparseIntArray { - val sparseIntArray = SparseIntArray() - if (cursor != null && cursor.moveToFirst()) { - do { - val ayah = cursor.getInt(1) - val time = cursor.getInt(2) - sparseIntArray.put(ayah, time) - } while (cursor.moveToNext()) - } - return sparseIntArray + return firstSurahMap to lastSurahMap } private fun getSurahSegment( @@ -245,17 +225,16 @@ class AudioShareUtils { } val tempAudioName = UUID.randomUUID().toString() + ".mp3" val destFile = File(audioCacheDirectory.path + File.separator + tempAudioName) - val soundFile = arrayOfNulls(1) + val soundFile = CheapSoundFile.create(path, null) try { - soundFile[0] = CheapSoundFile.create(path, null) val startTime = lowerCut.toFloat() / 1000 val endTime = upperCut.toFloat() / 1000 - val samplesPerFrame = soundFile[0]?.samplesPerFrame - val sampleRate = soundFile[0]?.sampleRate - val avg = sampleRate?.div(samplesPerFrame!!) - val startFrames = (startTime * avg!!).roundToInt() + val samplesPerFrame = soundFile.samplesPerFrame + val sampleRate = soundFile.sampleRate + val avg = sampleRate.div(samplesPerFrame) + val startFrames = (startTime * avg).roundToInt() val endFrames = (endTime * avg).roundToInt() - soundFile[0]?.WriteFile(destFile, startFrames, endFrames - startFrames) + soundFile.WriteFile(destFile, startFrames, endFrames - startFrames) } catch (e: IOException) { e.printStackTrace() }