/* * 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; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Date; import com.instacart.library.truetime.TrueTime; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.Process; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; import org.kontalk.Log; import org.kontalk.R; import org.kontalk.crypto.PersonalKey; import org.kontalk.ui.ConversationsActivity; import static org.kontalk.ui.MessagingNotification.NOTIFICATION_ID_KEYPAIR_GEN; /** Generates a key pair in the background. */ public class KeyPairGeneratorService extends Service { /** * Broadcasted when key pair generation has finished. * Send this intent to start generating a new key pair or broadcast back * the one just created. */ public static final String ACTION_GENERATE = "org.kontalk.keypair.GENERATE"; /** * Broadcasted if, after sending an {@link #ACTION_GENERATE}, the key pair * generator thread has started. */ public static final String ACTION_STARTED = "org.kontalk.keypair.STARTED"; public static final String EXTRA_KEY = "org.kontalk.keypair.KEY"; public static final String EXTRA_FOREGROUND = "org.kontalk.keypair.FOREGROUND"; private static final String NTP_DEFAULT_SERVER = "time.google.com"; private static final int NTP_MAX_RETRIES = 3; private GeneratorThread mThread; private volatile PersonalKey mKey; private LocalBroadcastManager lbm; @Override public int onStartCommand(Intent intent, int flags, int startId) { if (lbm == null) lbm = LocalBroadcastManager.getInstance(getApplicationContext()); String action = intent.getAction(); if (ACTION_GENERATE.equals(action)) { // start the keypair generator if (mThread == null) { if (intent.getBooleanExtra(EXTRA_FOREGROUND, false)) startForeground(); mThread = new GeneratorThread(this); mThread.start(); broadcastStarted(); } else { if (mKey != null) { broadcastKey(); } } } return START_REDELIVER_INTENT; } @Override public IBinder onBind(Intent intent) { return null; } private void startForeground() { Intent ni = new Intent(getApplicationContext(), ConversationsActivity.class); // FIXME this intent should actually open the ComposeMessage activity PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), NOTIFICATION_ID_KEYPAIR_GEN, ni, 0); Notification no = new NotificationCompat.Builder(this) .setOngoing(true) .setTicker(getString(R.string.notify_gen_keypair_ticker)) .setSmallIcon(R.drawable.ic_stat_notify) .setContentTitle(getString(R.string.notify_gen_keypair_title)) .setContentText(getString(R.string.notify_gen_keypair_text)) .setContentIntent(pi) .build(); startForeground(NOTIFICATION_ID_KEYPAIR_GEN, no); } private void stopForeground() { stopForeground(true); } private void broadcastKey() { Intent i = new Intent(ACTION_GENERATE); i.putExtra(EXTRA_KEY, mKey); lbm.sendBroadcast(i); } private void broadcastStarted() { Intent i = new Intent(ACTION_STARTED); lbm.sendBroadcast(i); } private void keypairGenerated(PersonalKey key) { mKey = key; broadcastKey(); } private static final class GeneratorThread extends Thread { private WeakReference<KeyPairGeneratorService> s; GeneratorThread(KeyPairGeneratorService service) { s = new WeakReference<>(service); } @Override public void run() { // set a low priority android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); KeyPairGeneratorService service = s.get(); if (service != null) { // we need the real time from the Internet Date timestamp = getRealtime(service); try { PersonalKey key = PersonalKey.create(timestamp); Log.v("KeyPair", "key pair generated: " + key); service.keypairGenerated(key); } catch (IOException e) { Log.v("KeyPair", "keypair generation failed", e); // TODO notify user } service.stopForeground(); } } private Date getRealtime(Context context) { try { return TrueTime.now(); } catch (IllegalStateException e) { int retryCount = 0; while (retryCount < NTP_MAX_RETRIES) { try { TrueTime.build() .withSharedPreferences(context) .withNtpHost(NTP_DEFAULT_SERVER) .initialize(); break; } catch (IOException ioe) { retryCount++; } } try { return TrueTime.now(); } catch (IllegalStateException ise) { Log.w("KeyPair", "unable to retrieve real time from network, using system time"); return new Date(); } } } } public interface PersonalKeyRunnable { void run(PersonalKey key); } public final static class KeyGeneratorReceiver extends BroadcastReceiver { private final Handler handler; private final PersonalKeyRunnable action; public KeyGeneratorReceiver(Handler handler, PersonalKeyRunnable action) { this.handler = handler; this.action = action; } @Override public void onReceive(Context context, final Intent intent) { // key has been generated if (KeyPairGeneratorService.ACTION_GENERATE.equals(intent.getAction())) { // we can stop the service now context.stopService(new Intent(context, KeyPairGeneratorService.class)); handler.post(new Runnable() { public void run() { PersonalKey key = intent.getParcelableExtra(KeyPairGeneratorService.EXTRA_KEY); action.run(key); } }); } // key generation has started else if (KeyPairGeneratorService.ACTION_STARTED.equals(intent.getAction())) { // simply run the action with null key handler.post(new Runnable() { public void run() { action.run(null); } }); } } } }