de.qspool.clementineremote.backend.ClementineSongDownloader.java Source code

Java tutorial

Introduction

Here is the source code for de.qspool.clementineremote.backend.ClementineSongDownloader.java

Source

/* 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.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.app.TaskStackBuilder;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import de.qspool.clementineremote.App;
import de.qspool.clementineremote.R;
import de.qspool.clementineremote.backend.elements.DownloaderResult;
import de.qspool.clementineremote.backend.elements.DownloaderResult.DownloadResult;
import de.qspool.clementineremote.backend.pb.ClementineMessage;
import de.qspool.clementineremote.backend.pb.ClementineMessageFactory;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.DownloadItem;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.MsgType;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.ResponseSongFileChunk;
import de.qspool.clementineremote.backend.player.MySong;
import de.qspool.clementineremote.ui.MainActivity;
import de.qspool.clementineremote.utils.Utilities;

public class ClementineSongDownloader extends AsyncTask<ClementineMessage, Integer, DownloaderResult> {

    private Context mContext;

    private SharedPreferences mSharedPref;

    private ClementineSimpleConnection mClient = new ClementineSimpleConnection();

    private NotificationManager mNotifyManager;

    private Builder mBuilder;

    private int mId;

    private MySong mCurrentSong = new MySong();

    private int mFileCount;

    private int mCurrentFile;

    private String mPlaylistName;

    private boolean mIsPlaylist = false;

    private boolean mCreatePlaylistDir = false;

    private boolean mCreatePlaylistArtistDir = false;

    private boolean mOverrideExistingFiles = false;

    private DownloadItem mItem;

    private int mCurrentProgress;

    private String mTitle;

    private String mSubtitle;

    private Uri mFileUri;

    public ClementineSongDownloader(Context context) {
        mContext = context;
        mSharedPref = PreferenceManager.getDefaultSharedPreferences(mContext);

        // Get preferences
        mCreatePlaylistDir = mSharedPref.getBoolean(App.SP_DOWNLOAD_SAVE_OWN_DIR, false);
        mCreatePlaylistArtistDir = mSharedPref.getBoolean(App.SP_DOWNLOAD_PLAYLIST_CRT_ARTIST_DIR, true);
        mOverrideExistingFiles = mSharedPref.getBoolean(App.SP_DOWNLOAD_OVERRIDE, false);

        // Get a new id
        mId = App.downloaders.size() + 1;

        // Show a toast that the download is starting
        Toast.makeText(mContext, R.string.player_download_started, Toast.LENGTH_SHORT).show();

        mNotifyManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        mBuilder = new NotificationCompat.Builder(mContext);
        mBuilder.setContentTitle(mContext.getString(R.string.download_noti_title))
                .setSmallIcon(R.drawable.ic_launcher).setOngoing(true);

        // Set the result intent
        mBuilder.setContentIntent(buildNotificationIntent());

        mBuilder.setPriority(-1);

        // Add this downloader to list
        App.downloaders.add(this);

        mTitle = mContext.getString(R.string.download_noti_title);
    }

    public void startDownload(ClementineMessage message) {
        mItem = message.getMessage().getRequestDownloadSongs().getDownloadItem();

        this.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
    }

    @Override
    protected DownloaderResult doInBackground(ClementineMessage... params) {
        if (mSharedPref.getBoolean(App.SP_WIFI_ONLY, false) && !Utilities.onWifi(mContext)) {
            return new DownloaderResult(DownloaderResult.DownloadResult.ONLY_WIFI);
        }

        // First create a connection
        if (!connect()) {
            return new DownloaderResult(DownloaderResult.DownloadResult.CONNECTION_ERROR);
        }

        // Start the download
        return startDownloading(params[0]);
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        mBuilder.setProgress(100, progress[0], false);
        StringBuilder sb = new StringBuilder();

        switch (mItem) {
        case APlaylist:
            sb.append(mContext.getString(R.string.download_noti_title_playlist));
            sb.append(" ");
            sb.append(mPlaylistName);
            break;
        case CurrentItem:
            sb.append(mContext.getString(R.string.download_noti_title_song));
            sb.append(" ");
            sb.append(mCurrentSong.getTitle());
            break;
        case ItemAlbum:
            sb.append(mContext.getString(R.string.download_noti_title_album));
            sb.append(" ");
            sb.append(mCurrentSong.getAlbum());
            break;
        }

        mTitle = sb.toString();

        sb = new StringBuilder();

        if (mCurrentSong.equals(new MySong())) {
            sb.append(mContext.getString(R.string.connectdialog_connecting));
        } else {
            sb.append("(");
            sb.append(mCurrentFile);
            sb.append("/");
            sb.append(mFileCount);
            sb.append(") ");
            sb.append(mCurrentSong.getArtist());
            sb.append(" - ");
            sb.append(mCurrentSong.getTitle());
        }

        mSubtitle = sb.toString();

        mBuilder.setContentTitle(mTitle);
        mBuilder.setContentText(mSubtitle);

        // Displays the progress bar for the first time.
        mNotifyManager.notify(mId, mBuilder.build());
    }

    @Override
    protected void onCancelled() {
        // When the loop is finished, updates the notification
        mSubtitle = mContext.getString(R.string.download_noti_canceled);
        mBuilder.setContentText(mSubtitle).setOngoing(false).setAutoCancel(true).setProgress(0, 0, false);
        mBuilder.setPriority(-2);
        mNotifyManager.cancel(mId);
        mNotifyManager.notify(mId, mBuilder.build());

        mCurrentProgress = 100;
    }

    @Override
    protected void onPostExecute(DownloaderResult result) {
        // When the loop is finished, updates the notification
        if (result == null) {
            mSubtitle = mContext.getString(R.string.download_noti_canceled);
        } else {
            mSubtitle = mContext.getString(result.getMessageId());
        }

        mBuilder.setContentTitle(mTitle);
        mBuilder.setContentText(mSubtitle);

        mCurrentProgress = 100;

        mBuilder.setOngoing(false);
        mBuilder.setProgress(0, 0, false);
        mBuilder.setAutoCancel(true);
        mBuilder.setPriority(-2);
        mNotifyManager.cancel(mId);
        mNotifyManager.notify(mId, mBuilder.build());
    }

    private PendingIntent buildNotificationIntent() {
        Intent intent = new Intent(App.mApp, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
        intent.putExtra(App.NOTIFICATION_ID, mId);
        intent.setData(Uri.parse("ClemetineDownload" + mId));

        // Create a TaskStack, so the app navigates correctly backwards
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(App.mApp);
        stackBuilder.addParentStack(MainActivity.class);
        stackBuilder.addNextIntent(intent);
        return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    /**
     * Connect to Clementine
     *
     * @return true if the connection was established, false if not
     */
    private boolean connect() {
        String ip = mSharedPref.getString(App.SP_KEY_IP, "");
        int port;
        try {
            port = Integer.valueOf(mSharedPref.getString(App.SP_KEY_PORT, String.valueOf(Clementine.DefaultPort)));
        } catch (NumberFormatException e) {
            port = Clementine.DefaultPort;
        }
        int authCode = mSharedPref.getInt(App.SP_LAST_AUTH_CODE, 0);

        return mClient
                .createConnection(ClementineMessageFactory.buildConnectMessage(ip, port, authCode, false, true));
    }

    /**
     * Start the Downlaod
     */
    private DownloaderResult startDownloading(ClementineMessage clementineMessage) {
        boolean downloadFinished = false;
        DownloaderResult result = new DownloaderResult(DownloadResult.SUCCESSFUL);
        File f = null;
        FileOutputStream fo = null;

        // Do we have a playlist?
        mIsPlaylist = (clementineMessage.getMessage().getRequestDownloadSongs()
                .getDownloadItem() == DownloadItem.APlaylist);
        if (mIsPlaylist) {
            int id = clementineMessage.getMessage().getRequestDownloadSongs().getPlaylistId();
            mPlaylistName = App.mClementine.getPlaylistManager().getPlaylist(id).getName();
        }

        publishProgress(0);

        // Now request the songs
        mClient.sendRequest(clementineMessage);

        while (!downloadFinished) {
            // Check if the user canceled the process
            if (isCancelled()) {
                // Close the stream and delete the incomplete file
                try {
                    if (fo != null) {
                        fo.flush();
                        fo.close();
                    }
                    if (f != null) {
                        f.delete();
                    }
                } catch (IOException e) {
                }

                break;
            }

            // Get the raw protocol buffer
            ClementineMessage message = mClient.getProtoc();

            // Check if an error occured
            if (message == null || message.isErrorMessage()) {
                result = new DownloaderResult(DownloadResult.CONNECTION_ERROR);
                break;
            }

            // Is the download forbidden?
            if (message.getMessageType() == MsgType.DISCONNECT) {
                result = new DownloaderResult(DownloadResult.FOBIDDEN);
                break;
            }

            // Download finished?
            if (message.getMessageType() == MsgType.DOWNLOAD_QUEUE_EMPTY) {
                break;
            }

            // Ignore other elements!
            if (message.getMessageType() != MsgType.SONG_FILE_CHUNK) {
                continue;
            }

            ResponseSongFileChunk chunk = message.getMessage().getResponseSongFileChunk();

            // If we received chunk no 0, then we have to decide wether to
            // accept the song offered or not
            if (chunk.getChunkNumber() == 0) {
                processSongOffer(chunk);

                // Update progress here to. If the first (and only) file exists and shall not be
                // overriten, the notification bar shows NULL.
                mCurrentSong = MySong.fromProtocolBuffer(chunk.getSongMetadata());
                publishProgress(mCurrentProgress);
                continue;
            }

            try {
                // Check if we need to create a new file
                if (f == null) {
                    // Check if we have enougth free space
                    if (chunk.getSize() > Utilities.getFreeSpaceExternal()) {
                        result = new DownloaderResult(DownloadResult.INSUFFIANT_SPACE);
                        break;
                    }

                    File dir = new File(BuildDirPath(chunk));
                    f = new File(BuildFilePath(chunk));

                    // User wants to override files, so delete it here!
                    // The check was already done in processSongOffer()
                    if (f.exists()) {
                        f.delete();
                    }

                    dir.mkdirs();
                    f.createNewFile();
                    fo = new FileOutputStream(f);

                    // File for download fragment
                    mFileUri = Uri.fromFile(f);
                }

                // Write chunk to sdcard
                fo.write(chunk.getData().toByteArray());

                // Have we downloaded all chunks?
                if (chunk.getChunkCount() == chunk.getChunkNumber()) {
                    // Index file
                    MediaScannerConnection.scanFile(mContext, new String[] { f.getAbsolutePath() }, null, null);
                    fo.flush();
                    fo.close();
                    f = null;
                }

                // Update notification
                updateProgress(chunk);
            } catch (IOException e) {
                result = new DownloaderResult(DownloaderResult.DownloadResult.NOT_MOUNTED);
                break;
            }

        }

        // Disconnect at the end
        mClient.disconnect(ClementineMessage.getMessage(MsgType.DISCONNECT));

        return result;
    }

    /**
     * This method checks if the offered file exists and sends a response to Clementine.
     * If the file does not exist -> Download file
     * otherwise
     * The user wants to override existing files -> Download file
     * otherwise
     * refuse file
     *
     * @param chunk The chunk with the metadata
     * @return a boolean indicating if the song will be sent or not
     */
    private boolean processSongOffer(ResponseSongFileChunk chunk) {
        File f = new File(BuildFilePath(chunk));
        boolean accept = true;

        if (f.exists() && !mOverrideExistingFiles) {
            accept = false;
        }

        mClient.sendRequest(ClementineMessageFactory.buildSongOfferResponse(accept));

        // File for download fragment
        if (f.exists()) {
            mFileUri = Uri.fromFile(f);
        }

        return accept;
    }

    /**
     * Updates the current notification
     *
     * @param chunk The current downloaded chunk
     */
    private void updateProgress(ResponseSongFileChunk chunk) {
        // Update notification
        mFileCount = chunk.getFileCount();
        mCurrentFile = chunk.getFileNumber();
        double progress = (((double) (chunk.getFileNumber() - 1) / (double) chunk.getFileCount())
                + (((double) chunk.getChunkNumber() / (double) chunk.getChunkCount())
                        / (double) chunk.getFileCount()))
                * 100;

        publishProgress((int) progress);

        mCurrentProgress = (int) progress;
    }

    /**
     * Return the folder where the file will be placed
     *
     * @param chunk The chunk
     */
    private String BuildDirPath(ResponseSongFileChunk chunk) {
        String defaultPath = Environment.getExternalStorageDirectory() + "/ClementineMusic";
        String path = mSharedPref.getString(App.SP_DOWNLOAD_DIR, defaultPath);

        StringBuilder sb = new StringBuilder();
        sb.append(path);
        sb.append(File.separator);
        if (mIsPlaylist && mCreatePlaylistDir) {
            sb.append(Utilities.removeInvalidFileCharacters(mPlaylistName));
            sb.append(File.separator);
        }

        // Create artist/album subfolder only when we have no playlist
        // or user set the settings
        if (!mIsPlaylist || mIsPlaylist && mCreatePlaylistArtistDir) {
            // Append artist name
            if (chunk.getSongMetadata().getAlbumartist().length() == 0) {
                sb.append(Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getArtist()));
            } else {
                sb.append(Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getAlbumartist()));
            }

            // append album
            sb.append(File.separator);
            sb.append(Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getAlbum()));
        }

        return sb.toString();
    }

    /**
     * Build the filename
     *
     * @param chunk The SongFileChunk
     * @return /sdcard/Music/Artist/Album/file.mp3
     */
    private String BuildFilePath(ResponseSongFileChunk chunk) {
        StringBuilder sb = new StringBuilder();
        sb.append(BuildDirPath(chunk));
        sb.append(File.separator);
        sb.append(Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getFilename()));

        return sb.toString();
    }

    public DownloadItem getItem() {
        return mItem;
    }

    public int getCurrentProgress() {
        return mCurrentProgress;
    }

    public String getTitle() {
        return mTitle;
    }

    public String getSubtitle() {
        return mSubtitle;
    }

    /**
     * Get the last downloaded file
     */
    public Uri getLastFileUri() {
        return mFileUri;
    }
}