Files
arcanechat-android/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java
T

529 lines
18 KiB
Java

package org.thoughtcrime.securesms.connect;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEventCenter;
import com.b44t.messenger.DcLot;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientProvider;
import org.thoughtcrime.securesms.util.Hash;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class ApplicationDcContext extends DcContext {
public static final String TAG = "DeltaChat";
@IntDef({RECIPIENT_TYPE_CHAT, RECIPIENT_TYPE_CONTACT})
public @interface RecipientType {
}
public static final int RECIPIENT_TYPE_CHAT = 0;
public static final int RECIPIENT_TYPE_CONTACT = 1;
public Context context;
public volatile boolean isScreenOn = false;
public ApplicationDcContext(Context context) {
super("android-dev");
this.context = context;
File dbfile = new File(context.getFilesDir(), "messenger.db");
open(dbfile.getAbsolutePath());
setConfig("e2ee_enabled", "1");
try {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
imapWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "imapWakeLock");
imapWakeLock.setReferenceCounted(false); // if the idle-thread is killed for any reasons, it is better not to rely on reference counting
smtpWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "smtpWakeLock");
smtpWakeLock.setReferenceCounted(false); // if the idle-thread is killed for any reasons, it is better not to rely on reference counting
afterForgroundWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "afterForegroundWakeLock");
afterForgroundWakeLock.setReferenceCounted(false);
} catch (Exception e) {
Log.e(TAG, "Cannot create wakeLocks");
}
new ForegroundDetector(ApplicationContext.getInstance(context));
startThreads(0);
TimerReceiver.scheduleNextAlarm(context);
BroadcastReceiver networkStateReceiver = new NetworkStateReceiver();
context.registerReceiver(networkStateReceiver, new IntentFilter(android.net.ConnectivityManager.CONNECTIVITY_ACTION));
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
BroadcastReceiver screenReceiver = new ScreenReceiver();
context.registerReceiver(screenReceiver, filter);
try {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
isScreenOn = pm.isScreenOn();
} catch (Exception e) {
}
if (!isScreenOn) {
context.startService(new Intent(context, KeepAliveService.class));
}
}
public File getImexDir() {
// DIRECTORY_DOCUMENTS is only available since KitKat;
// as we also support Ice Cream Sandwich and Jellybean (2017: 11% in total), this is no option.
// Moreover, DIRECTORY_DOWNLOADS seems to be easier accessible by the user,
// eg. "Download Managers" are nearly always installed.
// CAVE: do not use DownloadManager to add the file as it is deleted on uninstall then ...
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
}
public static HashMap<String, Integer> sharedFiles = new HashMap<>();
public void openForViewOrShare(int msg_id, String cmd) {
DcMsg msg = getMsg(msg_id);
String path = msg.getFile();
String mimeType = msg.getFilemime();
try {
File file = new File(path);
if (!file.exists()) {
Toast.makeText(context, context.getString(R.string.ShareActivity_file_not_found, path), Toast.LENGTH_LONG).show();
return;
}
Uri uri;
if (path.startsWith(getBlobdir())) {
uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + file.getName());
sharedFiles.put("/" + file.getName(), 1); // as different Android version handle uris in putExtra differently, we also check them on our own
} else {
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
} else {
uri = Uri.fromFile(file);
}
}
if (cmd.equals(Intent.ACTION_VIEW)) {
mimeType = checkMime(path, mimeType);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.startActivity(intent);
} else {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.startActivity(Intent.createChooser(intent, context.getString(R.string.ShareActivity_share_with)));
}
} catch (RuntimeException e) {
Toast.makeText(context, R.string.ShareActivity_unable_to_open_media, Toast.LENGTH_LONG).show();
Toast.makeText(context, "Media-Type: " + mimeType, Toast.LENGTH_LONG).show();
Log.i(TAG, "opening of external activity failed.", e);
}
}
private String checkMime(String path, String mimeType) {
if(mimeType == null || mimeType.equals("application/octet-stream")) {
String extension = MimeTypeMap.getFileExtensionFromUrl(path);
String newType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if(newType != null) return newType;
}
return mimeType;
}
/***********************************************************************************************
* create objects compatible to the database model of Signal
**********************************************************************************************/
@NonNull
public Recipient getRecipient(@RecipientType int recipientType, int id) {
switch (recipientType) {
case RECIPIENT_TYPE_CHAT:
return getRecipient(getChat(id));
case RECIPIENT_TYPE_CONTACT:
return getRecipient(getContact(id));
default:
throw new IllegalArgumentException("Wrong RecipientType");
}
}
@NonNull
public Recipient getRecipient(DcChat chat) {
int[] contactIds = getChatContacts(chat.getId());
List<Recipient> participants = new ArrayList<>();
for (int contactId : contactIds) {
participants.add(getRecipient(RECIPIENT_TYPE_CONTACT, contactId));
}
RecipientProvider.RecipientDetails recipientDetails = new RecipientProvider.RecipientDetails(chat.getName(), null, false, null, participants);
Recipient recipient = new Recipient(Address.fromChat(chat.getId()), recipientDetails);
if (!chat.isGroup()) {
String identifier = Hash.sha256(chat.getName() + chat.getSubtitle());
Uri systemContactPhoto = Prefs.getSystemContactPhoto(context, identifier);
if (systemContactPhoto != null) {
recipient.setSystemContactPhoto(systemContactPhoto);
}
}
return recipient;
}
@NonNull
public Recipient getRecipient(DcContact contact) {
RecipientProvider.RecipientDetails recipientDetails = new RecipientProvider.RecipientDetails(contact.getDisplayName(), null, false, null, null);
Recipient recipient = new Recipient(Address.fromContact(contact.getId()), recipientDetails);
String identifier = Hash.sha256(contact.getName() + contact.getAddr());
Uri systemContactPhoto = Prefs.getSystemContactPhoto(context, identifier);
if (systemContactPhoto != null) {
recipient.setSystemContactPhoto(systemContactPhoto);
}
if (contact.getId() == DcContact.DC_CONTACT_ID_SELF) {
recipient.setProfileAvatar("SELF");
}
int[] blockedContacts = getBlockedContacts();
boolean blocked = false;
for (int blockedContact : blockedContacts) {
if (contact.getId() == blockedContact) {
blocked = true;
break;
}
}
recipient.setBlocked(blocked);
return recipient;
}
@NonNull
public ThreadRecord getThreadRecord(DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
int chatId = chat.getId();
int distributionType = chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK ? ThreadRecord.DistributionTypes.ARCHIVE : ThreadRecord.DistributionTypes.CONVERSATION;
String body = summary.getText1();
if (!body.isEmpty()) {
body += ": ";
}
body += summary.getText2();
Recipient recipient = getRecipient(chat);
long date = summary.getTimestamp();
long count = 1;
int unreadCount = getFreshMsgCount(chatId);
long type = 0;//cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_TYPE));
boolean archived = chat.getArchived() != 0;
int status = 0;
long expiresIn = 0;
long lastSeen = 0;
boolean verified = chat.isVerified();
return new ThreadRecord(context, body, null, recipient, date, count,
unreadCount, chatId, 0, status, type,
distributionType, archived, expiresIn, lastSeen, 0, verified, summary);
}
/***********************************************************************************************
* Working Threads
**********************************************************************************************/
private final Object threadsCritical = new Object();
private boolean imapThreadStartedVal;
private final Object imapThreadStartedCond = new Object();
public Thread imapThread = null;
private PowerManager.WakeLock imapWakeLock = null;
private boolean smtpThreadStartedVal;
private final Object smtpThreadStartedCond = new Object();
public Thread smtpThread = null;
private PowerManager.WakeLock smtpWakeLock = null;
public PowerManager.WakeLock afterForgroundWakeLock = null;
public final static int INTERRUPT_IDLE = 0x01; // interrupt idle if the thread is already running
public void startThreads(int flags) {
synchronized (threadsCritical) {
if (imapThread == null || !imapThread.isAlive()) {
synchronized (imapThreadStartedCond) {
imapThreadStartedVal = false;
}
imapThread = new Thread(() -> {
// raise the starting condition
// after acquiring a wakelock so that the process is not terminated.
// as imapWakeLock is not reference counted that would result in a wakelock-gap is not needed here.
imapWakeLock.acquire();
synchronized (imapThreadStartedCond) {
imapThreadStartedVal = true;
imapThreadStartedCond.notifyAll();
}
Log.i(TAG, "###################### IMAP-Thread started. ######################");
while (true) {
imapWakeLock.acquire();
performImapJobs();
performImapFetch();
imapWakeLock.release();
performImapIdle();
}
}, "imapThread");
imapThread.setPriority(Thread.NORM_PRIORITY);
imapThread.start();
} else {
if ((flags & INTERRUPT_IDLE) != 0) {
interruptImapIdle();
}
}
if (smtpThread == null || !smtpThread.isAlive()) {
synchronized (smtpThreadStartedCond) {
smtpThreadStartedVal = false;
}
smtpThread = new Thread(() -> {
smtpWakeLock.acquire();
synchronized (smtpThreadStartedCond) {
smtpThreadStartedVal = true;
smtpThreadStartedCond.notifyAll();
}
Log.i(TAG, "###################### SMTP-Thread started. ######################");
while (true) {
smtpWakeLock.acquire();
performSmtpJobs();
smtpWakeLock.release();
performSmtpIdle();
}
}, "smtpThread");
smtpThread.setPriority(Thread.MAX_PRIORITY);
smtpThread.start();
}
}
}
public void waitForThreadsRunning() {
try {
synchronized (imapThreadStartedCond) {
while (!imapThreadStartedVal) {
imapThreadStartedCond.wait();
}
}
synchronized (smtpThreadStartedCond) {
while (!smtpThreadStartedVal) {
smtpThreadStartedCond.wait();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/***********************************************************************************************
* Tools
**********************************************************************************************/
public boolean isNetworkConnected() {
try {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null && netInfo.isConnected()) {
return true;
}
} catch (Exception e) {
}
return false;
}
/***********************************************************************************************
* Event Handling
**********************************************************************************************/
public DcEventCenter eventCenter = new DcEventCenter();
private final Object lastErrorLock = new Object();
private String lastErrorString = "";
private boolean showNextErrorAsToast = true;
public void captureNextError() {
synchronized (lastErrorLock) {
showNextErrorAsToast = false;
lastErrorString = "";
}
}
public boolean hasCapturedError() {
synchronized (lastErrorLock) {
return !lastErrorString.isEmpty();
}
}
public String getCapturedError() {
synchronized (lastErrorLock) {
return lastErrorString;
}
}
public void endCaptureNextError() {
synchronized (lastErrorLock) {
showNextErrorAsToast = true;
}
}
private void handleError(int event, boolean popUp, String string) {
// log error
boolean showAsToast;
Log.e(TAG, string);
synchronized (lastErrorLock) {
lastErrorString = string;
showAsToast = showNextErrorAsToast;
showNextErrorAsToast = true;
}
// show error to user
Util.runOnMain(() -> {
if (popUp && showAsToast) {
String toastString = string;
if (event == DC_EVENT_ERROR_NETWORK) {
if (!isNetworkConnected()) {
toastString = context.getString(R.string.error_no_network);
}
}
if (ForegroundDetector.getInstance().isForeground()) {
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show();
}
}
});
}
@Override
public long handleEvent(final int event, long data1, long data2) {
switch (event) {
case DC_EVENT_INFO:
Log.i(TAG, dataToString(data2));
break;
case DC_EVENT_WARNING:
Log.w(TAG, dataToString(data2));
break;
case DC_EVENT_ERROR:
handleError(event, true, dataToString(data2));
break;
case DC_EVENT_ERROR_NETWORK:
handleError(event, data1 != 0, dataToString(data2));
break;
case DC_EVENT_HTTP_GET:
String httpContent = null;
try {
URL url = new URL(dataToString(data1));
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
urlConnection.setConnectTimeout(10 * 1000);
InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());
BufferedReader r = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder total = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
total.append(line).append('\n');
}
httpContent = total.toString();
} finally {
urlConnection.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
return stringToData(httpContent);
case DC_EVENT_GET_STRING:
String s;
switch ((int) data1) { // the integers are defined in the core and used only here, an enum or sth. like that won't have a big benefit
case 3:
s = context.getString(R.string.summary_draft);
break;
case 8:
s = context.getString(R.string.menu_deaddrop);
break;
case 13:
s = context.getString(R.string.default_status_text);
break;
case 42:
s = context.getString(R.string.autocrypt__asm_subject);
break;
case 43:
s = context.getString(R.string.autocrypt__asm_general_body);
break;
default:
s = null;
break;
}
return stringToData(s);
default: {
final Object data1obj = data1IsString(event) ? dataToString(data1) : data1;
final Object data2obj = data2IsString(event) ? dataToString(data2) : data2;
if (eventCenter != null) {
eventCenter.sendToObservers(event, data1obj, data2obj);
}
}
break;
}
return 0;
}
}