/* * 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; import android.annotation.TargetApi; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; import com.fastbootmobile.encore.app.ui.AlbumArtImageView; import com.fastbootmobile.encore.app.ui.PlayPauseDrawable; import com.fastbootmobile.encore.framework.PlaybackProxy; import com.fastbootmobile.encore.model.Album; import com.fastbootmobile.encore.model.Artist; import com.fastbootmobile.encore.model.Playlist; import com.fastbootmobile.encore.model.SearchResult; import com.fastbootmobile.encore.model.Song; import com.fastbootmobile.encore.providers.ILocalCallback; import com.fastbootmobile.encore.providers.IMusicProvider; import com.fastbootmobile.encore.providers.ProviderAggregator; import com.fastbootmobile.encore.service.BasePlaybackCallback; import com.fastbootmobile.encore.service.NavHeadService; import com.fastbootmobile.encore.service.PlaybackService; import com.fastbootmobile.encore.utils.Utils; import com.fastbootmobile.encore.voice.VoiceActionHelper; import com.fastbootmobile.encore.voice.VoiceCommander; import com.fastbootmobile.encore.voice.VoiceRecognizer; import java.lang.ref.WeakReference; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; /** * */ public class DriveModeActivity extends AppActivity implements ILocalCallback, View.OnClickListener, SeekBar.OnSeekBarChangeListener, GestureDetector.OnGestureListener { private static final String TAG = "DriveModeActivity"; public static final String ACTION_FINISH = "com.fastbootmobile.encore.action.FINISH_DRIVE_MODE"; private static final int DELAY_SEEKBAR_UPDATE = 1000 / 15; // 15 Hz private static final int MSG_UPDATE_PLAYBACK_STATUS = 1; private static final int MSG_UPDATE_SEEKBAR = 2; private static final int MSG_UPDATE_TIME = 3; private static final int MSG_HIDE_SYSTEM_UI = 4; private static final String PREFS_DRIVE_MODE = "drive_mode"; private static final String PREF_ONBOARDING_DONE = "onboarding_is_done"; private boolean mBackPressed = false; private boolean mPausedForOnboarding = false; private DriveHandler mHandler; private DrivePlaybackCallback mPlaybackCallback; private View mDecorView; private PlayPauseDrawable mPlayDrawable; private ImageView mPlayButton; private ImageView mPreviousButton; private ImageView mSkipButton; private ImageView mVoiceButton; private ImageView mMapsButton; private TextView mTvTitle; private TextView mTvArtist; private TextView mTvAlbum; private TextView mTvCurrentTime; private AlbumArtImageView mIvAlbumArt; private SeekBar mSeek; private ProgressBar mPbVoiceLoading; private VoiceRecognizer mVoiceRecognizer; private VoiceCommander mVoiceCommander; private VoiceActionHelper mVoiceHelper; private GestureDetector mDetector; private BroadcastReceiver mBroadcastRcv = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ACTION_FINISH)) { finish(); } } }; private static class DriveHandler extends Handler { private WeakReference<DriveModeActivity> mParent; public DriveHandler(WeakReference<DriveModeActivity> parent) { mParent = parent; } @Override public void handleMessage(Message msg) { if (mParent.get() != null) { switch (msg.what) { case MSG_UPDATE_PLAYBACK_STATUS: mParent.get().updatePlaybackStatus(); break; case MSG_UPDATE_SEEKBAR: mParent.get().updateSeekBar(); break; case MSG_UPDATE_TIME: mParent.get().updateTime(); break; case MSG_HIDE_SYSTEM_UI: mParent.get().hideSystemUI(); break; } } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); // Allow for BluetoothReceiver to kill the activity on BT disconnect registerReceiver(mBroadcastRcv, new IntentFilter(ACTION_FINISH)); mDetector = new GestureDetector(this,this); mHandler = new DriveHandler(new WeakReference<>(this)); mPlaybackCallback = new DrivePlaybackCallback(); mVoiceCommander = new VoiceCommander(this); mVoiceRecognizer = new VoiceRecognizer(this); mVoiceHelper = new VoiceActionHelper(this); mVoiceRecognizer.setListener(new VoiceRecognizer.Listener() { @Override public void onReadyForSpeech() { setVoiceEmphasis(true, true); PlaybackProxy.pause(); } @Override public void onBeginningOfSpeech() { } @Override public void onEndOfSpeech() { setVoiceEmphasis(false, true); PlaybackProxy.play(); resetVoiceRms(); mHandler.postDelayed(new Runnable() { @Override public void run() { mPbVoiceLoading.setVisibility(View.GONE); } }, 1000); } @Override public void onRmsChanged(float rmsdB) { setVoiceRms(rmsdB); } @Override public void onError(int error) { setVoiceEmphasis(false, true); resetVoiceRms(); PlaybackProxy.play(); mPbVoiceLoading.setVisibility(View.GONE); mTvArtist.setAlpha(1.0f); if (!mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATUS)) { mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); } } @Override public void onResults(List<String> results) { if (results != null && results.size() > 0) { mTvArtist.setText(results.get(0)); mTvArtist.setAlpha(1.0f); mVoiceCommander.processResult(results, mVoiceHelper); mPbVoiceLoading.setVisibility(View.VISIBLE); mHandler.postDelayed(new Runnable() { @Override public void run() { mPbVoiceLoading.setVisibility(View.GONE); mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); } }, 2000); } } @Override public void onPartialResults(List<String> results) { if (results != null && results.size() > 0) { mTvArtist.setText(results.get(0)); mTvArtist.setAlpha(0.7f); mPbVoiceLoading.setVisibility(View.VISIBLE); } } }); setContentView(R.layout.activity_drive_mode); mDecorView = findViewById(R.id.rlDriveRoot); mPlayButton = (ImageView) findViewById(R.id.btnPlayPause); mPreviousButton = (ImageView) findViewById(R.id.btnPrevious); mSkipButton = (ImageView) findViewById(R.id.btnNext); mVoiceButton = (ImageView) findViewById(R.id.btnVoice); mMapsButton = (ImageView) findViewById(R.id.btnMaps); mTvTitle = (TextView) findViewById(R.id.tvTitle); mTvArtist = (TextView) findViewById(R.id.tvArtist); mTvAlbum = (TextView) findViewById(R.id.tvAlbum); mTvCurrentTime = (TextView) findViewById(R.id.tvCurrentTime); mIvAlbumArt = (AlbumArtImageView) findViewById(R.id.ivAlbumArt); mSeek = (SeekBar) findViewById(R.id.sbSeek); mPbVoiceLoading = (ProgressBar) findViewById(R.id.pbVoiceLoading); mPlayDrawable = new PlayPauseDrawable(getResources(), 1.5f, 1.6f); mPlayDrawable.setShape(PlayPauseDrawable.SHAPE_PLAY); mPlayButton.setImageDrawable(mPlayDrawable); mPlayButton.setOnClickListener(this); mPreviousButton.setOnClickListener(this); mSkipButton.setOnClickListener(this); mMapsButton.setOnClickListener(this); mVoiceButton.setOnClickListener(this); mSeek.setOnSeekBarChangeListener(this); mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); mHandler.sendEmptyMessageDelayed(MSG_UPDATE_SEEKBAR, DELAY_SEEKBAR_UPDATE); mHandler.sendEmptyMessage(MSG_UPDATE_TIME); SharedPreferences prefs = getSharedPreferences(PREFS_DRIVE_MODE, 0); if (!prefs.getBoolean(PREF_ONBOARDING_DONE, false)) { mPausedForOnboarding = true; prefs.edit().putBoolean(PREF_ONBOARDING_DONE, true).apply(); startActivity(new Intent(this, DriveTutorialActivity.class)); } } @TargetApi(Build.VERSION_CODES.KITKAT) private void hideSystemUI() { int stickyFlag = 0; if (Utils.hasKitKat()) { stickyFlag = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } mDecorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar | stickyFlag); } private void showSystemUI() { mDecorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } private void updatePlaybackStatus() { int state = PlaybackProxy.getState(); switch (state) { case PlaybackService.STATE_STOPPED: case PlaybackService.STATE_PAUSED: mPlayDrawable.setShape(PlayPauseDrawable.SHAPE_PLAY); mPlayDrawable.setBuffering(false); break; case PlaybackService.STATE_BUFFERING: mPlayDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE); mPlayDrawable.setBuffering(true); break; case PlaybackService.STATE_PAUSING: mPlayDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE); mPlayDrawable.setBuffering(true); break; case PlaybackService.STATE_PLAYING: mPlayDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE); mPlayDrawable.setBuffering(false); break; } Song currentTrack = PlaybackProxy.getCurrentTrack(); if (currentTrack != null && currentTrack.isLoaded()) { final ProviderAggregator aggregator = ProviderAggregator.getDefault(); mTvTitle.setText(currentTrack.getTitle()); if (currentTrack.getArtist() != null) { Artist artist = aggregator.retrieveArtist(currentTrack.getArtist(), currentTrack.getProvider()); if (artist != null && artist.getName() != null && !artist.getName().isEmpty()) { mTvArtist.setText(artist.getName()); } else if (artist != null && !artist.isLoaded()) { mTvArtist.setText(R.string.loading); } else { mTvArtist.setText(null); } } else { mTvArtist.setText(null); } if (currentTrack.getAlbum() != null) { Album album = aggregator.retrieveAlbum(currentTrack.getAlbum(), currentTrack.getProvider()); if (album != null && album.getName() != null && !album.getName().isEmpty()) { mTvAlbum.setText(album.getName()); } else if (album != null && !album.isLoaded()) { mTvAlbum.setText(R.string.loading); } else { mTvAlbum.setText(null); } } else { mTvAlbum.setText(null); } mIvAlbumArt.loadArtForSong(currentTrack); mSeek.setMax(currentTrack.getDuration()); } else if (currentTrack != null) { mTvTitle.setText(R.string.loading); mTvArtist.setText(null); mIvAlbumArt.setDefaultArt(); } else { // TODO: No song playing } } private void updateSeekBar() { int state = PlaybackProxy.getState(); if (state == PlaybackService.STATE_PLAYING) { int elapsedMs = PlaybackProxy.getCurrentTrackPosition(); mSeek.setProgress(elapsedMs); mHandler.sendEmptyMessageDelayed(MSG_UPDATE_SEEKBAR, DELAY_SEEKBAR_UPDATE); mSeek.setVisibility(View.VISIBLE); } else { mSeek.setVisibility(View.INVISIBLE); } } private void updateTime() { Calendar cal = GregorianCalendar.getInstance(); cal.setTime(new Date()); mTvCurrentTime.setText(String.format("%02d:%02d", cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))); mHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, 5000); } private void setButtonDarker(ImageView button, boolean darker) { if (darker) { button.animate().alpha(0.3f).setDuration(300).start(); } else { button.animate().alpha(1).setDuration(300).start(); } } private void setVoiceEmphasis(boolean emphasis, boolean voice) { setButtonDarker(mVoiceButton, !voice); setButtonDarker(mPreviousButton, emphasis); setButtonDarker(mPlayButton, emphasis); setButtonDarker(mMapsButton, emphasis); setButtonDarker(mSkipButton, emphasis); } private void setVoiceRms(float rmsdB) { Drawable drawable = mVoiceButton.getDrawable(); int red = (int) (((rmsdB + 2.0f) / 12.0f) * 255.0f); drawable.setColorFilter(((0xFFFF0000) | (red << 8) | red), PorterDuff.Mode.MULTIPLY); } private void resetVoiceRms() { Drawable drawable = mVoiceButton.getDrawable(); drawable.setColorFilter(null); } @Override protected void onPause() { ProviderAggregator.getDefault().removeUpdateCallback(this); PlaybackProxy.removeCallback(mPlaybackCallback); final int state = PlaybackProxy.getState(); if (!mBackPressed && !mPausedForOnboarding && state != PlaybackService.STATE_PAUSED && state != PlaybackService.STATE_STOPPED) { // Start NavHead for easy going back into Drive mode startService(new Intent(this, NavHeadService.class)); } super.onPause(); } @Override protected void onDestroy() { PlaybackProxy.removeCallback(mPlaybackCallback); // Hide NavHead as we're getting back into the app stopService(new Intent(this, NavHeadService.class)); unregisterReceiver(mBroadcastRcv); super.onDestroy(); } @Override protected void onResume() { super.onResume(); ProviderAggregator.getDefault().addUpdateCallback(this); PlaybackProxy.addCallback(mPlaybackCallback); mHandler.sendEmptyMessageDelayed(MSG_HIDE_SYSTEM_UI, 1000); mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); mHandler.sendEmptyMessageDelayed(MSG_UPDATE_SEEKBAR, DELAY_SEEKBAR_UPDATE); // Hide NavHead as we're getting back into the app stopService(new Intent(this, NavHeadService.class)); } private void startMaps() { Intent mapIntent = new Intent(Intent.ACTION_VIEW); mapIntent.setPackage("com.google.android.apps.maps"); try { startActivity(mapIntent); } catch (ActivityNotFoundException e) { // User doesn't have Google Maps Toast.makeText(this, R.string.toast_no_gmaps, Toast.LENGTH_SHORT).show(); } } public void onClickClose(View v) { finish(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); mHandler.sendEmptyMessageDelayed(MSG_HIDE_SYSTEM_UI, 1000); } @Override public void onBackPressed() { super.onBackPressed(); mBackPressed = true; // Stop NavHead if needed stopService(new Intent(this, NavHeadService.class)); } @Override public void onClick(View v) { if (v == mPlayButton) { final int state = PlaybackProxy.getState(); switch (state) { case PlaybackService.STATE_PLAYING: case PlaybackService.STATE_BUFFERING: PlaybackProxy.pause(); break; default: PlaybackProxy.play(); break; } } else if (v == mPreviousButton) { PlaybackProxy.previous(); } else if (v == mSkipButton) { PlaybackProxy.next(); } else if (v == mMapsButton) { startMaps(); } else if (v == mVoiceButton) { setVoiceEmphasis(true, false); mVoiceRecognizer.startListening(); } } @Override public void onSongUpdate(List<Song> s) { final Song currentTrack = PlaybackProxy.getCurrentTrack(); if (s.contains(currentTrack)) { if (!mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATUS)) { mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); } } mVoiceHelper.onSongUpdate(s); } @Override public void onAlbumUpdate(List<Album> a) { mVoiceHelper.onAlbumUpdate(a); } @Override public void onPlaylistUpdate(List<Playlist> p) { } @Override public void onPlaylistRemoved(String ref) { } @Override public void onArtistUpdate(List<Artist> a) { final Song currentTrack = PlaybackProxy.getCurrentTrack(); if (currentTrack != null) { for (Artist artist : a) { if (artist.getRef().equals(currentTrack.getArtist())) { if (!mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATUS)) { mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); } break; } } } mVoiceHelper.onArtistUpdate(a); } @Override public void onProviderConnected(IMusicProvider provider) { } @Override public void onSearchResult(List<SearchResult> searchResult) { mVoiceHelper.onSearchResult(searchResult); } private class DrivePlaybackCallback extends BasePlaybackCallback { @Override public void onPlaybackPause() throws RemoteException { if (!mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATUS)) { mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); } } @Override public void onSongStarted(final boolean buffering, Song s) throws RemoteException { if (!mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATUS)) { mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); mHandler.sendEmptyMessageDelayed(MSG_UPDATE_SEEKBAR, DELAY_SEEKBAR_UPDATE); } } @Override public void onPlaybackResume() throws RemoteException { if (!mHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATUS)) { mHandler.sendEmptyMessage(MSG_UPDATE_PLAYBACK_STATUS); mHandler.sendEmptyMessageDelayed(MSG_UPDATE_SEEKBAR, DELAY_SEEKBAR_UPDATE); } } } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { PlaybackProxy.seek(progress); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } @Override public boolean onTouchEvent(MotionEvent event) { this.mDetector.onTouchEvent(event); return super.onTouchEvent(event); } @Override public boolean onDown(MotionEvent e) { return true; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { int state = PlaybackProxy.getState(); if (state == PlaybackService.STATE_BUFFERING || state == PlaybackService.STATE_PLAYING) { PlaybackProxy.pause(); } else { PlaybackProxy.play(); } return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return true; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (Math.abs(velocityY) > Math.abs(velocityX)) { // Vertical gesture if (velocityY < -400) { startMaps(); } } else { // Horizontal gesture if (velocityX < -500) { PlaybackProxy.previous(); } else if (velocityX > 500) { PlaybackProxy.next(); } } return true; } }