org.kontalk.service.DownloadService.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.service.DownloadService.java

Source

/*
 * Kontalk Android client
 * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
    
 * 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 org.kontalk.service;

/*
 * TODO instead of using a notification ID per type, use a notification ID per
 * download.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;

import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.client.ClientHTTPConnection;
import org.kontalk.client.EndpointServer;
import org.kontalk.crypto.Coder;
import org.kontalk.crypto.DecryptException;
import org.kontalk.crypto.PersonalKey;
import org.kontalk.message.CompositeMessage;
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MyMessages.Messages;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.ui.ConversationsActivity;
import org.kontalk.ui.MessagingNotification;
import org.kontalk.ui.ProgressNotificationBuilder;
import org.kontalk.util.MediaStorage;
import org.kontalk.util.Preferences;

import static org.kontalk.ui.MessagingNotification.NOTIFICATION_ID_DOWNLOADING;
import static org.kontalk.ui.MessagingNotification.NOTIFICATION_ID_DOWNLOAD_ERROR;
import static org.kontalk.ui.MessagingNotification.NOTIFICATION_ID_DOWNLOAD_OK;

/**
 * The attachment download service.
 * TODO implement multiple downloads in queue or in parallel
 * @author Daniele Ricci
 */
public class DownloadService extends IntentService implements DownloadListener {
    private static final String TAG = MessageCenterService.TAG;

    /** A map to avoid duplicate downloads. */
    private static final Map<String, Long> sQueue = new LinkedHashMap<>();

    private static final String ACTION_DOWNLOAD_URL = "org.kontalk.action.DOWNLOAD_URL";
    private static final String ACTION_DOWNLOAD_ABORT = "org.kontalk.action.DOWNLOAD_ABORT";

    private static final String EXTRA_NOTIFY = "org.kontalk.download.notify";

    private ProgressNotificationBuilder mNotificationBuilder;
    private NotificationManager mNotificationManager;

    // data about the download currently being processed
    private Notification mCurrentNotification;
    private long mTotalBytes;

    private long mMessageId;
    private String mPeer;
    private boolean mEncrypted;
    private boolean mNotify;

    private ClientHTTPConnection mDownloadClient;
    private boolean mCanceled;

