com.kanedias.vanilla.lyrics.LyricsShowActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.kanedias.vanilla.lyrics.LyricsShowActivity.java

Source

/*
 * Copyright (C) 2016-2018 Oleg `Kanedias` Chernovskiy <adonai@xaker.ru>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.kanedias.vanilla.lyrics;

import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.provider.DocumentFile;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewSwitcher;

import com.kanedias.vanilla.plugins.DialogActivity;
import com.kanedias.vanilla.plugins.PluginUtils;
import com.kanedias.vanilla.plugins.saf.SafPermissionHandler;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static com.kanedias.vanilla.plugins.PluginConstants.*;
import static com.kanedias.vanilla.plugins.PluginUtils.checkAndRequestPermissions;
import static com.kanedias.vanilla.plugins.PluginUtils.havePermissions;
import static com.kanedias.vanilla.plugins.saf.SafUtils.findInDocumentTree;
import static com.kanedias.vanilla.plugins.saf.SafUtils.isSafNeeded;

/**
 * Main activity of Lyrics Search plugin. This will be presented as a dialog to the user
 * if one chooses it as the requested plugin.
 * <p/>
 *
 * This activity must be able to handle ACTION_WAKE_PLUGIN and ACTION_LAUNCH_PLUGIN
 * intents coming from Vanilla Music.
 *
 * <p/>
 * Casual conversation looks like this:
 * <pre>
 *     VanillaMusic                                 Plugin
 *          |                                         |
 *          |       ACTION_WAKE_PLUGIN broadcast      |
 *          |---------------------------------------->| (plugin init if just installed)
 *          |                                         |
 *          | ACTION_REQUEST_PLUGIN_PARAMS broadcast  |
 *          |---------------------------------------->| (this is handled by BroadcastReceiver)
 *          |                                         |
 *          |      ACTION_HANDLE_PLUGIN_PARAMS        |
 *          |<----------------------------------------| (plugin answer with name and desc)
 *          |                                         |
 *          |           ACTION_LAUNCH_PLUGIN          |
 *          |---------------------------------------->| (plugin is allowed to show window)
 * </pre>
 * <p/>
 *
 * @author Oleg Chernovskiy
 */
public class LyricsShowActivity extends DialogActivity {

    private static final String PLUGIN_TAG_EDIT_PKG = "com.kanedias.vanilla.audiotag";

    private SharedPreferences mPrefs;

    private TextView mLyricsText;
    private ViewSwitcher mSwitcher;
    private Button mOkButton, mWriteButton;

    private SafPermissionHandler mSafHandler;
    private LyricsEngine mEngine = new LyricsWikiEngine();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (handleLaunchPlugin()) {
            // no UI was required for handling the intent
            return;
        }

        setContentView(R.layout.activity_lyrics_show);

        mSafHandler = new SafPermissionHandler(this);
        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        mSwitcher = findViewById(R.id.loading_switcher);
        mLyricsText = findViewById(R.id.lyrics_text);
        mWriteButton = findViewById(R.id.write_button);
        mOkButton = findViewById(R.id.ok_button);

