/* * Copyright (C) 2014 Fastboot Mobile, LLC. * * 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.fastbootmobile.encore.app.fragments; import android.Manifest; import android.app.Activity; import android.app.SearchManager; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import android.support.v7.widget.CardView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.fastbootmobile.encore.app.MainActivity; import com.fastbootmobile.encore.app.R; import com.fastbootmobile.encore.app.SearchActivity; import com.fastbootmobile.encore.app.ui.AnimatedMicButton; import com.fastbootmobile.encore.framework.EchoPrint; import com.fastbootmobile.encore.providers.ProviderAggregator; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; /** * Fragment displaying the controls for the song fingerprinting and recognition system */ public class RecognitionFragment extends Fragment implements EchoPrint.PrintCallback { private static final String TAG = "RecognitionFragment"; private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 128; private static final int MSG_AUDIO_LEVEL = 1; private static final int MSG_RESULT = 2; private static final int MSG_NO_RESULT = 3; private static final int MSG_ERROR = 4; private static final int FADE_DURATION = 500; private EchoPrint mActivePrint; private EchoPrint.PrintResult mLastResult; private LinearLayout mButtonLayout; private AnimatedMicButton mRecognitionButton; private TextView mTvStatus; private TextView mTvDetails; private CardView mCardResult; private TextView mTvTitle; private TextView mTvArtist; private TextView mTvAlbum; private ImageView mIvArt; private Button mSearchButton; private TextView mTvOfflineError; private static class RecognitionHandler extends Handler { private WeakReference<RecognitionFragment> mParent; public RecognitionHandler(WeakReference<RecognitionFragment> parent) { mParent = parent; } @Override public void handleMessage(Message msg) { RecognitionFragment parent = mParent.get(); if (parent != null) { if (msg.what == MSG_AUDIO_LEVEL) { float value = (Float) msg.obj; if (value >= 0.0f) { parent.setVoiceLevel(value); } } else if (msg.what == MSG_RESULT) { parent.showLastResult(); } else if (msg.what == MSG_NO_RESULT) { parent.onNoResults(); } else if (msg.what == MSG_ERROR) { parent.showErrorToast(); parent.onNoResults(); parent.mHandler.removeCallbacks(parent.mStopRecognition); } } } } private RecognitionHandler mHandler; private Runnable mStopRecognition = new Runnable() { @Override public void run() { // As long as we haven't received either onNoMatch or onResult, the audio data // should be in a processing state mActivePrint.stopRecording(); mRecognitionButton.setActive(false); onStoppedAndRecognizing(); } }; /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @return A new instance of fragment RecognitionFragment. */ public static RecognitionFragment newInstance() { return new RecognitionFragment(); } public RecognitionFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler = new RecognitionHandler(new WeakReference<>(this)); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment RelativeLayout root = (RelativeLayout) inflater.inflate(R.layout.fragment_recognition, container, false); // Recognition layout mButtonLayout = (LinearLayout) root.findViewById(R.id.llRecognitionButton); mRecognitionButton = (AnimatedMicButton) root.findViewById(R.id.btnStartRec); mTvStatus = (TextView) root.findViewById(R.id.tvStatus); mTvDetails = (TextView) root.findViewById(R.id.tvDetailsText); // Result layout mCardResult = (CardView) root.findViewById(R.id.cardResult); mTvAlbum = (TextView) root.findViewById(R.id.tvAlbumName); mTvArtist = (TextView) root.findViewById(R.id.tvArtistName); mTvTitle = (TextView) root.findViewById(R.id.tvTrackName); mIvArt = (ImageView) root.findViewById(R.id.ivRecognitionArt); mSearchButton = (Button) root.findViewById(R.id.btnSearch); mTvOfflineError = (TextView) root.findViewById(R.id.tvErrorMessage); mTvOfflineError.setText(R.string.error_recognition_unavailable_offline); ProviderAggregator aggregator = ProviderAggregator.getDefault(); mTvOfflineError.setVisibility(aggregator.isOfflineMode() ? View.VISIBLE : View.GONE); mRecognitionButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (mActivePrint == null) { // Ensure we have the permissions to record audio if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { // No explanation needed, we can request the permission. ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST_RECORD_AUDIO); } else { startRecording(); } } else { mHandler.removeCallbacks(mStopRecognition); mStopRecognition.run(); } } }); mSearchButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent search = new Intent(getActivity(), SearchActivity.class); search.setAction(Intent.ACTION_SEARCH); search.putExtra(SearchManager.QUERY, mLastResult.ArtistName + " " + mLastResult.TrackName); startActivity(search); } }); return root; } @Override public void onAttach(Activity activity) { super.onAttach(activity); MainActivity mainActivity = (MainActivity) activity; mainActivity.onSectionAttached(MainActivity.SECTION_RECOGNITION); } private void startRecording() { mActivePrint = new EchoPrint(RecognitionFragment.this); mActivePrint.startRecording(); onRecognitionStartUI(); // The buffer has a max size of 20 seconds, so we force stop at around 19 seconds mHandler.postDelayed(mStopRecognition, 19000); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { switch (requestCode) { case MY_PERMISSIONS_REQUEST_RECORD_AUDIO: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startRecording(); } break; } } } @Override public void onResult(EchoPrint.PrintResult result) { mLastResult = result; mHandler.sendEmptyMessage(MSG_RESULT); mHandler.removeCallbacks(mStopRecognition); } @Override public void onNoMatch() { mHandler.sendEmptyMessage(MSG_NO_RESULT); } @Override public void onAudioLevel(final float level) { mHandler.obtainMessage(MSG_AUDIO_LEVEL, level).sendToTarget(); } @Override public void onError() { mHandler.obtainMessage(MSG_ERROR).sendToTarget(); } private void onStoppedAndRecognizing() { onRecognitionStopUI(); } private void loadAlbumArt(final String urlString, final ImageView iv) { new Thread() { public void run() { URL url; try { url = new URL(urlString); } catch (MalformedURLException e) { // Too bad mHandler.post(new Runnable() { @Override public void run() { mIvArt.setImageResource(R.drawable.album_placeholder); } }); return; } Log.d(TAG, "Loading album art: " + urlString); try { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); InputStream is = conn.getInputStream(); byte[] buffer = new byte[8192]; ByteArrayOutputStream baos = new ByteArrayOutputStream(); int read; while ((read = is.read(buffer)) > 0) { baos.write(buffer, 0, read); } final Bitmap bmp = BitmapFactory.decodeByteArray(baos.toByteArray(), 0, baos.size()); if (bmp != null) { mHandler.post(new Runnable() { @Override public void run() { iv.setImageBitmap(bmp); iv.setVisibility(View.VISIBLE); } }); } else { Log.e(TAG, "Null bitmap from image"); } } catch (IOException e) { Log.e(TAG, "Error downloading album art", e); mHandler.post(new Runnable() { @Override public void run() { iv.setImageResource(R.drawable.album_placeholder); iv.setVisibility(View.VISIBLE); } }); } } }.start(); } public void setVoiceLevel(float level) { mRecognitionButton.setLevel(level); } public void onRecognitionStartUI() { mRecognitionButton.setActive(true); if (mTvDetails.getAlpha() > 0) { // Hide the details text mTvDetails.animate() .alpha(0) .translationY(mTvDetails.getMeasuredHeight()) .setDuration(FADE_DURATION) .setInterpolator(new AccelerateDecelerateInterpolator()) .start(); } mTvStatus.setText(R.string.recognition_status_listening); hideResultCard(); } public void onRecognitionStopUI() { // Disable the button and prevent clicking it while we're processing mRecognitionButton.setActive(false); mRecognitionButton.setEnabled(false); mTvStatus.setText(R.string.recognition_status_recognizing); } public void onNoResults() { mActivePrint = null; showNoResult(); } public void showLastResult() { mActivePrint = null; showResultCard(); mTvAlbum.setText(mLastResult.AlbumName); mTvTitle.setText(mLastResult.TrackName); mTvArtist.setText(mLastResult.ArtistName); mRecognitionButton.setActive(false); mRecognitionButton.setEnabled(true); mTvStatus.setText(R.string.recognition_status_idle); // Load the album art in a thread loadAlbumArt(mLastResult.AlbumImageUrl, mIvArt); } public void showResultCard() { mCardResult.animate().alpha(1).translationY(-mCardResult.getMeasuredHeight()) .setDuration(FADE_DURATION).setInterpolator(new AccelerateDecelerateInterpolator()) .start(); mButtonLayout.animate().translationY(-mCardResult.getMeasuredHeight()) .setDuration(FADE_DURATION).setInterpolator(new AccelerateDecelerateInterpolator()) .start(); } public void hideResultCard() { mCardResult.animate().alpha(0).translationY(mCardResult.getMeasuredHeight()) .setDuration(FADE_DURATION).setInterpolator(new AccelerateDecelerateInterpolator()) .start(); mButtonLayout.animate().translationY(0) .setDuration(FADE_DURATION).setInterpolator(new AccelerateDecelerateInterpolator()) .start(); } public void showNoResult() { mRecognitionButton.setActive(false); mRecognitionButton.setEnabled(true); mTvStatus.setText(R.string.recognition_status_no_result); } public void showErrorToast() { Toast.makeText(getActivity(), R.string.toast_recognition_error, Toast.LENGTH_SHORT).show(); } }