mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Move all files to natural position.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class AccountAuthenticatorService extends Service {
|
||||
|
||||
private static AccountAuthenticatorImpl accountAuthenticator = null;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
|
||||
return getAuthenticator().getIBinder();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized AccountAuthenticatorImpl getAuthenticator() {
|
||||
if (accountAuthenticator == null) {
|
||||
accountAuthenticator = new AccountAuthenticatorImpl(this);
|
||||
}
|
||||
|
||||
return accountAuthenticator;
|
||||
}
|
||||
|
||||
private static class AccountAuthenticatorImpl extends AbstractAccountAuthenticator {
|
||||
|
||||
public AccountAuthenticatorImpl(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
|
||||
String[] requiredFeatures, Bundle options)
|
||||
throws NetworkErrorException
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType,
|
||||
Bundle options) throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthTokenLabel(String authTokenType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features)
|
||||
throws NetworkErrorException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType,
|
||||
Bundle options) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
public class AccountVerificationTimeoutException extends Exception {
|
||||
public AccountVerificationTimeoutException() {
|
||||
}
|
||||
|
||||
public AccountVerificationTimeoutException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public AccountVerificationTimeoutException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public AccountVerificationTimeoutException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Binder;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.SmsMigrator;
|
||||
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
// FIXME: This class is nuts.
|
||||
public class ApplicationMigrationService extends Service
|
||||
implements SmsMigrator.SmsMigrationProgressListener
|
||||
{
|
||||
private static final String TAG = ApplicationMigrationService.class.getSimpleName();
|
||||
public static final String MIGRATE_DATABASE = "org.thoughtcrime.securesms.ApplicationMigration.MIGRATE_DATABSE";
|
||||
public static final String COMPLETED_ACTION = "org.thoughtcrime.securesms.ApplicationMigrationService.COMPLETED";
|
||||
private static final String PREFERENCES_NAME = "SecureSMS";
|
||||
private static final String DATABASE_MIGRATED = "migrated";
|
||||
|
||||
private final BroadcastReceiver completedReceiver = new CompletedReceiver();
|
||||
private final Binder binder = new ApplicationMigrationBinder();
|
||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private WeakReference<Handler> handler = null;
|
||||
private NotificationCompat.Builder notification = null;
|
||||
private ImportState state = new ImportState(ImportState.STATE_IDLE, null);
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
registerCompletedReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent == null) return START_NOT_STICKY;
|
||||
|
||||
if (intent.getAction() != null && intent.getAction().equals(MIGRATE_DATABASE)) {
|
||||
executor.execute(new ImportRunnable());
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
unregisterCompletedReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public void setImportStateHandler(Handler handler) {
|
||||
this.handler = new WeakReference<>(handler);
|
||||
}
|
||||
|
||||
private void registerCompletedReceiver() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(COMPLETED_ACTION);
|
||||
|
||||
registerReceiver(completedReceiver, filter);
|
||||
}
|
||||
|
||||
private void unregisterCompletedReceiver() {
|
||||
unregisterReceiver(completedReceiver);
|
||||
}
|
||||
|
||||
private void notifyImportComplete() {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(COMPLETED_ACTION);
|
||||
|
||||
sendOrderedBroadcast(intent, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void progressUpdate(ProgressDescription progress) {
|
||||
setState(new ImportState(ImportState.STATE_MIGRATING_IN_PROGRESS, progress));
|
||||
}
|
||||
|
||||
public ImportState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
private void setState(ImportState state) {
|
||||
this.state = state;
|
||||
|
||||
if (this.handler != null) {
|
||||
Handler handler = this.handler.get();
|
||||
|
||||
if (handler != null) {
|
||||
handler.obtainMessage(state.state, state.progress).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
if (state.progress != null && state.progress.secondaryComplete == 0) {
|
||||
updateBackgroundNotification(state.progress.primaryTotal, state.progress.primaryComplete);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBackgroundNotification(int total, int complete) {
|
||||
notification.setProgress(total, complete, false);
|
||||
|
||||
((NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE))
|
||||
.notify(4242, notification.build());
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder initializeBackgroundNotification() {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NotificationChannels.OTHER);
|
||||
|
||||
builder.setSmallIcon(R.drawable.icon_notification);
|
||||
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon_notification));
|
||||
builder.setContentTitle(getString(R.string.ApplicationMigrationService_importing_text_messages));
|
||||
builder.setContentText(getString(R.string.ApplicationMigrationService_import_in_progress));
|
||||
builder.setOngoing(true);
|
||||
builder.setProgress(100, 0, false);
|
||||
// TODO [greyson] Navigation
|
||||
builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0));
|
||||
|
||||
stopForeground(true);
|
||||
startForeground(4242, builder.build());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private class ImportRunnable implements Runnable {
|
||||
|
||||
ImportRunnable() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
notification = initializeBackgroundNotification();
|
||||
PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
|
||||
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:migration");
|
||||
|
||||
try {
|
||||
wakeLock.acquire();
|
||||
|
||||
setState(new ImportState(ImportState.STATE_MIGRATING_BEGIN, null));
|
||||
|
||||
SmsMigrator.migrateDatabase(ApplicationMigrationService.this,
|
||||
ApplicationMigrationService.this);
|
||||
|
||||
setState(new ImportState(ImportState.STATE_MIGRATING_COMPLETE, null));
|
||||
|
||||
setDatabaseImported(ApplicationMigrationService.this);
|
||||
stopForeground(true);
|
||||
notifyImportComplete();
|
||||
stopSelf();
|
||||
} finally {
|
||||
wakeLock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ApplicationMigrationBinder extends Binder {
|
||||
public ApplicationMigrationService getService() {
|
||||
return ApplicationMigrationService.this;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CompletedReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationChannels.OTHER);
|
||||
builder.setSmallIcon(R.drawable.icon_notification);
|
||||
builder.setContentTitle(context.getString(R.string.ApplicationMigrationService_import_complete));
|
||||
builder.setContentText(context.getString(R.string.ApplicationMigrationService_system_database_import_is_complete));
|
||||
// TODO [greyson] Navigation
|
||||
builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0));
|
||||
builder.setWhen(System.currentTimeMillis());
|
||||
builder.setDefaults(Notification.DEFAULT_VIBRATE);
|
||||
builder.setAutoCancel(true);
|
||||
|
||||
Notification notification = builder.build();
|
||||
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(31337, notification);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ImportState {
|
||||
public static final int STATE_IDLE = 0;
|
||||
public static final int STATE_MIGRATING_BEGIN = 1;
|
||||
public static final int STATE_MIGRATING_IN_PROGRESS = 2;
|
||||
public static final int STATE_MIGRATING_COMPLETE = 3;
|
||||
|
||||
public int state;
|
||||
public ProgressDescription progress;
|
||||
|
||||
public ImportState(int state, ProgressDescription progress) {
|
||||
this.state = state;
|
||||
this.progress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDatabaseImported(Context context) {
|
||||
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
.getBoolean(DATABASE_MIGRATED, false);
|
||||
}
|
||||
|
||||
public static void setDatabaseImported(Context context) {
|
||||
context.getSharedPreferences(PREFERENCES_NAME, 0).edit().putBoolean(DATABASE_MIGRATED, true).apply();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.ContactsSyncAdapter;
|
||||
|
||||
public class ContactsSyncAdapterService extends Service {
|
||||
|
||||
private static ContactsSyncAdapter syncAdapter;
|
||||
|
||||
@Override
|
||||
public synchronized void onCreate() {
|
||||
if (syncAdapter == null) {
|
||||
syncAdapter = new ContactsSyncAdapter(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return syncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.service.chooser.ChooserTarget;
|
||||
import android.service.chooser.ChooserTargetService;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShareActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class DirectShareService extends ChooserTargetService {
|
||||
|
||||
private static final String TAG = DirectShareService.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName,
|
||||
IntentFilter matchedFilter)
|
||||
{
|
||||
List<ChooserTarget> results = new LinkedList<>();
|
||||
ComponentName componentName = new ComponentName(this, ShareActivity.class);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(this);
|
||||
Cursor cursor = threadDatabase.getRecentConversationList(10, false);
|
||||
|
||||
try {
|
||||
ThreadDatabase.Reader reader = threadDatabase.readerFor(cursor);
|
||||
ThreadRecord record;
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
Recipient recipient = Recipient.resolved(record.getRecipient().getId());
|
||||
String name = recipient.toShortString(this);
|
||||
|
||||
Bitmap avatar;
|
||||
|
||||
if (recipient.getContactPhoto() != null) {
|
||||
try {
|
||||
avatar = GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(recipient.getContactPhoto())
|
||||
.circleCrop()
|
||||
.submit(getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
||||
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width))
|
||||
.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
avatar = getFallbackDrawable(recipient);
|
||||
}
|
||||
} else {
|
||||
avatar = getFallbackDrawable(recipient);
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLong(ShareActivity.EXTRA_THREAD_ID, record.getThreadId());
|
||||
bundle.putString(ShareActivity.EXTRA_RECIPIENT_ID, recipient.getId().serialize());
|
||||
bundle.putInt(ShareActivity.EXTRA_DISTRIBUTION_TYPE, record.getDistributionType());
|
||||
bundle.setClassLoader(getClassLoader());
|
||||
|
||||
results.add(new ChooserTarget(name, Icon.createWithBitmap(avatar), 1.0f, componentName, bundle));
|
||||
}
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap getFallbackDrawable(@NonNull Recipient recipient) {
|
||||
Context themedContext = new ContextThemeWrapper(this, R.style.TextSecure_LightTheme);
|
||||
return BitmapUtil.createFromDrawable(recipient.getFallbackContactPhotoDrawable(themedContext, false),
|
||||
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
||||
getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DirectoryRefreshListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final long INTERVAL = TimeUnit.HOURS.toMillis(12);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getDirectoryRefreshTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
if (scheduledTime != 0 && TextSecurePreferences.isPushRegistered(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(true));
|
||||
}
|
||||
|
||||
long newTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setDirectoryRefreshTime(context, newTime);
|
||||
|
||||
return newTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
new DirectoryRefreshListener().onReceive(context, new Intent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
|
||||
public class ExpirationListener extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
ApplicationContext.getInstance(context).getExpiringMessageManager().checkSchedule();
|
||||
}
|
||||
|
||||
public static void setAlarm(Context context, long waitTimeMillis) {
|
||||
Intent intent = new Intent(context, ExpirationListener.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
|
||||
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
|
||||
|
||||
alarmManager.cancel(pendingIntent);
|
||||
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + waitTimeMillis, pendingIntent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.Context;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ExpiringMessageManager {
|
||||
|
||||
private static final String TAG = ExpiringMessageManager.class.getSimpleName();
|
||||
|
||||
private final TreeSet<ExpiringMessageReference> expiringMessageReferences = new TreeSet<>(new ExpiringMessageComparator());
|
||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final SmsDatabase smsDatabase;
|
||||
private final MmsDatabase mmsDatabase;
|
||||
private final Context context;
|
||||
|
||||
public ExpiringMessageManager(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
this.mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
executor.execute(new LoadTask());
|
||||
executor.execute(new ProcessTask());
|
||||
}
|
||||
|
||||
public void scheduleDeletion(long id, boolean mms, long expiresInMillis) {
|
||||
scheduleDeletion(id, mms, System.currentTimeMillis(), expiresInMillis);
|
||||
}
|
||||
|
||||
public void scheduleDeletion(long id, boolean mms, long startedAtTimestamp, long expiresInMillis) {
|
||||
long expiresAtMillis = startedAtTimestamp + expiresInMillis;
|
||||
|
||||
synchronized (expiringMessageReferences) {
|
||||
expiringMessageReferences.add(new ExpiringMessageReference(id, mms, expiresAtMillis));
|
||||
expiringMessageReferences.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void checkSchedule() {
|
||||
synchronized (expiringMessageReferences) {
|
||||
expiringMessageReferences.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadTask implements Runnable {
|
||||
public void run() {
|
||||
SmsDatabase.Reader smsReader = smsDatabase.readerFor(smsDatabase.getExpirationStartedMessages());
|
||||
MmsDatabase.Reader mmsReader = mmsDatabase.getExpireStartedMessages();
|
||||
|
||||
MessageRecord messageRecord;
|
||||
|
||||
while ((messageRecord = smsReader.getNext()) != null) {
|
||||
expiringMessageReferences.add(new ExpiringMessageReference(messageRecord.getId(),
|
||||
messageRecord.isMms(),
|
||||
messageRecord.getExpireStarted() + messageRecord.getExpiresIn()));
|
||||
}
|
||||
|
||||
while ((messageRecord = mmsReader.getNext()) != null) {
|
||||
expiringMessageReferences.add(new ExpiringMessageReference(messageRecord.getId(),
|
||||
messageRecord.isMms(),
|
||||
messageRecord.getExpireStarted() + messageRecord.getExpiresIn()));
|
||||
}
|
||||
|
||||
smsReader.close();
|
||||
mmsReader.close();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("InfiniteLoopStatement")
|
||||
private class ProcessTask implements Runnable {
|
||||
public void run() {
|
||||
while (true) {
|
||||
ExpiringMessageReference expiredMessage = null;
|
||||
|
||||
synchronized (expiringMessageReferences) {
|
||||
try {
|
||||
while (expiringMessageReferences.isEmpty()) expiringMessageReferences.wait();
|
||||
|
||||
ExpiringMessageReference nextReference = expiringMessageReferences.first();
|
||||
long waitTime = nextReference.expiresAtMillis - System.currentTimeMillis();
|
||||
|
||||
if (waitTime > 0) {
|
||||
ExpirationListener.setAlarm(context, waitTime);
|
||||
expiringMessageReferences.wait(waitTime);
|
||||
} else {
|
||||
expiredMessage = nextReference;
|
||||
expiringMessageReferences.remove(nextReference);
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (expiredMessage != null) {
|
||||
if (expiredMessage.mms) mmsDatabase.delete(expiredMessage.id);
|
||||
else smsDatabase.deleteMessage(expiredMessage.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExpiringMessageReference {
|
||||
private final long id;
|
||||
private final boolean mms;
|
||||
private final long expiresAtMillis;
|
||||
|
||||
private ExpiringMessageReference(long id, boolean mms, long expiresAtMillis) {
|
||||
this.id = id;
|
||||
this.mms = mms;
|
||||
this.expiresAtMillis = expiresAtMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof ExpiringMessageReference)) return false;
|
||||
|
||||
ExpiringMessageReference that = (ExpiringMessageReference)other;
|
||||
return this.id == that.id && this.mms == that.mms && this.expiresAtMillis == that.expiresAtMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int)this.id ^ (mms ? 1 : 0) ^ (int)expiresAtMillis;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExpiringMessageComparator implements Comparator<ExpiringMessageReference> {
|
||||
@Override
|
||||
public int compare(ExpiringMessageReference lhs, ExpiringMessageReference rhs) {
|
||||
if (lhs.expiresAtMillis < rhs.expiresAtMillis) return -1;
|
||||
else if (lhs.expiresAtMillis > rhs.expiresAtMillis) return 1;
|
||||
else if (lhs.id < rhs.id) return -1;
|
||||
else if (lhs.id > rhs.id) return 1;
|
||||
else if (!lhs.mms && rhs.mms) return -1;
|
||||
else if (lhs.mms && !rhs.mms) return 1;
|
||||
else return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class GenericForegroundService extends Service {
|
||||
|
||||
private static final String TAG = Log.tag(GenericForegroundService.class);
|
||||
|
||||
private final IBinder binder = new LocalBinder();
|
||||
|
||||
private static final int NOTIFICATION_ID = 827353982;
|
||||
private static final String EXTRA_TITLE = "extra_title";
|
||||
private static final String EXTRA_CHANNEL_ID = "extra_channel_id";
|
||||
private static final String EXTRA_ICON_RES = "extra_icon_res";
|
||||
private static final String EXTRA_ID = "extra_id";
|
||||
private static final String EXTRA_PROGRESS = "extra_progress";
|
||||
private static final String EXTRA_PROGRESS_MAX = "extra_progress_max";
|
||||
private static final String EXTRA_PROGRESS_INDETERMINATE = "extra_progress_indeterminate";
|
||||
|
||||
private static final String ACTION_START = "start";
|
||||
private static final String ACTION_STOP = "stop";
|
||||
|
||||
private static final AtomicInteger NEXT_ID = new AtomicInteger();
|
||||
|
||||
private final LinkedHashMap<Integer, Entry> allActiveMessages = new LinkedHashMap<>();
|
||||
|
||||
private static final Entry DEFAULTS = new Entry("", NotificationChannels.OTHER, R.drawable.ic_signal_grey_24dp, -1, 0, 0, false);
|
||||
|
||||
private @Nullable Entry lastPosted;
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent == null) {
|
||||
throw new IllegalStateException("Intent needs to be non-null.");
|
||||
}
|
||||
|
||||
synchronized (GenericForegroundService.class) {
|
||||
String action = intent.getAction();
|
||||
if (ACTION_START.equals(action)) handleStart(intent);
|
||||
else if (ACTION_STOP .equals(action)) handleStop(intent);
|
||||
else throw new IllegalStateException(String.format("Action needs to be %s or %s.", ACTION_START, ACTION_STOP));
|
||||
|
||||
updateNotification();
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void updateNotification() {
|
||||
Iterator<Entry> iterator = allActiveMessages.values().iterator();
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
postObligatoryForegroundNotification(iterator.next());
|
||||
} else {
|
||||
Log.i(TAG, "Last request. Ending foreground service.");
|
||||
postObligatoryForegroundNotification(lastPosted != null ? lastPosted : DEFAULTS);
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void handleStart(@NonNull Intent intent) {
|
||||
Entry entry = Entry.fromIntent(intent);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "handleStart() %s", entry));
|
||||
|
||||
allActiveMessages.put(entry.id, entry);
|
||||
}
|
||||
|
||||
private synchronized void handleStop(@NonNull Intent intent) {
|
||||
Log.i(TAG, "handleStop()");
|
||||
|
||||
int id = intent.getIntExtra(EXTRA_ID, -1);
|
||||
|
||||
Entry removed = allActiveMessages.remove(id);
|
||||
|
||||
if (removed == null) {
|
||||
Log.w(TAG, "Could not find entry to remove");
|
||||
}
|
||||
}
|
||||
|
||||
private void postObligatoryForegroundNotification(@NonNull Entry active) {
|
||||
lastPosted = active;
|
||||
// TODO [greyson] Navigation
|
||||
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, active.channelId)
|
||||
.setSmallIcon(active.iconRes)
|
||||
.setContentTitle(active.title)
|
||||
.setProgress(active.progressMax, active.progress, active.indeterminate)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) {
|
||||
return startForegroundTask(context, task, DEFAULTS.channelId);
|
||||
}
|
||||
|
||||
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId) {
|
||||
return startForegroundTask(context, task, channelId, DEFAULTS.iconRes);
|
||||
}
|
||||
|
||||
public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task, @NonNull String channelId, @DrawableRes int iconRes) {
|
||||
final int id = NEXT_ID.getAndIncrement();
|
||||
|
||||
Intent intent = new Intent(context, GenericForegroundService.class);
|
||||
intent.setAction(ACTION_START);
|
||||
intent.putExtra(EXTRA_TITLE, task);
|
||||
intent.putExtra(EXTRA_CHANNEL_ID, channelId);
|
||||
intent.putExtra(EXTRA_ICON_RES, iconRes);
|
||||
intent.putExtra(EXTRA_ID, id);
|
||||
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
|
||||
return new NotificationController(context, id);
|
||||
}
|
||||
|
||||
public static void stopForegroundTask(@NonNull Context context, int id) {
|
||||
Intent intent = new Intent(context, GenericForegroundService.class);
|
||||
intent.setAction(ACTION_STOP);
|
||||
intent.putExtra(EXTRA_ID, id);
|
||||
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
}
|
||||
|
||||
synchronized void replaceProgress(int id, int progressMax, int progress, boolean indeterminate) {
|
||||
Entry oldEntry = allActiveMessages.get(id);
|
||||
|
||||
if (oldEntry == null) {
|
||||
Log.w(TAG, "Failed to replace notification, it was not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Entry newEntry = new Entry(oldEntry.title, oldEntry.channelId, oldEntry.iconRes, oldEntry.id, progressMax, progress, indeterminate);
|
||||
|
||||
if (oldEntry.equals(newEntry)) {
|
||||
Log.d(TAG, String.format("handleReplace() skip, no change %s", newEntry));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format("handleReplace() %s", newEntry));
|
||||
|
||||
allActiveMessages.put(newEntry.id, newEntry);
|
||||
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
private static class Entry {
|
||||
final @NonNull String title;
|
||||
final @NonNull String channelId;
|
||||
final int id;
|
||||
final @DrawableRes int iconRes;
|
||||
final int progress;
|
||||
final int progressMax;
|
||||
final boolean indeterminate;
|
||||
|
||||
private Entry(@NonNull String title, @NonNull String channelId, @DrawableRes int iconRes, int id, int progressMax, int progress, boolean indeterminate) {
|
||||
this.title = title;
|
||||
this.channelId = channelId;
|
||||
this.iconRes = iconRes;
|
||||
this.id = id;
|
||||
this.progress = progress;
|
||||
this.progressMax = progressMax;
|
||||
this.indeterminate = indeterminate;
|
||||
}
|
||||
|
||||
private static Entry fromIntent(@NonNull Intent intent) {
|
||||
int id = intent.getIntExtra(EXTRA_ID, DEFAULTS.id);
|
||||
|
||||
String title = intent.getStringExtra(EXTRA_TITLE);
|
||||
if (title == null) title = DEFAULTS.title;
|
||||
|
||||
String channelId = intent.getStringExtra(EXTRA_CHANNEL_ID);
|
||||
if (channelId == null) channelId = DEFAULTS.channelId;
|
||||
|
||||
int iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULTS.iconRes);
|
||||
int progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULTS.progress);
|
||||
int progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULTS.progressMax);
|
||||
boolean indeterminate = intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULTS.indeterminate);
|
||||
|
||||
return new Entry(title, channelId, iconRes, id, progressMax, progress, indeterminate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.US, "ChannelId: %s Id: %d Progress: %d/%d %s", channelId, id, progress, progressMax, indeterminate ? "indeterminate" : "determinate");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Entry entry = (Entry) o;
|
||||
return id == entry.id &&
|
||||
iconRes == entry.iconRes &&
|
||||
progress == entry.progress &&
|
||||
progressMax == entry.progressMax &&
|
||||
indeterminate == entry.indeterminate &&
|
||||
Objects.equals(title, entry.title) &&
|
||||
Objects.equals(channelId, entry.channelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = title.hashCode();
|
||||
hashCode *= 31;
|
||||
hashCode += channelId.hashCode();
|
||||
hashCode *= 31;
|
||||
hashCode += id;
|
||||
hashCode *= 31;
|
||||
hashCode += iconRes;
|
||||
hashCode *= 31;
|
||||
hashCode += progress;
|
||||
hashCode *= 31;
|
||||
hashCode += progressMax;
|
||||
hashCode *= 31;
|
||||
hashCode += indeterminate ? 1 : 0;
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class LocalBinder extends Binder {
|
||||
GenericForegroundService getService() {
|
||||
// Return this instance of LocalService so clients can call public methods
|
||||
return GenericForegroundService.this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.Service;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.IncomingMessageProcessor.Processor;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.InvalidVersionException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class IncomingMessageObserver implements ConstraintObserver.Notifier {
|
||||
|
||||
private static final String TAG = IncomingMessageObserver.class.getSimpleName();
|
||||
|
||||
public static final int FOREGROUND_ID = 313399;
|
||||
private static final long REQUEST_TIMEOUT_MINUTES = 1;
|
||||
|
||||
private static SignalServiceMessagePipe pipe = null;
|
||||
private static SignalServiceMessagePipe unidentifiedPipe = null;
|
||||
|
||||
private final Context context;
|
||||
private final NetworkConstraint networkConstraint;
|
||||
private final SignalServiceNetworkAccess networkAccess;
|
||||
|
||||
private boolean appVisible;
|
||||
|
||||
|
||||
public IncomingMessageObserver(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.networkConstraint = new NetworkConstraint.Factory(ApplicationContext.getInstance(context)).create();
|
||||
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
|
||||
|
||||
new NetworkConstraintObserver(ApplicationContext.getInstance(context)).register(this);
|
||||
new MessageRetrievalThread().start();
|
||||
|
||||
if (TextSecurePreferences.isFcmDisabled(context)) {
|
||||
ContextCompat.startForegroundService(context, new Intent(context, ForegroundService.class));
|
||||
}
|
||||
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
onAppForegrounded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
onAppBackgrounded();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConstraintMet(@NonNull String reason) {
|
||||
synchronized (this) {
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onAppForegrounded() {
|
||||
appVisible = true;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
private synchronized void onAppBackgrounded() {
|
||||
appVisible = false;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
private synchronized boolean isConnectionNecessary() {
|
||||
boolean isGcmDisabled = TextSecurePreferences.isFcmDisabled(context);
|
||||
|
||||
Log.d(TAG, String.format("Network requirement: %s, app visible: %s, gcm disabled: %b",
|
||||
networkConstraint.isMet(), appVisible, isGcmDisabled));
|
||||
|
||||
return TextSecurePreferences.isPushRegistered(context) &&
|
||||
TextSecurePreferences.isWebsocketRegistered(context) &&
|
||||
(appVisible || isGcmDisabled) &&
|
||||
networkConstraint.isMet() &&
|
||||
!networkAccess.isCensored(context);
|
||||
}
|
||||
|
||||
private synchronized void waitForConnectionNecessary() {
|
||||
try {
|
||||
while (!isConnectionNecessary()) wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdown(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) {
|
||||
try {
|
||||
pipe.shutdown();
|
||||
unidentifiedPipe.shutdown();
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable SignalServiceMessagePipe getPipe() {
|
||||
return pipe;
|
||||
}
|
||||
|
||||
public static @Nullable SignalServiceMessagePipe getUnidentifiedPipe() {
|
||||
return unidentifiedPipe;
|
||||
}
|
||||
|
||||
private class MessageRetrievalThread extends Thread implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
MessageRetrievalThread() {
|
||||
super("MessageRetrievalService");
|
||||
setUncaughtExceptionHandler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
Log.i(TAG, "Waiting for websocket state change....");
|
||||
waitForConnectionNecessary();
|
||||
|
||||
Log.i(TAG, "Making websocket connection....");
|
||||
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||
|
||||
pipe = receiver.createMessagePipe();
|
||||
unidentifiedPipe = receiver.createUnidentifiedMessagePipe();
|
||||
|
||||
SignalServiceMessagePipe localPipe = pipe;
|
||||
SignalServiceMessagePipe unidentifiedLocalPipe = unidentifiedPipe;
|
||||
|
||||
try {
|
||||
while (isConnectionNecessary()) {
|
||||
try {
|
||||
Log.i(TAG, "Reading message...");
|
||||
localPipe.read(REQUEST_TIMEOUT_MINUTES, TimeUnit.MINUTES,
|
||||
envelope -> {
|
||||
Log.i(TAG, "Retrieved envelope! " + envelope.getSourceIdentifier());
|
||||
try (Processor processor = ApplicationDependencies.getIncomingMessageProcessor().acquire()) {
|
||||
processor.processEnvelope(envelope);
|
||||
}
|
||||
});
|
||||
} catch (TimeoutException e) {
|
||||
Log.w(TAG, "Application level read timeout...");
|
||||
} catch (InvalidVersionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Log.w(TAG, e);
|
||||
} finally {
|
||||
Log.w(TAG, "Shutting down pipe...");
|
||||
shutdown(localPipe, unidentifiedLocalPipe);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Looping...");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Log.w(TAG, "*** Uncaught exception!");
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ForegroundService extends Service {
|
||||
|
||||
@Override
|
||||
public @Nullable IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
super.onStartCommand(intent, flags, startId);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), NotificationChannels.OTHER);
|
||||
builder.setContentTitle(getApplicationContext().getString(R.string.MessageRetrievalService_signal));
|
||||
builder.setContentText(getApplicationContext().getString(R.string.MessageRetrievalService_background_connection_enabled));
|
||||
builder.setPriority(NotificationCompat.PRIORITY_MIN);
|
||||
builder.setWhen(0);
|
||||
builder.setSmallIcon(R.drawable.ic_signal_background_connection);
|
||||
startForeground(FOREGROUND_ID, builder.build());
|
||||
|
||||
return Service.START_STICKY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.thoughtcrime.securesms.service;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.DummyActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Small service that stays running to keep a key cached in memory.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class KeyCachingService extends Service {
|
||||
|
||||
private static final String TAG = KeyCachingService.class.getSimpleName();
|
||||
|
||||
public static final int SERVICE_RUNNING_ID = 4141;
|
||||
|
||||
public static final String KEY_PERMISSION = "org.thoughtcrime.securesms.ACCESS_SECRETS";
|
||||
public static final String NEW_KEY_EVENT = "org.thoughtcrime.securesms.service.action.NEW_KEY_EVENT";
|
||||
public static final String CLEAR_KEY_EVENT = "org.thoughtcrime.securesms.service.action.CLEAR_KEY_EVENT";
|
||||
public static final String LOCK_TOGGLED_EVENT = "org.thoughtcrime.securesms.service.action.LOCK_ENABLED_EVENT";
|
||||
private static final String PASSPHRASE_EXPIRED_EVENT = "org.thoughtcrime.securesms.service.action.PASSPHRASE_EXPIRED_EVENT";
|
||||
public static final String CLEAR_KEY_ACTION = "org.thoughtcrime.securesms.service.action.CLEAR_KEY";
|
||||
public static final String DISABLE_ACTION = "org.thoughtcrime.securesms.service.action.DISABLE";
|
||||
public static final String LOCALE_CHANGE_EVENT = "org.thoughtcrime.securesms.service.action.LOCALE_CHANGE_EVENT";
|
||||
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private final IBinder binder = new KeySetBinder();
|
||||
|
||||
private static MasterSecret masterSecret;
|
||||
|
||||
public KeyCachingService() {}
|
||||
|
||||
public static synchronized boolean isLocked(Context context) {
|
||||
return masterSecret == null && (!TextSecurePreferences.isPasswordDisabled(context) || TextSecurePreferences.isScreenLockEnabled(context));
|
||||
}
|
||||
|
||||
public static synchronized @Nullable MasterSecret getMasterSecret(Context context) {
|
||||
if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) {
|
||||
try {
|
||||
return MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w("KeyCachingService", e);
|
||||
}
|
||||
}
|
||||
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
public static void onAppForegrounded(@NonNull Context context) {
|
||||
ServiceUtil.getAlarmManager(context).cancel(buildExpirationPendingIntent(context));
|
||||
}
|
||||
|
||||
public static void onAppBackgrounded(@NonNull Context context) {
|
||||
startTimeoutIfAppropriate(context);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public void setMasterSecret(final MasterSecret masterSecret) {
|
||||
synchronized (KeyCachingService.class) {
|
||||
KeyCachingService.masterSecret = masterSecret;
|
||||
|
||||
foregroundService();
|
||||
broadcastNewSecret();
|
||||
startTimeoutIfAppropriate(this);
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (!ApplicationMigrations.isUpdate(KeyCachingService.this)) {
|
||||
MessageNotifier.updateNotification(KeyCachingService.this);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent == null) return START_NOT_STICKY;
|
||||
Log.d(TAG, "onStartCommand, " + intent.getAction());
|
||||
|
||||
if (intent.getAction() != null) {
|
||||
switch (intent.getAction()) {
|
||||
case CLEAR_KEY_ACTION: handleClearKey(); break;
|
||||
case PASSPHRASE_EXPIRED_EVENT: handleClearKey(); break;
|
||||
case DISABLE_ACTION: handleDisableService(); break;
|
||||
case LOCALE_CHANGE_EVENT: handleLocaleChanged(); break;
|
||||
case LOCK_TOGGLED_EVENT: handleLockToggled(); break;
|
||||
}
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.i(TAG, "onCreate()");
|
||||
super.onCreate();
|
||||
|
||||
if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) {
|
||||
try {
|
||||
MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
setMasterSecret(masterSecret);
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w("KeyCachingService", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.w(TAG, "KCS Is Being Destroyed!");
|
||||
handleClearKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for Android bug:
|
||||
* https://code.google.com/p/android/issues/detail?id=53313
|
||||
*/
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
Intent intent = new Intent(this, DummyActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleClearKey() {
|
||||
Log.i(TAG, "handleClearKey()");
|
||||
KeyCachingService.masterSecret = null;
|
||||
stopForeground(true);
|
||||
|
||||
Intent intent = new Intent(CLEAR_KEY_EVENT);
|
||||
intent.setPackage(getApplicationContext().getPackageName());
|
||||
|
||||
sendBroadcast(intent, KEY_PERMISSION);
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
MessageNotifier.updateNotification(KeyCachingService.this);
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void handleLockToggled() {
|
||||
stopForeground(true);
|
||||
|
||||
try {
|
||||
MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
setMasterSecret(masterSecret);
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisableService() {
|
||||
if (TextSecurePreferences.isPasswordDisabled(this) &&
|
||||
!TextSecurePreferences.isScreenLockEnabled(this))
|
||||
{
|
||||
stopForeground(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLocaleChanged() {
|
||||
dynamicLanguage.updateServiceLocale(this);
|
||||
foregroundService();
|
||||
}
|
||||
|
||||
private static void startTimeoutIfAppropriate(@NonNull Context context) {
|
||||
boolean appVisible = ApplicationContext.getInstance(context).isAppVisible();
|
||||
boolean secretSet = KeyCachingService.masterSecret != null;
|
||||
|
||||
boolean timeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(context);
|
||||
boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context);
|
||||
|
||||
long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context);
|
||||
boolean screenLockActive = screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(context);
|
||||
|
||||
if (!appVisible && secretSet && (passLockActive || screenLockActive)) {
|
||||
long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context);
|
||||
long screenLockTimeoutSeconds = TextSecurePreferences.getScreenLockTimeout(context);
|
||||
|
||||
long timeoutMillis;
|
||||
|
||||
if (!TextSecurePreferences.isPasswordDisabled(context)) timeoutMillis = TimeUnit.MINUTES.toMillis(passphraseTimeoutMinutes);
|
||||
else timeoutMillis = TimeUnit.SECONDS.toMillis(screenLockTimeoutSeconds);
|
||||
|
||||
Log.i(TAG, "Starting timeout: " + timeoutMillis);
|
||||
|
||||
AlarmManager alarmManager = ServiceUtil.getAlarmManager(context);
|
||||
PendingIntent expirationIntent = buildExpirationPendingIntent(context);
|
||||
|
||||
alarmManager.cancel(expirationIntent);
|
||||
alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeoutMillis, expirationIntent);
|
||||
}
|
||||
}
|
||||
|
||||
private void foregroundService() {
|
||||
if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) {
|
||||
stopForeground(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "foregrounding KCS");
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NotificationChannels.LOCKED_STATUS);
|
||||
|
||||
builder.setContentTitle(getString(R.string.KeyCachingService_passphrase_cached));
|
||||
builder.setContentText(getString(R.string.KeyCachingService_signal_passphrase_cached));
|
||||
builder.setSmallIcon(R.drawable.icon_cached);
|
||||
builder.setWhen(0);
|
||||
builder.setPriority(Notification.PRIORITY_MIN);
|
||||
|
||||
builder.addAction(R.drawable.ic_menu_lock_dark, getString(R.string.KeyCachingService_lock), buildLockIntent());
|
||||
builder.setContentIntent(buildLaunchIntent());
|
||||
|
||||
stopForeground(true);
|
||||
startForeground(SERVICE_RUNNING_ID, builder.build());
|
||||
}
|
||||
|
||||
private void broadcastNewSecret() {
|
||||
Log.i(TAG, "Broadcasting new secret...");
|
||||
|
||||
Intent intent = new Intent(NEW_KEY_EVENT);
|
||||
intent.setPackage(getApplicationContext().getPackageName());
|
||||
|
||||
sendBroadcast(intent, KEY_PERMISSION);
|
||||
}
|
||||
|
||||
private PendingIntent buildLockIntent() {
|
||||
Intent intent = new Intent(this, KeyCachingService.class);
|
||||
intent.setAction(PASSPHRASE_EXPIRED_EVENT);
|
||||
return PendingIntent.getService(getApplicationContext(), 0, intent, 0);
|
||||
}
|
||||
|
||||
private PendingIntent buildLaunchIntent() {
|
||||
// TODO [greyson] Navigation
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
|
||||
}
|
||||
|
||||
private static PendingIntent buildExpirationPendingIntent(@NonNull Context context) {
|
||||
Intent expirationIntent = new Intent(PASSPHRASE_EXPIRED_EVENT, null, context, KeyCachingService.class);
|
||||
return PendingIntent.getService(context, 0, expirationIntent, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent arg0) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public class KeySetBinder extends Binder {
|
||||
public KeyCachingService getService() {
|
||||
return KeyCachingService.this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocalBackupListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getNextBackupTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
if (TextSecurePreferences.isBackupEnabled(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new LocalBackupJob());
|
||||
}
|
||||
|
||||
return setNextBackupTimeToIntervalFromNow(context);
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
if (TextSecurePreferences.isBackupEnabled(context)) {
|
||||
new LocalBackupListener().onReceive(context, new Intent());
|
||||
}
|
||||
}
|
||||
|
||||
public static long setNextBackupTimeToIntervalFromNow(@NonNull Context context) {
|
||||
long nextTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setNextBackupTime(context, nextTime);
|
||||
|
||||
return nextTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.provider.Telephony;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.jobs.MmsReceiveJob;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class MmsListener extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = MmsListener.class.getSimpleName();
|
||||
|
||||
private boolean isRelevant(Context context, Intent intent) {
|
||||
if (!ApplicationMigrationService.isDatabaseImported(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION.equals(intent.getAction()) && Util.isDefaultSmsProvider(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Got MMS broadcast..." + intent.getAction());
|
||||
|
||||
if ((Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION.equals(intent.getAction()) &&
|
||||
Util.isDefaultSmsProvider(context)) ||
|
||||
(Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION.equals(intent.getAction()) &&
|
||||
isRelevant(context, intent)))
|
||||
{
|
||||
Log.i(TAG, "Relevant!");
|
||||
int subscriptionId = intent.getExtras().getInt("subscription", -1);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MmsReceiveJob(intent.getByteArrayExtra("data"), subscriptionId));
|
||||
|
||||
abortBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class NotificationController implements AutoCloseable {
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final int id;
|
||||
|
||||
private int progress;
|
||||
private int progressMax;
|
||||
private boolean indeterminate;
|
||||
private long percent = -1;
|
||||
|
||||
private final AtomicReference<GenericForegroundService> service = new AtomicReference<>();
|
||||
|
||||
NotificationController(@NonNull Context context, int id) {
|
||||
this.context = context;
|
||||
this.id = id;
|
||||
|
||||
bindToService();
|
||||
}
|
||||
|
||||
private void bindToService() {
|
||||
context.bindService(new Intent(context, GenericForegroundService.class), new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
GenericForegroundService.LocalBinder binder = (GenericForegroundService.LocalBinder) service;
|
||||
GenericForegroundService genericForegroundService = binder.getService();
|
||||
|
||||
NotificationController.this.service.set(genericForegroundService);
|
||||
|
||||
updateProgressOnService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
service.set(null);
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
GenericForegroundService.stopForegroundTask(context, id);
|
||||
}
|
||||
|
||||
public void setIndeterminateProgress() {
|
||||
setProgress(0, 0, true);
|
||||
}
|
||||
|
||||
public void setProgress(long newProgressMax, long newProgress) {
|
||||
setProgress((int) newProgressMax, (int) newProgress, false);
|
||||
}
|
||||
|
||||
private synchronized void setProgress(int newProgressMax, int newProgress, boolean indeterminant) {
|
||||
int newPercent = newProgressMax != 0 ? 100 * newProgress / newProgressMax : -1;
|
||||
|
||||
boolean same = newPercent == percent && indeterminate == indeterminant;
|
||||
|
||||
percent = newPercent;
|
||||
progress = newProgress;
|
||||
progressMax = newProgressMax;
|
||||
indeterminate = indeterminant;
|
||||
|
||||
if (same) return;
|
||||
|
||||
updateProgressOnService();
|
||||
}
|
||||
|
||||
private synchronized void updateProgressOnService() {
|
||||
GenericForegroundService genericForegroundService = service.get();
|
||||
|
||||
if (genericForegroundService == null) return;
|
||||
|
||||
genericForegroundService.replaceProgress(id, progressMax, progress, indeterminate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
* Respond to a PanicKit trigger Intent by locking the app. PanicKit provides a
|
||||
* common framework for creating "panic button" apps that can trigger actions
|
||||
* in "panic responder" apps. In this case, the response is to lock the app,
|
||||
* if it has been configured to do so via the Signal lock preference. If the
|
||||
* user has not set a passphrase, then the panic trigger intent does nothing.
|
||||
*/
|
||||
public class PanicResponderListener extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent != null && !TextSecurePreferences.isPasswordDisabled(context) &&
|
||||
"info.guardianproject.panic.action.TRIGGER".equals(intent.getAction()))
|
||||
{
|
||||
Intent lockIntent = new Intent(context, KeyCachingService.class);
|
||||
lockIntent.setAction(KeyCachingService.CLEAR_KEY_ACTION);
|
||||
context.startService(lockIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = PersistentAlarmManagerListener.class.getSimpleName();
|
||||
|
||||
protected abstract long getNextScheduledExecutionTime(Context context);
|
||||
protected abstract long onAlarm(Context context, long scheduledTime);
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
long scheduledTime = getNextScheduledExecutionTime(context);
|
||||
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
Intent alarmIntent = new Intent(context, getClass());
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0);
|
||||
|
||||
if (System.currentTimeMillis() >= scheduledTime) {
|
||||
scheduledTime = onAlarm(context, scheduledTime);
|
||||
}
|
||||
|
||||
Log.i(TAG, getClass() + " scheduling for: " + scheduledTime);
|
||||
|
||||
alarmManager.cancel(pendingIntent);
|
||||
alarmManager.set(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
|
||||
public class PersistentConnectionBootListener extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = PersistentConnectionBootListener.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent != null && Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
Log.i(TAG, "Received boot event. Application should be started, allowing non-GCM devices to start a foreground service.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Intent;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLDecoder;
|
||||
|
||||
public class QuickResponseService extends IntentService {
|
||||
|
||||
private static final String TAG = QuickResponseService.class.getSimpleName();
|
||||
|
||||
public QuickResponseService() {
|
||||
super("QuickResponseService");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
if (!TelephonyManager.ACTION_RESPOND_VIA_MESSAGE.equals(intent.getAction())) {
|
||||
Log.w(TAG, "Received unknown intent: " + intent.getAction());
|
||||
return;
|
||||
}
|
||||
|
||||
if (KeyCachingService.isLocked(this)) {
|
||||
Log.w(TAG, "Got quick response request when locked...");
|
||||
Toast.makeText(this, R.string.QuickResponseService_quick_response_unavailable_when_Signal_is_locked, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Rfc5724Uri uri = new Rfc5724Uri(intent.getDataString());
|
||||
String content = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
String number = uri.getPath();
|
||||
|
||||
if (number.contains("%")){
|
||||
number = URLDecoder.decode(number);
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.external(this, number);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
|
||||
if (!TextUtils.isEmpty(content)) {
|
||||
MessageSender.send(this, new OutgoingTextMessage(recipient, content, expiresIn, subscriptionId), -1, false, null);
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
Toast.makeText(this, R.string.QuickResponseService_problem_sending_message, Toast.LENGTH_LONG).show();
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RotateSenderCertificateListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getUnidentifiedAccessCertificateRotationTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
ApplicationDependencies.getJobManager().add(new RotateCertificateJob(context));
|
||||
|
||||
long nextTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setUnidentifiedAccessCertificateRotationTime(context, nextTime);
|
||||
|
||||
return nextTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
new RotateSenderCertificateListener().onReceive(context, new Intent());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RotateSignedPreKeyListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final long INTERVAL = TimeUnit.DAYS.toMillis(2);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getSignedPreKeyRotationTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
if (scheduledTime != 0 && TextSecurePreferences.isPushRegistered(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new RotateSignedPreKeyJob());
|
||||
}
|
||||
|
||||
long nextTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setSignedPreKeyRotationTime(context, nextTime);
|
||||
|
||||
return nextTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
new RotateSignedPreKeyListener().onReceive(context, new Intent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.telephony.SmsMessage;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSentJob;
|
||||
|
||||
public class SmsDeliveryListener extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = SmsDeliveryListener.class.getSimpleName();
|
||||
|
||||
public static final String SENT_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.SENT_SMS_ACTION";
|
||||
public static final String DELIVERED_SMS_ACTION = "org.thoughtcrime.securesms.SendReceiveService.DELIVERED_SMS_ACTION";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
long messageId = intent.getLongExtra("message_id", -1);
|
||||
int runAttempt = intent.getIntExtra("run_attempt", 0);
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case SENT_SMS_ACTION:
|
||||
int result = getResultCode();
|
||||
|
||||
jobManager.add(new SmsSentJob(messageId, SENT_SMS_ACTION, result, runAttempt));
|
||||
break;
|
||||
case DELIVERED_SMS_ACTION:
|
||||
byte[] pdu = intent.getByteArrayExtra("pdu");
|
||||
|
||||
if (pdu == null) {
|
||||
Log.w(TAG, "No PDU in delivery receipt!");
|
||||
break;
|
||||
}
|
||||
|
||||
SmsMessage message = SmsMessage.createFromPdu(pdu);
|
||||
|
||||
if (message == null) {
|
||||
Log.w(TAG, "Delivery receipt failed to parse!");
|
||||
break;
|
||||
}
|
||||
|
||||
int status = message.getStatus();
|
||||
|
||||
Log.i(TAG, "Original status: " + status);
|
||||
|
||||
// Note: https://developer.android.com/reference/android/telephony/SmsMessage.html#getStatus()
|
||||
// " CDMA: For not interfering with status codes from GSM, the value is shifted to the bits 31-16"
|
||||
// Note: https://stackoverflow.com/a/33240109
|
||||
if ("3gpp2".equals(intent.getStringExtra("format"))) {
|
||||
Log.w(TAG, "Correcting for CDMA delivery receipt...");
|
||||
if (status >> 24 <= 0) status = SmsDatabase.Status.STATUS_COMPLETE;
|
||||
else if (status >> 24 == 2) status = SmsDatabase.Status.STATUS_PENDING;
|
||||
else if (status >> 24 == 3) status = SmsDatabase.Status.STATUS_FAILED;
|
||||
}
|
||||
|
||||
jobManager.add(new SmsSentJob(messageId, DELIVERED_SMS_ACTION, status, runAttempt));
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Unknown action: " + intent.getAction());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Telephony;
|
||||
import android.telephony.SmsMessage;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.jobs.SmsReceiveJob;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class SmsListener extends BroadcastReceiver {
|
||||
|
||||
private static final String SMS_RECEIVED_ACTION = Telephony.Sms.Intents.SMS_RECEIVED_ACTION;
|
||||
private static final String SMS_DELIVERED_ACTION = Telephony.Sms.Intents.SMS_DELIVER_ACTION;
|
||||
|
||||
private boolean isExemption(SmsMessage message, String messageBody) {
|
||||
|
||||
// ignore CLASS0 ("flash") messages
|
||||
if (message.getMessageClass() == SmsMessage.MessageClass.CLASS_0)
|
||||
return true;
|
||||
|
||||
// ignore OTP messages from Sparebank1 (Norwegian bank)
|
||||
if (messageBody.startsWith("Sparebank1://otp?")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return
|
||||
message.getOriginatingAddress().length() < 7 &&
|
||||
(messageBody.toUpperCase().startsWith("//ANDROID:") || // Sprint Visual Voicemail
|
||||
messageBody.startsWith("//BREW:")); //BREW stands for “Binary Runtime Environment for Wireless"
|
||||
}
|
||||
|
||||
private SmsMessage getSmsMessageFromIntent(Intent intent) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
Object[] pdus = (Object[])bundle.get("pdus");
|
||||
|
||||
if (pdus == null || pdus.length == 0)
|
||||
return null;
|
||||
|
||||
return SmsMessage.createFromPdu((byte[])pdus[0]);
|
||||
}
|
||||
|
||||
private String getSmsMessageBodyFromIntent(Intent intent) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
Object[] pdus = (Object[])bundle.get("pdus");
|
||||
StringBuilder bodyBuilder = new StringBuilder();
|
||||
|
||||
if (pdus == null)
|
||||
return null;
|
||||
|
||||
for (Object pdu : pdus)
|
||||
bodyBuilder.append(SmsMessage.createFromPdu((byte[])pdu).getDisplayMessageBody());
|
||||
|
||||
return bodyBuilder.toString();
|
||||
}
|
||||
|
||||
private boolean isRelevant(Context context, Intent intent) {
|
||||
SmsMessage message = getSmsMessageFromIntent(intent);
|
||||
String messageBody = getSmsMessageBodyFromIntent(intent);
|
||||
|
||||
if (message == null && messageBody == null)
|
||||
return false;
|
||||
|
||||
if (isExemption(message, messageBody))
|
||||
return false;
|
||||
|
||||
if (!ApplicationMigrationService.isDatabaseImported(context))
|
||||
return false;
|
||||
|
||||
if (SMS_RECEIVED_ACTION.equals(intent.getAction()) && Util.isDefaultSmsProvider(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i("SMSListener", "Got SMS broadcast...");
|
||||
|
||||
if ((intent.getAction().equals(SMS_DELIVERED_ACTION)) ||
|
||||
(intent.getAction().equals(SMS_RECEIVED_ACTION)) && isRelevant(context, intent))
|
||||
{
|
||||
Log.i("SmsListener", "Constructing SmsReceiveJob...");
|
||||
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
|
||||
int subscriptionId = intent.getExtras().getInt("subscription", -1);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new SmsReceiveJob(pdus, subscriptionId));
|
||||
|
||||
abortBroadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
/**
|
||||
* Class to help manage scheduling events to happen in the future, whether the app is open or not.
|
||||
*/
|
||||
public abstract class TimedEventManager<E> {
|
||||
|
||||
private final Application application;
|
||||
private final Handler handler;
|
||||
|
||||
public TimedEventManager(@NonNull Application application, @NonNull String threadName) {
|
||||
HandlerThread handlerThread = new HandlerThread(threadName);
|
||||
handlerThread.start();
|
||||
|
||||
this.application = application;
|
||||
this.handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called whenever the underlying data of events has changed. Will appropriately
|
||||
* schedule new event executions.
|
||||
*/
|
||||
public void scheduleIfNecessary() {
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
|
||||
handler.post(() -> {
|
||||
E event = getNextClosestEvent();
|
||||
|
||||
if (event != null) {
|
||||
long delay = getDelayForEvent(event);
|
||||
|
||||
handler.postDelayed(() -> {
|
||||
executeEvent(event);
|
||||
scheduleIfNecessary();
|
||||
}, delay);
|
||||
|
||||
scheduleAlarm(application, delay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The next event that should be executed, or {@code null} if there are no events to execute.
|
||||
*/
|
||||
@WorkerThread
|
||||
protected @Nullable abstract E getNextClosestEvent();
|
||||
|
||||
/**
|
||||
* Execute the provided event.
|
||||
*/
|
||||
@WorkerThread
|
||||
protected abstract void executeEvent(@NonNull E event);
|
||||
|
||||
/**
|
||||
* @return How long before the provided event should be executed.
|
||||
*/
|
||||
@WorkerThread
|
||||
protected abstract long getDelayForEvent(@NonNull E event);
|
||||
|
||||
/**
|
||||
* Schedules an alarm to call {@link #scheduleIfNecessary()} after the specified delay. You can
|
||||
* use {@link #setAlarm(Context, long, Class)} as a helper method.
|
||||
*/
|
||||
@AnyThread
|
||||
protected abstract void scheduleAlarm(@NonNull Application application, long delay);
|
||||
|
||||
/**
|
||||
* Helper method to set an alarm.
|
||||
*/
|
||||
protected static void setAlarm(@NonNull Context context, long delay, @NonNull Class alarmClass) {
|
||||
Intent intent = new Intent(context, alarmClass);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
|
||||
AlarmManager alarmManager = ServiceUtil.getAlarmManager(context);
|
||||
|
||||
alarmManager.cancel(pendingIntent);
|
||||
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay, pendingIntent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil;
|
||||
import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class UpdateApkReadyListener extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = UpdateApkReadyListener.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "onReceive()");
|
||||
|
||||
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
|
||||
long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2);
|
||||
|
||||
if (downloadId == TextSecurePreferences.getUpdateApkDownloadId(context)) {
|
||||
Uri uri = getLocalUriForDownloadId(context, downloadId);
|
||||
String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context);
|
||||
|
||||
if (uri == null) {
|
||||
Log.w(TAG, "Downloaded local URI is null?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMatchingDigest(context, downloadId, encodedDigest)) {
|
||||
displayInstallNotification(context, uri);
|
||||
} else {
|
||||
Log.w(TAG, "Downloaded APK doesn't match digest...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void displayInstallNotification(Context context, Uri uri) {
|
||||
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setData(uri);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.APP_UPDATES)
|
||||
.setOngoing(true)
|
||||
.setContentTitle(context.getString(R.string.UpdateApkReadyListener_Signal_update))
|
||||
.setContentText(context.getString(R.string.UpdateApkReadyListener_a_new_version_of_signal_is_available_tap_to_update))
|
||||
.setSmallIcon(R.drawable.icon_notification)
|
||||
.setColor(context.getResources().getColor(R.color.textsecure_primary))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build();
|
||||
|
||||
ServiceUtil.getNotificationManager(context).notify(666, notification);
|
||||
}
|
||||
|
||||
private @Nullable Uri getLocalUriForDownloadId(Context context, long downloadId) {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
|
||||
Cursor cursor = downloadManager.query(query);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String localUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI));
|
||||
|
||||
if (localUri != null) {
|
||||
File localFile = new File(Uri.parse(localUri).getPath());
|
||||
return FileProviderUtil.getUriFor(context, localFile);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isMatchingDigest(Context context, long downloadId, String theirEncodedDigest) {
|
||||
try {
|
||||
if (theirEncodedDigest == null) return false;
|
||||
|
||||
byte[] theirDigest = Hex.fromStringCondensed(theirEncodedDigest);
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor());
|
||||
byte[] ourDigest = FileUtils.getFileDigest(fin);
|
||||
|
||||
fin.close();
|
||||
|
||||
return MessageDigest.isEqual(ourDigest, theirDigest);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class UpdateApkRefreshListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final String TAG = UpdateApkRefreshListener.class.getSimpleName();
|
||||
|
||||
private static final long INTERVAL = TimeUnit.HOURS.toMillis(6);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getUpdateApkRefreshTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
Log.i(TAG, "onAlarm...");
|
||||
|
||||
if (scheduledTime != 0 && BuildConfig.PLAY_STORE_DISABLED) {
|
||||
Log.i(TAG, "Queueing APK update job...");
|
||||
ApplicationDependencies.getJobManager().add(new UpdateApkJob());
|
||||
}
|
||||
|
||||
long newTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setUpdateApkRefreshTime(context, newTime);
|
||||
|
||||
return newTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
new UpdateApkRefreshListener().onReceive(context, new Intent());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* 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.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class VerificationCodeParser {
|
||||
|
||||
private static final Pattern CHALLENGE_PATTERN = Pattern.compile(".*Your (Signal|TextSecure) verification code:? ([0-9]{3,4})-([0-9]{3,4}).*", Pattern.DOTALL);
|
||||
|
||||
public static Optional<String> parse(Context context, String messageBody) {
|
||||
if (messageBody == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
Matcher challengeMatcher = CHALLENGE_PATTERN.matcher(messageBody);
|
||||
|
||||
if (!challengeMatcher.matches() || !TextSecurePreferences.isVerifying(context)) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(challengeMatcher.group(2) + challengeMatcher.group(3));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user