org.kontalk.service.UploadService.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.service.UploadService.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
 * upload.
 */

import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;

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

import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.provider.MessagesProvider;
import org.kontalk.provider.MessagesProviderUtils;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.ui.ConversationsActivity;
import org.kontalk.ui.ProgressNotificationBuilder;
import org.kontalk.upload.HTPPFileUploadConnection;
import org.kontalk.upload.UploadConnection;
import org.kontalk.util.MediaStorage;

import static org.kontalk.ui.MessagingNotification.NOTIFICATION_ID_UPLOADING;
import static org.kontalk.ui.MessagingNotification.NOTIFICATION_ID_UPLOAD_ERROR;

/**
 * Attachment upload service.
 * TODO implement multiple concurrent uploads
 * @author Daniele Ricci
 */
public class UploadService extends IntentService implements ProgressListener {
    private static final String TAG = MessageCenterService.TAG;

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

    public static final String ACTION_UPLOAD = "org.kontalk.action.UPLOAD";
    public static final String ACTION_UPLOAD_ABORT = "org.kontalk.action.UPLOAD_ABORT";

    /** Message database ID. Use with ACTION_UPLOAD. */
    public static final String EXTRA_DATABASE_ID = "org.kontalk.upload.DATABASE_ID";
    /** Message ID. Use with ACTION_UPLOAD. */
    public static final String EXTRA_MESSAGE_ID = "org.kontalk.upload.MESSAGE_ID";
    /** URL to post to. Use with ACTION_UPLOAD. */
    public static final String EXTRA_POST_URL = "org.kontalk.upload.POST_URL";
    /** URL to fetch from. Use with ACTION_UPLOAD. */
    public static final String EXTRA_GET_URL = "org.kontalk.upload.GET_URL";
    /** User(s) to send to. */
    public static final String EXTRA_USER = "org.kontalk.upload.USER";
    /** Group JID. */
    public static final String EXTRA_GROUP = "org.kontalk.upload.GROUP";
    /** Media MIME type. */
    public static final String EXTRA_MIME = "org.kontalk.upload.MIME";
    /** Preview file path. */
    public static final String EXTRA_PREVIEW_PATH = "org.kontalk.upload.PREVIEW_PATH";
    /** Encryption flag. */
    public static final String EXTRA_ENCRYPT = "org.kontalk.upload.ENCRYPT";
    /** Delete local file after sending attempt. */
    public static final String EXTRA_DELETE_ORIGINAL = "org.kontalk.upload.DELETE_ORIGINAL";
    // Intent data is the local file Uri

    private ProgressNotificationBuilder mNotificationBuilder;
    private NotificationManager mNotificationManager;

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

    private long mMessageId;
    private UploadConnection mConn;
    private boolean mCanceled;

    public UploadService() {
        super(UploadService.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_UPLOAD_ABORT.equals(intent.getAction())) {
                String filename = intent.getData().toString();
                // TODO check for race conditions on queue
                Long msgId = queue.get(filename);
                if (msgId != null) {
                    // interrupt worker if running
                    if (msgId == mMessageId) {
                        mConn.abort();
                        mCanceled = true;
                    }
                    // remove from queue - will never be processed
                    else
                        queue.remove(filename);
                }
                return START_NOT_STICKY;
            }
        }

        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;
        // check for unknown action
        if (!ACTION_UPLOAD.equals(intent.getAction()))
            return;

        // local file to upload
        Uri file = intent.getData();
        String filename = file.toString();
        // message database id
        long databaseId = intent.getLongExtra(EXTRA_DATABASE_ID, 0);
        // message id
        String msgId = intent.getStringExtra(EXTRA_MESSAGE_ID);
        // url to post to
        String url = intent.getStringExtra(EXTRA_POST_URL);
        // url to fetch from (will be requested to the connection if null)
        String fetchUrl = intent.getStringExtra(EXTRA_GET_URL);
        // group JID
        String groupJid = intent.getStringExtra(EXTRA_GROUP);
        // user(s) to send message to
        String[] to;
        if (groupJid != null) {
            to = intent.getStringArrayExtra(EXTRA_USER);
        } else {
            to = new String[] { intent.getStringExtra(EXTRA_USER) };
        }
        // media mime type
        String mime = intent.getStringExtra(EXTRA_MIME);
        // preview file path
        String previewPath = intent.getStringExtra(EXTRA_PREVIEW_PATH);
        // encryption flag
        boolean encrypt = intent.getBooleanExtra(EXTRA_ENCRYPT, false);
        // delete original
        boolean deleteOriginal = intent.getBooleanExtra(EXTRA_DELETE_ORIGINAL, false);

