package yuku.alkitab.base.ac; import android.app.Activity; import android.content.Intent; import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.ShareCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.text.Html; import android.text.InputType; import android.text.Spanned; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import android.webkit.WebViewClient; import com.afollestad.materialdialogs.MaterialDialog; import yuku.afw.V; import yuku.afw.storage.Preferences; import yuku.alkitab.base.App; import yuku.alkitab.base.S; import yuku.alkitab.base.U; import yuku.alkitab.base.ac.base.BaseLeftDrawerActivity; import yuku.alkitab.base.dialog.VersesDialog; import yuku.alkitab.base.storage.Prefkey; import yuku.alkitab.base.storage.SongDb; import yuku.alkitab.base.util.AlphanumComparator; import yuku.alkitab.base.util.Background; import yuku.alkitab.base.util.FontManager; import yuku.alkitab.base.util.Foreground; import yuku.alkitab.base.util.OsisBookNames; import yuku.alkitab.base.util.SongBookUtil; import yuku.alkitab.base.util.Sqlitil; import yuku.alkitab.base.util.TargetDecoder; import yuku.alkitab.base.widget.LeftDrawer; import yuku.alkitab.base.widget.TwofingerLinearLayout; import yuku.alkitab.debug.BuildConfig; import yuku.alkitab.debug.R; import yuku.alkitab.model.Book; import yuku.alkitab.model.SongInfo; import yuku.alkitab.util.IntArrayList; import yuku.alkitabintegration.display.Launcher; import yuku.kpri.model.Lyric; import yuku.kpri.model.Song; import yuku.kpri.model.Verse; import yuku.kpri.model.VerseKind; import yuku.kpriviewer.fr.SongFragment; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public class SongViewActivity extends BaseLeftDrawerActivity implements SongFragment.ShouldOverrideUrlLoadingHandler, LeftDrawer.Songs.Listener, MediaStateListener { public static final String TAG = SongViewActivity.class.getSimpleName(); private static final String BIBLE_PROTOCOL = "bible"; private static final int REQCODE_songList = 1; private static final int REQCODE_share = 2; private static final int REQCODE_downloadSongBook = 3; private static final String FRAGMENT_TAG_SONG = "song"; DrawerLayout drawerLayout; LeftDrawer.Songs leftDrawer; TwofingerLinearLayout root; ViewGroup no_song_data_container; View bDownload; View circular_progress; Bundle templateCustomVars; String currentBookName; Song currentSong; // for initially populating the search song activity SongListActivity.SearchState last_searchState = null; // state for keypad String state_originalCode; String state_tempCode; // cache of song codes for each book Map<String /* bookName */, List<String> /* ordered codes */> cache_codes = new HashMap<>(); final TwofingerLinearLayout.Listener song_container_listener = new TwofingerLinearLayout.Listener() { int textZoom = 0; // stays at 0 if zooming is not ready @Override public void onOnefingerLeft() { goTo(+1); } @Override public void onOnefingerRight() { goTo(-1); } @Override public void onTwofingerStart() { final Fragment f = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_SONG); if (f instanceof SongFragment) { textZoom = ((SongFragment) f).getWebViewTextZoom(); } } @Override public void onTwofingerScale(final float scale) { int newTextZoom = (int) (textZoom * scale); if (newTextZoom < 50) newTextZoom = 50; if (newTextZoom > 200) newTextZoom = 200; final Fragment f = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_SONG); if (f instanceof SongFragment) { ((SongFragment) f).setWebViewTextZoom(newTextZoom); } } @Override public void onTwofingerDragX(final float dx) { } @Override public void onTwofingerDragY(final float dy) { } @Override public void onTwofingerEnd(final TwofingerLinearLayout.Mode mode) { } }; @Override protected LeftDrawer getLeftDrawer() { return leftDrawer; } static class MediaState { boolean enabled; @DrawableRes int icon; @StringRes int label; boolean loading; } @NonNull final MediaState mediaState = new MediaState(); /** This method might be called from non-UI thread. Be careful when manipulating UI. */ @Override public void setMediaState(@NonNull final MediaPlayerController.ControllerState state) { if (state == MediaPlayerController.ControllerState.reset) { mediaState.enabled = false; mediaState.icon = R.drawable.ic_action_hollowplay; mediaState.label = R.string.menuPlay; } else if (state == MediaPlayerController.ControllerState.reset_media_known_to_exist || state == MediaPlayerController.ControllerState.paused || state == MediaPlayerController.ControllerState.complete) { mediaState.enabled = true; mediaState.icon = R.drawable.ic_action_play; mediaState.label = R.string.menuPlay; } else if (state == MediaPlayerController.ControllerState.playing) { mediaState.enabled = true; mediaState.icon = R.drawable.ic_action_pause; mediaState.label = R.string.menuPause; } mediaState.loading = (state == MediaPlayerController.ControllerState.preparing); //noinspection Convert2MethodRef runOnUiThread(() -> invalidateOptionsMenu()); } void goTo(final int dir) { if (currentBookName == null) return; if (currentSong == null) return; List<String> codes = cache_codes.get(currentBookName); if (codes == null) { final List<SongInfo> songInfos = S.getSongDb().listSongInfosByBookName(currentBookName); codes = new ArrayList<>(); for (SongInfo songInfo : songInfos) { codes.add(songInfo.code); } // sort codes based on numeric Collections.sort(codes, new AlphanumComparator()); cache_codes.put(currentBookName, codes); } // find index of current song final int pos = codes.indexOf(currentSong.code); if (pos == -1) { return; // should not happen } final int newPos = pos + dir; if (newPos < 0 || newPos >= codes.size()) { return; // can't go left or right } final String newCode = codes.get(newPos); final Song newSong = S.getSongDb().getSong(currentBookName, newCode); if (newSong == null) { return; // should not happen } displaySong(currentBookName, newSong); } static class MediaPlayerController { MediaPlayer mp = new MediaPlayer(); enum ControllerState { reset, reset_media_known_to_exist, preparing, playing, paused, complete, error, } ControllerState state = ControllerState.reset; // if this is a midi file, we need to manually download to local first boolean isMidiFile; String url; WeakReference<Activity> activityRef; WeakReference<MediaStateListener> mediaStateListenerRef; void setUI(Activity activity, MediaStateListener mediaStateListener) { activityRef = new WeakReference<>(activity); mediaStateListenerRef = new WeakReference<>(mediaStateListener); } private void setState(final ControllerState newState) { Log.d(TAG, "@@setState newState=" + newState); state = newState; updateMediaState(); } void updateMediaState() { if (mediaStateListenerRef == null) return; final MediaStateListener mediaStateListener = mediaStateListenerRef.get(); if (mediaStateListener == null) return; mediaStateListener.setMediaState(state); } void reset() { setState(ControllerState.reset); mp.reset(); } void mediaKnownToExist(String url, boolean isMidiFile) { setState(ControllerState.reset_media_known_to_exist); this.url = url; this.isMidiFile = isMidiFile; } void playOrPause(final boolean playInLoop) { if (state == ControllerState.reset) { // play button should be disabled } else if (state == ControllerState.reset_media_known_to_exist || state == ControllerState.complete || state == ControllerState.error) { if (isMidiFile) { final Handler handler = new Handler(); Background.run(() -> { try { setState(ControllerState.preparing); final byte[] bytes = App.downloadBytes(url); final File cacheFile = new File(App.context.getCacheDir(), "song_player_local_cache.mid"); final OutputStream output = new FileOutputStream(cacheFile); output.write(bytes); output.close(); // this is a background thread. We must go back to main thread, and check again if state is OK to prepare. handler.post(() -> { if (state == ControllerState.preparing) { // the following should be synchronous, since we are loading from local. mediaPlayerPrepare(true, cacheFile.getAbsolutePath(), playInLoop); } else { Log.d(TAG, "wrong state after downloading song file: " + state); } }); } catch (IOException e) { Log.e(TAG, "buffering to local cache", e); setState(ControllerState.error); } }); } else { mediaPlayerPrepare(false, url, playInLoop); } } else if (state == ControllerState.preparing) { // this is preparing. Don't do anything. } else if (state == ControllerState.playing) { // pause button pressed if (playInLoop) { // looping play is selected but we are already playing. So just set looping parameter. mp.setLooping(true); } else { mp.pause(); setState(ControllerState.paused); } } else if (state == ControllerState.paused) { // play button pressed when paused mp.setLooping(playInLoop); mp.start(); setState(ControllerState.playing); } } /** * @param url local path if isLocalPath is true, url (http/https) if isLocalPath is false */ private void mediaPlayerPrepare(boolean isLocalPath, final String url, final boolean playInLoop) { try { setState(ControllerState.preparing); mp.setOnPreparedListener(player -> { // only start playing if the current state is preparing, i.e., not error or reset. if (state == ControllerState.preparing) { player.setLooping(playInLoop); player.start(); setState(ControllerState.playing); } }); mp.setOnCompletionListener(player -> { Log.d(TAG, "@@onCompletion looping=" + player.isLooping()); if (!player.isLooping()) { player.reset(); setState(ControllerState.complete); } }); mp.setOnErrorListener((mp1, what, extra) -> { Log.e(TAG, "@@onError controller_state=" + state + " what=" + what + " extra=" + extra); if (state != ControllerState.reset) { // Errors can happen if we call MediaPlayer#reset when MediaPlayer state is Preparing. In this case, do not show error message. final Activity activity = activityRef.get(); if (activity != null) { if (!activity.isFinishing()) { new MaterialDialog.Builder(activity) .content(activity.getString(R.string.song_player_error_description, what, extra)) .positiveText(R.string.ok) .show(); } } } setState(ControllerState.error); return false; // let OnCompletionListener be called. }); if (isLocalPath) { final FileInputStream fis = new FileInputStream(url); final FileDescriptor fd = fis.getFD(); mp.setDataSource(fd); fis.close(); mp.prepare(); } else { mp.setDataSource(url); mp.prepareAsync(); } } catch (IOException e) { Log.e(TAG, "mp setDataSource", e); setState(ControllerState.error); } } boolean canHaveNewUrl() { return state == ControllerState.reset || state == ControllerState.reset_media_known_to_exist || state == ControllerState.error; } } // this have to be static to prevent double media player static MediaPlayerController mediaPlayerController = new MediaPlayerController(); public static Intent createIntent() { return new Intent(App.context, SongViewActivity.class); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_song_view); circular_progress = V.get(this, R.id.progress_circular); setCustomProgressBarIndeterminateVisible(false); drawerLayout = V.get(this, R.id.drawerLayout); leftDrawer = V.get(this, R.id.left_drawer); leftDrawer.configure(this, drawerLayout); final Toolbar toolbar = V.get(this, R.id.toolbar); setSupportActionBar(toolbar); final ActionBar actionBar = getSupportActionBar(); assert actionBar != null; actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeAsUpIndicator(R.drawable.ic_menu_white_24dp); drawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { @Override public void onDrawerOpened(final View drawerView) { drawer_opened(); } }); root = V.get(this, R.id.root); no_song_data_container = V.get(this, R.id.no_song_data_container); bDownload = V.get(this, R.id.bDownload); root.setListener(song_container_listener); // Before KitKat, the WebView can zoom and rewrap text by itself, // so we do not need custom implementation of scaling. if (Build.VERSION.SDK_INT < 19) { root.setTwofingerEnabled(false); } bDownload.setOnClickListener(v -> openDownloadSongBookPage()); } void openDownloadSongBookPage() { startActivityForResult( HelpActivity.createIntentWithOverflowMenu( BuildConfig.SERVER_HOST + "songs/downloads?app_versionCode=" + App.getVersionCode() + "&app_versionName=" + Uri.encode(App.getVersionName()), getString(R.string.sn_download_song_books), getString(R.string.sn_menu_private_song_book), AlertDialogActivity.createInputIntent( null, getString(R.string.sn_private_song_book_dialog_desc), getString(R.string.cancel), getString(R.string.ok), InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS, getString(R.string.sn_private_song_book_name_hint) ) ), REQCODE_downloadSongBook ); } @Override protected void onStart() { super.onStart(); { // apply background color, and clear window background to prevent overdraw getWindow().setBackgroundDrawableResource(android.R.color.transparent); V.get(this, android.R.id.content).setBackgroundColor(S.applied.backgroundColor); } templateCustomVars = new Bundle(); templateCustomVars.putString("background_color", String.format("#%06x", S.applied.backgroundColor & 0xffffff)); templateCustomVars.putString("text_color", String.format("#%06x", S.applied.fontColor & 0xffffff)); templateCustomVars.putString("verse_number_color", String.format("#%06x", S.applied.verseNumberColor & 0xffffff)); templateCustomVars.putString("text_size", S.applied.fontSize2dp + "px"); templateCustomVars.putString("line_spacing_mult", String.valueOf(S.applied.lineSpacingMult)); { String fontName = Preferences.getString(Prefkey.jenisHuruf, null); if (FontManager.isCustomFont(fontName)) { templateCustomVars.putString("custom_font_loader", String.format("@font-face{ font-family: '%s'; src: url('%s'); }", fontName, FontManager.getCustomFontUri(fontName))); } else { templateCustomVars.putString("custom_font_loader", ""); } templateCustomVars.putString("text_font", fontName); } { // show latest viewed song String bookName = Preferences.getString(Prefkey.song_last_bookName, null); String code = Preferences.getString(Prefkey.song_last_code, null); if (bookName == null || code == null) { displaySong(null, null, true); } else { final SongDb db = S.getSongDb(); displaySong(bookName, db.getSong(bookName, code), true); } } getWindow().getDecorView().setKeepScreenOn(Preferences.getBoolean(getString(R.string.pref_keepScreenOn_key), getResources().getBoolean(R.bool.pref_keepScreenOn_default))); } /** Used after deleting a song, and the current song is no longer available */ void displayAnySongOrFinish() { final SongDb db = S.getSongDb(); final Pair<String, Song> pair = db.getAnySong(); if (pair == null) { finish(); } else { displaySong(pair.first, pair.second); } } @Override protected void onResume() { super.onResume(); mediaPlayerController.setUI(this, this); mediaPlayerController.updateMediaState(); } static String getAudioFilename(String bookName, String code) { return String.format("songs/v2/%s_%s", bookName, code); } void checkAudioExistance() { if (currentBookName == null || currentSong == null) return; final String checkedBookName = currentBookName; final String checkedCode = currentSong.code; Background.run(() -> { try { final String filename = getAudioFilename(checkedBookName, checkedCode); final String response = App.downloadString(BuildConfig.SERVER_HOST + "addon/audio/exists?filename=" + Uri.encode(filename)); if (response.startsWith("OK")) { // make sure this is the correct one due to possible race condition if (U.equals(currentBookName, checkedBookName) && currentSong != null && U.equals(currentSong.code, checkedCode)) { runOnUiThread(() -> { if (mediaPlayerController.canHaveNewUrl()) { final String baseUrl = BuildConfig.SERVER_HOST + "addon/audio/"; final String url = baseUrl + getAudioFilename(currentBookName, currentSong.code); if (response.contains("extension=mp3")) { mediaPlayerController.mediaKnownToExist(url, false); } else { mediaPlayerController.mediaKnownToExist(url, true); } } else { Log.d(TAG, "mediaPlayerController can't have new URL at this moment."); } }); } } else { Log.d(TAG, "@@checkAudioExistance response: " + response); } } catch (IOException e) { Log.e(TAG, "@@checkAudioExistance", e); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_song_view, menu); new Handler().post(() -> { final View view = V.get(this, R.id.menuMediaControl); if (view == null) return; view.setOnLongClickListener(v -> { if (mediaState.icon == R.drawable.ic_action_play) { new MaterialDialog.Builder(this) .content(R.string.sn_play_in_loop) .negativeText(R.string.cancel) .positiveText(R.string.ok) .onPositive((dialog, which) -> mediaPlayerController.playOrPause(true)) .show(); return true; } return false; }); }); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { final MenuItem menuMediaControl = menu.findItem(R.id.menuMediaControl); menuMediaControl.setEnabled(mediaState.enabled); if (mediaState.icon != 0) menuMediaControl.setIcon(mediaState.icon); if (mediaState.label != 0) menuMediaControl.setTitle(mediaState.label); if (mediaState.loading) { setCustomProgressBarIndeterminateVisible(true); menuMediaControl.setVisible(false); } else { setCustomProgressBarIndeterminateVisible(false); menuMediaControl.setVisible(true); } final boolean songShown = currentBookName != null; final MenuItem menuCopy = menu.findItem(R.id.menuCopy); menuCopy.setVisible(songShown); final MenuItem menuShare = menu.findItem(R.id.menuShare); menuShare.setVisible(songShown); final MenuItem menuUpdateBook = menu.findItem(R.id.menuUpdateBook); menuUpdateBook.setVisible(songShown); final MenuItem menuDeleteSongBook = menu.findItem(R.id.menuDeleteSongBook); menuDeleteSongBook.setVisible(songShown); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: { leftDrawer.toggleDrawer(); } return true; case R.id.menuCopy: { if (currentSong != null) { U.copyToClipboard(convertSongToText(currentSong)); Snackbar.make(root, R.string.sn_copied, Snackbar.LENGTH_SHORT).show(); } } return true; case R.id.menuShare: { if (currentSong != null) { final Intent intent = ShareCompat.IntentBuilder.from(SongViewActivity.this) .setType("text/plain") .setSubject(SongBookUtil.escapeSongBookName(currentBookName).toString() + ' ' + currentSong.code + ' ' + currentSong.title) .setText(convertSongToText(currentSong).toString()) .getIntent(); startActivityForResult(ShareActivity.createIntent(intent, getString(R.string.sn_share_title)), REQCODE_share); } } return true; case R.id.menuSearch: { startActivityForResult(SongListActivity.createIntent(last_searchState), REQCODE_songList); } return true; case R.id.menuMediaControl: { if (currentBookName != null && currentSong != null) { mediaPlayerController.playOrPause(false); } } return true; case R.id.menuUpdateBook: { new MaterialDialog.Builder(this) .content(TextUtils.expandTemplate(getText(R.string.sn_update_book_explanation), SongBookUtil.escapeSongBookName(currentBookName))) .positiveText(R.string.sn_update_book_confirm_button) .onPositive((dialog, which) -> updateSongBook()) .negativeText(R.string.cancel) .show(); } return true; case R.id.menuDeleteSongBook: { new MaterialDialog.Builder(this) .content(TextUtils.expandTemplate(getText(R.string.sn_delete_song_book_explanation), SongBookUtil.escapeSongBookName(currentBookName))) .positiveText(R.string.delete) .onPositive((dialog, which) -> deleteSongBook()) .negativeText(R.string.cancel) .show(); } return true; } return super.onOptionsItemSelected(item); } protected void updateSongBook() { final SongBookUtil.SongBookInfo songBookInfo = SongBookUtil.getSongBookInfo(currentBookName); if (songBookInfo == null) { throw new RuntimeException("SongBookInfo named " + currentBookName + " was not found"); } final String currentSongCode = currentSong.code; final int dataFormatVersion = S.getSongDb().getDataFormatVersionForSongs(currentBookName); SongBookUtil.downloadSongBook(SongViewActivity.this, songBookInfo, dataFormatVersion, new SongBookUtil.OnDownloadSongBookListener() { @Override public void onFailedOrCancelled(SongBookUtil.SongBookInfo songBookInfo, Exception e) { showDownloadError(e); } @Override public void onDownloadedAndInserted(SongBookUtil.SongBookInfo songBookInfo) { final Song song = S.getSongDb().getSong(songBookInfo.name, currentSongCode); cache_codes.remove(songBookInfo.name); displaySong(songBookInfo.name, song); } }); } private void showDownloadError(final Exception e) { if (e == null) return; if (e instanceof SongBookUtil.NotOkException) { new MaterialDialog.Builder(this) .content("HTTP error " + ((SongBookUtil.NotOkException) e).code) .positiveText(R.string.ok) .show(); } else { new MaterialDialog.Builder(this) .content(e.getClass().getSimpleName() + ": " + e.getMessage()) .positiveText(R.string.ok) .show(); } } protected void deleteSongBook() { final MaterialDialog pd = new MaterialDialog.Builder(this) .content(R.string.please_wait_titik3) .cancelable(false) .progress(true, 0) .show(); final String bookName = currentBookName; Background.run(() -> { final int count = S.getSongDb().deleteSongBook(bookName); Foreground.run(() -> { pd.dismiss(); new MaterialDialog.Builder(this) .content(TextUtils.expandTemplate(getText(R.string.sn_delete_song_book_result), "" + count, SongBookUtil.escapeSongBookName(bookName))) .positiveText(R.string.ok) .dismissListener(dialog -> displayAnySongOrFinish()) .show(); }); }); } private StringBuilder convertSongToText(Song song) { // build text to copy final StringBuilder sb = new StringBuilder(); sb.append(SongBookUtil.escapeSongBookName(currentBookName)).append(' '); sb.append(song.code).append(". "); sb.append(song.title).append('\n'); if (song.title_original != null) sb.append('(').append(song.title_original).append(')').append('\n'); sb.append('\n'); if (song.authors_lyric != null && song.authors_lyric.size() > 0) sb.append(TextUtils.join("; ", song.authors_lyric)).append('\n'); if (song.authors_music != null && song.authors_music.size() > 0) sb.append(TextUtils.join("; ", song.authors_music)).append('\n'); if (song.tune != null) sb.append(song.tune.toUpperCase(Locale.getDefault())).append('\n'); sb.append('\n'); if (song.scriptureReferences != null) sb.append(renderScriptureReferences(null, song.scriptureReferences)).append('\n'); if (song.keySignature != null) sb.append(song.keySignature).append('\n'); if (song.timeSignature != null) sb.append(song.timeSignature).append('\n'); sb.append('\n'); for (int i = 0; i < song.lyrics.size(); i++) { Lyric lyric = song.lyrics.get(i); if (song.lyrics.size() > 1 || lyric.caption != null) { // otherwise, only lyric and has no name if (lyric.caption != null) { sb.append(lyric.caption).append('\n'); } else { sb.append(getString(R.string.sn_lyric_version_version, i+1)).append('\n'); } } int verse_normal_no = 0; for (Verse verse: lyric.verses) { if (verse.kind == VerseKind.NORMAL) { verse_normal_no++; } boolean skipPad = false; if (verse.kind == VerseKind.REFRAIN) { sb.append(getString(R.string.sn_lyric_refrain_marker)).append('\n'); } else { sb.append(String.format(Locale.US, "%2d: ", verse_normal_no)); skipPad = true; } for (String line: verse.lines) { if (!skipPad) { sb.append(" "); } else { skipPad = false; } sb.append(line).append("\n"); } sb.append('\n'); } sb.append('\n'); } return sb; } /** * Convert scripture ref lines like * B1.C1.V1-B2.C2.V2; B3.C3.V3 to: * <a href="protocol:B1.C1.V1-B2.C2.V2">Book 1 c1:v1-v2</a>; <a href="protocol:B3.C3.V3>Book 3 c3:v3</a> * @param protocol null to output text * @param line scripture ref in osis */ String renderScriptureReferences(String protocol, String line) { if (line == null || line.trim().length() == 0) return ""; StringBuilder sb = new StringBuilder(); String[] ranges = line.split("\\s*;\\s*"); for (String range: ranges) { String[] osisIds; if (range.indexOf('-') >= 0) { osisIds = range.split("\\s*-\\s*"); } else { osisIds = new String[] {range}; } if (osisIds.length == 1) { if (sb.length() != 0) { sb.append("; "); } String osisId = osisIds[0]; String readable = osisIdToReadable(line, osisId, null, null); if (readable != null) { appendScriptureReferenceLink(sb, protocol, osisId, readable); } } else if (osisIds.length == 2) { if (sb.length() != 0) { sb.append("; "); } int[] bcv = {-1, 0, 0}; String osisId0 = osisIds[0]; String readable0 = osisIdToReadable(line, osisId0, null, bcv); String osisId1 = osisIds[1]; String readable1 = osisIdToReadable(line, osisId1, bcv, null); if (readable0 != null && readable1 != null) { appendScriptureReferenceLink(sb, protocol, osisId0 + '-' + osisId1, readable0 + '-' + readable1); } } } return sb.toString(); } private void appendScriptureReferenceLink(StringBuilder sb, String protocol, String osisId, String readable) { if (protocol != null) { sb.append("<a href='"); sb.append(protocol); sb.append(':'); sb.append(osisId); sb.append("'>"); } sb.append(readable); if (protocol != null) { sb.append("</a>"); } } /** * @param compareWithRangeStart if this is the second part of a range, set this to non-null, with [0] is bookId and [1] chapter_1. * @param outBcv if not null and length is >= 3, will be filled with parsed bcv */ private String osisIdToReadable(String line, String osisId, int[] compareWithRangeStart, int[] outBcv) { String res = null; String[] parts = osisId.split("\\."); if (parts.length != 2 && parts.length != 3) { Log.w(TAG, "osisId invalid: " + osisId + " in " + line); } else { String bookName = parts[0]; int chapter_1 = Integer.parseInt(parts[1]); int verse_1 = parts.length < 3? 0: Integer.parseInt(parts[2]); int bookId = OsisBookNames.osisBookNameToBookId(bookName); if (outBcv != null && outBcv.length >= 3) { outBcv[0] = bookId; outBcv[1] = chapter_1; outBcv[2] = verse_1; } if (bookId < 0) { Log.w(TAG, "osisBookName invalid: " + bookName + " in " + line); } else { Book book = S.activeVersion.getBook(bookId); if (book != null) { boolean full = true; if (compareWithRangeStart != null) { if (compareWithRangeStart[0] == bookId) { if (compareWithRangeStart[1] == chapter_1) { res = String.valueOf(verse_1); full = false; } else { res = String.valueOf(chapter_1) + ':' + String.valueOf(verse_1); full = false; } } } if (full) { res = verse_1 == 0? book.reference(chapter_1): book.reference(chapter_1, verse_1); } } } } return res; } void displaySong(String bookName, @Nullable Song song) { displaySong(bookName, song, false); } void displaySong(String bookName, @Nullable Song song, boolean onCreate) { root.setVisibility(song != null ? View.VISIBLE : View.GONE); no_song_data_container.setVisibility(song != null? View.GONE: View.VISIBLE); if (!onCreate) { mediaPlayerController.reset(); } if (song == null) return; final LeftDrawer.Songs.Handle handle = leftDrawer.getHandle(); handle.setBookName(SongBookUtil.escapeSongBookName(bookName)); handle.setCode(song.code); setTitle(TextUtils.concat(SongBookUtil.escapeSongBookName(bookName), " ", song.code)); // construct rendition of scripture references String scripture_references = renderScriptureReferences(BIBLE_PROTOCOL, song.scriptureReferences); templateCustomVars.putString("scripture_references", scripture_references); final String copyright = SongBookUtil.getCopyright(bookName); templateCustomVars.putString("copyright", copyright != null? copyright: ""); templateCustomVars.putString("patch_text_open_link", getString(R.string.patch_text_open_link)); FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.replace(R.id.root, SongFragment.create(song, "templates/song.html", templateCustomVars), FRAGMENT_TAG_SONG); ft.commitAllowingStateLoss(); currentBookName = bookName; currentSong = song; { // save latest viewed song Preferences.setString(Prefkey.song_last_bookName, bookName); Preferences.setString(Prefkey.song_last_code, song.code); } checkAudioExistance(); } void drawer_opened() { if (currentBookName == null) return; final LeftDrawer.Songs.Handle handle = leftDrawer.getHandle(); handle.setOkButtonEnabled(false); handle.setAButtonEnabled(false); handle.setBButtonEnabled(false); handle.setCButtonEnabled(false); final String originalCode = currentSong == null ? "––––" : currentSong.code; handle.setCode(originalCode); state_originalCode = originalCode; state_tempCode = ""; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQCODE_songList: { if (resultCode == RESULT_OK) { SongListActivity.Result result = SongListActivity.obtainResult(data); if (result != null) { displaySong(result.bookName, S.getSongDb().getSong(result.bookName, result.code)); // store this for next search last_searchState = result.last_searchState; } } } return; case REQCODE_share: { if (resultCode == RESULT_OK) { ShareActivity.Result result = ShareActivity.obtainResult(data); if (result != null && result.chosenIntent != null) { startActivity(result.chosenIntent); } } } return; case REQCODE_downloadSongBook: { if (resultCode == RESULT_OK) { final Uri uri = data.getData(); if (uri != null) { downloadByAlkitabUri(uri); } else { final String input = data.getStringExtra(AlertDialogActivity.EXTRA_INPUT); if (!TextUtils.isEmpty(input)) { downloadByAlkitabUri(Uri.parse("alkitab:///addon/download?kind=songbook&type=ser&dataFormatVersion=3&name=_" + Uri.encode(input.toUpperCase()))); } } return; } } return; } super.onActivityResult(requestCode, resultCode, data); } private void downloadByAlkitabUri(final Uri uri) { if (!"alkitab".equals(uri.getScheme()) || !"/addon/download".equals(uri.getPath()) || !"songbook".equals(uri.getQueryParameter("kind")) || !"ser".equals(uri.getQueryParameter("type")) || uri.getQueryParameter("name") == null) { new MaterialDialog.Builder(this) .content("Invalid uri:\n\n" + uri) .positiveText(R.string.ok) .show(); return; } final String dataFormatVersion_s = uri.getQueryParameter("dataFormatVersion"); final int dataFormatVersion; try { dataFormatVersion = Integer.parseInt(dataFormatVersion_s); } catch (NumberFormatException|NullPointerException e) { new MaterialDialog.Builder(this) .content("Invalid uri:\n\n" + uri) .positiveText(R.string.ok) .show(); return; } if (!SongBookUtil.isSupportedDataFormatVersion(dataFormatVersion)) { new MaterialDialog.Builder(this) .content("Unsupported data format version: " + dataFormatVersion) .positiveText(R.string.ok) .show(); return; } final SongBookUtil.SongBookInfo info = new SongBookUtil.SongBookInfo(); info.name = uri.getQueryParameter("name"); info.title = uri.getQueryParameter("title"); info.copyright = uri.getQueryParameter("copyright"); SongBookUtil.downloadSongBook(this, info, dataFormatVersion, new SongBookUtil.OnDownloadSongBookListener() { @Override public void onDownloadedAndInserted(final SongBookUtil.SongBookInfo songBookInfo) { final String name = songBookInfo.name; final Song song = S.getSongDb().getFirstSongFromBook(name); displaySong(name, song); } @Override public void onFailedOrCancelled(final SongBookUtil.SongBookInfo songBookInfo, final Exception e) { showDownloadError(e); } }); } static class PatchTextExtraInfoJson { String type; String bookName; String code; } static String nonullbr(String s) { if (s == null) return ""; return "<br/>" + s; } static String nonullbr(List<String> s) { if (s == null || s.size() == 0) return ""; return "<br/>" + s.toString(); } @Override public boolean shouldOverrideUrlLoading(WebViewClient client, WebView view, String url) { Uri uri = Uri.parse(url); final String scheme = uri.getScheme(); if ("patchtext".equals(scheme)) { final Song song = currentSong; // do not proceed if the song is too old final int updateTime = S.getSongDb().getSongUpdateTime(currentBookName, song.code); if (updateTime == 0 || Sqlitil.nowDateTime() - updateTime > 21 * 86400) { new MaterialDialog.Builder(this) .content(TextUtils.expandTemplate(getText(R.string.sn_update_book_because_too_old), SongBookUtil.escapeSongBookName(currentBookName))) .positiveText(R.string.sn_update_book_confirm_button) .negativeText(R.string.cancel) .onPositive((dialog, which) -> updateSongBook()) .show(); } else { final PatchTextExtraInfoJson extraInfo = new PatchTextExtraInfoJson(); extraInfo.type = "song"; extraInfo.bookName = currentBookName; extraInfo.code = song.code; final String songHeader = song.code + " " + song.title + nonullbr(song.title_original) + nonullbr(song.tune) + nonullbr(song.keySignature) + nonullbr(song.timeSignature) + nonullbr(song.authors_lyric) + nonullbr(song.authors_music); final String songHtml = SongFragment.songToHtml(song, true); final Spanned baseBody = Html.fromHtml(songHeader + "\n\n" + songHtml); startActivity(PatchTextActivity.createIntent(baseBody, App.getDefaultGson().toJson(extraInfo), null)); } return true; } else if (BIBLE_PROTOCOL.equals(scheme)) { final IntArrayList ariRanges = TargetDecoder.decode("o:" + uri.getSchemeSpecificPart()); final VersesDialog versesDialog = VersesDialog.newInstance(ariRanges); versesDialog.setListener(new VersesDialog.VersesDialogListener() { @Override public void onVerseSelected(final VersesDialog dialog, final int ari) { startActivity(Launcher.openAppAtBibleLocationWithVerseSelected(ari)); } }); versesDialog.show(getSupportFragmentManager(), VersesDialog.class.getSimpleName()); return true; } return false; } @Override public void songKeypadButton_click(final View v) { if (currentBookName == null) return; if (state_tempCode == null) return; // only possible if the user is so fast that he presses the buttons before drawer opens for the first time final int[] numIds = { R.id.bDigit0, R.id.bDigit1, R.id.bDigit2, R.id.bDigit3, R.id.bDigit4, R.id.bDigit5, R.id.bDigit6, R.id.bDigit7, R.id.bDigit8, R.id.bDigit9, }; final int[] alphaIds = { R.id.bDigitA, R.id.bDigitB, R.id.bDigitC, // num = 10, 11, 12 }; final int id = v.getId(); int num = -1; for (int i = 0; i < numIds.length; i++) if (id == numIds[i]) num = i; for (int i = 0; i < alphaIds.length; i++) if (id == alphaIds[i]) num = 10 + i; // special code for alpha if (id == R.id.bBackspace) num = 20; // special code for backspace final LeftDrawer.Songs.Handle handle = leftDrawer.getHandle(); if (num >= 0) { // digits or letters or backspace if (num == 20) { // backspace if (state_tempCode.length() > 0) { state_tempCode = state_tempCode.substring(0, state_tempCode.length() - 1); } } else { if (state_tempCode.length() >= 4) state_tempCode = ""; // can't be more than 4 digits if (num <= 9) { // digits if (state_tempCode.length() == 0 && num == 0) { // nothing has been pressed and 0 is now pressed } else { state_tempCode += num; } } else if (num <= 19) { // letters final char letter = (char) ('A' + num - 10); if (state_tempCode.length() != 0) { state_tempCode += letter; } } } handle.setCode(state_tempCode); handle.setOkButtonEnabled(S.getSongDb().songExists(currentBookName, state_tempCode)); handle.setAButtonEnabled(state_tempCode.length() <= 3 && S.getSongDb().songExists(currentBookName, state_tempCode + "A")); handle.setBButtonEnabled(state_tempCode.length() <= 3 && S.getSongDb().songExists(currentBookName, state_tempCode + "B")); handle.setCButtonEnabled(state_tempCode.length() <= 3 && S.getSongDb().songExists(currentBookName, state_tempCode + "C")); } else if (id == R.id.bOk) { if (state_tempCode.length() > 0) { final Song song = S.getSongDb().getSong(currentBookName, state_tempCode); if (song != null) { displaySong(currentBookName, song); } else { handle.setCode(state_originalCode); // revert } } else { handle.setCode(state_originalCode); // revert } leftDrawer.closeDrawer(); } } @Override public void songBookSelected(final String name) { final Song song = S.getSongDb().getFirstSongFromBook(name); if (song != null) { displaySong(name, song); } state_tempCode = ""; } @Override public void moreSelected() { openDownloadSongBookPage(); } void setCustomProgressBarIndeterminateVisible(final boolean visible) { circular_progress.setVisibility(visible ? View.VISIBLE : View.GONE); } } interface MediaStateListener { void setMediaState(SongViewActivity.MediaPlayerController.ControllerState state); }