Merge branch 'dev' into disappear-2
commit
0b11e182ff
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application tools:node="merge">
|
||||
<meta-data
|
||||
android:name="com.huawei.hms.client.appid"
|
||||
android:value="appid=107205081">
|
||||
</meta-data>
|
||||
|
||||
<meta-data
|
||||
android:name="com.huawei.hms.client.cpid"
|
||||
android:value="cpid=30061000024605000">
|
||||
</meta-data>
|
||||
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.notifications.HuaweiPushService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,96 @@
|
||||
{
|
||||
"agcgw":{
|
||||
"backurl":"connect-dre.hispace.hicloud.com",
|
||||
"url":"connect-dre.dbankcloud.cn",
|
||||
"websocketbackurl":"connect-ws-dre.hispace.dbankcloud.com",
|
||||
"websocketurl":"connect-ws-dre.hispace.dbankcloud.cn"
|
||||
},
|
||||
"agcgw_all":{
|
||||
"CN":"connect-drcn.dbankcloud.cn",
|
||||
"CN_back":"connect-drcn.hispace.hicloud.com",
|
||||
"DE":"connect-dre.dbankcloud.cn",
|
||||
"DE_back":"connect-dre.hispace.hicloud.com",
|
||||
"RU":"connect-drru.hispace.dbankcloud.ru",
|
||||
"RU_back":"connect-drru.hispace.dbankcloud.cn",
|
||||
"SG":"connect-dra.dbankcloud.cn",
|
||||
"SG_back":"connect-dra.hispace.hicloud.com"
|
||||
},
|
||||
"websocketgw_all":{
|
||||
"CN":"connect-ws-drcn.hispace.dbankcloud.cn",
|
||||
"CN_back":"connect-ws-drcn.hispace.dbankcloud.com",
|
||||
"DE":"connect-ws-dre.hispace.dbankcloud.cn",
|
||||
"DE_back":"connect-ws-dre.hispace.dbankcloud.com",
|
||||
"RU":"connect-ws-drru.hispace.dbankcloud.ru",
|
||||
"RU_back":"connect-ws-drru.hispace.dbankcloud.cn",
|
||||
"SG":"connect-ws-dra.hispace.dbankcloud.cn",
|
||||
"SG_back":"connect-ws-dra.hispace.dbankcloud.com"
|
||||
},
|
||||
"client":{
|
||||
"cp_id":"890061000023000573",
|
||||
"product_id":"99536292102532562",
|
||||
"client_id":"954244311350791232",
|
||||
"client_secret":"555999202D718B6744DAD2E923B386DC17F3F4E29F5105CE0D061EED328DADEE",
|
||||
"project_id":"99536292102532562",
|
||||
"app_id":"107205081",
|
||||
"api_key":"DAEDABeddLEqUy0QRwa1THLwRA0OqrSuyci/HjNvVSmsdWsXRM2U2hRaCyqfvGYH1IFOKrauArssz/WPMLRHCYxliWf+DTj9bDwlWA==",
|
||||
"package_name":"network.loki.messenger"
|
||||
},
|
||||
"oauth_client":{
|
||||
"client_id":"107205081",
|
||||
"client_type":1
|
||||
},
|
||||
"app_info":{
|
||||
"app_id":"107205081",
|
||||
"package_name":"network.loki.messenger"
|
||||
},
|
||||
"service":{
|
||||
"analytics":{
|
||||
"collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||
"collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||
"collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||
"collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||
"resource_id":"p1",
|
||||
"channel_id":""
|
||||
},
|
||||
"edukit":{
|
||||
"edu_url":"edukit.edu.cloud.huawei.com.cn",
|
||||
"dh_url":"edukit.edu.cloud.huawei.com.cn"
|
||||
},
|
||||
"search":{
|
||||
"url":"https://search-dre.cloud.huawei.com"
|
||||
},
|
||||
"cloudstorage":{
|
||||
"storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia",
|
||||
"storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru",
|
||||
"storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu",
|
||||
"storage_url_de":"https://ops-dre.agcstorage.link",
|
||||
"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||
"storage_url_sg":"https://ops-dra.agcstorage.link",
|
||||
"storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn",
|
||||
"storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn"
|
||||
},
|
||||
"ml":{
|
||||
"mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
|
||||
}
|
||||
},
|
||||
"region":"DE",
|
||||
"configuration_version":"3.0",
|
||||
"appInfos":[
|
||||
{
|
||||
"package_name":"network.loki.messenger",
|
||||
"client":{
|
||||
"app_id":"107205081"
|
||||
},
|
||||
"app_info":{
|
||||
"package_name":"network.loki.messenger",
|
||||
"app_id":"107205081"
|
||||
},
|
||||
"oauth_client":{
|
||||
"client_type":1,
|
||||
"client_id":"107205081"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class HuaweiBindingModule {
|
||||
@Binds
|
||||
abstract fun bindTokenFetcher(tokenFetcher: HuaweiTokenFetcher): TokenFetcher
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.os.Bundle
|
||||
import com.huawei.hms.push.HmsMessageService
|
||||
import com.huawei.hms.push.RemoteMessage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.json.JSONException
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
|
||||
private val TAG = HuaweiPushService::class.java.simpleName
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HuaweiPushService: HmsMessageService() {
|
||||
@Inject lateinit var pushRegistry: PushRegistry
|
||||
@Inject lateinit var pushReceiver: PushReceiver
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage?) {
|
||||
Log.d(TAG, "onMessageReceived")
|
||||
message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?:
|
||||
pushReceiver.onPush(message?.data?.let(Base64::decode))
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String?) {
|
||||
pushRegistry.register(token)
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String?, bundle: Bundle?) {
|
||||
Log.d(TAG, "New HCM token: $token.")
|
||||
pushRegistry.register(token)
|
||||
}
|
||||
|
||||
override fun onDeletedMessages() {
|
||||
Log.d(TAG, "onDeletedMessages")
|
||||
pushRegistry.refresh(false)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.huawei.hms.aaid.HmsInstanceId
|
||||
import dagger.Lazy
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.session.libsignal.utilities.Log
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val APP_ID = "107205081"
|
||||
private const val TOKEN_SCOPE = "HCM"
|
||||
|
||||
@Singleton
|
||||
class HuaweiTokenFetcher @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val pushRegistry: Lazy<PushRegistry>,
|
||||
): TokenFetcher {
|
||||
override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run {
|
||||
// https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370
|
||||
// getToken may return an empty string, if so HuaweiPushService#onNewToken will be called.
|
||||
withContext(Dispatchers.IO) { getToken(APP_ID, TOKEN_SCOPE) }
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.utilities.Address;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable {
|
||||
void bind(@NonNull MessageRecord messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight);
|
||||
|
||||
MessageRecord getMessageRecord();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
void onMoreTextClicked(@NonNull Address conversationAddress, long messageId, boolean isMms);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import network.loki.messenger.BuildConfig
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DeviceModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provides() = BuildConfig.DEVICE
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.BaseAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.contacts.UserView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.session.libsession.utilities.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
|
||||
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final MessageRecord record;
|
||||
private final List<RecipientDeliveryStatus> members;
|
||||
private final boolean isPushGroup;
|
||||
|
||||
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
||||
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> members,
|
||||
boolean isPushGroup)
|
||||
{
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.record = record;
|
||||
this.isPushGroup = isPushGroup;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return members.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return members.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
try {
|
||||
return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(members.get(position).recipient.getAddress().serialize().getBytes()));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
UserView result = new UserView(context);
|
||||
Recipient recipient = members.get(position).getRecipient();
|
||||
result.setOpenGroupThreadID(record.getThreadId());
|
||||
result.bind(recipient, glideRequests, UserView.ActionIndicator.None, false);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovedToScrapHeap(View view) {
|
||||
((UserView)view).unbind();
|
||||
}
|
||||
|
||||
|
||||
static class RecipientDeliveryStatus {
|
||||
|
||||
enum Status {
|
||||
UNKNOWN, PENDING, SENT, DELIVERED, READ
|
||||
}
|
||||
|
||||
private final Recipient recipient;
|
||||
private final Status deliveryStatus;
|
||||
private final boolean isUnidentified;
|
||||
private final long timestamp;
|
||||
|
||||
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
|
||||
this.recipient = recipient;
|
||||
this.deliveryStatus = deliveryStatus;
|
||||
this.isUnidentified = isUnidentified;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
Status getDeliveryStatus() {
|
||||
return deliveryStatus;
|
||||
}
|
||||
|
||||
boolean isUnidentified() {
|
||||
return isUnidentified;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
public interface Unbindable {
|
||||
public void unbind();
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
public class Outliner {
|
||||
|
||||
private final float[] radii = new float[8];
|
||||
private final Path corners = new Path();
|
||||
private final RectF bounds = new RectF();
|
||||
private final Paint outlinePaint = new Paint();
|
||||
{
|
||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outlinePaint.setStrokeWidth(1f);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
public void setColor(@ColorInt int color) {
|
||||
outlinePaint.setColor(color);
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2;
|
||||
|
||||
bounds.left = halfStrokeWidth;
|
||||
bounds.top = halfStrokeWidth;
|
||||
bounds.right = canvas.getWidth() - halfStrokeWidth;
|
||||
bounds.bottom = canvas.getHeight() - halfStrokeWidth;
|
||||
|
||||
corners.reset();
|
||||
corners.addRoundRect(bounds, radii, Path.Direction.CW);
|
||||
|
||||
canvas.drawPath(corners, outlinePaint);
|
||||
}
|
||||
|
||||
public void setRadius(int radius) {
|
||||
setRadii(radius, radius, radius, radius);
|
||||
}
|
||||
|
||||
public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
radii[0] = radii[1] = topLeft;
|
||||
radii[2] = radii[3] = topRight;
|
||||
radii[4] = radii[5] = bottomRight;
|
||||
radii[6] = radii[7] = bottomLeft;
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji.parsing;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
|
||||
import org.session.libsession.utilities.ListenableFutureTask;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class EmojiPageBitmap {
|
||||
|
||||
private static final String TAG = EmojiPageBitmap.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final EmojiPageModel model;
|
||||
private final float decodeScale;
|
||||
|
||||
private SoftReference<Bitmap> bitmapReference;
|
||||
private ListenableFutureTask<Bitmap> task;
|
||||
|
||||
public EmojiPageBitmap(@NonNull Context context, @NonNull EmojiPageModel model, float decodeScale) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.model = model;
|
||||
this.decodeScale = decodeScale;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public ListenableFutureTask<Bitmap> get() {
|
||||
Util.assertMainThread();
|
||||
|
||||
if (bitmapReference != null && bitmapReference.get() != null) {
|
||||
return new ListenableFutureTask<>(bitmapReference.get());
|
||||
} else if (task != null) {
|
||||
return task;
|
||||
} else {
|
||||
Callable<Bitmap> callable = () -> {
|
||||
try {
|
||||
Log.i(TAG, "loading page " + model.getSpriteUri().toString());
|
||||
return loadPage();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
task = new ListenableFutureTask<>(callable);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override protected Void doInBackground(Void... params) {
|
||||
task.run();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override protected void onPostExecute(Void aVoid) {
|
||||
task = null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private Bitmap loadPage() throws IOException {
|
||||
if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
|
||||
|
||||
|
||||
float scale = decodeScale;
|
||||
AssetManager assetManager = context.getAssets();
|
||||
InputStream assetStream = assetManager.open(model.getSpriteUri().toString());
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
|
||||
if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) {
|
||||
Log.i(TAG, "Low memory detected. Changing sample size.");
|
||||
options.inSampleSize = 2;
|
||||
scale = decodeScale * 2;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch(model.getSpriteUri().toString());
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(assetStream, null, options);
|
||||
stopwatch.split("decode");
|
||||
|
||||
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, (int)(bitmap.getWidth() * scale), (int)(bitmap.getHeight() * scale), true);
|
||||
stopwatch.split("scale");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
bitmapReference = new SoftReference<>(scaledBitmap);
|
||||
Log.i(TAG, "onPageLoaded(" + model.getSpriteUri().toString() + ") originalByteCount: " + bitmap.getByteCount()
|
||||
+ " scaledByteCount: " + scaledBitmap.getByteCount()
|
||||
+ " scaledSize: " + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight());
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return model.getSpriteUri().toString();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.recyclerview;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class SmoothScrollingLinearLayoutManager extends LinearLayoutManager {
|
||||
|
||||
public SmoothScrollingLinearLayoutManager(Context context, boolean reverseLayout) {
|
||||
super(context, LinearLayoutManager.VERTICAL, reverseLayout);
|
||||
}
|
||||
|
||||
public void smoothScrollToPosition(@NonNull Context context, int position, float millisecondsPerInch) {
|
||||
final LinearSmoothScroller scroller = new LinearSmoothScroller(context) {
|
||||
@Override
|
||||
protected int getVerticalSnapPreference() {
|
||||
return LinearSmoothScroller.SNAP_TO_END;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
||||
return millisecondsPerInch / displayMetrics.densityDpi;
|
||||
}
|
||||
};
|
||||
|
||||
scroller.setTargetPosition(position);
|
||||
startSmoothScroll(scroller);
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.session.libsignal.messages.SharedContact;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import static org.session.libsession.utilities.Contact.*;
|
||||
|
||||
public class ContactModelMapper {
|
||||
|
||||
public static SharedContact.Builder localToRemoteBuilder(@NonNull Contact contact) {
|
||||
List<SharedContact.Phone> phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
|
||||
List<SharedContact.Email> emails = new ArrayList<>(contact.getEmails().size());
|
||||
List<SharedContact.PostalAddress> postalAddresses = new ArrayList<>(contact.getPostalAddresses().size());
|
||||
|
||||
for (Phone phone : contact.getPhoneNumbers()) {
|
||||
phoneNumbers.add(new SharedContact.Phone.Builder().setValue(phone.getNumber())
|
||||
.setType(localToRemoteType(phone.getType()))
|
||||
.setLabel(phone.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (Email email : contact.getEmails()) {
|
||||
emails.add(new SharedContact.Email.Builder().setValue(email.getEmail())
|
||||
.setType(localToRemoteType(email.getType()))
|
||||
.setLabel(email.getLabel())
|
||||
.build());
|
||||
}
|
||||
|
||||
for (PostalAddress postalAddress : contact.getPostalAddresses()) {
|
||||
postalAddresses.add(new SharedContact.PostalAddress.Builder().setType(localToRemoteType(postalAddress.getType()))
|
||||
.setLabel(postalAddress.getLabel())
|
||||
.setStreet(postalAddress.getStreet())
|
||||
.setPobox(postalAddress.getPoBox())
|
||||
.setNeighborhood(postalAddress.getNeighborhood())
|
||||
.setCity(postalAddress.getCity())
|
||||
.setRegion(postalAddress.getRegion())
|
||||
.setPostcode(postalAddress.getPostalCode())
|
||||
.setCountry(postalAddress.getCountry())
|
||||
.build());
|
||||
}
|
||||
|
||||
SharedContact.Name name = new SharedContact.Name.Builder().setDisplay(contact.getName().getDisplayName())
|
||||
.setGiven(contact.getName().getGivenName())
|
||||
.setFamily(contact.getName().getFamilyName())
|
||||
.setPrefix(contact.getName().getPrefix())
|
||||
.setSuffix(contact.getName().getSuffix())
|
||||
.setMiddle(contact.getName().getMiddleName())
|
||||
.build();
|
||||
|
||||
return new SharedContact.Builder().setName(name)
|
||||
.withOrganization(contact.getOrganization())
|
||||
.withPhones(phoneNumbers)
|
||||
.withEmails(emails)
|
||||
.withAddresses(postalAddresses);
|
||||
}
|
||||
|
||||
public static Contact remoteToLocal(@NonNull SharedContact sharedContact) {
|
||||
Name name = new Name(sharedContact.getName().getDisplay().orNull(),
|
||||
sharedContact.getName().getGiven().orNull(),
|
||||
sharedContact.getName().getFamily().orNull(),
|
||||
sharedContact.getName().getPrefix().orNull(),
|
||||
sharedContact.getName().getSuffix().orNull(),
|
||||
sharedContact.getName().getMiddle().orNull());
|
||||
|
||||
List<Phone> phoneNumbers = new LinkedList<>();
|
||||
if (sharedContact.getPhone().isPresent()) {
|
||||
for (SharedContact.Phone phone : sharedContact.getPhone().get()) {
|
||||
phoneNumbers.add(new Phone(phone.getValue(),
|
||||
remoteToLocalType(phone.getType()),
|
||||
phone.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<Email> emails = new LinkedList<>();
|
||||
if (sharedContact.getEmail().isPresent()) {
|
||||
for (SharedContact.Email email : sharedContact.getEmail().get()) {
|
||||
emails.add(new Email(email.getValue(),
|
||||
remoteToLocalType(email.getType()),
|
||||
email.getLabel().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
List<PostalAddress> postalAddresses = new LinkedList<>();
|
||||
if (sharedContact.getAddress().isPresent()) {
|
||||
for (SharedContact.PostalAddress postalAddress : sharedContact.getAddress().get()) {
|
||||
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
|
||||
postalAddress.getLabel().orNull(),
|
||||
postalAddress.getStreet().orNull(),
|
||||
postalAddress.getPobox().orNull(),
|
||||
postalAddress.getNeighborhood().orNull(),
|
||||
postalAddress.getCity().orNull(),
|
||||
postalAddress.getRegion().orNull(),
|
||||
postalAddress.getPostcode().orNull(),
|
||||
postalAddress.getCountry().orNull()));
|
||||
}
|
||||
}
|
||||
|
||||
Avatar avatar = null;
|
||||
if (sharedContact.getAvatar().isPresent()) {
|
||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(sharedContact.getAvatar().get().getAttachment().asPointer())).get();
|
||||
boolean isProfile = sharedContact.getAvatar().get().isProfile();
|
||||
|
||||
avatar = new Avatar(null, attachment, isProfile);
|
||||
}
|
||||
|
||||
return new Contact(name, sharedContact.getOrganization().orNull(), phoneNumbers, emails, postalAddresses, avatar);
|
||||
}
|
||||
|
||||
private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Phone.Type.HOME;
|
||||
case MOBILE: return Phone.Type.MOBILE;
|
||||
case WORK: return Phone.Type.WORK;
|
||||
default: return Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static Email.Type remoteToLocalType(SharedContact.Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return Email.Type.HOME;
|
||||
case MOBILE: return Email.Type.MOBILE;
|
||||
case WORK: return Email.Type.WORK;
|
||||
default: return Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static PostalAddress.Type remoteToLocalType(SharedContact.PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return PostalAddress.Type.HOME;
|
||||
case WORK: return PostalAddress.Type.WORK;
|
||||
default: return PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Phone.Type localToRemoteType(Phone.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Phone.Type.HOME;
|
||||
case MOBILE: return SharedContact.Phone.Type.MOBILE;
|
||||
case WORK: return SharedContact.Phone.Type.WORK;
|
||||
default: return SharedContact.Phone.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.Email.Type localToRemoteType(Email.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.Email.Type.HOME;
|
||||
case MOBILE: return SharedContact.Email.Type.MOBILE;
|
||||
case WORK: return SharedContact.Email.Type.WORK;
|
||||
default: return SharedContact.Email.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedContact.PostalAddress.Type localToRemoteType(PostalAddress.Type type) {
|
||||
switch (type) {
|
||||
case HOME: return SharedContact.PostalAddress.Type.HOME;
|
||||
case WORK: return SharedContact.PostalAddress.Type.WORK;
|
||||
default: return SharedContact.PostalAddress.Type.CUSTOM;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class FastCursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder, T>
|
||||
extends CursorRecyclerViewAdapter<VH>
|
||||
{
|
||||
private static final String TAG = FastCursorRecyclerViewAdapter.class.getSimpleName();
|
||||
|
||||
private final LinkedList<T> fastRecords = new LinkedList<>();
|
||||
private final List<Long> releasedRecordIds = new LinkedList<>();
|
||||
|
||||
protected FastCursorRecyclerViewAdapter(Context context, Cursor cursor) {
|
||||
super(context, cursor);
|
||||
}
|
||||
|
||||
public void addFastRecord(@NonNull T record) {
|
||||
fastRecords.addFirst(record);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void releaseFastRecord(long id) {
|
||||
synchronized (releasedRecordIds) {
|
||||
releasedRecordIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
protected void cleanFastRecords() {
|
||||
synchronized (releasedRecordIds) {
|
||||
Iterator<Long> releaseIdIterator = releasedRecordIds.iterator();
|
||||
|
||||
while (releaseIdIterator.hasNext()) {
|
||||
long releasedId = releaseIdIterator.next();
|
||||
Iterator<T> fastRecordIterator = fastRecords.iterator();
|
||||
|
||||
while (fastRecordIterator.hasNext()) {
|
||||
if (isRecordForId(fastRecordIterator.next(), releasedId)) {
|
||||
fastRecordIterator.remove();
|
||||
releaseIdIterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract T getRecordFromCursor(@NonNull Cursor cursor);
|
||||
protected abstract void onBindItemViewHolder(VH viewHolder, @NonNull T record);
|
||||
protected abstract long getItemId(@NonNull T record);
|
||||
protected abstract int getItemViewType(@NonNull T record);
|
||||
protected abstract boolean isRecordForId(@NonNull T record, long id);
|
||||
|
||||
@Override
|
||||
public int getItemViewType(@NonNull Cursor cursor) {
|
||||
T record = getRecordFromCursor(cursor);
|
||||
return getItemViewType(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor) {
|
||||
T record = getRecordFromCursor(cursor);
|
||||
onBindItemViewHolder(viewHolder, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindFastAccessItemViewHolder(VH viewHolder, int position) {
|
||||
int calculatedPosition = getCalculatedPosition(position);
|
||||
onBindItemViewHolder(viewHolder, fastRecords.get(calculatedPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFastAccessSize() {
|
||||
return fastRecords.size();
|
||||
}
|
||||
|
||||
protected T getRecordForPositionOrThrow(int position) {
|
||||
if (isFastAccessPosition(position)) {
|
||||
return fastRecords.get(getCalculatedPosition(position));
|
||||
} else {
|
||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||
return getRecordFromCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
protected int getFastAccessItemViewType(int position) {
|
||||
return getItemViewType(fastRecords.get(getCalculatedPosition(position)));
|
||||
}
|
||||
|
||||
protected boolean isFastAccessPosition(int position) {
|
||||
position = getCalculatedPosition(position);
|
||||
return position >= 0 && position < fastRecords.size();
|
||||
}
|
||||
|
||||
protected long getFastAccessItemId(int position) {
|
||||
return getItemId(fastRecords.get(getCalculatedPosition(position)));
|
||||
}
|
||||
|
||||
private int getCalculatedPosition(int position) {
|
||||
return hasHeaderView() ? position - 1 : position;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ContentModule {
|
||||
|
||||
@Provides
|
||||
fun providesContentResolver(@ApplicationContext context: Context) =context.contentResolver
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
@file:JvmName("FcmUtils")
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import com.google.firebase.iid.InstanceIdResult
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
fun getFcmInstanceId(body: (Task<InstanceIdResult>)->Unit): Job = MainScope().launch(Dispatchers.IO) {
|
||||
val task = FirebaseInstanceId.getInstance().instanceId
|
||||
while (!task.isComplete && isActive) {
|
||||
// wait for task to complete while we are active
|
||||
}
|
||||
if (!isActive) return@launch // don't 'complete' task if we were canceled
|
||||
withContext(Dispatchers.Main) {
|
||||
body(task)
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
|
||||
object LokiPushNotificationManager {
|
||||
private val maxRetryCount = 4
|
||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||
|
||||
private val server by lazy {
|
||||
PushNotificationAPI.server
|
||||
}
|
||||
private val pnServerPublicKey by lazy {
|
||||
PushNotificationAPI.serverPublicKey
|
||||
}
|
||||
|
||||
enum class ClosedGroupOperation {
|
||||
Subscribe, Unsubscribe;
|
||||
|
||||
val rawValue: String
|
||||
get() {
|
||||
return when (this) {
|
||||
Subscribe -> "subscribe_closed_group"
|
||||
Unsubscribe -> "unsubscribe_closed_group"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun unregister(token: String, context: Context) {
|
||||
val parameters = mapOf( "token" to token )
|
||||
val url = "$server/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
getResponseBody(request.build()).map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun register(token: String, publicKey: String, context: Context, force: Boolean) {
|
||||
val oldToken = TextSecurePreferences.getFCMToken(context)
|
||||
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
|
||||
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
|
||||
val parameters = mapOf( "token" to token, "pubKey" to publicKey )
|
||||
val url = "$server/register"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
getResponseBody(request.build()).map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||
TextSecurePreferences.setFCMToken(context, token)
|
||||
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = DatabaseComponent.get(context).lokiAPIDatabase().getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun performOperation(context: Context, operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) {
|
||||
if (!TextSecurePreferences.isUsingFCM(context)) { return }
|
||||
val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey )
|
||||
val url = "$server/${operation.rawValue}"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
getResponseBody(request.build()).map { json ->
|
||||
val code = json["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getResponseBody(request: Request): Promise<Map<*, *>, Exception> {
|
||||
return OnionRequestAPI.sendOnionRequest(request, server, pnServerPublicKey, Version.V2).map { response ->
|
||||
JsonUtil.fromJson(response.body, Map::class.java)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
interface PushManager {
|
||||
fun refresh(force: Boolean)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class PushNotificationService : FirebaseMessagingService() {
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
Log.d("Loki", "New FCM token: $token.")
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return
|
||||
LokiPushNotificationManager.register(token, userPublicKey, this, false)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Log.d("Loki", "Received a push notification.")
|
||||
val base64EncodedData = message.data?.get("ENCRYPTED_DATA")
|
||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
||||
if (data != null) {
|
||||
try {
|
||||
val envelopeAsData = MessageWrapper.unwrap(data).toByteArray()
|
||||
val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null)
|
||||
JobQueue.shared.add(job)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to unwrap data for message due to error: $e.")
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Failed to decode data for message.")
|
||||
val builder = NotificationCompat.Builder(this, NotificationChannels.OTHER)
|
||||
.setSmallIcon(network.loki.messenger.R.drawable.ic_notification)
|
||||
.setColor(this.getResources().getColor(network.loki.messenger.R.color.textsecure_primary))
|
||||
.setContentTitle("Session")
|
||||
.setContentText("You've got a new message.")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
with(NotificationManagerCompat.from(this)) {
|
||||
notify(11111, builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeletedMessages() {
|
||||
Log.d("Loki", "Called onDeletedMessages.")
|
||||
super.onDeletedMessages()
|
||||
val token = TextSecurePreferences.getFCMToken(this)!!
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this) ?: return
|
||||
LokiPushNotificationManager.register(token, userPublicKey, this, true)
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.interfaces.AEAD
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveParameters
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationMetadata
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.utilities.bencode.Bencode
|
||||
import org.session.libsession.utilities.bencode.BencodeList
|
||||
import org.session.libsession.utilities.bencode.BencodeString
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "PushHandler"
|
||||
|
||||
class PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
fun onPush(dataMap: Map<String, String>?) {
|
||||
onPush(dataMap?.asByteArray())
|
||||
}
|
||||
|
||||
fun onPush(data: ByteArray?) {
|
||||
if (data == null) {
|
||||
onPush()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val envelopeAsData = MessageWrapper.unwrap(data).toByteArray()
|
||||
val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null)
|
||||
JobQueue.shared.add(job)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to unwrap data for message due to error.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPush() {
|
||||
Log.d(TAG, "Failed to decode data for message.")
|
||||
val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER)
|
||||
.setSmallIcon(network.loki.messenger.R.drawable.ic_notification)
|
||||
.setColor(context.getColor(network.loki.messenger.R.color.textsecure_primary))
|
||||
.setContentTitle("Session")
|
||||
.setContentText("You've got a new message.")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
NotificationManagerCompat.from(context).notify(11111, builder.build())
|
||||
}
|
||||
|
||||
private fun Map<String, String>.asByteArray() =
|
||||
when {
|
||||
// this is a v2 push notification
|
||||
containsKey("spns") -> {
|
||||
try {
|
||||
decrypt(Base64.decode(this["enc_payload"]))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Invalid push notification", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
// old v1 push notification; we still need this for receiving legacy closed group notifications
|
||||
else -> this["ENCRYPTED_DATA"]?.let(Base64::decode)
|
||||
}
|
||||
|
||||
private fun decrypt(encPayload: ByteArray): ByteArray? {
|
||||
Log.d(TAG, "decrypt() called")
|
||||
|
||||
val encKey = getOrCreateNotificationKey()
|
||||
val nonce = encPayload.take(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
||||
val payload = encPayload.drop(AEAD.XCHACHA20POLY1305_IETF_NPUBBYTES).toByteArray()
|
||||
val padded = SodiumUtilities.decrypt(payload, encKey.asBytes, nonce)
|
||||
?: error("Failed to decrypt push notification")
|
||||
val decrypted = padded.dropLastWhile { it.toInt() == 0 }.toByteArray()
|
||||
val bencoded = Bencode.Decoder(decrypted)
|
||||
val expectedList = (bencoded.decode() as? BencodeList)?.values
|
||||
?: error("Failed to decode bencoded list from payload")
|
||||
|
||||
val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata")
|
||||
val metadata: PushNotificationMetadata = Json.decodeFromString(String(metadataJson))
|
||||
|
||||
return (expectedList.getOrNull(1) as? BencodeString)?.value.also {
|
||||
// null content is valid only if we got a "data_too_long" flag
|
||||
it?.let { check(metadata.data_len == it.size) { "wrong message data size" } }
|
||||
?: check(metadata.data_too_long) { "missing message data, but no too-long flag" }
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateNotificationKey(): Key {
|
||||
if (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.NOTIFICATION_KEY) == null) {
|
||||
// generate the key and store it
|
||||
val key = sodium.keygen(AEAD.Method.XCHACHA20_POLY1305_IETF)
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString)
|
||||
}
|
||||
return Key.fromHexString(
|
||||
IdentityKeyUtil.retrieve(
|
||||
context,
|
||||
IdentityKeyUtil.NOTIFICATION_KEY
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.combine.and
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.emptyPromise
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val TAG = PushRegistry::class.java.name
|
||||
|
||||
@Singleton
|
||||
class PushRegistry @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val device: Device,
|
||||
private val tokenManager: TokenManager,
|
||||
private val pushRegistryV2: PushRegistryV2,
|
||||
private val prefs: TextSecurePreferences,
|
||||
private val tokenFetcher: TokenFetcher,
|
||||
) {
|
||||
|
||||
private var pushRegistrationJob: Job? = null
|
||||
|
||||
fun refresh(force: Boolean): Job {
|
||||
Log.d(TAG, "refresh() called with: force = $force")
|
||||
|
||||
pushRegistrationJob?.apply {
|
||||
if (force) cancel() else if (isActive) return MainScope().launch {}
|
||||
}
|
||||
|
||||
return MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
register(tokenFetcher.fetch()).get()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "register failed", e)
|
||||
}
|
||||
}.also { pushRegistrationJob = it }
|
||||
}
|
||||
|
||||
fun register(token: String?): Promise<*, Exception> {
|
||||
Log.d(TAG, "refresh() called")
|
||||
|
||||
if (token?.isNotEmpty() != true) return emptyPromise()
|
||||
|
||||
prefs.setPushToken(token)
|
||||
|
||||
val userPublicKey = prefs.getLocalNumber() ?: return emptyPromise()
|
||||
val userEdKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return emptyPromise()
|
||||
|
||||
return when {
|
||||
prefs.isPushEnabled() -> register(token, userPublicKey, userEdKey)
|
||||
tokenManager.isRegistered -> unregister(token, userPublicKey, userEdKey)
|
||||
else -> emptyPromise()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register for push notifications.
|
||||
*/
|
||||
private fun register(
|
||||
token: String,
|
||||
publicKey: String,
|
||||
userEd25519Key: KeyPair,
|
||||
namespaces: List<Int> = listOf(Namespace.DEFAULT)
|
||||
): Promise<*, Exception> {
|
||||
Log.d(TAG, "register() called")
|
||||
|
||||
val v1 = PushRegistryV1.register(
|
||||
device = device,
|
||||
token = token,
|
||||
publicKey = publicKey
|
||||
) fail {
|
||||
Log.e(TAG, "register v1 failed", it)
|
||||
}
|
||||
|
||||
val v2 = pushRegistryV2.register(
|
||||
device, token, publicKey, userEd25519Key, namespaces
|
||||
) fail {
|
||||
Log.e(TAG, "register v2 failed", it)
|
||||
}
|
||||
|
||||
return v1 and v2 success {
|
||||
Log.d(TAG, "register v1 & v2 success")
|
||||
tokenManager.register()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregister(
|
||||
token: String,
|
||||
userPublicKey: String,
|
||||
userEdKey: KeyPair
|
||||
): Promise<*, Exception> = PushRegistryV1.unregister() and pushRegistryV2.unregister(
|
||||
device, token, userPublicKey, userEdKey
|
||||
) fail {
|
||||
Log.e(TAG, "unregisterBoth failed", it)
|
||||
} success {
|
||||
tokenManager.unregister()
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.goterl.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazysodium.SodiumAndroid
|
||||
import com.goterl.lazysodium.interfaces.Sign
|
||||
import com.goterl.lazysodium.utils.KeyPair
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Response
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.Server
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionRequest
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.SubscriptionResponse
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscribeResponse
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.UnsubscriptionRequest
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private val TAG = PushRegistryV2::class.java.name
|
||||
private const val maxRetryCount = 4
|
||||
|
||||
@Singleton
|
||||
class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) {
|
||||
private val sodium = LazySodiumAndroid(SodiumAndroid())
|
||||
|
||||
fun register(
|
||||
device: Device,
|
||||
token: String,
|
||||
publicKey: String,
|
||||
userEd25519Key: KeyPair,
|
||||
namespaces: List<Int>
|
||||
): Promise<SubscriptionResponse, Exception> {
|
||||
val pnKey = pushReceiver.getOrCreateNotificationKey()
|
||||
|
||||
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s
|
||||
// if we want to support passing namespace list, here is the place to do it
|
||||
val sigData = "MONITOR${publicKey}${timestamp}1${namespaces.joinToString(separator = ",")}".encodeToByteArray()
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEd25519Key.secretKey.asBytes)
|
||||
val requestParameters = SubscriptionRequest(
|
||||
pubkey = publicKey,
|
||||
session_ed25519 = userEd25519Key.publicKey.asHexString,
|
||||
namespaces = listOf(Namespace.DEFAULT),
|
||||
data = true, // only permit data subscription for now (?)
|
||||
service = device.service,
|
||||
sig_ts = timestamp,
|
||||
signature = Base64.encodeBytes(signature),
|
||||
service_info = mapOf("token" to token),
|
||||
enc_key = pnKey.asHexString,
|
||||
).let(Json::encodeToString)
|
||||
|
||||
return retryResponseBody<SubscriptionResponse>("subscribe", requestParameters) success {
|
||||
Log.d(TAG, "registerV2 success")
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(
|
||||
device: Device,
|
||||
token: String,
|
||||
userPublicKey: String,
|
||||
userEdKey: KeyPair
|
||||
): Promise<UnsubscribeResponse, Exception> {
|
||||
val timestamp = SnodeAPI.nowWithOffset / 1000 // get timestamp in ms -> s
|
||||
// if we want to support passing namespace list, here is the place to do it
|
||||
val sigData = "UNSUBSCRIBE${userPublicKey}${timestamp}".encodeToByteArray()
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
sodium.cryptoSignDetached(signature, sigData, sigData.size.toLong(), userEdKey.secretKey.asBytes)
|
||||
|
||||
val requestParameters = UnsubscriptionRequest(
|
||||
pubkey = userPublicKey,
|
||||
session_ed25519 = userEdKey.publicKey.asHexString,
|
||||
service = device.service,
|
||||
sig_ts = timestamp,
|
||||
signature = Base64.encodeBytes(signature),
|
||||
service_info = mapOf("token" to token),
|
||||
).let(Json::encodeToString)
|
||||
|
||||
return retryResponseBody<UnsubscribeResponse>("unsubscribe", requestParameters) success {
|
||||
Log.d(TAG, "unregisterV2 success")
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T: Response> retryResponseBody(path: String, requestParameters: String): Promise<T, Exception> =
|
||||
retryIfNeeded(maxRetryCount) { getResponseBody(path, requestParameters) }
|
||||
|
||||
private inline fun <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
|
||||
val server = Server.LATEST
|
||||
val url = "${server.url}/$path"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), requestParameters)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V4
|
||||
).map { response ->
|
||||
response.body!!.inputStream()
|
||||
.let { Json.decodeFromStream<T>(it) }
|
||||
.also { if (it.isFailure()) throw Exception("error: ${it.message}.") }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
interface TokenFetcher {
|
||||
suspend fun fetch(): String?
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val INTERVAL: Int = 12 * 60 * 60 * 1000
|
||||
|
||||
@Singleton
|
||||
class TokenManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
val hasValidRegistration get() = isRegistered && !isExpired
|
||||
val isRegistered get() = time > 0
|
||||
private val isExpired get() = currentTime() > time + INTERVAL
|
||||
|
||||
fun register() {
|
||||
time = currentTime()
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
time = 0
|
||||
}
|
||||
|
||||
private var time
|
||||
get() = TextSecurePreferences.getPushRegisterTime(context)
|
||||
set(value) = TextSecurePreferences.setPushRegisterTime(context, value)
|
||||
|
||||
private fun currentTime() = System.currentTimeMillis()
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import static org.thoughtcrime.securesms.preferences.ListPreferenceDialogKt.listPreferenceDialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
// Set up FCM toggle
|
||||
String fcmKey = "pref_key_use_fcm";
|
||||
((SwitchPreferenceCompat)findPreference(fcmKey)).setChecked(TextSecurePreferences.isUsingFCM(getContext()));
|
||||
this.findPreference(fcmKey)
|
||||
.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
TextSecurePreferences.setIsUsingFCM(getContext(), (boolean) newValue);
|
||||
ApplicationContext.getInstance(getContext()).registerForFCMIfNeeded(true);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
TextSecurePreferences.setNotificationRingtone(getContext(), NotificationChannels.getMessageRingtone(getContext()).toString());
|
||||
TextSecurePreferences.setNotificationVibrateEnabled(getContext(), NotificationChannels.getMessageVibrate(getContext()));
|
||||
}
|
||||
this.findPreference(TextSecurePreferences.RINGTONE_PREF)
|
||||
.setOnPreferenceChangeListener(new RingtoneSummaryListener());
|
||||
this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)
|
||||
.setOnPreferenceChangeListener(new NotificationPrivacyListener());
|
||||
this.findPreference(TextSecurePreferences.VIBRATE_PREF)
|
||||
.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
NotificationChannels.updateMessageVibrate(getContext(), (boolean) newValue);
|
||||
return true;
|
||||
});
|
||||
|
||||
this.findPreference(TextSecurePreferences.RINGTONE_PREF)
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
Uri current = TextSecurePreferences.getNotificationRingtone(getContext());
|
||||
|
||||
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
|
||||
|
||||
startActivityForResult(intent, 1);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
ListPreference listPreference = (ListPreference) preference;
|
||||
listPreference.setDialogMessage(R.string.preferences_notifications__content_message);
|
||||
listPreferenceDialog(getContext(), listPreference, () -> {
|
||||
initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
|
||||
return null;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
initializeListSummary((ListPreference) findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF));
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
this.findPreference(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(getContext()));
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF));
|
||||
initializeMessageVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.VIBRATE_PREF));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences_notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == 1 && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) {
|
||||
NotificationChannels.updateMessageRingtone(getContext(), uri);
|
||||
TextSecurePreferences.removeNotificationRingtone(getContext());
|
||||
} else {
|
||||
uri = uri == null ? Uri.EMPTY : uri;
|
||||
NotificationChannels.updateMessageRingtone(getContext(), uri);
|
||||
TextSecurePreferences.setNotificationRingtone(getContext(), uri.toString());
|
||||
}
|
||||
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF));
|
||||
}
|
||||
}
|
||||
|
||||
private class RingtoneSummaryListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
Uri value = (Uri) newValue;
|
||||
|
||||
if (value == null || TextUtils.isEmpty(value.toString())) {
|
||||
preference.setSummary(R.string.preferences__silent);
|
||||
} else {
|
||||
Ringtone tone = RingtoneManager.getRingtone(getActivity(), value);
|
||||
|
||||
if (tone != null) {
|
||||
preference.setSummary(tone.getTitle(getActivity()));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeRingtoneSummary(Preference pref) {
|
||||
RingtoneSummaryListener listener = (RingtoneSummaryListener) pref.getOnPreferenceChangeListener();
|
||||
Uri uri = TextSecurePreferences.getNotificationRingtone(getContext());
|
||||
|
||||
listener.onPreferenceChange(pref, uri);
|
||||
}
|
||||
|
||||
private void initializeMessageVibrateSummary(SwitchPreferenceCompat pref) {
|
||||
pref.setChecked(TextSecurePreferences.isNotificationVibrateEnabled(getContext()));
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final int onCapsResId = R.string.ApplicationPreferencesActivity_On;
|
||||
final int offCapsResId = R.string.ApplicationPreferencesActivity_Off;
|
||||
|
||||
return context.getString(TextSecurePreferences.isNotificationsEnabled(context) ? onCapsResId : offCapsResId);
|
||||
}
|
||||
|
||||
private class NotificationPrivacyListener extends ListSummaryListener {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
ApplicationContext.getInstance(getActivity()).messageNotifier.updateNotification(getActivity());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
||||
return super.onPreferenceChange(preference, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
package org.thoughtcrime.securesms.preferences
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.isNotificationsEnabled
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.PushRegistry
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationsPreferenceFragment : ListSummaryPreferenceFragment() {
|
||||
@Inject
|
||||
lateinit var pushRegistry: PushRegistry
|
||||
@Inject
|
||||
lateinit var prefs: TextSecurePreferences
|
||||
|
||||
override fun onCreate(paramBundle: Bundle?) {
|
||||
super.onCreate(paramBundle)
|
||||
|
||||
// Set up FCM toggle
|
||||
val fcmKey = "pref_key_use_fcm"
|
||||
val fcmPreference: SwitchPreferenceCompat = findPreference(fcmKey)!!
|
||||
fcmPreference.isChecked = prefs.isPushEnabled()
|
||||
fcmPreference.setOnPreferenceChangeListener { _: Preference, newValue: Any ->
|
||||
prefs.setPushEnabled(newValue as Boolean)
|
||||
val job = pushRegistry.refresh(true)
|
||||
|
||||
fcmPreference.isEnabled = false
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
job.join()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
fcmPreference.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
if (NotificationChannels.supported()) {
|
||||
prefs.setNotificationRingtone(
|
||||
NotificationChannels.getMessageRingtone(requireContext()).toString()
|
||||
)
|
||||
prefs.setNotificationVibrateEnabled(
|
||||
NotificationChannels.getMessageVibrate(requireContext())
|
||||
)
|
||||
}
|
||||
findPreference<Preference>(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceChangeListener = RingtoneSummaryListener()
|
||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceChangeListener = NotificationPrivacyListener()
|
||||
findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF)!!.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||
NotificationChannels.updateMessageVibrate(requireContext(), newValue as Boolean)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(TextSecurePreferences.RINGTONE_PREF)!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
val current = prefs.getNotificationRingtone()
|
||||
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
|
||||
intent.putExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_TYPE,
|
||||
RingtoneManager.TYPE_NOTIFICATION
|
||||
)
|
||||
intent.putExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
|
||||
Settings.System.DEFAULT_NOTIFICATION_URI
|
||||
)
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current)
|
||||
startActivityForResult(intent, 1)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF)!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener { preference: Preference ->
|
||||
val listPreference = preference as ListPreference
|
||||
listPreference.setDialogMessage(R.string.preferences_notifications__content_message)
|
||||
listPreferenceDialog(requireContext(), listPreference) {
|
||||
initializeListSummary(findPreference(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF))
|
||||
}
|
||||
true
|
||||
}
|
||||
initializeListSummary(findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIVACY_PREF) as ListPreference?)
|
||||
if (NotificationChannels.supported()) {
|
||||
findPreference<Preference>(TextSecurePreferences.NOTIFICATION_PRIORITY_PREF)!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(
|
||||
Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(requireContext())
|
||||
)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
}
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF))
|
||||
initializeMessageVibrateSummary(findPreference<Preference>(TextSecurePreferences.VIBRATE_PREF) as SwitchPreferenceCompat?)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_notifications)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == 1 && resultCode == Activity.RESULT_OK && data != null) {
|
||||
var uri = data.getParcelableExtra<Uri>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||
if (Settings.System.DEFAULT_NOTIFICATION_URI == uri) {
|
||||
NotificationChannels.updateMessageRingtone(requireContext(), uri)
|
||||
prefs.removeNotificationRingtone()
|
||||
} else {
|
||||
uri = uri ?: Uri.EMPTY
|
||||
NotificationChannels.updateMessageRingtone(requireContext(), uri)
|
||||
prefs.setNotificationRingtone(uri.toString())
|
||||
}
|
||||
initializeRingtoneSummary(findPreference(TextSecurePreferences.RINGTONE_PREF))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RingtoneSummaryListener : Preference.OnPreferenceChangeListener {
|
||||
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
|
||||
val value = newValue as? Uri
|
||||
if (value == null || TextUtils.isEmpty(value.toString())) {
|
||||
preference.setSummary(R.string.preferences__silent)
|
||||
} else {
|
||||
RingtoneManager.getRingtone(activity, value)
|
||||
?.getTitle(activity)
|
||||
?.let { preference.summary = it }
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeRingtoneSummary(pref: Preference?) {
|
||||
val listener = pref!!.onPreferenceChangeListener as RingtoneSummaryListener?
|
||||
val uri = prefs.getNotificationRingtone()
|
||||
listener!!.onPreferenceChange(pref, uri)
|
||||
}
|
||||
|
||||
private fun initializeMessageVibrateSummary(pref: SwitchPreferenceCompat?) {
|
||||
pref!!.isChecked = prefs.isNotificationVibrateEnabled()
|
||||
}
|
||||
|
||||
private inner class NotificationPrivacyListener : ListSummaryListener() {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
override fun onPreferenceChange(preference: Preference, value: Any): Boolean {
|
||||
object : AsyncTask<Void?, Void?, Void?>() {
|
||||
override fun doInBackground(vararg params: Void?): Void? {
|
||||
ApplicationContext.getInstance(activity).messageNotifier.updateNotification(activity!!)
|
||||
return null
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
return super.onPreferenceChange(preference, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Suppress("unused")
|
||||
private val TAG = NotificationsPreferenceFragment::class.java.simpleName
|
||||
fun getSummary(context: Context): CharSequence = when (isNotificationsEnabled(context)) {
|
||||
true -> R.string.ApplicationPreferencesActivity_On
|
||||
false -> R.string.ApplicationPreferencesActivity_Off
|
||||
}.let(context::getString)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ContactPreference extends Preference {
|
||||
|
||||
private ImageView messageButton;
|
||||
|
||||
private Listener listener;
|
||||
private boolean secure;
|
||||
|
||||
public ContactPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ContactPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ContactPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ContactPreference(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setWidgetLayoutResource(R.layout.recipient_preference_contact_widget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
||||
super.onBindViewHolder(view);
|
||||
|
||||
this.messageButton = (ImageView) view.findViewById(R.id.message);
|
||||
|
||||
if (listener != null) setListener(listener);
|
||||
setSecure(secure);
|
||||
}
|
||||
|
||||
public void setSecure(boolean secure) {
|
||||
this.secure = secure;
|
||||
|
||||
int color;
|
||||
|
||||
if (secure) {
|
||||
color = getContext().getResources().getColor(R.color.textsecure_primary);
|
||||
} else {
|
||||
color = getContext().getResources().getColor(R.color.grey_600);
|
||||
}
|
||||
|
||||
if (messageButton != null) messageButton.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
public void setListener(Listener listener) {
|
||||
this.listener = listener;
|
||||
|
||||
if (this.messageButton != null) this.messageButton.setOnClickListener(v -> listener.onMessageClicked());
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
public void onMessageClicked();
|
||||
public void onSecureCallClicked();
|
||||
public void onInSecureCallClicked();
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class NotificationSettingsPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
// TODO: if we want do the spans
|
||||
}
|
||||
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ProgressPreference extends Preference {
|
||||
|
||||
private View container;
|
||||
private TextView progressText;
|
||||
|
||||
public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ProgressPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ProgressPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ProgressPreference(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setWidgetLayoutResource(R.layout.preference_widget_progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
||||
super.onBindViewHolder(view);
|
||||
|
||||
this.container = view.findViewById(R.id.container);
|
||||
this.progressText = (TextView) view.findViewById(R.id.progress_text);
|
||||
|
||||
this.container.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setProgress(int count) {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
progressText.setText(getContext().getString(R.string.ProgressPreference_d_messages_so_far, count));
|
||||
}
|
||||
|
||||
public void setProgressVisible(boolean visible) {
|
||||
container.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.Emoji;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Contains the Emojis that have been used in reactions for a given message.
|
||||
*/
|
||||
class ThisMessageEmojiPageModel implements EmojiPageModel {
|
||||
|
||||
private final List<String> emoji;
|
||||
|
||||
ThisMessageEmojiPageModel(@NonNull List<String> emoji) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return RecentEmojiPageModel.KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconAttr() {
|
||||
return R.attr.emoji_category_recent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<String> getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Emoji> getDisplayEmoji() {
|
||||
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSpriteMap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getSpriteUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.ServiceState;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
public class TelephonyServiceState {
|
||||
|
||||
public boolean isConnected(Context context) {
|
||||
ListenThread listenThread = new ListenThread(context);
|
||||
listenThread.start();
|
||||
|
||||
return listenThread.get();
|
||||
}
|
||||
|
||||
private static class ListenThread extends Thread {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private boolean complete;
|
||||
private boolean result;
|
||||
|
||||
public ListenThread(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Looper looper = initializeLooper();
|
||||
ListenCallback callback = new ListenCallback(looper);
|
||||
|
||||
TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
telephonyManager.listen(callback, PhoneStateListener.LISTEN_SERVICE_STATE);
|
||||
|
||||
Looper.loop();
|
||||
|
||||
telephonyManager.listen(callback, PhoneStateListener.LISTEN_NONE);
|
||||
|
||||
set(callback.isConnected());
|
||||
}
|
||||
|
||||
private Looper initializeLooper() {
|
||||
Looper looper = Looper.myLooper();
|
||||
|
||||
if (looper == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
|
||||
return Looper.myLooper();
|
||||
}
|
||||
|
||||
public synchronized boolean get() {
|
||||
while (!complete) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private synchronized void set(boolean result) {
|
||||
this.result = result;
|
||||
this.complete = true;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ListenCallback extends PhoneStateListener {
|
||||
|
||||
private final Looper looper;
|
||||
private volatile boolean connected;
|
||||
|
||||
public ListenCallback(Looper looper) {
|
||||
this.looper = looper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceStateChanged(ServiceState serviceState) {
|
||||
this.connected = (serviceState.getState() == ServiceState.STATE_IN_SERVICE);
|
||||
looper.quit();
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return connected;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
/**
|
||||
* A simplified version of [android.content.ContextWrapper],
|
||||
* but properly supports [startActivityForResult] for the implementations.
|
||||
*/
|
||||
interface ContextProvider {
|
||||
fun getContext(): Context
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int)
|
||||
}
|
||||
|
||||
class ActivityContextProvider(private val activity: Activity): ContextProvider {
|
||||
|
||||
override fun getContext(): Context {
|
||||
return activity
|
||||
}
|
||||
|
||||
override fun startActivityForResult(intent: Intent, requestCode: Int) {
|
||||
activity.startActivityForResult(intent, requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
class FragmentContextProvider(private val fragment: Fragment): ContextProvider {
|
||||
|
||||
override fun getContext(): Context {
|
||||
return fragment.requireContext()
|
||||
}
|
||||
|
||||
override fun startActivityForResult(intent: Intent, requestCode: Int) {
|
||||
fragment.startActivityForResult(intent, requestCode)
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.text.Layout;
|
||||
import android.text.Selection;
|
||||
import android.text.Spannable;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class LongClickMovementMethod extends LinkMovementMethod {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static LongClickMovementMethod sInstance;
|
||||
|
||||
private final GestureDetector gestureDetector;
|
||||
private View widget;
|
||||
private LongClickCopySpan currentSpan;
|
||||
|
||||
private LongClickMovementMethod(final Context context) {
|
||||
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (currentSpan != null && widget != null) {
|
||||
currentSpan.onLongClick(widget);
|
||||
widget = null;
|
||||
currentSpan = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
if (currentSpan != null && widget != null) {
|
||||
currentSpan.onClick(widget);
|
||||
widget = null;
|
||||
currentSpan = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
|
||||
int action = event.getAction();
|
||||
|
||||
if (action == MotionEvent.ACTION_UP ||
|
||||
action == MotionEvent.ACTION_DOWN) {
|
||||
int x = (int) event.getX();
|
||||
int y = (int) event.getY();
|
||||
|
||||
x -= widget.getTotalPaddingLeft();
|
||||
y -= widget.getTotalPaddingTop();
|
||||
|
||||
x += widget.getScrollX();
|
||||
y += widget.getScrollY();
|
||||
|
||||
Layout layout = widget.getLayout();
|
||||
int line = layout.getLineForVertical(y);
|
||||
int off = layout.getOffsetForHorizontal(line, x);
|
||||
|
||||
LongClickCopySpan longClickCopySpan[] = buffer.getSpans(off, off, LongClickCopySpan.class);
|
||||
if (longClickCopySpan.length != 0) {
|
||||
LongClickCopySpan aSingleSpan = longClickCopySpan[0];
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
Selection.setSelection(buffer, buffer.getSpanStart(aSingleSpan),
|
||||
buffer.getSpanEnd(aSingleSpan));
|
||||
aSingleSpan.setHighlighted(true,
|
||||
ContextCompat.getColor(widget.getContext(), R.color.touch_highlight));
|
||||
} else {
|
||||
Selection.removeSelection(buffer);
|
||||
aSingleSpan.setHighlighted(false, Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
this.currentSpan = aSingleSpan;
|
||||
this.widget = widget;
|
||||
return gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
} else if (action == MotionEvent.ACTION_CANCEL) {
|
||||
// Remove Selections.
|
||||
LongClickCopySpan[] spans = buffer.getSpans(Selection.getSelectionStart(buffer),
|
||||
Selection.getSelectionEnd(buffer), LongClickCopySpan.class);
|
||||
for (LongClickCopySpan aSpan : spans) {
|
||||
aSpan.setHighlighted(false, Color.TRANSPARENT);
|
||||
}
|
||||
Selection.removeSelection(buffer);
|
||||
return gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
return super.onTouchEvent(widget, buffer, event);
|
||||
}
|
||||
|
||||
public static LongClickMovementMethod getInstance(Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new LongClickMovementMethod(context.getApplicationContext());
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
@ -1,54 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.utilities.ServiceUtil;
|
||||
import org.session.libsignal.utilities.Log;
|
||||
|
||||
public class WakeLockUtil {
|
||||
|
||||
private static final String TAG = WakeLockUtil.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* @param tag will be prefixed with "signal:" if it does not already start with it.
|
||||
*/
|
||||
public static WakeLock acquire(@NonNull Context context, int lockType, long timeout, @NonNull String tag) {
|
||||
tag = prefixTag(tag);
|
||||
try {
|
||||
PowerManager powerManager = ServiceUtil.getPowerManager(context);
|
||||
WakeLock wakeLock = powerManager.newWakeLock(lockType, tag);
|
||||
|
||||
wakeLock.acquire(timeout);
|
||||
Log.d(TAG, "Acquired wakelock with tag: " + tag);
|
||||
|
||||
return wakeLock;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Failed to acquire wakelock with tag: " + tag, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tag will be prefixed with "signal:" if it does not already start with it.
|
||||
*/
|
||||
public static void release(@NonNull WakeLock wakeLock, @NonNull String tag) {
|
||||
tag = prefixTag(tag);
|
||||
try {
|
||||
if (wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
Log.d(TAG, "Released wakelock with tag: " + tag);
|
||||
} else {
|
||||
Log.d(TAG, "Wakelock wasn't held at time of release: " + tag);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Failed to release wakelock with tag: " + tag, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String prefixTag(@NonNull String tag) {
|
||||
return tag.startsWith("signal:") ? tag : "signal:" + tag;
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
enum class AudioEvent {
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application tools:node="merge">
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.notifications.FirebasePushService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class FirebaseBindingModule {
|
||||
@Binds
|
||||
abstract fun bindTokenFetcher(tokenFetcher: FirebaseTokenFetcher): TokenFetcher
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Log
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "FirebasePushNotificationService"
|
||||
@AndroidEntryPoint
|
||||
class FirebasePushService : FirebaseMessagingService() {
|
||||
|
||||
@Inject lateinit var prefs: TextSecurePreferences
|
||||
@Inject lateinit var pushReceiver: PushReceiver
|
||||
@Inject lateinit var pushRegistry: PushRegistry
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
if (token == prefs.getPushToken()) return
|
||||
|
||||
pushRegistry.register(token)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Log.d(TAG, "Received a push notification.")
|
||||
pushReceiver.onPush(message.data)
|
||||
}
|
||||
|
||||
override fun onDeletedMessages() {
|
||||
Log.d(TAG, "Called onDeletedMessages.")
|
||||
pushRegistry.refresh(true)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FirebaseTokenFetcher @Inject constructor(): TokenFetcher {
|
||||
override suspend fun fetch() = withContext(Dispatchers.IO) {
|
||||
FirebaseInstanceId.getInstance().instanceId
|
||||
.also(Tasks::await)
|
||||
.takeIf { isActive } // don't 'complete' task if we were canceled
|
||||
?.run { result?.token ?: throw exception!! }
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class NoOpPushModule {
|
||||
@Binds
|
||||
abstract fun bindTokenFetcher(tokenFetcher: NoOpTokenFetcher): TokenFetcher
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NoOpTokenFetcher @Inject constructor() : TokenFetcher {
|
||||
override suspend fun fetch(): String? = null
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit 7eb87028355bfc89950102c52d5b2927a25b2e22
|
||||
Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d
|
@ -0,0 +1,126 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import com.goterl.lazysodium.utils.Key
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* N.B. all of these variable names will be named the same as the actual JSON utf-8 request/responses expected from the server.
|
||||
* Changing the variable names will break how data is serialized/deserialized.
|
||||
* If it's less than ideally named we can use [SerialName], such as for the push metadata which uses
|
||||
* single-letter keys to be as compact as possible.
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** array of integer namespaces to subscribe to, **must be sorted in ascending order** */
|
||||
val namespaces: List<Int>,
|
||||
/** if provided and true then notifications will include the body of the message (as long as it isn't too large) */
|
||||
val data: Boolean,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
/** 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 via libsodium using this key.
|
||||
* persist it on device */
|
||||
val enc_key: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UnsubscriptionRequest(
|
||||
/** the 33-byte account being subscribed to; typically a session ID */
|
||||
val pubkey: String,
|
||||
/** when the pubkey starts with 05 (i.e. a session ID) this is the ed25519 32-byte pubkey associated with the session ID */
|
||||
val session_ed25519: String?,
|
||||
/** 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth (new closed groups) */
|
||||
val subkey_tag: String? = null,
|
||||
/** the signature unix timestamp in seconds, not ms */
|
||||
val sig_ts: Long,
|
||||
/** the 64-byte ed25519 signature */
|
||||
val signature: String,
|
||||
/** the string identifying the notification service, "firebase" for android (currently) */
|
||||
val service: String,
|
||||
/** dict of service-specific data, currently just "token" field with device-specific token but different services might have other requirements */
|
||||
val service_info: Map<String, String>,
|
||||
)
|
||||
|
||||
/** invalid values, missing reuqired arguments etc, details in message */
|
||||
private const val UNPARSEABLE_ERROR = 1
|
||||
/** the "service" value is not active / valid */
|
||||
private const val SERVICE_NOT_AVAILABLE = 2
|
||||
/** something getting wrong internally talking to the backend */
|
||||
private const val SERVICE_TIMEOUT = 3
|
||||
/** other error processing the subscription (details in the message) */
|
||||
private const val GENERIC_ERROR = 4
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val added: Boolean? = null,
|
||||
val updated: Boolean? = null,
|
||||
): Response
|
||||
|
||||
@Serializable
|
||||
data class UnsubscribeResponse(
|
||||
override val error: Int? = null,
|
||||
override val message: String? = null,
|
||||
override val success: Boolean? = null,
|
||||
val removed: Boolean? = null,
|
||||
): Response
|
||||
|
||||
interface Response {
|
||||
val error: Int?
|
||||
val message: String?
|
||||
val success: Boolean?
|
||||
fun isSuccess() = success == true && error == null
|
||||
fun isFailure() = !isSuccess()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationMetadata(
|
||||
/** Account ID (such as Session ID or closed group ID) where the message arrived **/
|
||||
@SerialName("@")
|
||||
val account: String,
|
||||
|
||||
/** The hash of the message in the swarm. */
|
||||
@SerialName("#")
|
||||
val msg_hash: String,
|
||||
|
||||
/** The swarm namespace in which this message arrived. */
|
||||
@SerialName("n")
|
||||
val namespace: Int,
|
||||
|
||||
/** The length of the message data. This is always included, even if the message content
|
||||
* itself was too large to fit into the push notification. */
|
||||
@SerialName("l")
|
||||
val data_len: Int,
|
||||
|
||||
/** This will be true if the data was omitted because it was too long to fit in a push
|
||||
* notification (around 2.5kB of raw data), in which case the push notification includes
|
||||
* only this metadata but not the message content itself. */
|
||||
@SerialName("B")
|
||||
val data_too_long : Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationServerObject(
|
||||
val enc_payload: String,
|
||||
val spns: Int,
|
||||
) {
|
||||
fun decryptPayload(key: Key): Any {
|
||||
|
||||
TODO()
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushNotificationAPI {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val server = "https://live.apns.getsession.org"
|
||||
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
private val maxRetryCount = 4
|
||||
private val tokenExpirationInterval = 12 * 60 * 60 * 1000
|
||||
|
||||
enum class ClosedGroupOperation {
|
||||
Subscribe, Unsubscribe;
|
||||
|
||||
val rawValue: String
|
||||
get() {
|
||||
return when (this) {
|
||||
Subscribe -> "subscribe_closed_group"
|
||||
Unsubscribe -> "unsubscribe_closed_group"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unregister(token: String) {
|
||||
val parameters = mapOf( "token" to token )
|
||||
val url = "$server/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, false)
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(token: String, publicKey: String, force: Boolean) {
|
||||
val oldToken = TextSecurePreferences.getFCMToken(context)
|
||||
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
|
||||
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
|
||||
val parameters = mapOf( "token" to token, "pubKey" to publicKey )
|
||||
val url = "$server/register"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code != null && code != 0) {
|
||||
TextSecurePreferences.setIsUsingFCM(context, true)
|
||||
TextSecurePreferences.setFCMToken(context, token)
|
||||
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
|
||||
} else {
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.iterator().forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun performOperation(operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) {
|
||||
if (!TextSecurePreferences.isUsingFCM(context)) { return }
|
||||
val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey )
|
||||
val url = "$server/${operation.rawValue}"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body)
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, Version.V2).map { response ->
|
||||
val code = response.info["code"] as? Int
|
||||
if (code == null || code == 0) {
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${response.info["message"] as? String ?: "null"}.")
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import nl.komponents.kovenant.Promise
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.OnionResponse
|
||||
import org.session.libsession.snode.Version
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.emptyPromise
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.sideEffect
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object PushRegistryV1 {
|
||||
private val TAG = PushRegistryV1::class.java.name
|
||||
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
private const val maxRetryCount = 4
|
||||
|
||||
private val server = Server.LEGACY
|
||||
|
||||
fun register(
|
||||
device: Device,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
token: String? = TextSecurePreferences.getPushToken(context),
|
||||
publicKey: String? = TextSecurePreferences.getLocalNumber(context),
|
||||
legacyGroupPublicKeys: Collection<String> = MessagingModuleConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
||||
): Promise<*, Exception> = when {
|
||||
isPushEnabled -> retryIfNeeded(maxRetryCount) {
|
||||
Log.d(TAG, "register() called")
|
||||
doRegister(token, publicKey, device, legacyGroupPublicKeys)
|
||||
} fail { exception ->
|
||||
Log.d(TAG, "Couldn't register for FCM due to error", exception)
|
||||
}
|
||||
|
||||
else -> emptyPromise()
|
||||
}
|
||||
|
||||
private fun doRegister(token: String?, publicKey: String?, device: Device, legacyGroupPublicKeys: Collection<String>): Promise<*, Exception> {
|
||||
Log.d(TAG, "doRegister() called")
|
||||
|
||||
token ?: return emptyPromise()
|
||||
publicKey ?: return emptyPromise()
|
||||
|
||||
val parameters = mapOf(
|
||||
"token" to token,
|
||||
"pubKey" to publicKey,
|
||||
"device" to device.value,
|
||||
"legacyGroupPublicKeys" to legacyGroupPublicKeys
|
||||
)
|
||||
|
||||
val url = "${server.url}/register_legacy_groups_only"
|
||||
val body = RequestBody.create(
|
||||
MediaType.get("application/json"),
|
||||
JsonUtil.toJson(parameters)
|
||||
)
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return sendOnionRequest(request) sideEffect { response ->
|
||||
when (response.code) {
|
||||
null, 0 -> throw Exception("error: ${response.message}.")
|
||||
}
|
||||
} success {
|
||||
Log.d(TAG, "registerV1 success")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister push notifications for 1-1 conversations as this is now done in FirebasePushManager.
|
||||
*/
|
||||
fun unregister(): Promise<*, Exception> {
|
||||
Log.d(TAG, "unregisterV1 requested")
|
||||
|
||||
val token = TextSecurePreferences.getPushToken(context) ?: emptyPromise()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val parameters = mapOf("token" to token)
|
||||
val url = "${server.url}/unregister"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
sendOnionRequest(request) success {
|
||||
when (it.code) {
|
||||
null, 0 -> Log.d(TAG, "error: ${it.message}.")
|
||||
else -> Log.d(TAG, "unregisterV1 success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy Closed Groups
|
||||
|
||||
fun subscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("subscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
fun unsubscribeGroup(
|
||||
closedGroupPublicKey: String,
|
||||
isPushEnabled: Boolean = TextSecurePreferences.isPushEnabled(context),
|
||||
publicKey: String = MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!
|
||||
) = if (isPushEnabled) {
|
||||
performGroupOperation("unsubscribe_closed_group", closedGroupPublicKey, publicKey)
|
||||
} else emptyPromise()
|
||||
|
||||
private fun performGroupOperation(
|
||||
operation: String,
|
||||
closedGroupPublicKey: String,
|
||||
publicKey: String
|
||||
): Promise<*, Exception> {
|
||||
val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey)
|
||||
val url = "${server.url}/$operation"
|
||||
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
|
||||
val request = Request.Builder().url(url).post(body).build()
|
||||
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
sendOnionRequest(request) sideEffect {
|
||||
when (it.code) {
|
||||
0, null -> throw Exception(it.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendOnionRequest(request: Request): Promise<OnionResponse, Exception> = OnionRequestAPI.sendOnionRequest(
|
||||
request,
|
||||
server.url,
|
||||
server.publicKey,
|
||||
Version.V2
|
||||
)
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
enum class Server(val url: String, val publicKey: String) {
|
||||
LATEST("https://push.getsession.org", "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"),
|
||||
LEGACY("https://live.apns.getsession.org", "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049")
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
enum class Device(val value: String, val service: String = value) {
|
||||
ANDROID("android", "firebase"),
|
||||
HUAWEI("huawei");
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
package org.session.libsession.utilities.bencode
|
||||
|
||||
import java.util.LinkedList
|
||||
|
||||
object Bencode {
|
||||
class Decoder(source: ByteArray) {
|
||||
|
||||
private val iterator = LinkedList<Byte>().apply {
|
||||
addAll(source.asIterable())
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an element based on next marker assumed to be string/int/list/dict or return null
|
||||
*/
|
||||
fun decode(): BencodeElement? {
|
||||
val result = when (iterator.peek()?.toInt()?.toChar()) {
|
||||
in NUMBERS -> decodeString()
|
||||
INT_INDICATOR -> decodeInt()
|
||||
LIST_INDICATOR -> decodeList()
|
||||
DICT_INDICATOR -> decodeDict()
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a string element from iterator assumed to have structure `{length}:{data}`
|
||||
*/
|
||||
private fun decodeString(): BencodeString? {
|
||||
val lengthStrings = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != SEPARATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `:`
|
||||
val length = lengthStrings.toIntOrNull(10) ?: return null
|
||||
val remaining = (0 until length).map { iterator.pop() }.toByteArray()
|
||||
return BencodeString(remaining)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an int element from iterator assumed to have structure `i{int}e`
|
||||
*/
|
||||
private fun decodeInt(): BencodeElement? {
|
||||
iterator.pop() // drop `i`
|
||||
val intString = buildString {
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
append(iterator.pop().toInt().toChar())
|
||||
}
|
||||
}
|
||||
val asInt = intString.toIntOrNull(10) ?: return null
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeInteger(asInt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a list element from iterator assumed to have structure `l{data}e`
|
||||
*/
|
||||
private fun decodeList(): BencodeElement {
|
||||
iterator.pop() // drop `l`
|
||||
val listElements = mutableListOf<BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
decode()?.let { nextElement ->
|
||||
listElements += nextElement
|
||||
}
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeList(listElements)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a dict element from iterator assumed to have structure `d{data}e`
|
||||
*/
|
||||
private fun decodeDict(): BencodeElement? {
|
||||
iterator.pop() // drop `d`
|
||||
val dictElements = mutableMapOf<String,BencodeElement>()
|
||||
while (iterator.isNotEmpty() && iterator.peek()?.toInt()?.toChar() != END_INDICATOR) {
|
||||
val key = decodeString() ?: return null
|
||||
val value = decode() ?: return null
|
||||
dictElements += key.value.decodeToString() to value
|
||||
}
|
||||
iterator.pop() // drop `e`
|
||||
return BencodeDict(dictElements)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NUMBERS = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
|
||||
private const val INT_INDICATOR = 'i'
|
||||
private const val LIST_INDICATOR = 'l'
|
||||
private const val DICT_INDICATOR = 'd'
|
||||
private const val END_INDICATOR = 'e'
|
||||
private const val SEPARATOR = ':'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class BencodeElement {
|
||||
abstract fun encode(): ByteArray
|
||||
}
|
||||
|
||||
fun String.bencode() = BencodeString(this.encodeToByteArray())
|
||||
fun Int.bencode() = BencodeInteger(this)
|
||||
|
||||
data class BencodeString(val value: ByteArray): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append(value.size.toString())
|
||||
append(':')
|
||||
}.toByteArray() + value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeString
|
||||
|
||||
if (!value.contentEquals(other.value)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return value.contentHashCode()
|
||||
}
|
||||
}
|
||||
data class BencodeInteger(val value: Int): BencodeElement() {
|
||||
override fun encode(): ByteArray = buildString {
|
||||
append('i')
|
||||
append(value.toString())
|
||||
append('e')
|
||||
}.toByteArray()
|
||||
}
|
||||
data class BencodeList(val values: List<BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: BencodeElement) : this(values.toList())
|
||||
|
||||
override fun encode(): ByteArray = "l".toByteArray() +
|
||||
values.fold(byteArrayOf()) { array, element -> array + element.encode() } +
|
||||
"e".toByteArray()
|
||||
}
|
||||
data class BencodeDict(val values: Map<String, BencodeElement>): BencodeElement() {
|
||||
|
||||
constructor(vararg values: Pair<String, BencodeElement>) : this(values.toMap())
|
||||
|
||||
override fun encode(): ByteArray = "d".toByteArray() +
|
||||
values.entries.fold(byteArrayOf()) { array, (key, value) ->
|
||||
array + key.bencode().encode() + value.encode()
|
||||
} + "e".toByteArray()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as BencodeDict
|
||||
|
||||
if (values != other.values) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return values.hashCode()
|
||||
}
|
||||
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue