commit
6b146c762e
@ -1,17 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationListItem extends Unbindable {
|
||||
|
||||
public void bind(@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ClearProfileAvatarActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.ClearProfileActivity_remove_profile_photo)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
|
||||
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("delete", true);
|
||||
setResult(Activity.RESULT_OK, result);
|
||||
finish();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class LinkPreviewsIntroFragment extends Fragment {
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public static LinkPreviewsIntroFragment newInstance() {
|
||||
LinkPreviewsIntroFragment fragment = new LinkPreviewsIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public LinkPreviewsIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.experience_upgrade_link_previews_fragment, container, false);
|
||||
|
||||
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
|
||||
controller.onLinkPreviewsFinished();
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onLinkPreviewsFinished();
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitLogFragment;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Activity for submitting logcat logs to a pastebin service.
|
||||
*/
|
||||
public class LogSubmitActivity extends BaseActionBarActivity implements SubmitLogFragment.OnLogSubmittedListener {
|
||||
|
||||
private static final String TAG = LogSubmitActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
setContentView(R.layout.log_submit_activity);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
SubmitLogFragment fragment = SubmitLogFragment.newInstance();
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
transaction.replace(R.id.fragment_container, fragment);
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__thanks, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__log_fetch_failed, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startActivity(Intent intent) {
|
||||
try {
|
||||
super.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(this, R.string.log_submit_activity__no_browser_installed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
public class TextSecureExpiredException extends Exception {
|
||||
public TextSecureExpiredException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class TypingIndicatorIntroFragment extends Fragment {
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public static TypingIndicatorIntroFragment newInstance() {
|
||||
TypingIndicatorIntroFragment fragment = new TypingIndicatorIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public TypingIndicatorIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.experience_upgrade_typing_indicators_fragment, container, false);
|
||||
View yesButton = view.findViewById(R.id.experience_yes_button);
|
||||
View noButton = view.findViewById(R.id.experience_no_button);
|
||||
|
||||
((TypingIndicatorView) view.findViewById(R.id.typing_indicator)).startAnimation();
|
||||
|
||||
yesButton.setOnClickListener(v -> onButtonClicked(true));
|
||||
noButton.setOnClickListener(v -> onButtonClicked(false));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void onButtonClicked(boolean typingEnabled) {
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(getContext(), typingEnabled);
|
||||
|
||||
controller.onTypingIndicatorsFinished();
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onTypingIndicatorsFinished();
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import org.session.libsession.utilities.ViewUtil;
|
||||
|
||||
public class ThreadPhotoRailView extends FrameLayout {
|
||||
|
||||
@NonNull private final RecyclerView recyclerView;
|
||||
@Nullable private OnItemClickedListener listener;
|
||||
|
||||
public ThreadPhotoRailView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ThreadPhotoRailView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ThreadPhotoRailView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
inflate(context, R.layout.recipient_preference_photo_rail, this);
|
||||
|
||||
this.recyclerView = ViewUtil.findById(this, R.id.photo_list);
|
||||
this.recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
|
||||
this.recyclerView.setItemAnimator(new DefaultItemAnimator());
|
||||
this.recyclerView.setNestedScrollingEnabled(false);
|
||||
}
|
||||
|
||||
public void setListener(@Nullable OnItemClickedListener listener) {
|
||||
this.listener = listener;
|
||||
|
||||
if (this.recyclerView.getAdapter() != null) {
|
||||
((ThreadPhotoRailAdapter)this.recyclerView.getAdapter()).setListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursor(@NonNull GlideRequests glideRequests, @Nullable Cursor cursor) {
|
||||
this.recyclerView.setAdapter(new ThreadPhotoRailAdapter(getContext(), glideRequests, cursor, this.listener));
|
||||
}
|
||||
|
||||
private static class ThreadPhotoRailAdapter extends CursorRecyclerViewAdapter<ThreadPhotoRailAdapter.ThreadPhotoViewHolder> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ThreadPhotoRailAdapter.class.getSimpleName();
|
||||
|
||||
@NonNull private final GlideRequests glideRequests;
|
||||
|
||||
@Nullable private OnItemClickedListener clickedListener;
|
||||
|
||||
private ThreadPhotoRailAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable Cursor cursor,
|
||||
@Nullable OnItemClickedListener listener)
|
||||
{
|
||||
super(context, cursor);
|
||||
this.glideRequests = glideRequests;
|
||||
this.clickedListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThreadPhotoViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
View itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.recipient_preference_photo_rail_item, parent, false);
|
||||
|
||||
return new ThreadPhotoViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ThreadPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
ThumbnailView imageView = viewHolder.imageView;
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
||||
Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment());
|
||||
|
||||
if (slide != null) {
|
||||
imageView.setImageResource(glideRequests, slide, false, false);
|
||||
}
|
||||
|
||||
imageView.setOnClickListener(v -> {
|
||||
if (clickedListener != null) clickedListener.onItemClicked(mediaRecord);
|
||||
});
|
||||
}
|
||||
|
||||
public void setListener(@Nullable OnItemClickedListener listener) {
|
||||
this.clickedListener = listener;
|
||||
}
|
||||
|
||||
static class ThreadPhotoViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
ThumbnailView imageView;
|
||||
|
||||
ThreadPhotoViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.imageView = ViewUtil.findById(itemView, R.id.thumbnail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnItemClickedListener {
|
||||
void onItemClicked(MediaDatabase.MediaRecord mediaRecord);
|
||||
}
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
/**
|
||||
* Class for creating simple tooltips to show throughout the app. Utilizes a popup window so you
|
||||
* don't have to worry about view hierarchies or anything.
|
||||
*/
|
||||
public class TooltipPopup extends PopupWindow {
|
||||
|
||||
public static final int POSITION_ABOVE = 0;
|
||||
public static final int POSITION_BELOW = 1;
|
||||
public static final int POSITION_START = 2;
|
||||
public static final int POSITION_END = 3;
|
||||
|
||||
private static final int POSITION_LEFT = 4;
|
||||
private static final int POSITION_RIGHT = 5;
|
||||
|
||||
private final View anchor;
|
||||
private final ImageView arrow;
|
||||
private final int position;
|
||||
|
||||
public static Builder forTarget(@NonNull View anchor) {
|
||||
return new Builder(anchor);
|
||||
}
|
||||
|
||||
private TooltipPopup(@NonNull View anchor,
|
||||
int rawPosition,
|
||||
@NonNull String text,
|
||||
@ColorInt int backgroundTint,
|
||||
@ColorInt int textColor,
|
||||
@Nullable Object iconGlideModel,
|
||||
@Nullable OnDismissListener dismissListener)
|
||||
{
|
||||
super(LayoutInflater.from(anchor.getContext()).inflate(R.layout.tooltip, null),
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
this.anchor = anchor;
|
||||
this.position = getRtlPosition(anchor.getContext(), rawPosition);
|
||||
|
||||
switch (rawPosition) {
|
||||
case POSITION_ABOVE: arrow = getContentView().findViewById(R.id.tooltip_arrow_bottom); break;
|
||||
case POSITION_BELOW: arrow = getContentView().findViewById(R.id.tooltip_arrow_top); break;
|
||||
case POSITION_START: arrow = getContentView().findViewById(R.id.tooltip_arrow_end); break;
|
||||
case POSITION_END: arrow = getContentView().findViewById(R.id.tooltip_arrow_start); break;
|
||||
default: throw new AssertionError("Invalid position!");
|
||||
}
|
||||
|
||||
arrow.setVisibility(View.VISIBLE);
|
||||
|
||||
TextView textView = getContentView().findViewById(R.id.tooltip_text);
|
||||
textView.setText(text);
|
||||
|
||||
if (textColor != 0) {
|
||||
textView.setTextColor(textColor);
|
||||
}
|
||||
|
||||
View bubble = getContentView().findViewById(R.id.tooltip_bubble);
|
||||
|
||||
if (backgroundTint == 0) {
|
||||
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(anchor.getContext(), R.attr.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
|
||||
arrow.setColorFilter(ThemeUtil.getThemedColor(anchor.getContext(), R.attr.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
bubble.getBackground().setColorFilter(backgroundTint, PorterDuff.Mode.MULTIPLY);
|
||||
arrow.setColorFilter(backgroundTint, PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
|
||||
if (iconGlideModel != null) {
|
||||
ImageView iconView = getContentView().findViewById(R.id.tooltip_icon);
|
||||
iconView.setVisibility(View.VISIBLE);
|
||||
GlideApp.with(anchor.getContext()).load(iconGlideModel).into(iconView);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
setElevation(10);
|
||||
}
|
||||
|
||||
getContentView().setOnClickListener(v -> dismiss());
|
||||
|
||||
setOnDismissListener(dismissListener);
|
||||
setBackgroundDrawable(null);
|
||||
setOutsideTouchable(true);
|
||||
}
|
||||
|
||||
private void show() {
|
||||
if (anchor.getWidth() == 0 && anchor.getHeight() == 0) {
|
||||
anchor.post(this::show);
|
||||
return;
|
||||
}
|
||||
|
||||
getContentView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
|
||||
int tooltipSpacing = anchor.getContext().getResources().getDimensionPixelOffset(R.dimen.tooltip_popup_margin);
|
||||
|
||||
int xoffset;
|
||||
int yoffset;
|
||||
|
||||
switch (position) {
|
||||
case POSITION_ABOVE:
|
||||
xoffset = 0;
|
||||
yoffset = -(2 * anchor.getWidth() + tooltipSpacing);
|
||||
onLayout(() -> setArrowHorizontalPosition(arrow, anchor));
|
||||
break;
|
||||
case POSITION_BELOW:
|
||||
xoffset = 0;
|
||||
yoffset = tooltipSpacing;
|
||||
onLayout(() -> setArrowHorizontalPosition(arrow, anchor));
|
||||
break;
|
||||
case POSITION_LEFT:
|
||||
xoffset = -getContentView().getMeasuredWidth() - tooltipSpacing;
|
||||
yoffset = -(getContentView().getMeasuredHeight()/2 + anchor.getHeight()/2);
|
||||
break;
|
||||
case POSITION_RIGHT:
|
||||
xoffset = anchor.getWidth() + tooltipSpacing;
|
||||
yoffset = -(getContentView().getMeasuredHeight()/2 + anchor.getHeight()/2);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Invalid tooltip position!");
|
||||
}
|
||||
|
||||
showAsDropDown(anchor, xoffset, yoffset);
|
||||
}
|
||||
|
||||
private void onLayout(@NonNull Runnable runnable) {
|
||||
getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
getContentView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void setArrowHorizontalPosition(@NonNull View arrow, @NonNull View anchor) {
|
||||
int arrowCenterX = getAbsolutePosition(arrow)[0] + arrow.getWidth()/2;
|
||||
int anchorCenterX = getAbsolutePosition(anchor)[0] + anchor.getWidth()/2;
|
||||
|
||||
arrow.setX(anchorCenterX - arrowCenterX);
|
||||
}
|
||||
|
||||
private static int[] getAbsolutePosition(@NonNull View view) {
|
||||
int[] position = new int[2];
|
||||
view.getLocationOnScreen(position);
|
||||
return position;
|
||||
}
|
||||
|
||||
private static int getRtlPosition(@NonNull Context context, int position) {
|
||||
if (position == POSITION_ABOVE || position == POSITION_BELOW) {
|
||||
return position;
|
||||
} else if (context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
|
||||
return position == POSITION_START ? POSITION_RIGHT : POSITION_LEFT;
|
||||
} else {
|
||||
return position == POSITION_START ? POSITION_LEFT : POSITION_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final View anchor;
|
||||
|
||||
private int backgroundTint;
|
||||
private int textColor;
|
||||
private int textResId;
|
||||
private Object iconGlideModel;
|
||||
private OnDismissListener dismissListener;
|
||||
|
||||
private Builder(@NonNull View anchor) {
|
||||
this.anchor = anchor;
|
||||
}
|
||||
|
||||
public Builder setBackgroundTint(int color) {
|
||||
this.backgroundTint = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTextColor(int color) {
|
||||
this.textColor = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setText(@StringRes int stringResId) {
|
||||
this.textResId = stringResId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIconGlideModel(Object model) {
|
||||
this.iconGlideModel = model;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setOnDismissListener(OnDismissListener dismissListener) {
|
||||
this.dismissListener = dismissListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TooltipPopup show(int position) {
|
||||
String text = anchor.getContext().getString(textResId);
|
||||
TooltipPopup tooltip = new TooltipPopup(anchor, position, text, backgroundTint, textColor, iconGlideModel, dismissListener);
|
||||
|
||||
tooltip.show();
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.location;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.session.libsignal.utilities.concurrent.ListenableFuture;
|
||||
import org.session.libsignal.utilities.concurrent.SettableFuture;
|
||||
|
||||
import org.session.libsession.utilities.ViewUtil;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class SignalMapView extends LinearLayout {
|
||||
|
||||
private ImageView imageView;
|
||||
private TextView textView;
|
||||
|
||||
public SignalMapView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SignalMapView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
public SignalMapView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
private void initialize(Context context) {
|
||||
setOrientation(LinearLayout.VERTICAL);
|
||||
LayoutInflater.from(context).inflate(R.layout.signal_map_view, this, true);
|
||||
|
||||
this.imageView = ViewUtil.findById(this, R.id.image_view);
|
||||
this.textView = ViewUtil.findById(this, R.id.address_view);
|
||||
}
|
||||
|
||||
public ListenableFuture<Bitmap> display(final SignalPlace place) {
|
||||
final SettableFuture<Bitmap> future = new SettableFuture<>();
|
||||
|
||||
this.imageView.setVisibility(View.GONE);
|
||||
|
||||
this.textView.setText(place.getDescription());
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.location;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.session.libsignal.utilities.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SignalPlace {
|
||||
|
||||
/* Loki - Temporary Placeholders */
|
||||
class LatLng {
|
||||
double latitude;
|
||||
double longitude;
|
||||
LatLng(double latitude, double longitude) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
}
|
||||
|
||||
class Place {
|
||||
public CharSequence getName() { return ""; }
|
||||
public CharSequence getAddress() { return ""; }
|
||||
LatLng getLatLng() { return new LatLng(0, 0); }
|
||||
}
|
||||
|
||||
private static final String URL = "https://maps.google.com/maps";
|
||||
private static final String TAG = SignalPlace.class.getSimpleName();
|
||||
|
||||
@JsonProperty
|
||||
private CharSequence name;
|
||||
|
||||
@JsonProperty
|
||||
private CharSequence address;
|
||||
|
||||
@JsonProperty
|
||||
private double latitude;
|
||||
|
||||
@JsonProperty
|
||||
private double longitude;
|
||||
|
||||
public SignalPlace(Place place) {
|
||||
this.name = place.getName();
|
||||
this.address = place.getAddress();
|
||||
this.latitude = place.getLatLng().latitude;
|
||||
this.longitude = place.getLatLng().longitude;
|
||||
}
|
||||
|
||||
public SignalPlace() {}
|
||||
|
||||
@JsonIgnore
|
||||
public LatLng getLatLong() {
|
||||
return new LatLng(latitude, longitude);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getDescription() {
|
||||
String description = "";
|
||||
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
description += (name + "\n");
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(address)) {
|
||||
description += (address + "\n");
|
||||
}
|
||||
|
||||
description += Uri.parse(URL)
|
||||
.buildUpon()
|
||||
.appendQueryParameter("q", String.format("%s,%s", latitude, longitude))
|
||||
.build().toString();
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
public @Nullable String serialize() {
|
||||
try {
|
||||
return JsonUtil.toJsonThrows(this);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static SignalPlace deserialize(@NonNull String serialized) throws IOException {
|
||||
return JsonUtil.fromJson(serialized, SignalPlace.class);
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package org.thoughtcrime.securesms.contactshare;
|
||||
|
||||
public interface Selectable {
|
||||
void setSelected(boolean selected);
|
||||
boolean isSelected();
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ConversationStickerSuggestionAdapter extends RecyclerView.Adapter<ConversationStickerSuggestionAdapter.StickerSuggestionViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<StickerRecord> stickers;
|
||||
|
||||
public ConversationStickerSuggestionAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.stickers = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull StickerSuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new StickerSuggestionViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.sticker_suggestion_list_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull StickerSuggestionViewHolder viewHolder, int i) {
|
||||
viewHolder.bind(glideRequests, eventListener, stickers.get(i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull StickerSuggestionViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return stickers.size();
|
||||
}
|
||||
|
||||
public void setStickers(@NonNull List<StickerRecord> stickers) {
|
||||
this.stickers.clear();
|
||||
this.stickers.addAll(stickers);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class StickerSuggestionViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView image;
|
||||
|
||||
StickerSuggestionViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.image = itemView.findViewById(R.id.sticker_suggestion_item_image);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, @NonNull StickerRecord sticker) {
|
||||
glideRequests.load(new DecryptableUri(sticker.getUri()))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(image);
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
eventListener.onStickerSuggestionClicked(sticker);
|
||||
});
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onStickerSuggestionClicked(@NonNull StickerRecord sticker);
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.database.CursorList;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.util.CloseableLiveData;
|
||||
import org.session.libsession.utilities.Throttler;
|
||||
|
||||
class ConversationStickerViewModel extends ViewModel {
|
||||
|
||||
private static final int SEARCH_LIMIT = 10;
|
||||
|
||||
private final Application application;
|
||||
private final StickerSearchRepository repository;
|
||||
private final CloseableLiveData<CursorList<StickerRecord>> stickers;
|
||||
private final MutableLiveData<Boolean> stickersAvailable;
|
||||
private final Throttler availabilityThrottler;
|
||||
private final ContentObserver packObserver;
|
||||
|
||||
private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
this.stickers = new CloseableLiveData<>();
|
||||
this.stickersAvailable = new MutableLiveData<>();
|
||||
this.availabilityThrottler = new Throttler(500);
|
||||
this.packObserver = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));
|
||||
}
|
||||
};
|
||||
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
|
||||
}
|
||||
|
||||
@NonNull LiveData<CursorList<StickerRecord>> getStickerResults() {
|
||||
return stickers;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getStickersAvailability() {
|
||||
repository.getStickerFeatureAvailability(stickersAvailable::postValue);
|
||||
return stickersAvailable;
|
||||
}
|
||||
|
||||
void onInputTextUpdated(@NonNull String text) {
|
||||
if (TextUtils.isEmpty(text) || text.length() > SEARCH_LIMIT) {
|
||||
stickers.setValue(CursorList.emptyList());
|
||||
} else {
|
||||
repository.searchByEmoji(text, stickers::postValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
stickers.close();
|
||||
application.getContentResolver().unregisterContentObserver(packObserver);
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
private final Application application;
|
||||
private final StickerSearchRepository repository;
|
||||
|
||||
public Factory(@NonNull Application application, @NonNull StickerSearchRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationStickerViewModel(application, repository));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
* are generated. These are:
|
||||
*
|
||||
* 1) A 128bit symmetric encryption key.
|
||||
* 2) A 160bit symmetric MAC key.
|
||||
* 3) An ECC keypair.
|
||||
*
|
||||
* The first two, along with the ECC keypair's private key, are
|
||||
* then encrypted on disk using PBE.
|
||||
*
|
||||
* This class represents the ECC keypair.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class AsymmetricMasterSecret {
|
||||
|
||||
private final ECPublicKey djbPublicKey;
|
||||
private final ECPrivateKey djbPrivateKey;
|
||||
|
||||
|
||||
public AsymmetricMasterSecret(ECPublicKey djbPublicKey, ECPrivateKey djbPrivateKey)
|
||||
{
|
||||
this.djbPublicKey = djbPublicKey;
|
||||
this.djbPrivateKey = djbPrivateKey;
|
||||
}
|
||||
|
||||
public ECPublicKey getDjbPublicKey() {
|
||||
return djbPublicKey;
|
||||
}
|
||||
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return djbPrivateKey;
|
||||
}
|
||||
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
public class InvalidPassphraseException extends Exception {
|
||||
|
||||
public InvalidPassphraseException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidPassphraseException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidPassphraseException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidPassphraseException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.utilities.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Class that handles encryption for local storage.
|
||||
*
|
||||
* The protocol format is roughly:
|
||||
*
|
||||
* 1) 16 byte random IV.
|
||||
* 2) AES-CBC(plaintext)
|
||||
* 3) HMAC-SHA1 of 1 and 2
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterCipher {
|
||||
|
||||
private static final String TAG = MasterCipher.class.getSimpleName();
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final Cipher encryptingCipher;
|
||||
private final Cipher decryptingCipher;
|
||||
private final Mac hmac;
|
||||
|
||||
public MasterCipher(MasterSecret masterSecret) {
|
||||
try {
|
||||
this.masterSecret = masterSecret;
|
||||
this.encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.hmac = Mac.getInstance("HmacSHA1");
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException nspe) {
|
||||
throw new AssertionError(nspe);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptKey(ECPrivateKey privateKey) {
|
||||
return encryptBytes(privateKey.serialize());
|
||||
}
|
||||
|
||||
public String encryptBody(@NonNull String body) {
|
||||
return encryptAndEncodeBytes(body.getBytes());
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws InvalidMessageException {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKey decryptKey(byte[] key)
|
||||
throws org.session.libsignal.libsignal.InvalidKeyException
|
||||
{
|
||||
try {
|
||||
return Curve.decodePrivatePoint(decryptBytes(key));
|
||||
} catch (InvalidMessageException ime) {
|
||||
throw new org.session.libsignal.libsignal.InvalidKeyException(ime);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(@NonNull byte[] decodedBody) throws InvalidMessageException {
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
byte[] encryptedBody = verifyMacBody(mac, decodedBody);
|
||||
|
||||
Cipher cipher = getDecryptingCipher(masterSecret.getEncryptionKey(), encryptedBody);
|
||||
byte[] encrypted = getDecryptedBody(cipher, encryptedBody);
|
||||
|
||||
return encrypted;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
throw new InvalidMessageException(ge);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptBytes(byte[] body) {
|
||||
try {
|
||||
Cipher cipher = getEncryptingCipher(masterSecret.getEncryptionKey());
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
|
||||
byte[] encryptedBody = getEncryptedBody(cipher, body);
|
||||
byte[] encryptedAndMacBody = getMacBody(mac, encryptedBody);
|
||||
|
||||
return encryptedAndMacBody;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
Log.w("bodycipher", ge);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean verifyMacFor(String content, byte[] theirMac) {
|
||||
byte[] ourMac = getMacFor(content);
|
||||
Log.i(TAG, "Our Mac: " + Hex.toString(ourMac));
|
||||
Log.i(TAG, "Thr Mac: " + Hex.toString(theirMac));
|
||||
return Arrays.equals(ourMac, theirMac);
|
||||
}
|
||||
|
||||
public byte[] getMacFor(String content) {
|
||||
Log.w(TAG, "Macing: " + content);
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
return mac.doFinal(content.getBytes());
|
||||
} catch (GeneralSecurityException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeAndDecryptBytes(String body) throws InvalidMessageException {
|
||||
try {
|
||||
byte[] decodedBody = Base64.decode(body);
|
||||
return decryptBytes(decodedBody);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException("Bad Base64 Encoding...", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encryptAndEncodeBytes(@NonNull byte[] bytes) {
|
||||
byte[] encryptedAndMacBody = encryptBytes(bytes);
|
||||
return Base64.encodeBytes(encryptedAndMacBody);
|
||||
}
|
||||
|
||||
private byte[] verifyMacBody(@NonNull Mac hmac, @NonNull byte[] encryptedAndMac) throws InvalidMessageException {
|
||||
if (encryptedAndMac.length < hmac.getMacLength()) {
|
||||
throw new InvalidMessageException("length(encrypted body + MAC) < length(MAC)");
|
||||
}
|
||||
|
||||
byte[] encrypted = new byte[encryptedAndMac.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, 0, encrypted, 0, encrypted.length);
|
||||
|
||||
byte[] remoteMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, encryptedAndMac.length - remoteMac.length, remoteMac, 0, remoteMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encrypted);
|
||||
|
||||
if (!Arrays.equals(remoteMac, localMac))
|
||||
throw new InvalidMessageException("MAC doesen't match.");
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
private byte[] getDecryptedBody(Cipher cipher, byte[] encryptedBody) throws IllegalBlockSizeException, BadPaddingException {
|
||||
return cipher.doFinal(encryptedBody, cipher.getBlockSize(), encryptedBody.length - cipher.getBlockSize());
|
||||
}
|
||||
|
||||
private byte[] getEncryptedBody(Cipher cipher, byte[] body) throws IllegalBlockSizeException, BadPaddingException {
|
||||
byte[] encrypted = cipher.doFinal(body);
|
||||
byte[] iv = cipher.getIV();
|
||||
|
||||
byte[] ivAndBody = new byte[iv.length + encrypted.length];
|
||||
System.arraycopy(iv, 0, ivAndBody, 0, iv.length);
|
||||
System.arraycopy(encrypted, 0, ivAndBody, iv.length, encrypted.length);
|
||||
|
||||
return ivAndBody;
|
||||
}
|
||||
|
||||
private Mac getMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(key);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private byte[] getMacBody(Mac hmac, byte[] encryptedBody) {
|
||||
byte[] mac = hmac.doFinal(encryptedBody);
|
||||
byte[] encryptedAndMac = new byte[encryptedBody.length + mac.length];
|
||||
|
||||
System.arraycopy(encryptedBody, 0, encryptedAndMac, 0, encryptedBody.length);
|
||||
System.arraycopy(mac, 0, encryptedAndMac, encryptedBody.length, mac.length);
|
||||
|
||||
return encryptedAndMac;
|
||||
}
|
||||
|
||||
private Cipher getDecryptingCipher(SecretKeySpec key, byte[] encryptedBody) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = new IvParameterSpec(encryptedBody, 0, decryptingCipher.getBlockSize());
|
||||
decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
return decryptingCipher;
|
||||
}
|
||||
|
||||
private Cipher getEncryptingCipher(SecretKeySpec key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
encryptingCipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
return encryptingCipher;
|
||||
}
|
||||
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
* are generated. These are:
|
||||
*
|
||||
* 1) A 128bit symmetric encryption key.
|
||||
* 2) A 160bit symmetric MAC key.
|
||||
* 3) An ECC keypair.
|
||||
*
|
||||
* The first two, along with the ECC keypair's private key, are
|
||||
* then encrypted on disk using PBE.
|
||||
*
|
||||
* This class represents 1 and 2.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecret implements Parcelable {
|
||||
|
||||
private final SecretKeySpec encryptionKey;
|
||||
private final SecretKeySpec macKey;
|
||||
|
||||
public static final Parcelable.Creator<MasterSecret> CREATOR = new Parcelable.Creator<MasterSecret>() {
|
||||
@Override
|
||||
public MasterSecret createFromParcel(Parcel in) {
|
||||
return new MasterSecret(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MasterSecret[] newArray(int size) {
|
||||
return new MasterSecret[size];
|
||||
}
|
||||
};
|
||||
|
||||
public MasterSecret(SecretKeySpec encryptionKey, SecretKeySpec macKey) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
private MasterSecret(Parcel in) {
|
||||
byte[] encryptionKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(encryptionKeyBytes);
|
||||
|
||||
byte[] macKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(macKeyBytes);
|
||||
|
||||
this.encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
|
||||
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
|
||||
// SecretKeySpec does an internal copy in its constructor.
|
||||
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
|
||||
Arrays.fill(macKeyBytes, (byte)0x00);
|
||||
}
|
||||
|
||||
|
||||
public SecretKeySpec getEncryptionKey() {
|
||||
return this.encryptionKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return this.macKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(encryptionKey.getEncoded().length);
|
||||
out.writeByteArray(encryptionKey.getEncoded());
|
||||
out.writeInt(macKey.getEncoded().length);
|
||||
out.writeByteArray(macKey.getEncoded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public MasterSecret parcelClone() {
|
||||
Parcel thisParcel = Parcel.obtain();
|
||||
Parcel thatParcel = Parcel.obtain();
|
||||
byte[] bytes = null;
|
||||
|
||||
thisParcel.writeValue(this);
|
||||
bytes = thisParcel.marshall();
|
||||
|
||||
thatParcel.unmarshall(bytes, 0, bytes.length);
|
||||
thatParcel.setDataPosition(0);
|
||||
|
||||
MasterSecret that = (MasterSecret)thatParcel.readValue(MasterSecret.class.getClassLoader());
|
||||
|
||||
thisParcel.recycle();
|
||||
thatParcel.recycle();
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
}
|
@ -1,375 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.PBEParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Helper class for generating and securely storing a MasterSecret.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecretUtil {
|
||||
|
||||
public static final String UNENCRYPTED_PASSPHRASE = "unencrypted";
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
private static final String ASYMMETRIC_LOCAL_PUBLIC_DJB = "asymmetric_master_secret_curve25519_public";
|
||||
private static final String ASYMMETRIC_LOCAL_PRIVATE_DJB = "asymmetric_master_secret_curve25519_private";
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
MasterSecret masterSecret,
|
||||
String newPassphrase)
|
||||
{
|
||||
try {
|
||||
byte[] combinedSecrets = Util.combine(masterSecret.getEncryptionKey().getEncoded(),
|
||||
masterSecret.getMacKey().getEncoded());
|
||||
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(newPassphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, combinedSecrets, newPassphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, newPassphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return masterSecret;
|
||||
} catch (GeneralSecurityException gse) {
|
||||
throw new AssertionError(gse);
|
||||
}
|
||||
}
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
String originalPassphrase,
|
||||
String newPassphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
MasterSecret masterSecret = getMasterSecret(context, originalPassphrase);
|
||||
changeMasterSecretPassphrase(context, masterSecret, newPassphrase);
|
||||
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
public static MasterSecret getMasterSecret(Context context, String passphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
try {
|
||||
byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret");
|
||||
byte[] macSalt = retrieve(context, "mac_salt");
|
||||
int iterations = retrieve(context, "passphrase_iterations", 100);
|
||||
byte[] encryptedMasterSecret = verifyMac(macSalt, iterations, encryptedAndMacdMasterSecret, passphrase);
|
||||
byte[] encryptionSalt = retrieve(context, "encryption_salt");
|
||||
byte[] combinedSecrets = decryptWithPassphrase(encryptionSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
byte[] encryptionSecret = Util.split(combinedSecrets, 16, 20)[0];
|
||||
byte[] macSecret = Util.split(combinedSecrets, 16, 20)[1];
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
} catch (IOException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret getAsymmetricMasterSecret(@NonNull Context context,
|
||||
@Nullable MasterSecret masterSecret)
|
||||
{
|
||||
try {
|
||||
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
byte[] djbPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_DJB);
|
||||
|
||||
ECPublicKey djbPublicKey = null;
|
||||
ECPrivateKey djbPrivateKey = null;
|
||||
|
||||
if (djbPublicBytes != null) {
|
||||
djbPublicKey = Curve.decodePoint(djbPublicBytes, 0);
|
||||
}
|
||||
|
||||
if (masterSecret != null) {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
|
||||
if (djbPrivateBytes != null) {
|
||||
djbPrivateKey = masterCipher.decryptKey(djbPrivateBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return new AsymmetricMasterSecret(djbPublicKey, djbPrivateKey);
|
||||
} catch (InvalidKeyException | IOException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret generateAsymmetricMasterSecret(Context context,
|
||||
MasterSecret masterSecret)
|
||||
{
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
|
||||
save(context, ASYMMETRIC_LOCAL_PUBLIC_DJB, keyPair.getPublicKey().serialize());
|
||||
save(context, ASYMMETRIC_LOCAL_PRIVATE_DJB, masterCipher.encryptKey(keyPair.getPrivateKey()));
|
||||
|
||||
return new AsymmetricMasterSecret(keyPair.getPublicKey(), keyPair.getPrivateKey());
|
||||
}
|
||||
|
||||
public static MasterSecret generateMasterSecret(Context context, String passphrase) {
|
||||
try {
|
||||
byte[] encryptionSecret = generateEncryptionSecret();
|
||||
byte[] macSecret = generateMacSecret();
|
||||
byte[] masterSecret = Util.combine(encryptionSecret, macSecret);
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(passphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, masterSecret, passphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasAsymmericMasterSecret(Context context) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.contains(ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
}
|
||||
|
||||
public static boolean isPassphraseInitialized(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return preferences.getBoolean("passphrase_initialized", false);
|
||||
}
|
||||
|
||||
public static void clear(Context context) {
|
||||
context.getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, int value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putInt(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, byte[] value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putString(key, Base64.encodeBytes(value))
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, boolean value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putBoolean(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] retrieve(Context context, String key) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
String encodedValue = settings.getString(key, "");
|
||||
|
||||
if (TextUtils.isEmpty(encodedValue)) return null;
|
||||
else return Base64.decode(encodedValue);
|
||||
}
|
||||
|
||||
private static int retrieve(Context context, String key, int defaultValue) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.getInt(key, defaultValue);
|
||||
}
|
||||
|
||||
private static byte[] generateEncryptionSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(128);
|
||||
|
||||
SecretKey key = generator.generateKey();
|
||||
return key.getEncoded();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
Log.w("keyutil", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateMacSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1");
|
||||
return generator.generateKey().getEncoded();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[16];
|
||||
random.nextBytes(salt);
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
private static int generateIterationCount(String passphrase, byte[] salt) {
|
||||
int TARGET_ITERATION_TIME = 50; //ms
|
||||
int MINIMUM_ITERATION_COUNT = 100; //default for low-end devices
|
||||
int BENCHMARK_ITERATION_COUNT = 10000; //baseline starting iteration count
|
||||
|
||||
try {
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, BENCHMARK_ITERATION_COUNT);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
skf.generateSecret(keyspec);
|
||||
long finishTime = System.currentTimeMillis();
|
||||
|
||||
int scaledIterationTarget = (int) (((double)BENCHMARK_ITERATION_COUNT / (double)(finishTime - startTime)) * TARGET_ITERATION_TIME);
|
||||
|
||||
if (scaledIterationTarget < MINIMUM_ITERATION_COUNT) return MINIMUM_ITERATION_COUNT;
|
||||
else return scaledIterationTarget;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey getKeyFromPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
return skf.generateSecret(keyspec);
|
||||
}
|
||||
|
||||
private static Cipher getCipherFromPassphrase(String passphrase, byte[] salt, int iterations, int opMode)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
|
||||
cipher.init(opMode, key, new PBEParameterSpec(salt, iterations));
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private static byte[] encryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.ENCRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static byte[] decryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException, IOException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.DECRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static Mac getMacForPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
byte[] pbkdf2 = key.getEncoded();
|
||||
SecretKeySpec hmacKey = new SecretKeySpec(pbkdf2, "HmacSHA1");
|
||||
Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(hmacKey);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private static byte[] verifyMac(byte[] macSalt, int iterations, byte[] encryptedAndMacdData, String passphrase) throws InvalidPassphraseException, GeneralSecurityException, IOException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
|
||||
byte[] encryptedData = new byte[encryptedAndMacdData.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, 0, encryptedData, 0, encryptedData.length);
|
||||
|
||||
byte[] givenMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, encryptedAndMacdData.length-hmac.getMacLength(), givenMac, 0, givenMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encryptedData);
|
||||
|
||||
if (Arrays.equals(givenMac, localMac)) return encryptedData;
|
||||
else throw new InvalidPassphraseException("MAC Error");
|
||||
}
|
||||
|
||||
private static byte[] macWithPassphrase(byte[] macSalt, int iterations, byte[] data, String passphrase) throws GeneralSecurityException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
byte[] mac = hmac.doFinal(data);
|
||||
byte[] result = new byte[data.length + mac.length];
|
||||
|
||||
System.arraycopy(data, 0, result, 0, data.length);
|
||||
System.arraycopy(mac, 0, result, data.length, mac.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.utilities.Hex;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.session.libsession.utilities.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class PublicKey {
|
||||
|
||||
private static final String TAG = PublicKey.class.getSimpleName();
|
||||
|
||||
public static final int KEY_SIZE = 3 + ECPublicKey.KEY_SIZE;
|
||||
|
||||
private final ECPublicKey publicKey;
|
||||
private int id;
|
||||
|
||||
public PublicKey(PublicKey publicKey) {
|
||||
this.id = publicKey.id;
|
||||
|
||||
// FIXME :: This not strictly an accurate copy constructor.
|
||||
this.publicKey = publicKey.publicKey;
|
||||
}
|
||||
|
||||
public PublicKey(int id, ECPublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
Log.i(TAG, "PublicKey Length: " + (bytes.length - offset));
|
||||
|
||||
if ((bytes.length - offset) < KEY_SIZE)
|
||||
throw new InvalidKeyException("Provided bytes are too short.");
|
||||
|
||||
this.id = Conversions.byteArrayToMedium(bytes, offset);
|
||||
this.publicKey = Curve.decodePoint(bytes, offset + 3);
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes) throws InvalidKeyException {
|
||||
this(bytes, 0);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return publicKey.getType();
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ECPublicKey getKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return Hex.toString(getFingerprintBytes());
|
||||
}
|
||||
|
||||
public byte[] getFingerprintBytes() {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(serialize());
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
Log.w("LocalKeyPair", nsae);
|
||||
throw new IllegalArgumentException("SHA-1 isn't supported!");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] keyIdBytes = Conversions.mediumToByteArray(id);
|
||||
byte[] serializedPoint = publicKey.serialize();
|
||||
|
||||
Log.i(TAG, "Serializing public key point: " + Hex.toString(serializedPoint));
|
||||
|
||||
return Util.combine(keyIdBytes, serializedPoint);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
|
||||
/**
|
||||
* This class processes key exchange interactions.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SecurityEvent {
|
||||
|
||||
public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE";
|
||||
|
||||
public static void broadcastSecurityUpdateEvent(Context context) {
|
||||
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
|
||||
intent.setPackage(context.getPackageName());
|
||||
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
|
||||
}
|
||||
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsignal.libsignal.SignalProtocolAddress;
|
||||
import org.session.libsignal.libsignal.state.SessionStore;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
|
||||
public class SessionUtil {
|
||||
|
||||
public static boolean hasSession(Context context, @NonNull Address address) {
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context);
|
||||
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(address.serialize(), SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
|
||||
return sessionStore.containsSession(axolotlAddress);
|
||||
}
|
||||
|
||||
public static void archiveSiblingSessions(Context context, SignalProtocolAddress address) {
|
||||
TextSecureSessionStore sessionStore = new TextSecureSessionStore(context);
|
||||
sessionStore.archiveSiblingSessions(address);
|
||||
}
|
||||
|
||||
public static void archiveAllSessions(Context context) {
|
||||
new TextSecureSessionStore(context).archiveAllSessions();
|
||||
}
|
||||
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.libsignal.SignalProtocolAddress;
|
||||
import org.session.libsignal.libsignal.protocol.CiphertextMessage;
|
||||
import org.session.libsignal.libsignal.state.SessionRecord;
|
||||
import org.session.libsignal.libsignal.state.SessionStore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TextSecureSessionStore implements SessionStore {
|
||||
|
||||
private static final String TAG = TextSecureSessionStore.class.getSimpleName();
|
||||
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
@NonNull private final Context context;
|
||||
|
||||
public TextSecureSessionStore(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionRecord loadSession(@NonNull SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(Address.Companion.fromSerialized(address.getName()), address.getDeviceId());
|
||||
|
||||
if (sessionRecord == null) {
|
||||
Log.w(TAG, "No existing session information found.");
|
||||
return new SessionRecord();
|
||||
}
|
||||
|
||||
return sessionRecord;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSession(@NonNull SignalProtocolAddress address, @NonNull SessionRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
DatabaseFactory.getSessionDatabase(context).store(Address.Companion.fromSerialized(address.getName()), address.getDeviceId(), record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsSession(SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(Address.Companion.fromSerialized(address.getName()), address.getDeviceId());
|
||||
|
||||
return sessionRecord != null &&
|
||||
sessionRecord.getSessionState().hasSenderChain() &&
|
||||
sessionRecord.getSessionState().getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSession(SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
DatabaseFactory.getSessionDatabase(context).delete(Address.Companion.fromSerialized(address.getName()), address.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllSessions(String name) {
|
||||
synchronized (FILE_LOCK) {
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(Address.Companion.fromSerialized(name));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getSubDeviceSessions(String name) {
|
||||
synchronized (FILE_LOCK) {
|
||||
return DatabaseFactory.getSessionDatabase(context).getSubDevices(Address.Companion.fromSerialized(name));
|
||||
}
|
||||
}
|
||||
|
||||
public void archiveSiblingSessions(@NonNull SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
List<SessionDatabase.SessionRow> sessions = DatabaseFactory.getSessionDatabase(context).getAllFor(Address.Companion.fromSerialized(address.getName()));
|
||||
|
||||
for (SessionDatabase.SessionRow row : sessions) {
|
||||
if (row.getDeviceId() != address.getDeviceId()) {
|
||||
row.getRecord().archiveCurrentState();
|
||||
storeSession(new SignalProtocolAddress(row.getAddress().serialize(), row.getDeviceId()), row.getRecord());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void archiveAllSessions(@NonNull String hexEncodedPublicKey) {
|
||||
SignalProtocolAddress address = new SignalProtocolAddress(hexEncodedPublicKey, -1);
|
||||
archiveSiblingSessions(address);
|
||||
}
|
||||
|
||||
public void archiveAllSessions() {
|
||||
synchronized (FILE_LOCK) {
|
||||
List<SessionDatabase.SessionRow> sessions = DatabaseFactory.getSessionDatabase(context).getAll();
|
||||
|
||||
for (SessionDatabase.SessionRow row : sessions) {
|
||||
row.getRecord().archiveCurrentState();
|
||||
storeSession(new SignalProtocolAddress(row.getAddress().serialize(), row.getDeviceId()), row.getRecord());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.text.TextUtils;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.LegacyMmsConnection.Apn;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Database to query APN and MMSC information
|
||||
*/
|
||||
public class ApnDatabase {
|
||||
private static final String TAG = ApnDatabase.class.getSimpleName();
|
||||
|
||||
private final SQLiteDatabase db;
|
||||
private final Context context;
|
||||
|
||||
private static final String DATABASE_NAME = "apns.db";
|
||||
private static final String ASSET_PATH = "databases" + File.separator + DATABASE_NAME;
|
||||
|
||||
private static final String TABLE_NAME = "apns";
|
||||
private static final String ID_COLUMN = "_id";
|
||||
private static final String MCC_MNC_COLUMN = "mccmnc";
|
||||
private static final String MCC_COLUMN = "mcc";
|
||||
private static final String MNC_COLUMN = "mnc";
|
||||
private static final String CARRIER_COLUMN = "carrier";
|
||||
private static final String APN_COLUMN = "apn";
|
||||
private static final String MMSC_COLUMN = "mmsc";
|
||||
private static final String PORT_COLUMN = "port";
|
||||
private static final String TYPE_COLUMN = "type";
|
||||
private static final String PROTOCOL_COLUMN = "protocol";
|
||||
private static final String BEARER_COLUMN = "bearer";
|
||||
private static final String ROAMING_PROTOCOL_COLUMN = "roaming_protocol";
|
||||
private static final String CARRIER_ENABLED_COLUMN = "carrier_enabled";
|
||||
private static final String MMS_PROXY_COLUMN = "mmsproxy";
|
||||
private static final String MMS_PORT_COLUMN = "mmsport";
|
||||
private static final String PROXY_COLUMN = "proxy";
|
||||
private static final String MVNO_MATCH_DATA_COLUMN = "mvno_match_data";
|
||||
private static final String MVNO_TYPE_COLUMN = "mvno";
|
||||
private static final String AUTH_TYPE_COLUMN = "authtype";
|
||||
private static final String USER_COLUMN = "user";
|
||||
private static final String PASSWORD_COLUMN = "password";
|
||||
private static final String SERVER_COLUMN = "server";
|
||||
|
||||
private static final String BASE_SELECTION = MCC_MNC_COLUMN + " = ?";
|
||||
|
||||
private static ApnDatabase instance = null;
|
||||
|
||||
public synchronized static ApnDatabase getInstance(Context context) throws IOException {
|
||||
if (instance == null) instance = new ApnDatabase(context.getApplicationContext());
|
||||
return instance;
|
||||
}
|
||||
|
||||
private ApnDatabase(final Context context) throws IOException {
|
||||
this.context = context;
|
||||
|
||||
File dbFile = context.getDatabasePath(DATABASE_NAME);
|
||||
|
||||
if (!dbFile.getParentFile().exists() && !dbFile.getParentFile().mkdir()) {
|
||||
throw new IOException("couldn't make databases directory");
|
||||
}
|
||||
|
||||
Util.copy(context.getAssets().open(ASSET_PATH, AssetManager.ACCESS_STREAMING),
|
||||
new FileOutputStream(dbFile));
|
||||
|
||||
try {
|
||||
this.db = SQLiteDatabase.openDatabase(context.getDatabasePath(DATABASE_NAME).getPath(),
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
|
||||
} catch (SQLiteException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Apn getCustomApnParameters() {
|
||||
String mmsc = TextSecurePreferences.getMmscUrl(context).trim();
|
||||
|
||||
if (!TextUtils.isEmpty(mmsc) && !mmsc.startsWith("http"))
|
||||
mmsc = "http://" + mmsc;
|
||||
|
||||
String proxy = TextSecurePreferences.getMmscProxy(context);
|
||||
String port = TextSecurePreferences.getMmscProxyPort(context);
|
||||
String user = TextSecurePreferences.getMmscUsername(context);
|
||||
String pass = TextSecurePreferences.getMmscPassword(context);
|
||||
|
||||
return new Apn(mmsc, proxy, port, user, pass);
|
||||
}
|
||||
|
||||
public Apn getDefaultApnParameters(String mccmnc, String apn) {
|
||||
if (mccmnc == null) {
|
||||
Log.w(TAG, "mccmnc was null, returning null");
|
||||
return Apn.EMPTY;
|
||||
}
|
||||
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
if (apn != null) {
|
||||
Log.d(TAG, "Querying table for MCC+MNC " + mccmnc + " and APN name " + apn);
|
||||
cursor = db.query(TABLE_NAME, null,
|
||||
BASE_SELECTION + " AND " + APN_COLUMN + " = ?",
|
||||
new String[] {mccmnc, apn},
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
if (cursor != null) cursor.close();
|
||||
Log.d(TAG, "Querying table for MCC+MNC " + mccmnc + " without APN name");
|
||||
cursor = db.query(TABLE_NAME, null,
|
||||
BASE_SELECTION,
|
||||
new String[] {mccmnc},
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
Apn params = new Apn(cursor.getString(cursor.getColumnIndexOrThrow(MMSC_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(MMS_PROXY_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(MMS_PORT_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(USER_COLUMN)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD_COLUMN)));
|
||||
Log.d(TAG, "Returning preferred APN " + params);
|
||||
return params;
|
||||
}
|
||||
|
||||
Log.w(TAG, "No matching APNs found, returning null");
|
||||
|
||||
return Apn.EMPTY;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Optional<Apn> getMmsConnectionParameters(String mccmnc, String apn) {
|
||||
Apn customApn = getCustomApnParameters();
|
||||
Apn defaultApn = getDefaultApnParameters(mccmnc, apn);
|
||||
Apn result = new Apn(customApn, defaultApn,
|
||||
TextSecurePreferences.getUseCustomMmsc(context),
|
||||
TextSecurePreferences.getUseCustomMmscProxy(context),
|
||||
TextSecurePreferences.getUseCustomMmscProxyPort(context),
|
||||
TextSecurePreferences.getUseCustomMmscUsername(context),
|
||||
TextSecurePreferences.getUseCustomMmscPassword(context));
|
||||
|
||||
if (TextUtils.isEmpty(result.getMmsc())) return Optional.absent();
|
||||
else return Optional.of(result);
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
public class NotInDirectoryException extends Throwable {
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.session.libsignal.libsignal.state.PreKeyRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class OneTimePreKeyDatabase extends Database {
|
||||
|
||||
private static final String TAG = OneTimePreKeyDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "one_time_prekeys";
|
||||
private static final String ID = "_id";
|
||||
public static final String KEY_ID = "key_id";
|
||||
public static final String PUBLIC_KEY = "public_key";
|
||||
public static final String PRIVATE_KEY = "private_key";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
KEY_ID + " INTEGER UNIQUE, " +
|
||||
PUBLIC_KEY + " TEXT NOT NULL, " +
|
||||
PRIVATE_KEY + " TEXT NOT NULL);";
|
||||
|
||||
OneTimePreKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public @Nullable PreKeyRecord getPreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?",
|
||||
new String[] {String.valueOf(keyId)},
|
||||
null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
try {
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY))));
|
||||
|
||||
return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey));
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void insertPreKey(int keyId, PreKeyRecord record) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(KEY_ID, keyId);
|
||||
contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize()));
|
||||
|
||||
database.replace(TABLE_NAME, null, contentValues);
|
||||
}
|
||||
|
||||
public void removePreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, KEY_ID + " = ?", new String[] {String.valueOf(keyId)});
|
||||
}
|
||||
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
|
||||
import org.session.libsignal.libsignal.state.SessionRecord;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SessionDatabase extends Database {
|
||||
|
||||
private static final String TAG = SessionDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "sessions";
|
||||
|
||||
private static final String ID = "_id";
|
||||
public static final String ADDRESS = "address";
|
||||
public static final String DEVICE = "device";
|
||||
public static final String RECORD = "record";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
"(" + ID + " INTEGER PRIMARY KEY, " + ADDRESS + " TEXT NOT NULL, " +
|
||||
DEVICE + " INTEGER NOT NULL, " + RECORD + " BLOB NOT NULL, " +
|
||||
"UNIQUE(" + ADDRESS + "," + DEVICE + ") ON CONFLICT REPLACE);";
|
||||
|
||||
SessionDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void store(@NonNull Address address, int deviceId, @NonNull SessionRecord record) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(ADDRESS, address.serialize());
|
||||
values.put(DEVICE, deviceId);
|
||||
values.put(RECORD, record.serialize());
|
||||
|
||||
database.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
public @Nullable SessionRecord load(@NonNull Address address, int deviceId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, new String[]{RECORD},
|
||||
ADDRESS + " = ? AND " + DEVICE + " = ?",
|
||||
new String[] {address.serialize(), String.valueOf(deviceId)},
|
||||
null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
try {
|
||||
return new SessionRecord(cursor.getBlob(cursor.getColumnIndexOrThrow(RECORD)));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull List<SessionRow> getAllFor(@NonNull Address address) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<SessionRow> results = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null,
|
||||
ADDRESS + " = ?",
|
||||
new String[] {address.serialize()},
|
||||
null, null, null))
|
||||
{
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
try {
|
||||
results.add(new SessionRow(address,
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE)),
|
||||
new SessionRecord(cursor.getBlob(cursor.getColumnIndexOrThrow(RECORD)))));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public @NonNull List<SessionRow> getAll() {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<SessionRow> results = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
try {
|
||||
results.add(new SessionRow(Address.Companion.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE)),
|
||||
new SessionRecord(cursor.getBlob(cursor.getColumnIndexOrThrow(RECORD)))));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public @NonNull List<Integer> getSubDevices(@NonNull Address address) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<Integer> results = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, new String[] {DEVICE},
|
||||
ADDRESS + " = ?",
|
||||
new String[] {address.serialize()},
|
||||
null, null, null))
|
||||
{
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int device = cursor.getInt(cursor.getColumnIndexOrThrow(DEVICE));
|
||||
|
||||
if (device != SignalServiceAddress.DEFAULT_DEVICE_ID) {
|
||||
results.add(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void delete(@NonNull Address address, int deviceId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
database.delete(TABLE_NAME, ADDRESS + " = ? AND " + DEVICE + " = ?",
|
||||
new String[] {address.serialize(), String.valueOf(deviceId)});
|
||||
}
|
||||
|
||||
public void deleteAllFor(@NonNull Address address) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, ADDRESS + " = ?", new String[] {address.serialize()});
|
||||
}
|
||||
|
||||
public static final class SessionRow {
|
||||
private final Address address;
|
||||
private final int deviceId;
|
||||
private final SessionRecord record;
|
||||
|
||||
public SessionRow(Address address, int deviceId, SessionRecord record) {
|
||||
this.address = address;
|
||||
this.deviceId = deviceId;
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public int getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public SessionRecord getRecord() {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.libsignal.InvalidKeyException;
|
||||
import org.session.libsignal.libsignal.ecc.Curve;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
|
||||
import org.session.libsignal.libsignal.ecc.ECPublicKey;
|
||||
import org.session.libsignal.libsignal.state.SignedPreKeyRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SignedPreKeyDatabase extends Database {
|
||||
|
||||
private static final String TAG = SignedPreKeyDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "signed_prekeys";
|
||||
|
||||
private static final String ID = "_id";
|
||||
public static final String KEY_ID = "key_id";
|
||||
public static final String PUBLIC_KEY = "public_key";
|
||||
public static final String PRIVATE_KEY = "private_key";
|
||||
public static final String SIGNATURE = "signature";
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
KEY_ID + " INTEGER UNIQUE, " +
|
||||
PUBLIC_KEY + " TEXT NOT NULL, " +
|
||||
PRIVATE_KEY + " TEXT NOT NULL, " +
|
||||
SIGNATURE + " TEXT NOT NULL, " +
|
||||
TIMESTAMP + " INTEGER DEFAULT 0);";
|
||||
|
||||
SignedPreKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public @Nullable SignedPreKeyRecord getSignedPreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?",
|
||||
new String[] {String.valueOf(keyId)},
|
||||
null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
try {
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY))));
|
||||
byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE)));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
|
||||
return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull List<SignedPreKeyRecord> getAllSignedPreKeys() {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
List<SignedPreKeyRecord> results = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
try {
|
||||
int keyId = cursor.getInt(cursor.getColumnIndexOrThrow(KEY_ID));
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY))));
|
||||
byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE)));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
|
||||
results.add(new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature));
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void insertSignedPreKey(int keyId, SignedPreKeyRecord record) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(KEY_ID, keyId);
|
||||
contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize()));
|
||||
contentValues.put(SIGNATURE, Base64.encodeBytes(record.getSignature()));
|
||||
contentValues.put(TIMESTAMP, record.getTimestamp());
|
||||
|
||||
database.replace(TABLE_NAME, null, contentValues);
|
||||
}
|
||||
|
||||
|
||||
public void removeSignedPreKey(int keyId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, KEY_ID + " = ? AND " + SIGNATURE + " IS NOT NULL", new String[] {String.valueOf(keyId)});
|
||||
}
|
||||
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
public class SmsMigrator {
|
||||
|
||||
private static final String TAG = SmsMigrator.class.getSimpleName();
|
||||
|
||||
private static void addStringToStatement(SQLiteStatement statement, Cursor cursor,
|
||||
int index, String key)
|
||||
{
|
||||
int columnIndex = cursor.getColumnIndexOrThrow(key);
|
||||
|
||||
if (cursor.isNull(columnIndex)) {
|
||||
statement.bindNull(index);
|
||||
} else {
|
||||
statement.bindString(index, cursor.getString(columnIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addIntToStatement(SQLiteStatement statement, Cursor cursor,
|
||||
int index, String key)
|
||||
{
|
||||
int columnIndex = cursor.getColumnIndexOrThrow(key);
|
||||
|
||||
if (cursor.isNull(columnIndex)) {
|
||||
statement.bindNull(index);
|
||||
} else {
|
||||
statement.bindLong(index, cursor.getLong(columnIndex));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static void addTranslatedTypeToStatement(SQLiteStatement statement, Cursor cursor, int index, String key)
|
||||
{
|
||||
int columnIndex = cursor.getColumnIndexOrThrow(key);
|
||||
|
||||
if (cursor.isNull(columnIndex)) {
|
||||
statement.bindLong(index, SmsDatabase.Types.BASE_INBOX_TYPE);
|
||||
} else {
|
||||
long theirType = cursor.getLong(columnIndex);
|
||||
statement.bindLong(index, SmsDatabase.Types.translateFromSystemBaseType(theirType));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAppropriateTypeForMigration(Cursor cursor, int columnIndex) {
|
||||
long systemType = cursor.getLong(columnIndex);
|
||||
long ourType = SmsDatabase.Types.translateFromSystemBaseType(systemType);
|
||||
|
||||
return ourType == MmsSmsColumns.Types.BASE_INBOX_TYPE ||
|
||||
ourType == MmsSmsColumns.Types.BASE_SENT_TYPE ||
|
||||
ourType == MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE;
|
||||
}
|
||||
|
||||
private static void getContentValuesForRow(Context context, Cursor cursor,
|
||||
long threadId, SQLiteStatement statement)
|
||||
{
|
||||
String theirAddress = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
|
||||
statement.bindString(1, Address.Companion.fromExternal(context, theirAddress).serialize());
|
||||
|
||||
addIntToStatement(statement, cursor, 2, SmsDatabase.PERSON);
|
||||
addIntToStatement(statement, cursor, 3, SmsDatabase.DATE_RECEIVED);
|
||||
addIntToStatement(statement, cursor, 4, SmsDatabase.DATE_RECEIVED);
|
||||
addIntToStatement(statement, cursor, 5, SmsDatabase.PROTOCOL);
|
||||
addIntToStatement(statement, cursor, 6, SmsDatabase.READ);
|
||||
addIntToStatement(statement, cursor, 7, SmsDatabase.STATUS);
|
||||
addTranslatedTypeToStatement(statement, cursor, 8, SmsDatabase.TYPE);
|
||||
addIntToStatement(statement, cursor, 9, SmsDatabase.REPLY_PATH_PRESENT);
|
||||
addStringToStatement(statement, cursor, 10, SmsDatabase.SUBJECT);
|
||||
addStringToStatement(statement, cursor, 11, SmsDatabase.BODY);
|
||||
addStringToStatement(statement, cursor, 12, SmsDatabase.SERVICE_CENTER);
|
||||
|
||||
statement.bindLong(13, threadId);
|
||||
}
|
||||
|
||||
private static String getTheirCanonicalAddress(Context context, String theirRecipientId) {
|
||||
Uri uri = Uri.parse("content://mms-sms/canonical-address/" + theirRecipientId);
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getString(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IllegalStateException iae) {
|
||||
Log.w("SmsMigrator", iae);
|
||||
return null;
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Set<Recipient> getOurRecipients(Context context, String theirRecipients) {
|
||||
StringTokenizer tokenizer = new StringTokenizer(theirRecipients.trim(), " ");
|
||||
Set<Recipient> recipientList = new HashSet<>();
|
||||
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String theirRecipientId = tokenizer.nextToken();
|
||||
String address = getTheirCanonicalAddress(context, theirRecipientId);
|
||||
|
||||
if (address != null) {
|
||||
recipientList.add(Recipient.from(context, Address.Companion.fromExternal(context, address), true));
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientList.isEmpty()) return null;
|
||||
else return recipientList;
|
||||
}
|
||||
|
||||
private static void migrateConversation(Context context, SmsMigrationProgressListener listener,
|
||||
ProgressDescription progress,
|
||||
long theirThreadId, long ourThreadId)
|
||||
{
|
||||
SmsDatabase ourSmsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
Cursor cursor = null;
|
||||
SQLiteStatement statement = null;
|
||||
|
||||
try {
|
||||
Uri uri = Uri.parse("content://sms/conversations/" + theirThreadId);
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
} catch (SQLiteException e) {
|
||||
/// Work around for weird sony-specific (?) bug: #4309
|
||||
Log.w(TAG, e);
|
||||
return;
|
||||
}
|
||||
|
||||
SQLiteDatabase transaction = ourSmsDatabase.beginTransaction();
|
||||
statement = ourSmsDatabase.createInsertStatement(transaction);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int typeColumn = cursor.getColumnIndex(SmsDatabase.TYPE);
|
||||
|
||||
if (cursor.isNull(typeColumn) || isAppropriateTypeForMigration(cursor, typeColumn)) {
|
||||
getContentValuesForRow(context, cursor, ourThreadId, statement);
|
||||
statement.execute();
|
||||
}
|
||||
|
||||
listener.progressUpdate(new ProgressDescription(progress, cursor.getCount(), cursor.getPosition()));
|
||||
}
|
||||
|
||||
ourSmsDatabase.endTransaction(transaction);
|
||||
DatabaseFactory.getThreadDatabase(context).update(ourThreadId, true);
|
||||
DatabaseFactory.getThreadDatabase(context).notifyConversationListeners(ourThreadId);
|
||||
|
||||
} finally {
|
||||
if (statement != null)
|
||||
statement.close();
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void migrateDatabase(Context context, SmsMigrationProgressListener listener)
|
||||
{
|
||||
// if (context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).getBoolean("migrated", false))
|
||||
// return;
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
Uri threadListUri = Uri.parse("content://mms-sms/conversations?simple=true");
|
||||
cursor = context.getContentResolver().query(threadListUri, null, null, null, "date ASC");
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long theirThreadId = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
||||
String theirRecipients = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
|
||||
Set<Recipient> ourRecipients = getOurRecipients(context, theirRecipients);
|
||||
ProgressDescription progress = new ProgressDescription(cursor.getCount(), cursor.getPosition(), 100, 0);
|
||||
|
||||
if (ourRecipients != null) {
|
||||
if (ourRecipients.size() == 1) {
|
||||
long ourThreadId = threadDatabase.getOrCreateThreadIdFor(ourRecipients.iterator().next());
|
||||
migrateConversation(context, listener, progress, theirThreadId, ourThreadId);
|
||||
} else if (ourRecipients.size() > 1) {
|
||||
ourRecipients.add(Recipient.from(context, Address.Companion.fromSerialized(TextSecurePreferences.getLocalNumber(context)), true));
|
||||
|
||||
List<Address> memberAddresses = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : ourRecipients) {
|
||||
memberAddresses.add(recipient.getAddress());
|
||||
}
|
||||
|
||||
String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, null);
|
||||
Recipient ourGroupRecipient = Recipient.from(context, Address.Companion.fromSerialized(ourGroupId), true);
|
||||
long ourThreadId = threadDatabase.getOrCreateThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
|
||||
migrateConversation(context, listener, progress, theirThreadId, ourThreadId);
|
||||
}
|
||||
}
|
||||
|
||||
progress.incrementPrimaryComplete();
|
||||
listener.progressUpdate(progress);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
context.getSharedPreferences("SecureSMS", Context.MODE_PRIVATE).edit()
|
||||
.putBoolean("migrated", true).apply();
|
||||
}
|
||||
|
||||
public interface SmsMigrationProgressListener {
|
||||
void progressUpdate(ProgressDescription description);
|
||||
}
|
||||
|
||||
public static class ProgressDescription {
|
||||
public final int primaryTotal;
|
||||
public int primaryComplete;
|
||||
public final int secondaryTotal;
|
||||
public final int secondaryComplete;
|
||||
|
||||
ProgressDescription(int primaryTotal, int primaryComplete,
|
||||
int secondaryTotal, int secondaryComplete)
|
||||
{
|
||||
this.primaryTotal = primaryTotal;
|
||||
this.primaryComplete = primaryComplete;
|
||||
this.secondaryTotal = secondaryTotal;
|
||||
this.secondaryComplete = secondaryComplete;
|
||||
}
|
||||
|
||||
ProgressDescription(ProgressDescription that, int secondaryTotal, int secondaryComplete) {
|
||||
this.primaryComplete = that.primaryComplete;
|
||||
this.primaryTotal = that.primaryTotal;
|
||||
this.secondaryComplete = secondaryComplete;
|
||||
this.secondaryTotal = secondaryTotal;
|
||||
}
|
||||
|
||||
void incrementPrimaryComplete() {
|
||||
primaryComplete += 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,481 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.IncomingSticker;
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class StickerDatabase extends Database {
|
||||
|
||||
private static final String TAG = Log.tag(StickerDatabase.class);
|
||||
|
||||
public static final String TABLE_NAME = "sticker";
|
||||
public static final String _ID = "_id";
|
||||
static final String PACK_ID = "pack_id";
|
||||
private static final String PACK_KEY = "pack_key";
|
||||
private static final String PACK_TITLE = "pack_title";
|
||||
private static final String PACK_AUTHOR = "pack_author";
|
||||
private static final String STICKER_ID = "sticker_id";
|
||||
private static final String EMOJI = "emoji";
|
||||
private static final String COVER = "cover";
|
||||
private static final String INSTALLED = "installed";
|
||||
private static final String LAST_USED = "last_used";
|
||||
public static final String FILE_PATH = "file_path";
|
||||
public static final String FILE_LENGTH = "file_length";
|
||||
public static final String FILE_RANDOM = "file_random";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
PACK_ID + " TEXT NOT NULL, " +
|
||||
PACK_KEY + " TEXT NOT NULL, " +
|
||||
PACK_TITLE + " TEXT NOT NULL, " +
|
||||
PACK_AUTHOR + " TEXT NOT NULL, " +
|
||||
STICKER_ID + " INTEGER, " +
|
||||
COVER + " INTEGER, " +
|
||||
EMOJI + " TEXT NOT NULL, " +
|
||||
LAST_USED + " INTEGER, " +
|
||||
INSTALLED + " INTEGER," +
|
||||
FILE_PATH + " TEXT NOT NULL, " +
|
||||
FILE_LENGTH + " INTEGER, " +
|
||||
FILE_RANDOM + " BLOB, " +
|
||||
"UNIQUE(" + PACK_ID + ", " + STICKER_ID + ", " + COVER + ") ON CONFLICT IGNORE)";
|
||||
|
||||
public static final String[] CREATE_INDEXES = {
|
||||
"CREATE INDEX IF NOT EXISTS sticker_pack_id_index ON " + TABLE_NAME + " (" + PACK_ID + ");",
|
||||
"CREATE INDEX IF NOT EXISTS sticker_sticker_id_index ON " + TABLE_NAME + " (" + STICKER_ID + ");"
|
||||
};
|
||||
|
||||
private static final String DIRECTORY = "stickers";
|
||||
|
||||
private final AttachmentSecret attachmentSecret;
|
||||
|
||||
public StickerDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) {
|
||||
super(context, databaseHelper);
|
||||
this.attachmentSecret = attachmentSecret;
|
||||
}
|
||||
|
||||
public void insertSticker(@NonNull IncomingSticker sticker, @NonNull InputStream dataStream) throws IOException {
|
||||
FileInfo fileInfo = saveStickerImage(dataStream);
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
contentValues.put(PACK_ID, sticker.getPackId());
|
||||
contentValues.put(PACK_KEY, sticker.getPackKey());
|
||||
contentValues.put(PACK_TITLE, sticker.getPackTitle());
|
||||
contentValues.put(PACK_AUTHOR, sticker.getPackAuthor());
|
||||
contentValues.put(STICKER_ID, sticker.getStickerId());
|
||||
contentValues.put(EMOJI, sticker.getEmoji());
|
||||
contentValues.put(COVER, sticker.isCover() ? 1 : 0);
|
||||
contentValues.put(INSTALLED, sticker.isInstalled() ? 1 : 0);
|
||||
contentValues.put(FILE_PATH, fileInfo.getFile().getAbsolutePath());
|
||||
contentValues.put(FILE_LENGTH, fileInfo.getLength());
|
||||
contentValues.put(FILE_RANDOM, fileInfo.getRandom());
|
||||
|
||||
long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
if (id > 0) {
|
||||
notifyStickerListeners();
|
||||
|
||||
if (sticker.isCover()) {
|
||||
notifyStickerPackListeners();
|
||||
|
||||
if (sticker.isInstalled()) {
|
||||
broadcastInstallEvent(sticker.getPackId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable StickerRecord getSticker(@NonNull String packId, int stickerId, boolean isCover) {
|
||||
String selection = PACK_ID + " = ? AND " + STICKER_ID + " = ? AND " + COVER + " = ?";
|
||||
String[] args = new String[] { packId, String.valueOf(stickerId), String.valueOf(isCover ? 1 : 0) };
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, "1")) {
|
||||
return new StickerRecordReader(cursor).getNext();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable StickerPackRecord getStickerPack(@NonNull String packId) {
|
||||
String query = PACK_ID + " = ? AND " + COVER + " = ?";
|
||||
String[] args = new String[] { packId, "1" };
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null, "1")) {
|
||||
return new StickerPackRecordReader(cursor).getNext();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Cursor getInstalledStickerPacks() {
|
||||
String selection = COVER + " = ? AND " + INSTALLED + " = ?";
|
||||
String[] args = new String[] { "1", "1" };
|
||||
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null);
|
||||
|
||||
setNotifyStickerPackListeners(cursor);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @Nullable Cursor getStickersByEmoji(@NonNull String emoji) {
|
||||
String selection = EMOJI + " LIKE ? AND " + COVER + " = ?";
|
||||
String[] args = new String[] { "%"+emoji+"%", "0" };
|
||||
|
||||
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null);
|
||||
setNotifyStickerListeners(cursor);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @Nullable Cursor getAllStickerPacks() {
|
||||
return getAllStickerPacks(null);
|
||||
}
|
||||
|
||||
public @Nullable Cursor getAllStickerPacks(@Nullable String limit) {
|
||||
String query = COVER + " = ?";
|
||||
String[] args = new String[] { "1" };
|
||||
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null, limit);
|
||||
setNotifyStickerPackListeners(cursor);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @Nullable Cursor getStickersForPack(@NonNull String packId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String selection = PACK_ID + " = ? AND " + COVER + " = ?";
|
||||
String[] args = new String[] { packId, "0" };
|
||||
|
||||
Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null);
|
||||
setNotifyStickerListeners(cursor);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @Nullable Cursor getRecentlyUsedStickers(int limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String selection = LAST_USED + " > ? AND " + COVER + " = ?";
|
||||
String[] args = new String[] { "0", "0" };
|
||||
|
||||
Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, LAST_USED + " DESC", String.valueOf(limit));
|
||||
setNotifyStickerListeners(cursor);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @Nullable InputStream getStickerStream(long rowId) throws IOException {
|
||||
String selection = _ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(rowId) };
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
|
||||
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(FILE_RANDOM));
|
||||
|
||||
if (path != null) {
|
||||
return ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(path), 0);
|
||||
} else {
|
||||
Log.w(TAG, "getStickerStream("+rowId+") - No sticker data");
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "getStickerStream("+rowId+") - Sticker not found.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isPackInstalled(@NonNull String packId) {
|
||||
StickerPackRecord record = getStickerPack(packId);
|
||||
|
||||
return (record != null && record.isInstalled());
|
||||
}
|
||||
|
||||
public boolean isPackAvailableAsReference(@NonNull String packId) {
|
||||
return getStickerPack(packId) != null;
|
||||
}
|
||||
|
||||
public void updateStickerLastUsedTime(long rowId, long lastUsed) {
|
||||
String selection = _ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(rowId) };
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(LAST_USED, lastUsed);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, selection, args);
|
||||
|
||||
notifyStickerListeners();
|
||||
notifyStickerPackListeners();
|
||||
}
|
||||
|
||||
public void markPackAsInstalled(@NonNull String packKey) {
|
||||
updatePackInstalled(databaseHelper.getWritableDatabase(), packKey, true);
|
||||
notifyStickerPackListeners();
|
||||
}
|
||||
|
||||
public void deleteOrphanedPacks() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String query = "SELECT " + PACK_ID + " FROM " + TABLE_NAME + " WHERE " + INSTALLED + " = ? AND " +
|
||||
PACK_ID + " NOT IN (" +
|
||||
"SELECT DISTINCT " + AttachmentDatabase.STICKER_PACK_ID + " FROM " + AttachmentDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + AttachmentDatabase.STICKER_PACK_ID + " NOT NULL" +
|
||||
")";
|
||||
String[] args = new String[] { "0" };
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
boolean performedDelete = false;
|
||||
|
||||
try (Cursor cursor = db.rawQuery(query, args)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String packId = cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID));
|
||||
|
||||
if (!BlessedPacks.contains(packId)) {
|
||||
deletePack(db, packId);
|
||||
performedDelete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
||||
if (performedDelete) {
|
||||
notifyStickerPackListeners();
|
||||
notifyStickerListeners();
|
||||
}
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void uninstallPack(@NonNull String packId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
|
||||
updatePackInstalled(db, packId, false);
|
||||
deleteStickersInPackExceptCover(db, packId);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
notifyStickerPackListeners();
|
||||
notifyStickerListeners();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePackInstalled(@NonNull SQLiteDatabase db, @NonNull String packId, boolean installed) {
|
||||
StickerPackRecord existing = getStickerPack(packId);
|
||||
|
||||
if (existing != null && existing.isInstalled() == installed) {
|
||||
return;
|
||||
}
|
||||
|
||||
String selection = PACK_ID + " = ?";
|
||||
String[] args = new String[]{ packId };
|
||||
ContentValues values = new ContentValues(1);
|
||||
|
||||
values.put(INSTALLED, installed ? 1 : 0);
|
||||
db.update(TABLE_NAME, values, selection, args);
|
||||
|
||||
if (installed) {
|
||||
broadcastInstallEvent(packId);
|
||||
}
|
||||
}
|
||||
|
||||
private FileInfo saveStickerImage(@NonNull InputStream inputStream) throws IOException {
|
||||
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
|
||||
File file = File.createTempFile("sticker", ".mms", partsDirectory);
|
||||
Pair<byte[], OutputStream> out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, file, false);
|
||||
long length = Util.copy(inputStream, out.second);
|
||||
|
||||
return new FileInfo(file, length, out.first);
|
||||
}
|
||||
|
||||
private void deleteSticker(@NonNull SQLiteDatabase db, long rowId, @Nullable String filePath) {
|
||||
String selection = _ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(rowId) };
|
||||
|
||||
db.delete(TABLE_NAME, selection, args);
|
||||
|
||||
if (!TextUtils.isEmpty(filePath)) {
|
||||
new File(filePath).delete();
|
||||
}
|
||||
}
|
||||
|
||||
private void deletePack(@NonNull SQLiteDatabase db, @NonNull String packId) {
|
||||
String selection = PACK_ID + " = ?";
|
||||
String[] args = new String[] { packId };
|
||||
|
||||
db.delete(TABLE_NAME, selection, args);
|
||||
|
||||
deleteStickersInPack(db, packId);
|
||||
}
|
||||
|
||||
private void deleteStickersInPack(@NonNull SQLiteDatabase db, @NonNull String packId) {
|
||||
String selection = PACK_ID + " = ?";
|
||||
String[] args = new String[] { packId };
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String filePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
|
||||
|
||||
deleteSticker(db, rowId, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
db.delete(TABLE_NAME, selection, args);
|
||||
}
|
||||
|
||||
private void deleteStickersInPackExceptCover(@NonNull SQLiteDatabase db, @NonNull String packId) {
|
||||
String selection = PACK_ID + " = ? AND " + COVER + " = ?";
|
||||
String[] args = new String[] { packId, "0" };
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, selection, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(_ID));
|
||||
String filePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH));
|
||||
|
||||
deleteSticker(db, rowId, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastInstallEvent(@NonNull String packId) {
|
||||
StickerPackRecord pack = getStickerPack(packId);
|
||||
|
||||
if (pack != null) {
|
||||
EventBus.getDefault().postSticky(new StickerPackInstallEvent(new DecryptableUri(pack.getCover().getUri())));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FileInfo {
|
||||
private final File file;
|
||||
private final long length;
|
||||
private final byte[] random;
|
||||
|
||||
private FileInfo(@NonNull File file, long length, @NonNull byte[] random) {
|
||||
this.file = file;
|
||||
this.length = length;
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public byte[] getRandom() {
|
||||
return random;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StickerRecordReader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
||||
public StickerRecordReader(@Nullable Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public @Nullable StickerRecord getNext() {
|
||||
if (cursor == null || !cursor.moveToNext()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getCurrent();
|
||||
}
|
||||
|
||||
public @NonNull StickerRecord getCurrent() {
|
||||
return new StickerRecord(cursor.getLong(cursor.getColumnIndexOrThrow(_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PACK_KEY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(EMOJI)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(FILE_LENGTH)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(COVER)) == 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StickerPackRecordReader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
||||
public StickerPackRecordReader(@Nullable Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
public @Nullable StickerPackRecord getNext() {
|
||||
if (cursor == null || !cursor.moveToNext()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getCurrent();
|
||||
}
|
||||
|
||||
public @NonNull StickerPackRecord getCurrent() {
|
||||
StickerRecord cover = new StickerRecordReader(cursor).getCurrent();
|
||||
|
||||
return new StickerPackRecord(cursor.getString(cursor.getColumnIndexOrThrow(PACK_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PACK_KEY)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PACK_TITLE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PACK_AUTHOR)),
|
||||
cover,
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(INSTALLED)) == 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
|
||||
public class BlockedContactsLoader extends AbstractCursorLoader {
|
||||
|
||||
public BlockedContactsLoader(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
return DatabaseFactory.getRecipientDatabase(getContext())
|
||||
.getBlocked();
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConversationListLoader extends AbstractCursorLoader {
|
||||
|
||||
private final String filter;
|
||||
private final boolean archived;
|
||||
|
||||
public ConversationListLoader(Context context, String filter, boolean archived) {
|
||||
super(context);
|
||||
this.filter = filter;
|
||||
this.archived = archived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
if (filter != null && filter.trim().length() != 0) return getFilteredConversationList(filter);
|
||||
else if (!archived) return getUnarchivedConversationList();
|
||||
else return getArchivedConversationList();
|
||||
}
|
||||
|
||||
private Cursor getUnarchivedConversationList() {
|
||||
List<Cursor> cursorList = new LinkedList<>();
|
||||
cursorList.add(DatabaseFactory.getThreadDatabase(context).getConversationList());
|
||||
|
||||
int archivedCount = DatabaseFactory.getThreadDatabase(context)
|
||||
.getArchivedConversationListCount();
|
||||
|
||||
if (archivedCount > 0) {
|
||||
MatrixCursor switchToArchiveCursor = new MatrixCursor(new String[] {
|
||||
ThreadDatabase.ID, ThreadDatabase.DATE, ThreadDatabase.MESSAGE_COUNT,
|
||||
ThreadDatabase.ADDRESS, ThreadDatabase.SNIPPET, ThreadDatabase.READ, ThreadDatabase.UNREAD_COUNT,
|
||||
ThreadDatabase.TYPE, ThreadDatabase.SNIPPET_TYPE, ThreadDatabase.SNIPPET_URI,
|
||||
ThreadDatabase.ARCHIVED, ThreadDatabase.STATUS, ThreadDatabase.DELIVERY_RECEIPT_COUNT,
|
||||
ThreadDatabase.EXPIRES_IN, ThreadDatabase.LAST_SEEN, ThreadDatabase.READ_RECEIPT_COUNT}, 1);
|
||||
|
||||
|
||||
if (cursorList.get(0).getCount() <= 0) {
|
||||
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
|
||||
"-1", null, 1, 0, ThreadDatabase.DistributionTypes.INBOX_ZERO,
|
||||
0, null, 0, -1, 0, 0, 0, -1});
|
||||
}
|
||||
|
||||
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
|
||||
"-1", null, 1, 0, ThreadDatabase.DistributionTypes.ARCHIVE,
|
||||
0, null, 0, -1, 0, 0, 0, -1});
|
||||
|
||||
cursorList.add(switchToArchiveCursor);
|
||||
}
|
||||
|
||||
return new MergeCursor(cursorList.toArray(new Cursor[0]));
|
||||
}
|
||||
|
||||
private Cursor getArchivedConversationList() {
|
||||
return DatabaseFactory.getThreadDatabase(context).getArchivedConversationList();
|
||||
}
|
||||
|
||||
private Cursor getFilteredConversationList(String filter) {
|
||||
List<String> numbers = ContactAccessor.getInstance().getNumbersForThreadSearchFilter(context, filter);
|
||||
List<Address> addresses = new LinkedList<>();
|
||||
|
||||
for (String number : numbers) {
|
||||
addresses.add(Address.Companion.fromExternal(context, number));
|
||||
}
|
||||
|
||||
return DatabaseFactory.getThreadDatabase(context).getFilteredConversationList(addresses);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
|
||||
public class CountryListLoader extends AsyncTaskLoader<ArrayList<Map<String, String>>> {
|
||||
|
||||
public CountryListLoader(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Map<String, String>> loadInBackground() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private static class RegionComparator implements Comparator<Map<String, String>> {
|
||||
@Override
|
||||
public int compare(Map<String, String> lhs, Map<String, String> rhs) {
|
||||
return lhs.get("country_name").compareTo(rhs.get("country_name"));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class IncomingSticker {
|
||||
|
||||
private final String packKey;
|
||||
private final String packId;
|
||||
private final String packTitle;
|
||||
private final String packAuthor;
|
||||
private final int stickerId;
|
||||
private final String emoji;
|
||||
private final boolean isCover;
|
||||
private final boolean isInstalled;
|
||||
|
||||
public IncomingSticker(@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
@NonNull String packTitle,
|
||||
@NonNull String packAuthor,
|
||||
int stickerId,
|
||||
@NonNull String emoji,
|
||||
boolean isCover,
|
||||
boolean isInstalled)
|
||||
{
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.packTitle = packTitle;
|
||||
this.packAuthor = packAuthor;
|
||||
this.stickerId = stickerId;
|
||||
this.emoji = emoji;
|
||||
this.isCover = isCover;
|
||||
this.isInstalled = isInstalled;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackTitle() {
|
||||
return packTitle;
|
||||
}
|
||||
|
||||
public @NonNull String getPackAuthor() {
|
||||
return packAuthor;
|
||||
}
|
||||
|
||||
public int getStickerId() {
|
||||
return stickerId;
|
||||
}
|
||||
|
||||
public @NonNull String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public boolean isCover() {
|
||||
return isCover;
|
||||
}
|
||||
|
||||
public boolean isInstalled() {
|
||||
return isInstalled;
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
|
||||
public class Sticker {
|
||||
|
||||
private final String packId;
|
||||
private final String packKey;
|
||||
private final int stickerId;
|
||||
private final Attachment attachment;
|
||||
|
||||
public Sticker(@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
int stickerId,
|
||||
@NonNull Attachment attachment)
|
||||
{
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.stickerId = stickerId;
|
||||
this.attachment = attachment;
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public int getStickerId() {
|
||||
return stickerId;
|
||||
}
|
||||
|
||||
public @NonNull Attachment getAttachment() {
|
||||
return attachment;
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a record for a sticker pack in the {@link org.thoughtcrime.securesms.database.StickerDatabase}.
|
||||
*/
|
||||
public final class StickerPackRecord {
|
||||
|
||||
private final String packId;
|
||||
private final String packKey;
|
||||
private final Optional<String> title;
|
||||
private final Optional<String> author;
|
||||
private final StickerRecord cover;
|
||||
private final boolean installed;
|
||||
|
||||
public StickerPackRecord(@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
@NonNull String title,
|
||||
@NonNull String author,
|
||||
@NonNull StickerRecord cover,
|
||||
boolean installed)
|
||||
{
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.title = TextUtils.isEmpty(title) ? Optional.absent() : Optional.of(title);
|
||||
this.author = TextUtils.isEmpty(author) ? Optional.absent() : Optional.of(author);
|
||||
this.cover = cover;
|
||||
this.installed = installed;
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public @NonNull StickerRecord getCover() {
|
||||
return cover;
|
||||
}
|
||||
|
||||
public boolean isInstalled() {
|
||||
return installed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
StickerPackRecord record = (StickerPackRecord) o;
|
||||
return installed == record.installed &&
|
||||
packId.equals(record.packId) &&
|
||||
packKey.equals(record.packKey) &&
|
||||
title.equals(record.title) &&
|
||||
author.equals(record.author) &&
|
||||
cover.equals(record.cover);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(packId, packKey, title, author, cover, installed);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a record for a sticker pack in the {@link org.thoughtcrime.securesms.database.StickerDatabase}.
|
||||
*/
|
||||
public final class StickerRecord {
|
||||
|
||||
private final long rowId;
|
||||
private final String packId;
|
||||
private final String packKey;
|
||||
private final int stickerId;
|
||||
private final String emoji;
|
||||
private final long size;
|
||||
private final boolean isCover;
|
||||
|
||||
public StickerRecord(long rowId,
|
||||
@NonNull String packId,
|
||||
@NonNull String packKey,
|
||||
int stickerId,
|
||||
@NonNull String emoji,
|
||||
long size,
|
||||
boolean isCover)
|
||||
{
|
||||
this.rowId = rowId;
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.stickerId = stickerId;
|
||||
this.emoji = emoji;
|
||||
this.size = size;
|
||||
this.isCover = isCover;
|
||||
}
|
||||
|
||||
public long getRowId() {
|
||||
return rowId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackId() {
|
||||
return packId;
|
||||
}
|
||||
|
||||
public @NonNull String getPackKey() {
|
||||
return packKey;
|
||||
}
|
||||
|
||||
public int getStickerId() {
|
||||
return stickerId;
|
||||
}
|
||||
|
||||
public @NonNull Uri getUri() {
|
||||
return PartAuthority.getStickerUri(rowId);
|
||||
}
|
||||
|
||||
public @NonNull String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public boolean isCover() {
|
||||
return isCover;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
StickerRecord that = (StickerRecord) o;
|
||||
return rowId == that.rowId &&
|
||||
stickerId == that.stickerId &&
|
||||
size == that.size &&
|
||||
isCover == that.isCover &&
|
||||
packId.equals(that.packId) &&
|
||||
packKey.equals(that.packKey) &&
|
||||
emoji.equals(that.emoji);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(rowId, packId, packKey, stickerId, emoji, size, isCover);
|
||||
}
|
||||
}
|
@ -1,335 +0,0 @@
|
||||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.GroupRecord;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceContent;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup.Type;
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
import static org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
|
||||
public class GroupMessageProcessor {
|
||||
|
||||
private static final String TAG = GroupMessageProcessor.class.getSimpleName();
|
||||
|
||||
public static @Nullable Long process(@NonNull Context context,
|
||||
@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
boolean outgoing)
|
||||
{
|
||||
if (!message.getGroupInfo().isPresent() || message.getGroupInfo().get().getGroupId() == null) {
|
||||
Log.w(TAG, "Received group message with no id! Ignoring...");
|
||||
return null;
|
||||
}
|
||||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
SignalServiceGroup group = message.getGroupInfo().get();
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
Optional<GroupRecord> record = database.getGroup(id);
|
||||
|
||||
if (record.isPresent() && group.getType() == Type.UPDATE) {
|
||||
return handleGroupUpdate(context, content, group, record.get(), outgoing);
|
||||
} else if (!record.isPresent() && group.getType() == Type.UPDATE) {
|
||||
return handleGroupCreate(context, content, group, outgoing, message.getTimestamp());
|
||||
} else if (record.isPresent() && group.getType() == Type.QUIT) {
|
||||
return handleGroupLeave(context, content, group, record.get(), outgoing);
|
||||
} else if (record.isPresent() && group.getType() == Type.REQUEST_INFO) {
|
||||
return handleGroupInfoRequest(context, content, group, record.get());
|
||||
} else {
|
||||
Log.w(TAG, "Received unknown type, ignoring...");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Long handleGroupCreate(@NonNull Context context,
|
||||
@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group,
|
||||
boolean outgoing,
|
||||
Long formationTimestamp)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
builder.setType(GroupContext.Type.UPDATE);
|
||||
|
||||
SignalServiceAttachment avatar = group.getAvatar().orNull();
|
||||
List<Address> members = group.getMembers().isPresent() ? new LinkedList<>() : null;
|
||||
List<Address> admins = group.getAdmins().isPresent() ? new LinkedList<>() : null;
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
for (String member : group.getMembers().get()) {
|
||||
members.add(Address.Companion.fromExternal(context, member));
|
||||
}
|
||||
}
|
||||
|
||||
// Loki - Parse admins
|
||||
if (group.getAdmins().isPresent()) {
|
||||
for (String admin : group.getAdmins().get()) {
|
||||
admins.add(Address.Companion.fromExternal(context, admin));
|
||||
}
|
||||
}
|
||||
|
||||
database.create(id, group.getName().orNull(), members,
|
||||
avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null, admins, formationTimestamp);
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
ClosedGroupsProtocol.establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
|
||||
}
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
}
|
||||
|
||||
private static @Nullable Long handleGroupUpdate(@NonNull Context context,
|
||||
@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group,
|
||||
@NonNull GroupRecord groupRecord,
|
||||
boolean outgoing)
|
||||
{
|
||||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group));
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
|
||||
String userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
if (userMasterDevice == null) { userMasterDevice = TextSecurePreferences.getLocalNumber(context); }
|
||||
|
||||
if (content.getSender().equals(userMasterDevice)) {
|
||||
long threadId = threadDatabase.getThreadIdIfExistsFor(recipient);
|
||||
return threadId == -1 ? null : threadId;
|
||||
}
|
||||
|
||||
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
|
||||
// Loki - Only update the group if the group admin sent the message
|
||||
String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender());
|
||||
if (masterDevice == null) { masterDevice = content.getSender(); }
|
||||
if (!groupRecord.getAdmins().contains(Address.Companion.fromSerialized(masterDevice))) {
|
||||
Log.d("Loki", "Received a group update message from a non-admin user for: " + id +"; ignoring.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Loki - Only process update messages if we're part of the group
|
||||
Address userMasterDeviceAddress = Address.Companion.fromSerialized(userMasterDevice);
|
||||
if (!groupRecord.getMembers().contains(userMasterDeviceAddress) &&
|
||||
!group.getMembers().or(Collections.emptyList()).contains(userMasterDevice)) {
|
||||
Log.d("Loki", "Received a group update message from a group we're not a member of: " + id + "; ignoring.");
|
||||
database.setActive(id, false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Set<Address> currentMembers = new HashSet<>(groupRecord.getMembers());
|
||||
Set<Address> newMembers = new HashSet<>();
|
||||
|
||||
for (String messageMember : group.getMembers().get()) {
|
||||
newMembers.add(Address.Companion.fromExternal(context, messageMember));
|
||||
}
|
||||
|
||||
// Added members are the members who are present in newMembers but not in currentMembers
|
||||
Set<Address> addedMembers = new HashSet<>(newMembers);
|
||||
addedMembers.removeAll(currentMembers);
|
||||
|
||||
// Kicked members are members who are present in currentMembers but not in newMembers
|
||||
Set<Address> removedMembers = new HashSet<>(currentMembers);
|
||||
removedMembers.removeAll(newMembers);
|
||||
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
builder.setType(GroupContext.Type.UPDATE);
|
||||
|
||||
// Update our group members if they're different
|
||||
if (!currentMembers.equals(newMembers)) {
|
||||
database.updateMembers(id, new LinkedList<>(newMembers));
|
||||
}
|
||||
|
||||
// Add any new or removed members to the group context.
|
||||
// This will allow us later to iterate over them to check if they left or were added for UI purposes.
|
||||
for (Address addedMember : addedMembers) {
|
||||
builder.addNewMembers(addedMember.serialize());
|
||||
}
|
||||
|
||||
for (Address removedMember : removedMembers) {
|
||||
builder.addRemovedMembers(removedMember.serialize());
|
||||
}
|
||||
|
||||
if (group.getName().isPresent() || group.getAvatar().isPresent()) {
|
||||
SignalServiceAttachment avatar = group.getAvatar().orNull();
|
||||
database.update(id, group.getName().orNull(), avatar != null ? avatar.asPointer() : null);
|
||||
}
|
||||
|
||||
if (group.getName().isPresent() && group.getName().get().equals(groupRecord.getTitle())) {
|
||||
builder.clearName();
|
||||
}
|
||||
|
||||
// If we were removed then we need to disable the chat
|
||||
if (removedMembers.contains(Address.Companion.fromSerialized(userMasterDevice))) {
|
||||
database.setActive(id, false);
|
||||
} else {
|
||||
if (!groupRecord.isActive()) database.setActive(id, true);
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
ClosedGroupsProtocol.establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
|
||||
}
|
||||
}
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
}
|
||||
|
||||
private static Long handleGroupInfoRequest(@NonNull Context context,
|
||||
@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group,
|
||||
@NonNull GroupRecord record)
|
||||
{
|
||||
String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender());
|
||||
if (masterDevice == null) { masterDevice = content.getSender(); }
|
||||
if (record.getMembers().contains(Address.Companion.fromSerialized(masterDevice))) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new PushGroupUpdateJob(content.getSender(), group.getGroupId()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Long handleGroupLeave(@NonNull Context context,
|
||||
@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group,
|
||||
@NonNull GroupRecord record,
|
||||
boolean outgoing)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
List<Address> members = record.getMembers();
|
||||
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
builder.setType(GroupContext.Type.QUIT);
|
||||
|
||||
String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender());
|
||||
if (masterDevice == null) { masterDevice = content.getSender(); }
|
||||
if (members.contains(Address.Companion.fromExternal(context, masterDevice))) {
|
||||
database.removeMember(id, Address.Companion.fromExternal(context, masterDevice));
|
||||
if (outgoing) database.setActive(id, false);
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static @Nullable Long storeMessage(@NonNull Context context,
|
||||
@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group,
|
||||
@NonNull GroupContext storage,
|
||||
boolean outgoing)
|
||||
{
|
||||
if (group.getAvatar().isPresent()) {
|
||||
ApplicationContext.getInstance(context).getJobManager()
|
||||
.add(new AvatarDownloadJob(GroupUtil.getEncodedId(group)));
|
||||
}
|
||||
|
||||
try {
|
||||
if (outgoing) {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group));
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
Address senderAddress = Address.Companion.fromExternal(context, content.getSender());
|
||||
MessageRecord existingMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), senderAddress);
|
||||
long messageId;
|
||||
if (existingMessage != null) {
|
||||
messageId = existingMessage.getId();
|
||||
} else {
|
||||
messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
}
|
||||
|
||||
mmsDatabase.markAsSent(messageId, true);
|
||||
|
||||
return threadId;
|
||||
} else {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
String body = Base64.encodeBytes(storage.toByteArray());
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(Address.Companion.fromExternal(context, content.getSender()), content.getSenderDevice(), content.getTimestamp(), body, Optional.of(group), 0, content.isNeedsReceipt());
|
||||
IncomingGroupMessage groupMessage = new IncomingGroupMessage(incoming, storage, body);
|
||||
|
||||
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(groupMessage);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
return insertResult.get().getThreadId();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static GroupContext.Builder createGroupContext(SignalServiceGroup group) {
|
||||
GroupContext.Builder builder = GroupContext.newBuilder();
|
||||
builder.setId(ByteString.copyFrom(group.getGroupId()));
|
||||
|
||||
if (group.getAvatar().isPresent() && group.getAvatar().get().isPointer()) {
|
||||
builder.setAvatar(AttachmentPointer.newBuilder()
|
||||
.setId(group.getAvatar().get().asPointer().getId())
|
||||
.setKey(ByteString.copyFrom(group.getAvatar().get().asPointer().getKey()))
|
||||
.setContentType(group.getAvatar().get().getContentType()));
|
||||
}
|
||||
|
||||
if (group.getName().isPresent()) {
|
||||
builder.setName(group.getName().get());
|
||||
}
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
builder.addAllMembers(group.getMembers().get());
|
||||
}
|
||||
|
||||
if (group.getAdmins().isPresent()) {
|
||||
builder.addAllAdmins(group.getAdmins().get());
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.migration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Takes a persisted data blob stored by WorkManager and converts it to our {@link Data} class.
|
||||
*/
|
||||
final class DataMigrator {
|
||||
|
||||
private static final String TAG = Log.tag(DataMigrator.class);
|
||||
|
||||
static final Data convert(@NonNull byte[] workManagerData) {
|
||||
Map<String, Object> values = parseWorkManagerDataMap(workManagerData);
|
||||
|
||||
Data.Builder builder = new Data.Builder();
|
||||
|
||||
for (Map.Entry<String, Object> entry : values.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null) {
|
||||
builder.putString(entry.getKey(), null);
|
||||
} else {
|
||||
Class type = value.getClass();
|
||||
|
||||
if (type == String.class) {
|
||||
builder.putString(entry.getKey(), (String) value);
|
||||
} else if (type == String[].class) {
|
||||
builder.putStringArray(entry.getKey(), (String[]) value);
|
||||
} else if (type == Integer.class || type == int.class) {
|
||||
builder.putInt(entry.getKey(), (int) value);
|
||||
} else if (type == Integer[].class || type == int[].class) {
|
||||
builder.putIntArray(entry.getKey(), convertToIntArray(value, type));
|
||||
} else if (type == Long.class || type == long.class) {
|
||||
builder.putLong(entry.getKey(), (long) value);
|
||||
} else if (type == Long[].class || type == long[].class) {
|
||||
builder.putLongArray(entry.getKey(), convertToLongArray(value, type));
|
||||
} else if (type == Float.class || type == float.class) {
|
||||
builder.putFloat(entry.getKey(), (float) value);
|
||||
} else if (type == Float[].class || type == float[].class) {
|
||||
builder.putFloatArray(entry.getKey(), convertToFloatArray(value, type));
|
||||
} else if (type == Double.class || type == double.class) {
|
||||
builder.putDouble(entry.getKey(), (double) value);
|
||||
} else if (type == Double[].class || type == double[].class) {
|
||||
builder.putDoubleArray(entry.getKey(), convertToDoubleArray(value, type));
|
||||
} else if (type == Boolean.class || type == boolean.class) {
|
||||
builder.putBoolean(entry.getKey(), (boolean) value);
|
||||
} else if (type == Boolean[].class || type == boolean[].class) {
|
||||
builder.putBooleanArray(entry.getKey(), convertToBooleanArray(value, type));
|
||||
} else {
|
||||
Log.w(TAG, "Encountered unexpected type '" + type + "'. Skipping.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static @NonNull Map<String, Object> parseWorkManagerDataMap(@NonNull byte[] bytes) throws IllegalStateException {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||
ObjectInputStream objectInputStream = null;
|
||||
|
||||
try {
|
||||
objectInputStream = new ObjectInputStream(inputStream);
|
||||
|
||||
for (int i = objectInputStream.readInt(); i > 0; i--) {
|
||||
map.put(objectInputStream.readUTF(), objectInputStream.readObject());
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
Log.w(TAG, "Failed to read WorkManager data.", e);
|
||||
} finally {
|
||||
try {
|
||||
inputStream.close();
|
||||
|
||||
if (objectInputStream != null) {
|
||||
objectInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to close streams after reading WorkManager data.", e);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static int[] convertToIntArray(Object value, Class type) {
|
||||
if (type == int[].class) {
|
||||
return (int[]) value;
|
||||
}
|
||||
|
||||
Integer[] casted = (Integer[]) value;
|
||||
int[] output = new int[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static long[] convertToLongArray(Object value, Class type) {
|
||||
if (type == long[].class) {
|
||||
return (long[]) value;
|
||||
}
|
||||
|
||||
Long[] casted = (Long[]) value;
|
||||
long[] output = new long[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static float[] convertToFloatArray(Object value, Class type) {
|
||||
if (type == float[].class) {
|
||||
return (float[]) value;
|
||||
}
|
||||
|
||||
Float[] casted = (Float[]) value;
|
||||
float[] output = new float[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static double[] convertToDoubleArray(Object value, Class type) {
|
||||
if (type == double[].class) {
|
||||
return (double[]) value;
|
||||
}
|
||||
|
||||
Double[] casted = (Double[]) value;
|
||||
double[] output = new double[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static boolean[] convertToBooleanArray(Object value, Class type) {
|
||||
if (type == boolean[].class) {
|
||||
return (boolean[]) value;
|
||||
}
|
||||
|
||||
Boolean[] casted = (Boolean[]) value;
|
||||
boolean[] output = new boolean[casted.length];
|
||||
|
||||
for (int i = 0; i < casted.length; i++) {
|
||||
output[i] = casted[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
@ -1,285 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
import com.google.android.mms.pdu_alt.PduBody;
|
||||
import com.google.android.mms.pdu_alt.PduPart;
|
||||
import com.google.android.mms.pdu_alt.RetrieveConf;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.ApnUnavailableException;
|
||||
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.MmsRadioException;
|
||||
import org.thoughtcrime.securesms.mms.PartParser;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.session.libsignal.libsignal.DuplicateMessageException;
|
||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||
import org.session.libsignal.libsignal.LegacyMessageException;
|
||||
import org.session.libsignal.libsignal.NoSessionException;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MmsDownloadJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "MmsDownloadJob";
|
||||
|
||||
private static final String TAG = MmsDownloadJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_THREAD_ID = "thread_id";
|
||||
private static final String KEY_AUTOMATIC = "automatic";
|
||||
|
||||
private long messageId;
|
||||
private long threadId;
|
||||
private boolean automatic;
|
||||
private MessageNotifier messageNotifier;
|
||||
|
||||
public MmsDownloadJob(long messageId, long threadId, boolean automatic) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("mms-operation")
|
||||
.setMaxAttempts(25)
|
||||
.build(),
|
||||
messageId,
|
||||
threadId,
|
||||
automatic);
|
||||
|
||||
}
|
||||
|
||||
private MmsDownloadJob(@NonNull Job.Parameters parameters, long messageId, long threadId, boolean automatic) {
|
||||
super(parameters);
|
||||
|
||||
this.messageId = messageId;
|
||||
this.threadId = threadId;
|
||||
this.automatic = automatic;
|
||||
this.messageNotifier = ApplicationContext.getInstance(context).messageNotifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putLong(KEY_THREAD_ID, threadId)
|
||||
.putBoolean(KEY_AUTOMATIC, automatic)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
if (automatic && KeyCachingService.isLocked(context)) {
|
||||
DatabaseFactory.getMmsDatabase(context).markIncomingNotificationReceived(threadId);
|
||||
messageNotifier.updateNotification(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Optional<MmsDatabase.MmsNotificationInfo> notification = database.getNotification(messageId);
|
||||
|
||||
if (!notification.isPresent()) {
|
||||
Log.w(TAG, "No notification for ID: " + messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (notification.get().getContentLocation() == null) {
|
||||
throw new MmsException("Notification content location was null.");
|
||||
}
|
||||
|
||||
if (!TextSecurePreferences.isPushRegistered(context)) {
|
||||
throw new MmsException("Not registered");
|
||||
}
|
||||
|
||||
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_CONNECTING);
|
||||
|
||||
String contentLocation = notification.get().getContentLocation();
|
||||
byte[] transactionId = new byte[0];
|
||||
|
||||
try {
|
||||
if (notification.get().getTransactionId() != null) {
|
||||
transactionId = notification.get().getTransactionId().getBytes(CharacterSets.MIMENAME_ISO_8859_1);
|
||||
} else {
|
||||
Log.w(TAG, "No transaction ID!");
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Downloading mms at " + Uri.parse(contentLocation).getHost() + ", subscription ID: " + notification.get().getSubscriptionId());
|
||||
|
||||
RetrieveConf retrieveConf = new CompatMmsConnection(context).retrieve(contentLocation, transactionId, notification.get().getSubscriptionId());
|
||||
|
||||
if (retrieveConf == null) {
|
||||
throw new MmsException("RetrieveConf was null");
|
||||
}
|
||||
|
||||
storeRetrievedMms(contentLocation, messageId, threadId, retrieveConf, notification.get().getSubscriptionId(), notification.get().getFrom());
|
||||
} catch (ApnUnavailableException e) {
|
||||
Log.w(TAG, e);
|
||||
handleDownloadError(messageId, threadId, MmsDatabase.Status.DOWNLOAD_APN_UNAVAILABLE,
|
||||
automatic);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
handleDownloadError(messageId, threadId,
|
||||
MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
|
||||
automatic);
|
||||
} catch (MmsRadioException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
handleDownloadError(messageId, threadId,
|
||||
MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
|
||||
automatic);
|
||||
} catch (DuplicateMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsDecryptDuplicate(messageId, threadId);
|
||||
} catch (LegacyMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsLegacyVersion(messageId, threadId);
|
||||
} catch (NoSessionException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsNoSession(messageId, threadId);
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsDecryptFailed(messageId, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
database.markDownloadState(messageId, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE);
|
||||
|
||||
if (automatic) {
|
||||
database.markIncomingNotificationReceived(threadId);
|
||||
messageNotifier.updateNotification(context, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void storeRetrievedMms(String contentLocation,
|
||||
long messageId, long threadId, RetrieveConf retrieved,
|
||||
int subscriptionId, @Nullable Address notificationFrom)
|
||||
throws MmsException, NoSessionException, DuplicateMessageException, InvalidMessageException,
|
||||
LegacyMessageException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Optional<Address> group = Optional.absent();
|
||||
Set<Address> members = new HashSet<>();
|
||||
String body = null;
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
|
||||
Address from;
|
||||
|
||||
if (retrieved.getFrom() != null) {
|
||||
from = Address.Companion.fromExternal(context, Util.toIsoString(retrieved.getFrom().getTextString()));
|
||||
} else if (notificationFrom != null) {
|
||||
from = notificationFrom;
|
||||
} else {
|
||||
from = Address.Companion.getUNKNOWN();
|
||||
}
|
||||
|
||||
if (retrieved.getTo() != null) {
|
||||
for (EncodedStringValue toValue : retrieved.getTo()) {
|
||||
members.add(Address.Companion.fromExternal(context, Util.toIsoString(toValue.getTextString())));
|
||||
}
|
||||
}
|
||||
|
||||
if (retrieved.getCc() != null) {
|
||||
for (EncodedStringValue ccValue : retrieved.getCc()) {
|
||||
members.add(Address.Companion.fromExternal(context, Util.toIsoString(ccValue.getTextString())));
|
||||
}
|
||||
}
|
||||
|
||||
members.add(from);
|
||||
members.add(Address.Companion.fromExternal(context, TextSecurePreferences.getLocalNumber(context)));
|
||||
|
||||
if (retrieved.getBody() != null) {
|
||||
body = PartParser.getMessageText(retrieved.getBody());
|
||||
PduBody media = PartParser.getSupportedMediaParts(retrieved.getBody());
|
||||
|
||||
for (int i=0;i<media.getPartsNum();i++) {
|
||||
PduPart part = media.getPart(i);
|
||||
|
||||
if (part.getData() != null) {
|
||||
Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory();
|
||||
String name = null;
|
||||
|
||||
if (part.getName() != null) name = Util.toIsoString(part.getName());
|
||||
|
||||
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
||||
part.getData().length, name, false, false, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (members.size() > 2) {
|
||||
group = Optional.of(Address.Companion.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), new LinkedList<>())));
|
||||
}
|
||||
|
||||
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false);
|
||||
Optional<InsertResult> insertResult = database.insertMessageInbox(message, contentLocation, threadId);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
database.delete(messageId);
|
||||
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownloadError(long messageId, long threadId, int downloadStatus, boolean automatic)
|
||||
{
|
||||
MmsDatabase db = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
db.markDownloadState(messageId, downloadStatus);
|
||||
|
||||
if (automatic) {
|
||||
db.markIncomingNotificationReceived(threadId);
|
||||
messageNotifier.updateNotification(context, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<MmsDownloadJob> {
|
||||
@Override
|
||||
public @NonNull MmsDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new MmsDownloadJob(parameters,
|
||||
data.getLong(KEY_MESSAGE_ID),
|
||||
data.getLong(KEY_THREAD_ID),
|
||||
data.getBoolean(KEY_AUTOMATIC));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.android.mms.pdu_alt.GenericPdu;
|
||||
import com.google.android.mms.pdu_alt.NotificationInd;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.PduParser;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class MmsReceiveJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "MmsReceiveJob";
|
||||
|
||||
private static final String TAG = MmsReceiveJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_DATA = "data";
|
||||
private static final String KEY_SUBSCRIPTION_ID = "subscription_id";
|
||||
|
||||
private byte[] data;
|
||||
private int subscriptionId;
|
||||
|
||||
public MmsReceiveJob(byte[] data, int subscriptionId) {
|
||||
this(new Job.Parameters.Builder().setMaxAttempts(25).build(), data, subscriptionId);
|
||||
}
|
||||
|
||||
private MmsReceiveJob(@NonNull Job.Parameters parameters, byte[] data, int subscriptionId) {
|
||||
super(parameters);
|
||||
|
||||
this.data = data;
|
||||
this.subscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putString(KEY_DATA, Base64.encodeBytes(data))
|
||||
.putInt(KEY_SUBSCRIPTION_ID, subscriptionId)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() {
|
||||
if (data == null) {
|
||||
Log.w(TAG, "Received NULL pdu, ignoring...");
|
||||
return;
|
||||
}
|
||||
|
||||
PduParser parser = new PduParser(data);
|
||||
GenericPdu pdu = null;
|
||||
|
||||
try {
|
||||
pdu = parser.parse();
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
if (isNotification(pdu) && !isBlocked(pdu)) {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu, subscriptionId);
|
||||
|
||||
Log.i(TAG, "Inserted received MMS notification...");
|
||||
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new MmsDownloadJob(messageAndThreadId.first,
|
||||
messageAndThreadId.second,
|
||||
true));
|
||||
} else if (isNotification(pdu)) {
|
||||
Log.w(TAG, "*** Received blocked MMS, ignoring...");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isBlocked(GenericPdu pdu) {
|
||||
if (pdu.getFrom() != null && pdu.getFrom().getTextString() != null) {
|
||||
Recipient recipients = Recipient.from(context, Address.Companion.fromExternal(context, Util.toIsoString(pdu.getFrom().getTextString())), false);
|
||||
return recipients.isBlocked();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isNotification(GenericPdu pdu) {
|
||||
return pdu != null && pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<MmsReceiveJob> {
|
||||
@Override
|
||||
public @NonNull MmsReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
try {
|
||||
return new MmsReceiveJob(parameters, Base64.decode(data.getString(KEY_DATA)), data.getInt(KEY_SUBSCRIPTION_ID));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,326 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.android.mms.dom.smil.parser.SmilXmlSerializer;
|
||||
import com.google.android.mms.ContentType;
|
||||
import com.google.android.mms.InvalidHeaderValueException;
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
import com.google.android.mms.pdu_alt.PduBody;
|
||||
import com.google.android.mms.pdu_alt.PduComposer;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.android.mms.pdu_alt.PduPart;
|
||||
import com.google.android.mms.pdu_alt.SendConf;
|
||||
import com.google.android.mms.pdu_alt.SendReq;
|
||||
import com.google.android.mms.smil.SmilHelper;
|
||||
import com.klinker.android.send_message.Utils;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.CompatMmsConnection;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.MmsSendResult;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.session.libsignal.utilities.Hex;
|
||||
import org.session.libsession.utilities.NumberUtil;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MmsSendJob extends SendJob {
|
||||
|
||||
public static final String KEY = "MmsSendJob";
|
||||
|
||||
private static final String TAG = MmsSendJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
|
||||
private long messageId;
|
||||
|
||||
public MmsSendJob(long messageId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("mms-operation")
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(25)
|
||||
.build(),
|
||||
messageId);
|
||||
}
|
||||
|
||||
private MmsSendJob(@NonNull Job.Parameters parameters, long messageId) {
|
||||
super(parameters);
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSend() throws MmsException, NoSuchMessageException, IOException {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
|
||||
if (database.isSent(messageId)) {
|
||||
Log.w(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.i(TAG, "Sending message: " + messageId);
|
||||
|
||||
SendReq pdu = constructSendPdu(message);
|
||||
|
||||
validateDestinations(message, pdu);
|
||||
|
||||
final byte[] pduBytes = getPduBytes(pdu);
|
||||
final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes, message.getSubscriptionId());
|
||||
final MmsSendResult result = getSendResult(sendConf, pdu);
|
||||
|
||||
database.markAsSent(messageId, false);
|
||||
markAttachmentsUploaded(messageId, message.getAttachments());
|
||||
|
||||
Log.i(TAG, "Sent message: " + messageId);
|
||||
} catch (UndeliverableMessageException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
} catch (InsecureFallbackApprovalException e) {
|
||||
Log.w(TAG, e);
|
||||
database.markAsPendingInsecureSmsFallback(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.i(TAG, "onCanceled() messageId: " + messageId);
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
|
||||
private byte[] getPduBytes(SendReq message)
|
||||
throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException
|
||||
{
|
||||
byte[] pduBytes = new PduComposer(context, message).make();
|
||||
|
||||
if (pduBytes == null) {
|
||||
throw new UndeliverableMessageException("PDU composition failed, null payload");
|
||||
}
|
||||
|
||||
return pduBytes;
|
||||
}
|
||||
|
||||
private MmsSendResult getSendResult(SendConf conf, SendReq message)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
if (conf == null) {
|
||||
throw new UndeliverableMessageException("No M-Send.conf received in response to send.");
|
||||
} else if (conf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
|
||||
throw new UndeliverableMessageException("Got bad response: " + conf.getResponseStatus());
|
||||
} else if (isInconsistentResponse(message, conf)) {
|
||||
throw new UndeliverableMessageException("Mismatched response!");
|
||||
} else {
|
||||
return new MmsSendResult(conf.getMessageId(), conf.getResponseStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInconsistentResponse(SendReq message, SendConf response) {
|
||||
Log.i(TAG, "Comparing: " + Hex.toString(message.getTransactionId()));
|
||||
Log.i(TAG, "With: " + Hex.toString(response.getTransactionId()));
|
||||
return !Arrays.equals(message.getTransactionId(), response.getTransactionId());
|
||||
}
|
||||
|
||||
private void validateDestinations(EncodedStringValue[] destinations) throws UndeliverableMessageException {
|
||||
if (destinations == null) return;
|
||||
|
||||
for (EncodedStringValue destination : destinations) {
|
||||
if (destination == null || !NumberUtil.isValidSmsOrEmail(destination.getString())) {
|
||||
throw new UndeliverableMessageException("Invalid destination: " +
|
||||
(destination == null ? null : destination.getString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDestinations(OutgoingMediaMessage media, SendReq message) throws UndeliverableMessageException {
|
||||
validateDestinations(message.getTo());
|
||||
validateDestinations(message.getCc());
|
||||
validateDestinations(message.getBcc());
|
||||
|
||||
if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) {
|
||||
throw new UndeliverableMessageException("No to, cc, or bcc specified!");
|
||||
}
|
||||
|
||||
if (media.isSecure()) {
|
||||
throw new UndeliverableMessageException("Attempt to send encrypted MMS?");
|
||||
}
|
||||
}
|
||||
|
||||
private SendReq constructSendPdu(OutgoingMediaMessage message)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
SendReq req = new SendReq();
|
||||
String lineNumber = getMyNumber(context);
|
||||
Address destination = message.getRecipient().getAddress();
|
||||
MediaConstraints mediaConstraints = MediaConstraints.getMmsMediaConstraints(message.getSubscriptionId());
|
||||
List<Attachment> scaledAttachments = scaleAndStripExifFromAttachments(mediaConstraints, message.getAttachments());
|
||||
|
||||
if (!TextUtils.isEmpty(lineNumber)) {
|
||||
req.setFrom(new EncodedStringValue(lineNumber));
|
||||
} else {
|
||||
req.setFrom(new EncodedStringValue(TextSecurePreferences.getLocalNumber(context)));
|
||||
}
|
||||
|
||||
if (destination.isMmsGroup()) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(destination.toGroupString(), false);
|
||||
|
||||
for (Recipient member : members) {
|
||||
if (message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) {
|
||||
req.addBcc(new EncodedStringValue(member.getAddress().serialize()));
|
||||
} else {
|
||||
req.addTo(new EncodedStringValue(member.getAddress().serialize()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
req.addTo(new EncodedStringValue(destination.serialize()));
|
||||
}
|
||||
|
||||
req.setDate(System.currentTimeMillis() / 1000);
|
||||
|
||||
PduBody body = new PduBody();
|
||||
int size = 0;
|
||||
|
||||
if (!TextUtils.isEmpty(message.getBody())) {
|
||||
PduPart part = new PduPart();
|
||||
String name = String.valueOf(System.currentTimeMillis());
|
||||
part.setData(Util.toUtf8Bytes(message.getBody()));
|
||||
part.setCharset(CharacterSets.UTF_8);
|
||||
part.setContentType(ContentType.TEXT_PLAIN.getBytes());
|
||||
part.setContentId(name.getBytes());
|
||||
part.setContentLocation((name + ".txt").getBytes());
|
||||
part.setName((name + ".txt").getBytes());
|
||||
|
||||
body.addPart(part);
|
||||
size += getPartSize(part);
|
||||
}
|
||||
|
||||
for (Attachment attachment : scaledAttachments) {
|
||||
try {
|
||||
if (attachment.getDataUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!");
|
||||
|
||||
String fileName = attachment.getFileName();
|
||||
PduPart part = new PduPart();
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = String.valueOf(Math.abs(Util.getSecureRandom().nextLong()));
|
||||
String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType());
|
||||
|
||||
if (fileExtension != null) fileName = fileName + "." + fileExtension;
|
||||
}
|
||||
|
||||
if (attachment.getContentType().startsWith("text")) {
|
||||
part.setCharset(CharacterSets.UTF_8);
|
||||
}
|
||||
|
||||
part.setContentType(attachment.getContentType().getBytes());
|
||||
part.setContentLocation(fileName.getBytes());
|
||||
part.setName(fileName.getBytes());
|
||||
|
||||
int index = fileName.lastIndexOf(".");
|
||||
String contentId = (index == -1) ? fileName : fileName.substring(0, index);
|
||||
part.setContentId(contentId.getBytes());
|
||||
part.setData(Util.readFully(PartAuthority.getAttachmentStream(context, attachment.getDataUri())));
|
||||
|
||||
body.addPart(part);
|
||||
size += getPartSize(part);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
SmilXmlSerializer.serialize(SmilHelper.createSmilDocument(body), out);
|
||||
PduPart smilPart = new PduPart();
|
||||
smilPart.setContentId("smil".getBytes());
|
||||
smilPart.setContentLocation("smil.xml".getBytes());
|
||||
smilPart.setContentType(ContentType.APP_SMIL.getBytes());
|
||||
smilPart.setData(out.toByteArray());
|
||||
body.addPart(0, smilPart);
|
||||
|
||||
req.setBody(body);
|
||||
req.setMessageSize(size);
|
||||
req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes());
|
||||
req.setExpiry(7 * 24 * 60 * 60);
|
||||
|
||||
try {
|
||||
req.setPriority(PduHeaders.PRIORITY_NORMAL);
|
||||
req.setDeliveryReport(PduHeaders.VALUE_NO);
|
||||
req.setReadReport(PduHeaders.VALUE_NO);
|
||||
} catch (InvalidHeaderValueException e) {}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
private long getPartSize(PduPart part) {
|
||||
return part.getName().length + part.getContentLocation().length +
|
||||
part.getContentType().length + part.getData().length +
|
||||
part.getContentId().length;
|
||||
}
|
||||
|
||||
private void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
|
||||
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
||||
if (recipient != null) {
|
||||
ApplicationContext.getInstance(context).messageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
private String getMyNumber(Context context) throws UndeliverableMessageException {
|
||||
try {
|
||||
return Utils.getMyPhoneNumber(context);
|
||||
} catch (SecurityException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<MmsSendJob> {
|
||||
@Override
|
||||
public @NonNull MmsSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new MmsSendJob(parameters, data.getLong(KEY_MESSAGE_ID));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.session.libsession.messaging.threads.Address;
|
||||
import org.session.libsession.messaging.threads.GroupRecord;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.session.libsession.utilities.GroupUtil;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.crypto.UntrustedIdentityException;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup.Type;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class PushGroupUpdateJob extends BaseJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "PushGroupUpdateJob";
|
||||
|
||||
private static final String TAG = PushGroupUpdateJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_SOURCE = "source";
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private String source;
|
||||
private byte[] groupId;
|
||||
|
||||
public PushGroupUpdateJob(String source, byte[] groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
source,
|
||||
groupId);
|
||||
}
|
||||
|
||||
private PushGroupUpdateJob(@NonNull Job.Parameters parameters, String source, byte[] groupId) {
|
||||
super(parameters);
|
||||
|
||||
this.source = source;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return new Data.Builder().putString(KEY_SOURCE, source)
|
||||
.putString(KEY_GROUP_ID, GroupUtil.getEncodedClosedGroupID(groupId))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException, UntrustedIdentityException {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupRecord> record = groupDatabase.getGroup(GroupUtil.getEncodedClosedGroupID(groupId));
|
||||
SignalServiceAttachment avatar = null;
|
||||
|
||||
if (record == null) {
|
||||
Log.w(TAG, "No information for group record info request: " + new String(groupId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.get().getAvatar() != null) {
|
||||
avatar = SignalServiceAttachmentStream.newStreamBuilder()
|
||||
.withContentType("image/jpeg")
|
||||
.withStream(new ByteArrayInputStream(record.get().getAvatar()))
|
||||
.withLength(record.get().getAvatar().length)
|
||||
.build();
|
||||
}
|
||||
|
||||
List<String> members = new LinkedList<>();
|
||||
for (Address member : record.get().getMembers()) {
|
||||
members.add(member.serialize());
|
||||
}
|
||||
|
||||
List<String> admins = new LinkedList<>();
|
||||
for (Address admin : record.get().getAdmins()) {
|
||||
admins.add(admin.serialize());
|
||||
}
|
||||
|
||||
SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE)
|
||||
.withAvatar(avatar)
|
||||
.withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
|
||||
.withMembers(members)
|
||||
.withAdmins(admins)
|
||||
.withName(record.get().getTitle())
|
||||
.build();
|
||||
|
||||
Address groupAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedClosedGroupID(groupId));
|
||||
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
|
||||
|
||||
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
|
||||
.asGroupMessage(groupContext)
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withExpiration(groupRecipient.getExpireMessages())
|
||||
.build();
|
||||
|
||||
messageSender.sendMessage(0, new SignalServiceAddress(source),
|
||||
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.Companion.fromSerialized(source), false)),
|
||||
message, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
Log.w(TAG, e);
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<PushGroupUpdateJob> {
|
||||
@Override
|
||||
public @NonNull PushGroupUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new PushGroupUpdateJob(parameters,
|
||||
data.getString(KEY_SOURCE),
|
||||
GroupUtil.getDecodedGroupIDAsData(data.getString(KEY_GROUP_ID)));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class PushNotificationReceiveJob extends PushReceivedJob implements InjectableType {
|
||||
|
||||
public static final String KEY = "PushNotificationReceiveJob";
|
||||
|
||||
private static final String TAG = PushNotificationReceiveJob.class.getSimpleName();
|
||||
|
||||
@Inject SignalServiceMessageReceiver receiver;
|
||||
|
||||
public PushNotificationReceiveJob(Context context) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("__notification_received")
|
||||
.setMaxAttempts(3)
|
||||
.setMaxInstances(1)
|
||||
.build());
|
||||
setContext(context);
|
||||
}
|
||||
|
||||
private PushNotificationReceiveJob(@NonNull Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
pullAndProcessMessages(receiver, TAG, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public void pullAndProcessMessages(SignalServiceMessageReceiver receiver, String tag, long startTime) throws IOException {
|
||||
synchronized (PushReceivedJob.RECEIVE_LOCK) {
|
||||
receiver.retrieveMessages(envelope -> {
|
||||
Log.i(tag, "Retrieved an envelope." + timeSuffix(startTime));
|
||||
processEnvelope(envelope, false);
|
||||
Log.i(tag, "Successfully processed an envelope." + timeSuffix(startTime));
|
||||
});
|
||||
TextSecurePreferences.setNeedsMessagePull(context, false);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
Log.w(TAG, e);
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "***** Failed to download pending message!");
|
||||
// MessageNotifier.notifyMessagesPending(getContext());
|
||||
}
|
||||
|
||||
private static String timeSuffix(long startTime) {
|
||||
return " (" + (System.currentTimeMillis() - startTime) + " ms elapsed)";
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<PushNotificationReceiveJob> {
|
||||
@Override
|
||||
public @NonNull PushNotificationReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new PushNotificationReceiveJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.telephony.SmsMessage;
|
||||
|
||||
import org.session.libsession.messaging.jobs.Data;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SmsReceiveJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "SmsReceiveJob";
|
||||
|
||||
private static final String TAG = SmsReceiveJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_PDUS = "pdus";
|
||||
private static final String KEY_SUBSCRIPTION_ID = "subscription_id";
|
||||
|
||||
private @Nullable Object[] pdus;
|
||||
|
||||
private int subscriptionId;
|
||||
|
||||
public SmsReceiveJob(@Nullable Object[] pdus, int subscriptionId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(SqlCipherMigrationConstraint.KEY)
|
||||
.setMaxAttempts(25)
|
||||
.build(),
|
||||
pdus,
|
||||
subscriptionId);
|
||||
}
|
||||
|
||||
private SmsReceiveJob(@NonNull Job.Parameters parameters, @Nullable Object[] pdus, int subscriptionId) {
|
||||
super(parameters);
|
||||
|
||||
this.pdus = pdus;
|
||||
this.subscriptionId = subscriptionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
Data serialize() {
|
||||
String[] encoded = new String[pdus.length];
|
||||
for (int i = 0; i < pdus.length; i++) {
|
||||
encoded[i] = Base64.encodeBytes((byte[]) pdus[i]);
|
||||
}
|
||||
|
||||
return new Data.Builder().putStringArray(KEY_PDUS, encoded)
|
||||
.putInt(KEY_SUBSCRIPTION_ID, subscriptionId)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRun() throws MigrationPendingException {
|
||||
Log.i(TAG, "onRun()");
|
||||
|
||||
Optional<IncomingTextMessage> message = assembleMessageFragments(pdus, subscriptionId);
|
||||
|
||||
if (message.isPresent() && !isBlocked(message.get())) {
|
||||
Optional<InsertResult> insertResult = storeMessage(message.get());
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
}
|
||||
} else if (message.isPresent()) {
|
||||
Log.w(TAG, "*** Received blocked SMS, ignoring...");
|
||||
} else {
|
||||
Log.w(TAG, "*** Failed to assemble message fragments!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
return exception instanceof MigrationPendingException;
|
||||
}
|
||||
|
||||
private boolean isBlocked(IncomingTextMessage message) {
|
||||
if (message.getSender() != null) {
|
||||
Recipient recipient = Recipient.from(context, message.getSender(), false);
|
||||
return recipient.isBlocked();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Optional<InsertResult> storeMessage(IncomingTextMessage message) throws MigrationPendingException {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
database.ensureMigration();
|
||||
|
||||
if (TextSecurePreferences.getNeedsSqlCipherMigration(context)) {
|
||||
throw new MigrationPendingException();
|
||||
}
|
||||
|
||||
if (message.isSecureMessage()) {
|
||||
IncomingTextMessage placeholder = new IncomingTextMessage(message, "");
|
||||
Optional<InsertResult> insertResult = database.insertMessageInbox(placeholder);
|
||||
database.markAsLegacyVersion(insertResult.get().getMessageId());
|
||||
|
||||
return insertResult;
|
||||
} else {
|
||||
return database.insertMessageInbox(message);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IncomingTextMessage> assembleMessageFragments(@Nullable Object[] pdus, int subscriptionId) {
|
||||
if (pdus == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
List<IncomingTextMessage> messages = new LinkedList<>();
|
||||
|
||||
for (Object pdu : pdus) {
|
||||
messages.add(new IncomingTextMessage(context, SmsMessage.createFromPdu((byte[])pdu), subscriptionId));
|
||||
}
|
||||
|
||||
if (messages.isEmpty()) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(new IncomingTextMessage(messages));
|
||||
}
|
||||
|
||||
private class MigrationPendingException extends Exception {
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<SmsReceiveJob> {
|
||||
@Override
|
||||
public @NonNull SmsReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
try {
|
||||
int subscriptionId = data.getInt(KEY_SUBSCRIPTION_ID);
|
||||
String[] encoded = data.getStringArray(KEY_PDUS);
|
||||
Object[] pdus = new Object[encoded.length];
|
||||
|
||||
for (int i = 0; i < encoded.length; i++) {
|
||||
pdus[i] = Base64.decode(encoded[i]);
|
||||
}
|
||||
|
||||
return new SmsReceiveJob(parameters, pdus, subscriptionId);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue