Java tutorial
/* * 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); } }