Java tutorial
/* This file is part of the Android Clementine Remote. * Copyright (C) 2013, Andreas Muttscheller <asfa194@gmail.com> * * 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 de.qspool.clementineremote.backend; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.graphics.Bitmap; import android.media.AudioManager; import android.media.AudioManager.OnAudioFocusChangeListener; import android.media.MediaMetadataRetriever; import android.media.RemoteControlClient; import android.media.RemoteControlClient.MetadataEditor; import android.net.TrafficStats; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.support.v4.app.NotificationCompat; import android.util.Log; import java.util.ArrayList; import java.util.Date; import de.qspool.clementineremote.App; import de.qspool.clementineremote.R; import de.qspool.clementineremote.backend.event.OnConnectionListener; import de.qspool.clementineremote.backend.pb.ClementineMessage; import de.qspool.clementineremote.backend.pb.ClementineMessage.ErrorMessage; import de.qspool.clementineremote.backend.pb.ClementineMessage.MessageGroup; import de.qspool.clementineremote.backend.pb.ClementineMessageFactory; import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.Message.Builder; import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.MsgType; import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.ReasonDisconnect; import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.ResponseDisconnect; import de.qspool.clementineremote.backend.pebble.Pebble; import de.qspool.clementineremote.backend.player.MySong; import de.qspool.clementineremote.backend.receivers.ClementineMediaButtonEventReceiver; /** * This Thread-Class is used to communicate with Clementine */ public class ClementinePlayerConnection extends ClementineSimpleConnection implements Runnable { public ClementineConnectionHandler mHandler; private final String TAG = getClass().getSimpleName(); private final long KEEP_ALIVE_TIMEOUT = 25000; // 25 Second timeout private final int MAX_RECONNECTS = 5; public final static int PROCESS_PROTOC = 874456; private Handler mUiHandler; private int mLeftReconnects; private long mLastKeepAlive; private NotificationCompat.Builder mNotifyBuilder; private NotificationManager mNotificationManager; private int mNotificationWidth; private int mNotificationHeight; private MySong mLastSong = null; private Clementine.State mLastState; private AudioManager mAudioManager; private ComponentName mClementineMediaButtonEventReceiver; private RemoteControlClient mRcClient; private BroadcastReceiver mMediaButtonBroadcastReceiver; private ArrayList<OnConnectionListener> mListeners = new ArrayList<>(); private ClementineMessage mRequestConnect; private PowerManager.WakeLock mWakeLock; private Pebble mPebble; private long mStartTx; private long mStartRx; private long mStartTime; private Thread mIncomingThread; /** * Add a new listener for closed connections * * @param listener The listener object */ public void setOnConnectionListener(OnConnectionListener listener) { mListeners.add(listener); } public void run() { // Start the thread mNotificationManager = (NotificationManager) App.mApp.getSystemService(Context.NOTIFICATION_SERVICE); Looper.prepare(); mHandler = new ClementineConnectionHandler(this); mPebble = new Pebble(); // Get a Wakelock Object PowerManager pm = (PowerManager) App.mApp.getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Clementine"); Resources res = App.mApp.getResources(); mNotificationHeight = (int) res.getDimension(android.R.dimen.notification_large_icon_height); mNotificationWidth = (int) res.getDimension(android.R.dimen.notification_large_icon_width); mAudioManager = (AudioManager) App.mApp.getSystemService(Context.AUDIO_SERVICE); mClementineMediaButtonEventReceiver = new ComponentName(App.mApp.getPackageName(), ClementineMediaButtonEventReceiver.class.getName()); mMediaButtonBroadcastReceiver = new ClementineMediaButtonEventReceiver(); fireOnConnectionReady(); Looper.loop(); } public void setNotificationBuilder(NotificationCompat.Builder builder) { mNotifyBuilder = builder; } /** * Try to connect to Clementine * * @param message The Request Object. Stores the ip to connect to. */ @Override public boolean createConnection(ClementineMessage message) { // Reset the connected flag mLastKeepAlive = 0; // Now try to connect and set the input and output streams boolean connected = super.createConnection(message); // Check if Clementine dropped the connection. // Is possible when we connect from a public ip and clementine rejects it if (connected && !mSocket.isClosed()) { // Now we are connected mLastSong = null; mLastState = App.mClementine.getState(); // Setup the MediaButtonReceiver and the RemoteControlClient registerRemoteControlClient(); // Register MediaButtonReceiver IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON); App.mApp.registerReceiver(mMediaButtonBroadcastReceiver, filter); updateNotification(); // The device shall be awake mWakeLock.acquire(); // We can now reconnect MAX_RECONNECTS times when // we get a keep alive timeout mLeftReconnects = MAX_RECONNECTS; // Set the current time to last keep alive setLastKeepAlive(System.currentTimeMillis()); // Until we get a new connection request from ui, // don't request the first data a second time mRequestConnect = ClementineMessageFactory.buildConnectMessage(message.getIp(), message.getPort(), message.getMessage().getRequestConnect().getAuthCode(), false, message.getMessage().getRequestConnect().getDownloader()); // Save started transmitted bytes int uid = App.mApp.getApplicationInfo().uid; mStartTx = TrafficStats.getUidTxBytes(uid); mStartRx = TrafficStats.getUidRxBytes(uid); mStartTime = new Date().getTime(); // Create a new thread for reading data from Clementine. // This is done blocking, so we receive the data directly instead of // waiting for the handler and still be able to send commands directly. mIncomingThread = new Thread(new Runnable() { @Override public void run() { while (isConnected() && !mIncomingThread.isInterrupted()) { checkKeepAlive(); ClementineMessage m = getProtoc(); if (!m.isErrorMessage() || m.getErrorMessage() != ErrorMessage.TIMEOUT) { Message msg = Message.obtain(); msg.obj = m; msg.arg1 = PROCESS_PROTOC; mHandler.sendMessage(msg); } } Log.d(TAG, "reading thread exit"); } }); mIncomingThread.start(); } else { sendUiMessage(new ClementineMessage(ErrorMessage.NO_CONNECTION)); } return connected; } /** * Process the received protocol buffer * * @param clementineMessage The Message received from Clementine */ protected void processProtocolBuffer(ClementineMessage clementineMessage) { // Close the connection if we have an old proto verion if (clementineMessage.isErrorMessage()) { closeConnection(clementineMessage); } else if (clementineMessage.getTypeGroup() == MessageGroup.GUI_RELOAD) { sendUiMessage(clementineMessage); // Now update the notification and the remote control client if (App.mClementine.getCurrentSong() != mLastSong) { mLastSong = App.mClementine.getCurrentSong(); updateNotification(); updateRemoteControlClient(); mPebble.sendMusicUpdateToPebble(); } if (App.mClementine.getState() != mLastState) { mLastState = App.mClementine.getState(); updateRemoteControlClient(); } } else if (clementineMessage.getMessageType() == MsgType.DISCONNECT) { closeConnection(clementineMessage); } else { sendUiMessage(clementineMessage); } } /** * Send a message to the ui thread * * @param obj The Message containing data */ private void sendUiMessage(Object obj) { Message msg = Message.obtain(); msg.obj = obj; // Send the Messages if (mUiHandler != null) { mUiHandler.sendMessage(msg); } } /** * Send a request to clementine * * @param message The request as a RequestToThread object * @return true if data was sent, false if not */ @Override public boolean sendRequest(ClementineMessage message) { // Send the request to Clementine boolean ret = super.sendRequest(message); // If we lost connection, try to reconnect if (!ret) { // if (mRequestConnect != null) { ret = super.createConnection(mRequestConnect); } if (!ret) { // Failed. Close connection Builder builder = ClementineMessage.getMessageBuilder(MsgType.DISCONNECT); ResponseDisconnect.Builder disc = builder.getResponseDisconnectBuilder(); disc.setReasonDisconnect(ReasonDisconnect.Server_Shutdown); builder.setResponseDisconnect(disc); closeConnection(new ClementineMessage(builder)); } } return ret; } /** * Disconnect from Clementine * * @param message The RequestDisconnect Object */ @Override public void disconnect(ClementineMessage message) { if (isConnected()) { // Set the Connected flag to false, so the loop in // checkForData() is interrupted super.disconnect(message); // and close the connection closeConnection(message); } } public long getStartTx() { return mStartTx; } public long getStartRx() { return mStartRx; } public long getStartTime() { return mStartTime; } /** * Close the socket and the streams */ private void closeConnection(ClementineMessage clementineMessage) { // Disconnect socket closeSocket(); // Cancel Notification mNotificationManager.cancel(App.NOTIFY_ID); unregisterRemoteControlClient(); App.mApp.unregisterReceiver(mMediaButtonBroadcastReceiver); mWakeLock.release(); sendUiMessage(clementineMessage); // Close thread Looper.myLooper().quit(); try { mIncomingThread.join(); Log.d(TAG, "joined!"); } catch (InterruptedException e) { e.printStackTrace(); } // Fire the listener fireOnConnectionClosed(clementineMessage); } /** * Fire the event to all listeners * * @param clementineMessage The Disconnect message. */ private void fireOnConnectionClosed(ClementineMessage clementineMessage) { for (OnConnectionListener listener : mListeners) { listener.onConnectionClosed(clementineMessage); } } /** * Fire the event to all listeners */ private void fireOnConnectionReady() { for (OnConnectionListener listener : mListeners) { listener.onConnectionReady(); } } /** * Set the ui Handler, to which the thread should talk to * * @param playerHandler The Handler */ public void setUiHandler(Handler playerHandler) { this.mUiHandler = playerHandler; } /** * Check the keep alive timeout. * If we reached the timeout, we can assume, that we lost the connection */ private void checkKeepAlive() { if (mLastKeepAlive > 0 && (System.currentTimeMillis() - mLastKeepAlive) > KEEP_ALIVE_TIMEOUT) { // Check if we shall reconnect while (mLeftReconnects > 0) { closeSocket(); if (super.createConnection(mRequestConnect)) { mLeftReconnects = MAX_RECONNECTS; break; } mLeftReconnects--; } // We tried, but the server isn't there anymore if (mLeftReconnects == 0) { Message msg = Message.obtain(); msg.obj = new ClementineMessage(ErrorMessage.KEEP_ALIVE_TIMEOUT); msg.arg1 = PROCESS_PROTOC; mHandler.sendMessage(msg); } } } /** * Set the last keep alive timestamp * * @param lastKeepAlive The time */ public void setLastKeepAlive(long lastKeepAlive) { this.mLastKeepAlive = lastKeepAlive; } /** * Update the notification with the new track info */ private void updateNotification() { if (mLastSong != null) { Bitmap scaledArt = Bitmap.createScaledBitmap(mLastSong.getArt(), mNotificationWidth, mNotificationHeight, false); mNotifyBuilder.setLargeIcon(scaledArt); mNotifyBuilder.setContentTitle(mLastSong.getArtist()); mNotifyBuilder.setContentText(mLastSong.getTitle() + " / " + mLastSong.getAlbum()); } else { mNotifyBuilder.setContentTitle(App.mApp.getString(R.string.app_name)); mNotifyBuilder.setContentText(App.mApp.getString(R.string.player_nosong)); } mNotificationManager.notify(App.NOTIFY_ID, mNotifyBuilder.build()); } /** * Register the RemoteControlClient */ private void registerRemoteControlClient() { // Request AudioFocus, so the widget is shown on the lock-screen mAudioManager.requestAudioFocus(mOnAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); mAudioManager.registerMediaButtonEventReceiver(mClementineMediaButtonEventReceiver); // Create the intent Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setComponent(mClementineMediaButtonEventReceiver); PendingIntent mediaPendingIntent = PendingIntent.getBroadcast(App.mApp.getApplicationContext(), 0, mediaButtonIntent, 0); // Create the client mRcClient = new RemoteControlClient(mediaPendingIntent); if (App.mClementine.getState() == Clementine.State.PLAY) { mRcClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); } else { mRcClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); } mRcClient.setTransportControlFlags( RemoteControlClient.FLAG_KEY_MEDIA_NEXT | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | RemoteControlClient.FLAG_KEY_MEDIA_PLAY | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE); mAudioManager.registerRemoteControlClient(mRcClient); } /** * Unregister the RemoteControlClient */ private void unregisterRemoteControlClient() { // Disconnect EventReceiver and RemoteControlClient mAudioManager.unregisterMediaButtonEventReceiver(mClementineMediaButtonEventReceiver); if (mRcClient != null) { mAudioManager.unregisterRemoteControlClient(mRcClient); mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener); } } /** * Update the RemoteControlClient */ private void updateRemoteControlClient() { // Update playstate if (App.mClementine.getState() == Clementine.State.PLAY) { mRcClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); } else { mRcClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); } // Get the metadata editor if (mLastSong != null && mLastSong.getArt() != null) { RemoteControlClient.MetadataEditor editor = mRcClient.editMetadata(false); editor.putBitmap(MetadataEditor.BITMAP_KEY_ARTWORK, mLastSong.getArt()); // The RemoteControlClients displays the following info: // METADATA_KEY_TITLE (white) - METADATA_KEY_ALBUMARTIST (grey) - METADATA_KEY_ALBUM (grey) // // So i put the metadata not in the "correct" fields to display artist, track and album // TODO: Fix it when changed in newer android versions editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, mLastSong.getAlbum()); editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, mLastSong.getArtist()); editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, mLastSong.getTitle()); editor.apply(); } } private OnAudioFocusChangeListener mOnAudioFocusChangeListener = new OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { } }; }