    public DownloadService() {
        super(DownloadService.class.getSimpleName());
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // crappy firmware - as per docs, intent can't be null in this case
        if (intent != null) {
            if (mNotificationManager == null)
                mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

            if (ACTION_DOWNLOAD_ABORT.equals(intent.getAction())) {
                final Uri uri = intent.getData();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        onDownloadAbort(uri);
                    }
                }).start();
            }
        }

        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // crappy firmware - as per docs, intent can't be null in this case
        if (intent == null)
            return;

        String action = intent.getAction();

        if (ACTION_DOWNLOAD_URL.equals(action)) {
            onDownloadURL(intent.getData(), intent.getExtras());
        }
    }

    private void onDownloadURL(Uri uri, Bundle args) {
        String url = uri.toString();

        // check if download has already been queued
        if (sQueue.get(url) != null)
            return;

        // notify user about download immediately
        startForeground(0);
        mCanceled = false;

        mNotify = args.getBoolean(EXTRA_NOTIFY, true);

        if (mDownloadClient == null) {
            mDownloadClient = new ClientHTTPConnection(this);
        }

        try {
            // check if external storage is available
            if (!MediaStorage.isExternalStorageAvailable()) {
                errorNotification(getString(R.string.notify_ticker_external_storage),
                        getString(R.string.notify_text_external_storage));
                return;
            }

            mMessageId = args.getLong(CompositeMessage.MSG_ID, 0);
            mPeer = args.getString(CompositeMessage.MSG_SENDER);
            mEncrypted = args.getBoolean(CompositeMessage.MSG_ENCRYPTED, false);
            sQueue.put(url, mMessageId);

            Date date;
            long timestamp = args.getLong(CompositeMessage.MSG_TIMESTAMP);
            if (timestamp > 0)
                date = new Date(timestamp);
            else
                date = new Date();

            String mime = args.getString(CompositeMessage.MSG_MIME);
            // this will be used if the server doesn't provide one
            // if the server provides a filename, only the path will be used
            File defaultFile = CompositeMessage.getIncomingFile(mime, date);
            if (defaultFile == null) {
                defaultFile = MediaStorage.getIncomingFile(date, "bin");
            }

            // download content
            mDownloadClient.downloadAutofilename(url, defaultFile, date, this);
        } catch (Exception e) {
            error(url, null, e);
        } finally {
            sQueue.remove(url);
            mMessageId = 0;
            mPeer = null;
        }
    }

    void onDownloadAbort(Uri uri) {
        String url = uri.toString();
        Long msgId = sQueue.get(url);
        if (msgId != null) {
            // interrupt worker if running
            if (msgId == mMessageId) {
                mDownloadClient.abort();
                mCanceled = true;
            }
            // remove from queue - will never be processed
            else
                sQueue.remove(url);
        }
    }

    public void startForeground(long totalBytes) {
        Log.d(TAG, "starting foreground progress notification");
        mTotalBytes = totalBytes;

        Intent ni = new Intent(getApplicationContext(), ConversationsActivity.class);
        // FIXME this intent should actually open the ComposeMessage activity
        PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), NOTIFICATION_ID_DOWNLOADING, ni, 0);

        if (mNotificationBuilder == null) {
            mNotificationBuilder = new ProgressNotificationBuilder(getApplicationContext(),
                    R.layout.progress_notification, getString(R.string.downloading_attachment),
                    R.drawable.ic_stat_notify, pi);
        }

        // if we don't know the content length yet, start an interminate progress
        foregroundNotification(totalBytes > 0 ? 0 : -1);
        startForeground(NOTIFICATION_ID_DOWNLOADING, mCurrentNotification);
    }

    private void foregroundNotification(int progress) {
        mCurrentNotification = mNotificationBuilder
                .progress(progress, R.string.attachment_download, R.string.downloading_attachment).build();
    }

    public void stopForeground() {
        stopForeground(true);
        mCurrentNotification = null;
        mTotalBytes = 0;
    }

    @Override
    public void start(String url, File destination, long length) {
        startForeground(length);
    }

    @Override
    public void completed(String url, String mime, File destination) {
        Uri uri = Uri.fromFile(destination);

        ContentValues values = null;

        // encrypted file?
        if (mEncrypted) {
            mCurrentNotification = mNotificationBuilder
                    .progress(-1, R.string.attachment_download, R.string.decrypting_attachment).build();
            // send the updates to the notification manager
            mNotificationManager.notify(NOTIFICATION_ID_DOWNLOADING, mCurrentNotification);

            InputStream in = null;
            OutputStream out = null;
            try {
                EndpointServer server = Preferences.getEndpointServer(this);
                PersonalKey key = ((Kontalk) getApplicationContext()).getPersonalKey();
                Coder coder = Keyring.getDecryptCoder(this, server, key, mPeer);
                if (coder != null) {
                    in = new FileInputStream(destination);

                    File outFile = new File(destination + ".new");
                    out = new FileOutputStream(outFile);
                    List<DecryptException> errors = new LinkedList<>();
                    coder.decryptFile(in, true, out, errors);

                    // TODO process errors

                    // delete old file and rename the decrypted one
                    destination.delete();
                    outFile.renameTo(destination);

                    // save this for later
                    values = new ContentValues(3);
                    values.put(Messages.ATTACHMENT_ENCRYPTED, false);
                    values.put(Messages.ATTACHMENT_LENGTH, destination.length());
                }
            } catch (Exception e) {
                Log.e(TAG, "decryption failed!", e);
                errorNotification(getString(R.string.notify_ticker_download_error),
                        getString(R.string.notify_text_decryption_error));
                return;
            } finally {
                try {
                    if (in != null)
                        in.close();
                } catch (IOException e) {
                    // ignored
                }
                try {
                    if (out != null)
                        out.close();
                } catch (IOException e) {
                    // ignored
                }
            }
        }

        // update messages.localUri
        if (values == null)
            values = new ContentValues(1);
        values.put(Messages.ATTACHMENT_LOCAL_URI, uri.toString());
        getContentResolver().update(ContentUris.withAppendedId(Messages.CONTENT_URI, mMessageId), values, null,
                null);

        // update media store
        MediaStorage.scanFile(this, destination, mime);

        // stop foreground
        stopForeground();

        // notify only if conversation is not open
        if (!MessagingNotification.isPaused(mPeer) && mNotify) {

            // detect mime type if not available
            if (mime == null)
                mime = getContentResolver().getType(uri);

            // create intent for download complete notification
            Intent i = new Intent(Intent.ACTION_VIEW);
            i.setDataAndType(uri, mime);
            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), NOTIFICATION_ID_DOWNLOAD_OK, i,
                    0);

            // create notification
            NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext())
                    .setSmallIcon(R.drawable.ic_stat_notify)
                    .setContentTitle(getString(R.string.notify_title_download_completed))
                    .setContentText(getString(R.string.notify_text_download_completed))
                    .setTicker(getString(R.string.notify_ticker_download_completed)).setContentIntent(pi)
                    .setPriority(NotificationCompat.PRIORITY_LOW).setAutoCancel(true);

            // notify!!
            mNotificationManager.notify(NOTIFICATION_ID_DOWNLOAD_OK, builder.build());
        }
    }

    @Override
    public void error(String url, File destination, Throwable exc) {
        Log.e(TAG, "download error", exc);
        stopForeground();
        if (!mCanceled) {
            ReportingManager.logException(exc);
            errorNotification(getString(R.string.notify_ticker_download_error),
                    getString(R.string.notify_text_download_error));
        }
    }

    private void errorNotification(String ticker, String text) {
        // create intent for download error notification
        Intent i = new Intent(this, ConversationsActivity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), NOTIFICATION_ID_DOWNLOAD_ERROR, i, 0);

        // create notification
        NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext())
                .setSmallIcon(R.drawable.ic_stat_notify)
                .setContentTitle(getString(R.string.notify_title_download_error)).setContentText(text)
                .setTicker(ticker).setPriority(NotificationCompat.PRIORITY_LOW)
                .setCategory(NotificationCompat.CATEGORY_ERROR).setContentIntent(pi).setAutoCancel(true);

        // notify!!
        mNotificationManager.notify(NOTIFICATION_ID_DOWNLOAD_ERROR, builder.build());
    }

    @Override
    public void progress(String url, File destination, long bytes) {
        if (mCurrentNotification != null) {
            int progress = (int) ((100 * bytes) / mTotalBytes);
            foregroundNotification(progress);
            // send the updates to the notification manager
            mNotificationManager.notify(NOTIFICATION_ID_DOWNLOADING, mCurrentNotification);
        }
    }

    public static boolean isQueued(String url) {
        return sQueue.containsKey(url);
    }

    public static void start(Context context, long databaseId, String sender, String mime, long timestamp,
            boolean encrypted, String url) {
        start(context, databaseId, sender, mime, timestamp, encrypted, url, true);
    }

    public static void start(Context context, long databaseId, String sender, String mime, long timestamp,
            boolean encrypted, String url, boolean notify) {
        Intent i = new Intent(context, DownloadService.class);
        i.setAction(DownloadService.ACTION_DOWNLOAD_URL);
        i.putExtra(CompositeMessage.MSG_ID, databaseId);
        i.putExtra(CompositeMessage.MSG_SENDER, sender);
        i.putExtra(CompositeMessage.MSG_MIME, mime);
        i.putExtra(CompositeMessage.MSG_TIMESTAMP, timestamp);
        i.putExtra(CompositeMessage.MSG_ENCRYPTED, encrypted);
        i.putExtra(EXTRA_NOTIFY, notify);
        i.setData(Uri.parse(url));
        context.startService(i);
    }

    public static void abort(Context context, Uri uri) {
        Intent i = new Intent(context, DownloadService.class);
        i.setAction(DownloadService.ACTION_DOWNLOAD_ABORT);
        i.setData(uri);
        context.startService(i);
    }

}