        setupUI();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = new MenuInflater(this);
        inflater.inflate(R.menu.lyrics_options, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        for (int i = 0; i < menu.size(); i++) {
            MenuItem item = menu.getItem(i);
            switch (item.getItemId()) {
            case R.id.reload_option:
                // show only when loading is complete
                item.setVisible(mSwitcher.getDisplayedChild() == 1);
                continue;
            default:
                break;
            }
        }
        return super.onPrepareOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.reload_option:
            // show loading circle
            mSwitcher.setDisplayedChild(0);
            handleUiIntent(false);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    /**
     * Handle incoming intent that may possible be ping, other plugin request or user-interactive plugin request
     * @return true if intent was handled internally, false if activity startup is required
     */
    private boolean handleLaunchPlugin() {
        if (TextUtils.equals(getIntent().getAction(), ACTION_WAKE_PLUGIN)) {
            // just show that we're okay
            Log.i(LOG_TAG, "Plugin enabled!");
            finish();
            return true;
        }

        if (pluginInstalled(this, PLUGIN_TAG_EDIT_PKG) && !getIntent().hasExtra(EXTRA_PARAM_P2P)) {
            // it's user-requested, try to retrieve lyrics from the tag first
            Intent readLyrics = new Intent(ACTION_LAUNCH_PLUGIN);
            readLyrics.setPackage(PLUGIN_TAG_EDIT_PKG);
            readLyrics.putExtra(EXTRA_PARAM_URI, (Uri) getIntent().getParcelableExtra(EXTRA_PARAM_URI));
            readLyrics.putExtra(EXTRA_PARAM_PLUGIN_APP, getApplicationInfo());
            readLyrics.putExtra(EXTRA_PARAM_P2P, P2P_READ_TAG);
            readLyrics.putExtra(EXTRA_PARAM_P2P_KEY, new String[] { "LYRICS" }); // tag name
            readLyrics.putExtras(getIntent());
            startActivity(readLyrics);
            finish(); // end this activity instance, it will be re-created by incoming intent from Tag Editor
            return true;
        }

        return false;
    }

    @Override
    protected void onResume() {
        super.onResume();

        // UI is initialized now
        handleUiIntent(true);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        // we only request one permission
        if (!havePermissions(this, WRITE_EXTERNAL_STORAGE)) {
            // user denied our request, leave activity as-is
            return;
        }

        for (int i = 0; i < permissions.length; ++i) {
            if (TextUtils.equals(permissions[i], WRITE_EXTERNAL_STORAGE)
                    && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                // continue persist process started in Write... -> *.lrc file
                persistAsLrcFile();
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (mSafHandler.onActivityResult(requestCode, resultCode, data)) {
            persistAsLrcFile();
        }
    }

    /**
     * Handle user-interactive intent after activity was initialized. Loads lyrics from various sources if necessary.
     * @param useLocal true if tag info or *.lrc file can be used to retrieve lyrics, false if only network is allowed
     */
    private void handleUiIntent(boolean useLocal) {
        if (useLocal && !TextUtils.isEmpty(mLyricsText.getText())) {
            // we already have lyrics loaded, skip trying to get it from elsewhere
            return;
        }

        // check if this is an answer from tag plugin
        if (useLocal && TextUtils.equals(getIntent().getStringExtra(EXTRA_PARAM_P2P), P2P_READ_TAG)) {
            String[] fields = getIntent().getStringArrayExtra(EXTRA_PARAM_P2P_VAL);
            if (fields != null && fields.length > 0 && !TextUtils.isEmpty(fields[0])) {
                // start activity with retrieved lyrics
                String lyrics = getIntent().getStringArrayExtra(EXTRA_PARAM_P2P_VAL)[0];
                showFetchedLyrics(lyrics);
                return;
            }
        }

        // try to load from *.lrc file nearby
        if (useLocal && loadFromFile()) {
            return;
        }

        // we didn't receive lyrics from tag plugin, try to retrieve it via lyrics engine
        new LyricsFetcher().execute(getIntent());
    }

    /**
     * This plugin also has P2P functionality with others.
     * <br/>
     * Tag plugin - Uses provided field retrieval interface for LYRICS tag:
     * <p/>
     * <pre>
     *     Lyrics Plugin                               Tag Editor Plugin
     *          |                                         |
     *          |       P2P intent with lyrics request    |
     *          |---------------------------------------->|
     *          |                                         |
     *          |       P2P intent with lyrics response   |
     *          |<----------------------------------------| (can be empty if no embedded lyrics found)
     *          |                                         |
     *
     *     At this point lyrics plugin starts activity with either
     *     extras from lyrics response (if found) or with original intent
     * </pre>
     *
     * @param ctx context to resolve activities from
     * @param pkgName package name of the plugin to be queried
     */
    private static boolean pluginInstalled(Context ctx, String pkgName) {
        List<ResolveInfo> resolved = ctx.getPackageManager().queryIntentActivities(new Intent(ACTION_LAUNCH_PLUGIN),
                0);
        for (ResolveInfo pkg : resolved) {
            if (TextUtils.equals(pkg.activityInfo.packageName, pkgName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Retrieves companion name for lyrics file from media passed
     *
     * @param mediaFile - original media file that the lyrics was requested for
     * @return string representing name with extension for lyrics companion file
     */
    @NonNull
    private static String lyricsForFile(File mediaFile) {
        String mfName = mediaFile.getName();
        return mfName.indexOf(".") > 0 ? mfName.substring(0, mfName.lastIndexOf(".")) + ".lrc" : mfName + ".lrc";
    }

    /**
     * Try to load lyrics tag from companion *.lrc file nearby
     *
     * @return true if lyrics was loaded from file, false otherwise
     */
    private boolean loadFromFile() {
        // used didn't write any *.lrc file, skip loading
        // better, should we ask user for it in a dialog?
        if (!PluginUtils.havePermissions(this, WRITE_EXTERNAL_STORAGE)) {
            return false;
        }

        Uri fileUri = getIntent().getParcelableExtra(EXTRA_PARAM_URI);
        if (fileUri == null || fileUri.getPath() == null) {
            // wrong intent passed?
            return false;
        }

        File media = new File(fileUri.getPath());
        String lyricsFileName = lyricsForFile(media);
        File lyricsFile = new File(media.getParentFile(), lyricsFileName);
        if (!lyricsFile.exists()) {
            return false;
        }

        try {
            String lyricsText = new String(PluginUtils.readFully(new FileInputStream(lyricsFile)), "UTF-8");
            showFetchedLyrics(lyricsText);
        } catch (IOException e) {
            Log.e(LOG_TAG, "Failed to read lyrics text from file!", e);
            return false;
        }
        return true;
    }

    /**
     * Initialize UI elements with handlers and action listeners
     */
    private void setupUI() {
        mWriteButton.setOnClickListener(new SelectWriteAction());
        mOkButton.setOnClickListener(v -> finish());
    }

    /**
     * External lyrics fetcher (using network). Operates asynchronously, notifies dialog when finishes.
     * On no result (no such lyrics, couldn't fetch etc.) shows toast about this, on success updates dialog text.
     */
    private class LyricsFetcher extends AsyncTask<Intent, Void, String> {

        @Override
        protected String doInBackground(Intent... params) {
            String title = getIntent().getStringExtra(EXTRA_PARAM_SONG_TITLE);
            String artist = getIntent().getStringExtra(EXTRA_PARAM_SONG_ARTIST);
            return mEngine.getLyrics(artist, title);
        }

        @Override
        protected void onPostExecute(String lyrics) {
            if (TextUtils.isEmpty(lyrics)) {
                // no lyrics - show excuse
                Toast.makeText(LyricsShowActivity.this, R.string.lyrics_not_found, Toast.LENGTH_SHORT).show();
            }

            showFetchedLyrics(lyrics);
        }
    }

    /**
     * Stop spinning animation and show lyrics for the song.
     * Write button wil lbe active after that as lyrics will be available for persisting.
     *
     * @param lyrics retrieved song lyrics
     */
    private void showFetchedLyrics(String lyrics) {
        if (TextUtils.isEmpty(lyrics)) {
            // nothing found
            mWriteButton.setEnabled(false);
        } else {
            // some lyrics was extracted
            mWriteButton.setEnabled(true);
        }
        mLyricsText.setText(lyrics);
        mSwitcher.setDisplayedChild(1);
        invalidateOptionsMenu();
    }

    /**
     * Write lyrics as a *.lrc file - selects SAF/File routine based on target access.
     * Resulting file should be placed in the same directory as media file but with *.lrc extension instead.
     */
    private void persistAsLrcFile() {
        Uri fileUri = getIntent().getParcelableExtra(EXTRA_PARAM_URI);
        if (fileUri == null || fileUri.getPath() == null) {
            // wrong intent passed?
            return;
        }

        File mediaFile = new File(fileUri.getPath());
        if (!mediaFile.exists()) {
            // file deleted while launching intent or player db is not refreshed
            return;
        }

        String lrcFilename = lyricsForFile(mediaFile);
        File lrcTarget = new File(mediaFile.getParent(), lrcFilename);
        byte[] data = mLyricsText.getText().toString().getBytes(Charset.forName("UTF-8"));
        if (isSafNeeded(mediaFile, this)) {
            if (mPrefs.contains(PREF_SDCARD_URI)) {
                // we already got the permission!
                writeThroughSaf(data, mediaFile, lrcTarget.getName());
                return;
            }

            // request SAF permissions in handler
            mSafHandler.handleFile(mediaFile);
        } else {
            writeThroughFile(data, lrcTarget);
        }
    }

    /**
     * Write to *.lrc file through file-based API
     *
     * @param data     - data to write
     * @param target   - target file for writing metadata into
     */
    private void writeThroughFile(byte[] data, File target) {
        try {
            FileOutputStream fos = new FileOutputStream(target);
            fos.write(data);
            fos.close();

            Toast.makeText(this, R.string.file_written_successfully, Toast.LENGTH_SHORT).show();
        } catch (Exception e) {
            Toast.makeText(this, getString(R.string.error_writing_file) + e.getLocalizedMessage(),
                    Toast.LENGTH_LONG).show();
            Log.e(LOG_TAG, "Failed to write to file descriptor provided by SAF!", e);
        }
    }

    /**
     * Write lyrics to *.lrc file through SAF framework - the only way to do it in Android > 4.4 when working with SD card
     */
    private void writeThroughSaf(byte[] data, File original, String name) {
        DocumentFile originalRef;
        if (mPrefs.contains(PREF_SDCARD_URI)) {
            // no sorcery can allow you to gain URI to the document representing file you've been provided with
            // you have to find it again now using Document API

            // /storage/volume/Music/some.mp3 will become [storage, volume, music, some.mp3]
            List<String> pathSegments = new ArrayList<>(Arrays.asList(original.getAbsolutePath().split("/")));
            Uri allowedSdRoot = Uri.parse(mPrefs.getString(PREF_SDCARD_URI, ""));
            originalRef = findInDocumentTree(DocumentFile.fromTreeUri(this, allowedSdRoot), pathSegments);
        } else {
            // user will click the button again
            return;
        }

        if (originalRef == null || originalRef.getParentFile() == null) {
            // nothing selected or invalid file?
            Toast.makeText(this, R.string.saf_nothing_selected, Toast.LENGTH_LONG).show();
            return;
        }

        DocumentFile lrcFileRef = originalRef.getParentFile().createFile("image/*", name);
        if (lrcFileRef == null) {
            // couldn't create file?
            Toast.makeText(this, R.string.saf_write_error, Toast.LENGTH_LONG).show();
            return;
        }

        try {
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(lrcFileRef.getUri(), "rw");
            if (pfd == null) {
                // should not happen
                Log.e(LOG_TAG, "SAF provided incorrect URI!" + lrcFileRef.getUri());
                return;
            }

            FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor());
            fos.write(data);
            fos.close();

            // rescan original file
            Toast.makeText(this, R.string.file_written_successfully, Toast.LENGTH_SHORT).show();
        } catch (Exception e) {
            Toast.makeText(this, getString(R.string.saf_write_error) + e.getLocalizedMessage(), Toast.LENGTH_LONG)
                    .show();
            Log.e(LOG_TAG, "Failed to write to file descriptor provided by SAF!", e);
        }
    }

    /**
     * Write to the song tag using Tag Editor Plugin
     */
    private void writeToFileTag() {
        String lyrics = mLyricsText.getText().toString();
        Intent request = new Intent(ACTION_LAUNCH_PLUGIN);
        request.setPackage(PLUGIN_TAG_EDIT_PKG);
        request.putExtra(EXTRA_PARAM_URI, (Uri) getIntent().getParcelableExtra(EXTRA_PARAM_URI));
        request.putExtra(EXTRA_PARAM_PLUGIN_APP, getApplicationInfo());
        request.putExtra(EXTRA_PARAM_P2P, P2P_WRITE_TAG);
        request.putExtra(EXTRA_PARAM_P2P_KEY, new String[] { "LYRICS" }); // tag name
        request.putExtra(EXTRA_PARAM_P2P_VAL, new String[] { lyrics }); // tag value
        startActivity(request);
    }

    /**
     * CLick listener for P2P integration, sends intent to write retrieved lyrics to local file tag or to
     * lyrics file
     */
    private class SelectWriteAction implements View.OnClickListener {

        @Override
        public void onClick(View v) {
            List<String> actions = new ArrayList<>();
            actions.add(getString(R.string.write_to_lrc));

            // if tag editor is installed, show `write to tag` button
            if (pluginInstalled(LyricsShowActivity.this, PLUGIN_TAG_EDIT_PKG)) {
                actions.add(getString(R.string.write_to_tag));
            }

            new AlertDialog.Builder(LyricsShowActivity.this)
                    .setItems(actions.toArray(new CharSequence[0]), (dialog, which) -> {
                        switch (which) {
                        case 0: // to lyrics file
                            if (!checkAndRequestPermissions(LyricsShowActivity.this, WRITE_EXTERNAL_STORAGE)) {
                                return;
                            }

                            persistAsLrcFile();
                            break;
                        case 1: // to media file tag
                            writeToFileTag();
                            break;
                        }
                    }).create().show();
        }
    }
}