        // check if upload has already been queued
        if (queue.get(filename) != null)
            return;

        try {
            // notify user about upload immediately
            long length = MediaStorage.getLength(this, file);
            Log.v(TAG, "file size is " + length + " bytes");

            mTotalBytes = length;
            startForeground(0);

            mCanceled = false;

            // TODO used class here should be decided by the caller
            mConn = new HTPPFileUploadConnection(this, url);

            mMessageId = databaseId;
            queue.put(filename, mMessageId);

            // upload content
            String mediaUrl = mConn.upload(file, length, mime, encrypt, to, this);
            if (mediaUrl == null)
                mediaUrl = fetchUrl;
            Log.d(TAG, "uploaded with media URL: " + mediaUrl);

            // update message fetch_url
            MessagesProvider.uploaded(this, databaseId, mediaUrl);

            // send message with fetch url to server
            if (groupJid != null) {
                MessageCenterService.sendGroupUploadedMedia(this, groupJid, to, mime, file, length, previewPath,
                        mediaUrl, encrypt, databaseId, msgId);
            } else {
                MessageCenterService.sendUploadedMedia(this, to[0], mime, file, length, previewPath, mediaUrl,
                        encrypt, databaseId, msgId);
            }

            // end operations
            completed();
        } catch (Exception e) {
            error(url, null, e);
        } finally {
            // only file uri are supported for delete
            if (deleteOriginal && "file".equals(file.getScheme()))
                new File(file.getPath()).delete();

            queue.remove(filename);
            mMessageId = 0;
        }
    }

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

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

        if (mNotificationBuilder == null) {
            mNotificationBuilder = new ProgressNotificationBuilder(getApplicationContext(),
                    R.layout.progress_notification, getString(R.string.sending_message), 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_UPLOADING, mCurrentNotification);
    }

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

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

    @Override
    public void start(UploadConnection conn) {
        startForeground(mTotalBytes);
    }

    public void completed() {
        stopForeground();

        // upload completed - no need for notification

        // TODO broadcast upload completed intent
    }

    public void error(String url, File destination, Throwable exc) {
        Log.e(TAG, "upload error", exc);
        stopForeground();
        if (!mCanceled) {
            ReportingManager.logException(exc);
            errorNotification(getString(R.string.notify_ticker_upload_error),
                    getString(R.string.notify_text_upload_error));
        }
    }

    private void errorNotification(String ticker, String text) {
        errorNotification(this, mNotificationManager, ticker, text);
    }

    public static void errorNotification(Context context, String ticker, String text) {
        errorNotification(context, ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)),
                ticker, text);
    }

    private static void errorNotification(Context context, NotificationManager nm, String ticker, String text) {
        // create intent for upload error notification
        Intent i = new Intent(context, ConversationsActivity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent pi = PendingIntent.getActivity(context.getApplicationContext(), NOTIFICATION_ID_UPLOAD_ERROR,
                i, 0);

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

        // notify!!
        nm.notify(NOTIFICATION_ID_UPLOAD_ERROR, builder.build());
    }

    @Override
    public void progress(UploadConnection conn, long bytes) {
        if (mCanceled || !MessagesProviderUtils.exists(this, mMessageId)) {
            Log.v(TAG, "upload canceled or message deleted - aborting");
            mConn.abort();
            mCanceled = true;
        }

        if (mCurrentNotification != null) {
            int progress = (int) ((100 * bytes) / mTotalBytes);
            foregroundNotification(progress);
            // send the updates to the notification manager
            mNotificationManager.notify(NOTIFICATION_ID_UPLOADING, mCurrentNotification);
        }
    }

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