diff --git a/app/build.gradle b/app/build.gradle index 0dc518ac3..b2876f5d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,8 +80,8 @@ protobuf { } } -def canonicalVersionCode = 704 -def canonicalVersionName = "4.71.1" +def canonicalVersionCode = 705 +def canonicalVersionName = "4.71.2" def postFixSize = 10 def abiPostFix = ['universal' : 0, @@ -122,7 +122,7 @@ android { buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" - buildConfigField "String", "CDS_MRENCLAVE", "\"a3bfdf9717e35ce4f6fd3888133d65a71a880cd6a26c41d1e47647ed3e7704fa\"" + buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\"" buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"" buildConfigField "String", "KBS_SERVICE_ID", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"" buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\"" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a93978503..298775a11 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -523,6 +523,11 @@ + + diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java index 79dff419d..04d52757a 100644 --- a/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java @@ -96,9 +96,7 @@ public class APNGParser { } } } catch (IOException e) { - if (!(e instanceof FormatException)) { - e.printStackTrace(); - } + return false; } return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index b6b7ffa05..b1a13a1eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.Log; @@ -70,6 +71,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.webrtc.voiceengine.WebRtcAudioManager; @@ -157,6 +159,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi KeyCachingService.onAppForegrounded(this); ApplicationDependencies.getFrameRateTracker().begin(); ApplicationDependencies.getMegaphoneRepository().onAppForegrounded(); + checkBuildExpiration(); } @Override @@ -192,6 +195,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi return persistentLogger; } + public void checkBuildExpiration() { + if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) { + Log.w(TAG, "Build expired!"); + SignalStore.misc().markClientDeprecated(); + } + } + private void initializeSecurityProvider() { try { Class.forName("org.signal.aesgcmprovider.AesGcmCipher"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java index b418155ed..b5089cf4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java @@ -2,16 +2,25 @@ package org.thoughtcrime.securesms.components.reminder; import android.content.Context; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.PlayStoreUtil; -import org.thoughtcrime.securesms.util.Util; +import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.util.PlayStoreUtil; + +import java.util.List; + +/** + * Showed when a build has fully expired (either via the compile-time constant, or remote + * deprecation). + */ public class ExpiredBuildReminder extends Reminder { public ExpiredBuildReminder(final Context context) { - super(context.getString(R.string.reminder_header_expired_build), - context.getString(R.string.reminder_header_expired_build_details)); + super(null, context.getString(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired)); + setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)); + addAction(new Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now)); } @Override @@ -19,8 +28,17 @@ public class ExpiredBuildReminder extends Reminder { return false; } - public static boolean isEligible() { - return Util.getDaysTillBuildExpiry() <= 0; + @Override + public List getActions() { + return super.getActions(); } + @Override + public @NonNull Importance getImportance() { + return Importance.TERMINAL; + } + + public static boolean isEligible() { + return SignalStore.misc().isClientDeprecated(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java index 28477d003..93d4853d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java @@ -6,20 +6,24 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.Util; +import java.util.concurrent.TimeUnit; + +/** + * Reminder that is shown when a build is getting close to expiry (either because of the + * compile-time constant, or remote deprecation). + */ public class OutdatedBuildReminder extends Reminder { public OutdatedBuildReminder(final Context context) { - super(context.getString(R.string.reminder_header_outdated_build), - getPluralsText(context)); + super(null, getPluralsText(context)); + setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)); + addAction(new Action(context.getString(R.string.OutdatedBuildReminder_update_now), R.id.reminder_action_update_now)); } private static CharSequence getPluralsText(final Context context) { - int days = Util.getDaysTillBuildExpiry() - 1; - if (days == 0) { - return context.getString(R.string.reminder_header_outdated_build_details_today); - } - return context.getResources().getQuantityString(R.plurals.reminder_header_outdated_build_details, days, days); + int days = getDaysUntilExpiry() - 1; + return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days); } @Override @@ -28,7 +32,10 @@ public class OutdatedBuildReminder extends Reminder { } public static boolean isEligible() { - return Util.getDaysTillBuildExpiry() <= 10; + return getDaysUntilExpiry() <= 10; } + private static int getDaysUntilExpiry() { + return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry()); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java index 109d9f449..df3c946fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java @@ -58,7 +58,7 @@ public abstract class Reminder { return Importance.NORMAL; } - public void addAction(@NonNull Action action) { + protected void addAction(@NonNull Action action) { actions.add(action); } @@ -71,7 +71,7 @@ public abstract class Reminder { } public enum Importance { - NORMAL, ERROR + NORMAL, ERROR, TERMINAL } public final class Action { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java index 2ea65728d..a87d49985 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.components.reminder; -import android.annotation.TargetApi; import android.content.Context; -import android.os.Build.VERSION_CODES; import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -19,7 +17,6 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.ViewUtil; import java.util.List; @@ -48,7 +45,6 @@ public final class ReminderView extends FrameLayout { initialize(); } - @TargetApi(VERSION_CODES.HONEYCOMB) public ReminderView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(); @@ -56,14 +52,14 @@ public final class ReminderView extends FrameLayout { private void initialize() { LayoutInflater.from(getContext()).inflate(R.layout.reminder_header, this, true); - progressBar = ViewUtil.findById(this, R.id.reminder_progress); - progressText = ViewUtil.findById(this, R.id.reminder_progress_text); - container = ViewUtil.findById(this, R.id.container); - closeButton = ViewUtil.findById(this, R.id.cancel); - title = ViewUtil.findById(this, R.id.reminder_title); - text = ViewUtil.findById(this, R.id.reminder_text); - space = ViewUtil.findById(this, R.id.reminder_space); - actionsRecycler = ViewUtil.findById(this, R.id.reminder_actions); + progressBar = findViewById(R.id.reminder_progress); + progressText = findViewById(R.id.reminder_progress_text); + container = findViewById(R.id.container); + closeButton = findViewById(R.id.cancel); + title = findViewById(R.id.reminder_title); + text = findViewById(R.id.reminder_text); + space = findViewById(R.id.reminder_space); + actionsRecycler = findViewById(R.id.reminder_actions); } public void showReminder(final Reminder reminder) { @@ -76,9 +72,26 @@ public final class ReminderView extends FrameLayout { title.setVisibility(GONE); space.setVisibility(VISIBLE); } + + if (!reminder.isDismissable()) { + space.setVisibility(GONE); + } + text.setText(reminder.getText()); - container.setBackgroundResource(reminder.getImportance() == Reminder.Importance.ERROR ? R.drawable.reminder_background_error - : R.drawable.reminder_background_normal); + + switch (reminder.getImportance()) { + case NORMAL: + container.setBackgroundResource(R.drawable.reminder_background_normal); + break; + case ERROR: + container.setBackgroundResource(R.drawable.reminder_background_error); + break; + case TERMINAL: + container.setBackgroundResource(R.drawable.reminder_background_terminal); + break; + default: + throw new IllegalStateException(); + } setOnClickListener(reminder.getOkListener()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java index 2c64a7a54..45bbec70a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java @@ -98,12 +98,12 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu } public void clearVerticalBoundaries() { - setVerticalBoundaries(0, parent.getMeasuredHeight()); + setVerticalBoundaries(parent.getTop(), parent.getMeasuredHeight() + parent.getTop()); } public void setVerticalBoundaries(int topBoundary, int bottomBoundary) { - extraPaddingTop = topBoundary; - extraPaddingBottom = parent.getMeasuredHeight() - bottomBoundary; + extraPaddingTop = topBoundary - parent.getTop(); + extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary; if (isAnimating) { fling(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java index e03e970d9..8cd3ab718 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java @@ -216,6 +216,8 @@ public class DirectoryHelper { return; } + Stopwatch stopwatch = new Stopwatch("refresh"); + DirectoryResult result; if (FeatureFlags.cds()) { @@ -224,6 +226,8 @@ public class DirectoryHelper { result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers); } + stopwatch.split("network"); + if (result.getNumberRewrites().size() > 0) { Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers."); recipientDatabase.updatePhoneNumbers(result.getNumberRewrites()); @@ -238,10 +242,16 @@ public class DirectoryHelper { .map(recipientDatabase::getOrInsertFromE164) .collect(Collectors.toSet()); + stopwatch.split("process-cds"); + recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds); + stopwatch.split("update-registered"); + updateContactsDatabase(context, activeIds, true, result.getNumberRewrites()); + stopwatch.split("contacts-db"); + if (TextSecurePreferences.isMultiDevice(context)) { ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob()); } @@ -258,6 +268,8 @@ public class DirectoryHelper { } else { TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true); } + + stopwatch.stop(TAG); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index b33b619ed..f59a7aa03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -240,6 +240,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MessageUtil; +import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode; @@ -1685,6 +1686,7 @@ public class ConversationActivity extends PassphraseRequiredActivity reminderView.get().showReminder(new UnauthorizedReminder(this)); } else if (ExpiredBuildReminder.isEligible()) { reminderView.get().showReminder(new ExpiredBuildReminder(this)); + reminderView.get().setOnActionClickListener(this::handleReminderAction); } else if (ServiceOutageReminder.isEligible(this)) { ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob()); reminderView.get().showReminder(new ServiceOutageReminder(this)); @@ -1710,6 +1712,9 @@ public class ConversationActivity extends PassphraseRequiredActivity case R.id.reminder_action_view_insights: InsightsLauncher.showInsightsDashboard(getSupportFragmentManager()); break; + case R.id.reminder_action_update_now: + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this); + break; default: throw new IllegalArgumentException("Unknown ID: " + reminderActionId); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index e8021604f..b200b3055 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -56,6 +56,7 @@ import androidx.appcompat.widget.TooltipCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.view.ViewCompat; import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner; import androidx.lifecycle.ViewModelProviders; @@ -117,6 +118,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AvatarUtil; +import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.SnapToTopDataObserver; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; @@ -176,6 +178,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private ViewGroup megaphoneContainer; private SnapToTopDataObserver snapToTopDataObserver; private Drawable archiveDrawable; + private LifecycleObserver visibilityLifecycleObserver; public static ConversationListFragment newInstance() { return new ConversationListFragment(); @@ -214,6 +217,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode cameraFab.show(); reminderView.setOnDismissListener(this::updateReminders); + reminderView.setOnActionClickListener(this::onReminderAction); list.setLayoutManager(new LinearLayoutManager(requireActivity())); list.setItemAnimator(new DeleteItemAnimator()); @@ -272,6 +276,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode public void onStart() { super.onStart(); ConversationFragment.prepare(requireContext()); + ProcessLifecycleOwner.get().getLifecycle().addObserver(visibilityLifecycleObserver); } @Override @@ -283,6 +288,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode EventBus.getDefault().unregister(this); } + @Override + public void onStop() { + super.onStop(); + ProcessLifecycleOwner.get().getLifecycle().removeObserver(visibilityLifecycleObserver); + } + @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { menu.clear(); @@ -412,6 +423,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewModel.onMegaphoneCompleted(event); } + private void onReminderAction(@IdRes int reminderActionId) { + if (reminderActionId == R.id.reminder_action_update_now) { + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); + } + } + private void hideKeyboard() { InputMethodManager imm = ServiceUtil.getInputMethodManager(requireContext()); imm.hideSoftInputFromWindow(requireView().getWindowToken(), 0); @@ -508,12 +525,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList); viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState); - ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() { + visibilityLifecycleObserver = new DefaultLifecycleObserver() { @Override public void onStart(@NonNull LifecycleOwner owner) { viewModel.onVisible(); } - }); + }; } private void onSearchResultChanged(@Nullable SearchResult result) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index fe8893cc9..41cd65058 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -15,6 +15,7 @@ import com.google.android.gms.common.util.ArrayUtils; import net.sqlcipher.database.SQLiteConstraintException; import net.sqlcipher.database.SQLiteDatabase; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.profiles.ProfileKey; @@ -28,8 +29,9 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.v2.ProfileKeySet; +import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; -import org.thoughtcrime.securesms.jobs.WakeGroupV2Job; +import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; @@ -860,7 +862,17 @@ public class RecipientDatabase extends Database { GroupId.V2 groupId = GroupId.v2(insert.getMasterKey()); Recipient recipient = Recipient.externalGroup(context, groupId); - ApplicationDependencies.getJobManager().add(new WakeGroupV2Job(insert.getMasterKey())); + Log.i(TAG, "Creating restore placeholder for " + groupId); + + DatabaseFactory.getGroupDatabase(context) + .create(insert.getMasterKey(), + DecryptedGroup.newBuilder() + .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) + .build()); + + Log.i(TAG, "Scheduling request for latest group info for " + groupId); + + ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId)); threadDatabase.setArchived(recipient.getId(), insert.isArchived()); needsRefresh.add(recipient.getId()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java index 77588d49d..7edcc1e0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.stickers.BlessedPacks; import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent; import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.Util; import java.io.Closeable; @@ -105,6 +106,12 @@ public class StickerDatabase extends Database { contentValues.put(FILE_RANDOM, fileInfo.getRandom()); long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); + if (id == -1) { + String selection = PACK_ID + " = ? AND " + STICKER_ID + " = ? AND " + COVER + " = ?"; + String[] args = SqlUtil.buildArgs(sticker.getPackId(), sticker.getStickerId(), (sticker.isCover() ? 1 : 0)); + + id = databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, selection, args); + } if (id > 0) { notifyStickerListeners(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java index 8b2901577..5b8b26a7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java @@ -21,8 +21,9 @@ final class GroupStateMapper { private static final String TAG = Log.tag(GroupStateMapper.class); - static final int LATEST = Integer.MAX_VALUE; - static final int PLACEHOLDER_REVISION = -1; + static final int LATEST = Integer.MAX_VALUE; + static final int PLACEHOLDER_REVISION = -1; + static final int RESTORE_PLACEHOLDER_REVISION = -2; private static final Comparator BY_REVISION = (o1, o2) -> Integer.compare(o1.getRevision(), o2.getRevision()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 3af96db4a..38276b4cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -65,9 +65,20 @@ public final class GroupsV2StateProcessor { private static final String TAG = Log.tag(GroupsV2StateProcessor.class); - public static final int LATEST = GroupStateMapper.LATEST; + public static final int LATEST = GroupStateMapper.LATEST; + + /** + * Used to mark a group state as a placeholder when there is partial knowledge (title and avater) + * gathered from a group join link. + */ public static final int PLACEHOLDER_REVISION = GroupStateMapper.PLACEHOLDER_REVISION; + /** + * Used to mark a group state as a placeholder when you have no knowledge at all of the group + * e.g. from a group master key from a storage service restore. + */ + public static final int RESTORE_PLACEHOLDER_REVISION = GroupStateMapper.RESTORE_PLACEHOLDER_REVISION; + private final Context context; private final JobManager jobManager; private final RecipientDatabase recipientDatabase; @@ -176,7 +187,8 @@ public final class GroupsV2StateProcessor { if (inputGroupState == null) { try { - inputGroupState = queryServer(localState, revision == LATEST && localState == null); + boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION); + inputGroupState = queryServer(localState, latestRevisionOnly); } catch (GroupNotAMemberException e) { if (localState != null && signedGroupChange != null) { try { @@ -212,7 +224,12 @@ public final class GroupsV2StateProcessor { updateLocalDatabaseGroupState(inputGroupState, newLocalState); determineProfileSharing(inputGroupState, newLocalState); - insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries()); + if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) { + Log.i(TAG, "Inserting single update message for restore placeholder"); + insertUpdateMessages(timestamp, Collections.singleton(new LocalGroupLogEntry(newLocalState, null))); + } else { + insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries()); + } persistLearnedProfileKeys(inputGroupState); GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 668b3578b..5e51c4b63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -101,7 +101,6 @@ public final class JobManagerFactories { put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory()); put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory()); - put(WakeGroupV2Job.KEY, new WakeGroupV2Job.Factory()); put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory()); put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory()); put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory()); @@ -153,6 +152,7 @@ public final class JobManagerFactories { put("Argon2TestJob", new FailingJob.Factory()); put("Argon2TestMigrationJob", new PassingMigrationJob.Factory()); put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory()); + put("WakeGroupV2Job", new FailingJob.Factory()); }}; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java index ae773ef7e..1ce3aee3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java @@ -8,9 +8,8 @@ import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.MediaConstraints; -import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.Util; import java.util.List; @@ -26,7 +25,7 @@ public abstract class SendJob extends BaseJob { @Override public final void onRun() throws Exception { - if (Util.getDaysTillBuildExpiry() <= 0) { + if (SignalStore.misc().isClientDeprecated()) { throw new TextSecureExpiredException(String.format("TextSecure expired (build %d, now %d)", BuildConfig.BUILD_TIMESTAMP, System.currentTimeMillis())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StickerDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StickerDownloadJob.java index 37a30a024..a8c8ba682 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StickerDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StickerDownloadJob.java @@ -5,15 +5,18 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.StickerDatabase; import org.thoughtcrime.securesms.database.model.IncomingSticker; +import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.util.Hex; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import java.io.FileNotFoundException; import java.io.InputStream; import java.util.concurrent.TimeUnit; @@ -76,9 +79,16 @@ public class StickerDownloadJob extends BaseJob { protected void onRun() throws Exception { StickerDatabase db = DatabaseFactory.getStickerDatabase(context); - if (db.getSticker(sticker.getPackId(), sticker.getStickerId(), sticker.isCover()) != null) { - Log.w(TAG, "Sticker already downloaded."); - return; + StickerRecord stickerRecord = db.getSticker(sticker.getPackId(), sticker.getStickerId(), sticker.isCover()); + if (stickerRecord != null) { + try (InputStream stream = PartAuthority.getAttachmentStream(context, stickerRecord.getUri())) { + if (stream != null) { + Log.w(TAG, "Sticker already downloaded."); + return; + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Sticker file no longer exists, downloading again."); + } } if (!db.isPackInstalled(sticker.getPackId()) && !sticker.isCover()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/WakeGroupV2Job.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/WakeGroupV2Job.java deleted file mode 100644 index 108795842..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/WakeGroupV2Job.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; - -import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.groups.GroupMasterKey; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.database.MessageDatabase; -import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; -import org.thoughtcrime.securesms.groups.GroupChangeBusyException; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.GroupManager; -import org.thoughtcrime.securesms.groups.GroupNotAMemberException; -import org.thoughtcrime.securesms.groups.GroupProtoUtil; -import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; -import org.thoughtcrime.securesms.jobmanager.Data; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.Hex; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; - -import java.io.IOException; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -/** - * Use to create and show a thread for an unknown GV2 group. - */ -public final class WakeGroupV2Job extends BaseJob { - - public static final String KEY = "WakeGroupV2Job"; - - private static final String TAG = Log.tag(WakeGroupV2Job.class); - - private static final String KEY_GROUP_MASTER_KEY = "group_id"; - - private final GroupMasterKey groupMasterKey; - - public WakeGroupV2Job(@NonNull GroupMasterKey groupMasterKey) { - this(new Parameters.Builder() - .setQueue("RequestGroupV2InfoJob::" + GroupId.v2(groupMasterKey)) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - groupMasterKey); - } - - private WakeGroupV2Job(@NonNull Parameters parameters, @NonNull GroupMasterKey groupMasterKey) { - super(parameters); - - this.groupMasterKey = groupMasterKey; - } - - @Override - public @NonNull Data serialize() { - return new Data.Builder().putString(KEY_GROUP_MASTER_KEY, Hex.toStringCondensed(groupMasterKey.serialize())) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBusyException { - Log.i(TAG, "Waking group"); - - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - GroupId.V2 groupId = GroupId.v2(groupMasterKey); - - if (groupDatabase.findGroup(groupId)) { - Log.w(TAG, "Group already exists " + groupId); - return; - } else { - GroupManager.updateGroupFromServer(context, groupMasterKey, GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null); - Log.i(TAG, "Group created " + groupId); - } - - Optional group = groupDatabase.getGroup(groupId); - if (!group.isPresent()) { - Log.w(TAG, "Failed to create group from server " + groupId); - return; - } - - Log.i(TAG, "Waking group " + groupId); - try { - Recipient groupRecipient = Recipient.externalGroup(context, groupId); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - GroupDatabase.V2GroupProperties v2GroupProperties = group.get().requireV2GroupProperties(); - DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(v2GroupProperties.getGroupMasterKey(), v2GroupProperties.getDecryptedGroup(), null, null); - MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); - OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, decryptedGroupV2Context, null, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - - long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); - - mmsDatabase.markAsSent(messageId, true); - } catch (MmsException e) { - Log.w(TAG, e); - } - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - return e instanceof PushNetworkException || - e instanceof NoCredentialForRedemptionTimeException || - e instanceof GroupChangeBusyException; - } - - @Override - public void onFailure() { - } - - public static final class Factory implements Job.Factory { - - @Override - public @NonNull WakeGroupV2Job create(@NonNull Parameters parameters, @NonNull Data data) { - try { - return new WakeGroupV2Job(parameters, - new GroupMasterKey(Hex.fromStringCondensed(data.getString(KEY_GROUP_MASTER_KEY)))); - } catch (InvalidInputException | IOException e) { - throw new AssertionError(e); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index 58df4c97d..1a49d01f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -8,6 +8,7 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time"; private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time"; private static final String USERNAME_SHOW_REMINDER = "username.show.reminder"; + private static final String CLIENT_DEPRECATED = "misc.client_deprecated"; MiscellaneousValues(@NonNull KeyValueStore store) { super(store); @@ -45,4 +46,16 @@ public final class MiscellaneousValues extends SignalStoreValues { public boolean shouldShowUsernameReminder() { return getBoolean(USERNAME_SHOW_REMINDER, true); } + + public boolean isClientDeprecated() { + return getBoolean(CLIENT_DEPRECATED, false); + } + + public void markClientDeprecated() { + putBoolean(CLIENT_DEPRECATED, true); + } + + public void clearClientDeprecated() { + putBoolean(CLIENT_DEPRECATED, false); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index 57274988b..4b47c4cf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -17,6 +17,7 @@ import com.google.android.collect.Sets; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.stickers.StickerUrl; +import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.OptionalUtil; @@ -203,18 +204,11 @@ public final class LinkPreviewUtil { @SuppressLint("ObsoleteSdkInt") public long getDate() { - SimpleDateFormat format; - if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); - } else { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); - } - return Stream.of(values.get(KEY_PUBLISHED_TIME_1), values.get(KEY_PUBLISHED_TIME_2), values.get(KEY_MODIFIED_TIME_1), values.get(KEY_MODIFIED_TIME_2)) - .map(dateString -> parseDate(format, dateString)) + .map(DateUtils::parseIso8601) .filter(time -> time > 0) .findFirst() .orElse(0L); @@ -223,19 +217,6 @@ public final class LinkPreviewUtil { public @NonNull Optional getDescription() { return OptionalUtil.absentIfEmpty(values.get(KEY_DESCRIPTION_URL)); } - - private static long parseDate(DateFormat dateFormat, String dateString) { - if (Util.isEmpty(dateString)) { - return 0; - } - - try { - return dateFormat.parse(dateString).getTime(); - } catch (ParseException e) { - Log.w(TAG, "Failed to parse date.", e); - return 0; - } - } } public interface HtmlDecoder { diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/ClientDeprecatedActivity.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ClientDeprecatedActivity.java new file mode 100644 index 000000000..7ab4bd66d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ClientDeprecatedActivity.java @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.megaphone; + +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; + +import org.thoughtcrime.securesms.PassphraseRequiredActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.PlayStoreUtil; +import org.thoughtcrime.securesms.util.Util; + +/** + * Shown when a users build fully expires. Controlled by {@link Megaphones.Event#CLIENT_DEPRECATED}. + */ +public class ClientDeprecatedActivity extends PassphraseRequiredActivity { + + private final DynamicTheme theme = new DynamicNoActionBarTheme(); + + @Override + protected void onCreate(Bundle savedInstanceState, boolean ready) { + setContentView(R.layout.client_deprecated_activity); + + findViewById(R.id.client_deprecated_update_button).setOnClickListener(v -> onUpdateClicked()); + findViewById(R.id.client_deprecated_dont_update_button).setOnClickListener(v -> onDontUpdateClicked()); + } + + @Override + protected void onPreCreate() { + theme.onCreate(this); + } + + @Override + protected void onResume() { + super.onResume(); + theme.onResume(this); + } + + @Override + public void onBackPressed() { + // Disabled + } + + private void onUpdateClicked() { + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this); + } + + private void onDontUpdateClicked() { + new AlertDialog.Builder(this) + .setTitle(R.string.ClientDeprecatedActivity_warning) + .setMessage(R.string.ClientDeprecatedActivity_your_version_of_signal_has_expired_you_can_view_your_message_history) + .setPositiveButton(R.string.ClientDeprecatedActivity_dont_update, (dialog, which) -> { + ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.CLIENT_DEPRECATED, () -> { + Util.runOnMain(this::finish); + }); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java index 1b6094229..a001cb3e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java @@ -20,7 +20,7 @@ public class Megaphone { private final Event event; private final Style style; - private final boolean mandatory; + private final Priority priority; private final boolean canSnooze; private final int titleRes; private final int bodyRes; @@ -33,7 +33,7 @@ public class Megaphone { private Megaphone(@NonNull Builder builder) { this.event = builder.event; this.style = builder.style; - this.mandatory = builder.mandatory; + this.priority = builder.priority; this.canSnooze = builder.canSnooze; this.titleRes = builder.titleRes; this.bodyRes = builder.bodyRes; @@ -48,8 +48,8 @@ public class Megaphone { return event; } - public boolean isMandatory() { - return mandatory; + public @NonNull Priority getPriority() { + return priority; } public boolean canSnooze() { @@ -97,7 +97,7 @@ public class Megaphone { private final Event event; private final Style style; - private boolean mandatory; + private Priority priority; private boolean canSnooze; private int titleRes; private int bodyRes; @@ -111,13 +111,14 @@ public class Megaphone { public Builder(@NonNull Event event, @NonNull Style style) { this.event = event; this.style = style; + this.priority = Priority.DEFAULT; } /** * Prioritizes this megaphone over others that do not set this flag. */ - public @NonNull Builder setMandatory(boolean mandatory) { - this.mandatory = mandatory; + public @NonNull Builder setPriority(@NonNull Priority priority) { + this.priority = priority; return this; } @@ -192,6 +193,20 @@ public class Megaphone { POPUP } + enum Priority { + DEFAULT(0), HIGH(1), CLIENT_EXPIRATION(1000); + + int priorityValue; + + Priority(int priorityValue) { + this.priorityValue = priorityValue; + } + + public int getPriorityValue() { + return priorityValue; + } + } + public interface EventListener { void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController listener); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java index 95ff61eba..a0d4c73c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java @@ -5,6 +5,7 @@ import android.content.Context; import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.annimon.stream.Collectors; @@ -100,6 +101,11 @@ public class MegaphoneRepository { @AnyThread public void markFinished(@NonNull Event event) { + markFinished(event, null); + } + + @AnyThread + public void markFinished(@NonNull Event event, @Nullable Runnable onComplete) { executor.execute(() -> { MegaphoneRecord record = databaseCache.get(event); if (record != null && record.isFinished()) { @@ -108,6 +114,10 @@ public class MegaphoneRepository { database.markFinished(event); resetDatabaseCache(); + + if (onComplete != null) { + onComplete.run(); + } }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 0721ae3b0..4f5766a62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -34,12 +34,12 @@ import java.util.Objects; * Creating a new megaphone: * - Add an enum to {@link Event} * - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)} - * - Include the event in {@link #buildDisplayOrder()} + * - Include the event in {@link #buildDisplayOrder(Context)} * * Common patterns: * - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}. * - For events guarded by feature flags, set a {@link ForeverSchedule} with false in - * {@link #buildDisplayOrder()}. + * {@link #buildDisplayOrder(Context)}. * - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)} * based on whatever properties you're interested in. */ @@ -65,15 +65,9 @@ public final class Megaphones { .map(Map.Entry::getKey) .map(records::get) .map(record -> Megaphones.forRecord(context, record)) + .sortBy(m -> -m.getPriority().getPriorityValue()) .toList(); - boolean hasOptional = Stream.of(megaphones).anyMatch(m -> !m.isMandatory()); - boolean hasMandatory = Stream.of(megaphones).anyMatch(Megaphone::isMandatory); - - if (hasOptional && hasMandatory) { - megaphones = Stream.of(megaphones).filter(Megaphone::isMandatory).toList(); - } - if (megaphones.size() > 0) { return megaphones.get(0); } else { @@ -93,6 +87,7 @@ public final class Megaphones { put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER); put(Event.MENTIONS, shouldShowMentionsMegaphone() ? ALWAYS : NEVER); put(Event.LINK_PREVIEWS, shouldShowLinkPreviewsMegaphone(context) ? ALWAYS : NEVER); + put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER); }}; } @@ -110,6 +105,8 @@ public final class Megaphones { return buildMentionsMegaphone(); case LINK_PREVIEWS: return buildLinkPreviewsMegaphone(); + case CLIENT_DEPRECATED: + return buildClientDeprecatedMegaphone(context); default: throw new IllegalArgumentException("Event not handled!"); } @@ -117,14 +114,14 @@ public final class Megaphones { private static @NonNull Megaphone buildReactionsMegaphone() { return new Megaphone.Builder(Event.REACTIONS, Megaphone.Style.REACTIONS) - .setMandatory(false) + .setPriority(Megaphone.Priority.DEFAULT) .build(); } private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull MegaphoneRecord record) { if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) { return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN) - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) .enableSnooze(null) .setOnVisibleListener((megaphone, listener) -> { if (new NetworkConstraint.Factory(ApplicationDependencies.getApplication()).create().isMet()) { @@ -134,7 +131,7 @@ public final class Megaphones { .build(); } else { return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.BASIC) - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) .setImage(R.drawable.kbs_pin_megaphone) .setTitle(R.string.KbsMegaphone__create_a_pin) .setBody(R.string.KbsMegaphone__pins_keep_information_thats_stored_with_signal_encrytped) @@ -184,7 +181,7 @@ public final class Megaphones { private static @NonNull Megaphone buildMessageRequestsMegaphone(@NonNull Context context) { return new Megaphone.Builder(Event.MESSAGE_REQUESTS, Megaphone.Style.FULLSCREEN) .disableSnooze() - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) .setOnVisibleListener(((megaphone, listener) -> { listener.onMegaphoneNavigationRequested(new Intent(context, MessageRequestMegaphoneActivity.class), ConversationListFragment.MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME); @@ -202,7 +199,17 @@ public final class Megaphones { private static @NonNull Megaphone buildLinkPreviewsMegaphone() { return new Megaphone.Builder(Event.LINK_PREVIEWS, Megaphone.Style.LINK_PREVIEWS) - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) + .build(); + } + + private static @NonNull Megaphone buildClientDeprecatedMegaphone(@NonNull Context context) { + return new Megaphone.Builder(Event.CLIENT_DEPRECATED, Megaphone.Style.FULLSCREEN) + .disableSnooze() + .setPriority(Megaphone.Priority.HIGH) + .setOnVisibleListener((megaphone, listener) -> { + listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class)); + }) .build(); } @@ -224,7 +231,8 @@ public final class Megaphones { PIN_REMINDER("pin_reminder"), MESSAGE_REQUESTS("message_requests"), MENTIONS("mentions"), - LINK_PREVIEWS("link_previews"); + LINK_PREVIEWS("link_previews"), + CLIENT_DEPRECATED("client_deprecated"); private final String key; diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 47df39184..5f8ab8703 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -75,6 +75,7 @@ public class ApplicationMigrations { if (!isUpdate(context)) { Log.d(TAG, "Not an update. Skipping."); + VersionTracker.updateLastSeenVersion(context); return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/DeprecatedClientPreventionInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/DeprecatedClientPreventionInterceptor.java new file mode 100644 index 000000000..cadf5427b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/net/DeprecatedClientPreventionInterceptor.java @@ -0,0 +1,41 @@ +package org.thoughtcrime.securesms.net; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; +import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * Disallows network requests when your client has been deprecated. When the client is deprecated, + * we simply fake a 499 response. + */ +public final class DeprecatedClientPreventionInterceptor implements Interceptor { + + private static final String TAG = Log.tag(DeprecatedClientPreventionInterceptor.class); + + @Override + public @NonNull Response intercept(@NonNull Chain chain) throws IOException { + if (SignalStore.misc().isClientDeprecated()) { + Log.w(TAG, "Preventing request because client is deprecated."); + return new Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .receivedResponseAtMillis(System.currentTimeMillis()) + .message("") + .body(ResponseBody.create(null, "")) + .code(499) + .build(); + } else { + return chain.proceed(chain.request()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java new file mode 100644 index 000000000..ff5b542ba --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.net; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Response; + +/** + * Marks the client as remotely-deprecated when it receives a 499 response. + */ +public final class RemoteDeprecationDetectorInterceptor implements Interceptor { + + private static final String TAG = Log.tag(RemoteDeprecationDetectorInterceptor.class); + + @Override + public @NonNull Response intercept(@NonNull Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + + if (response.code() == 499 && !SignalStore.misc().isClientDeprecated()) { + Log.w(TAG, "Received 499. Client version is deprecated."); + SignalStore.misc().markClientDeprecated(); + } + + return response; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java index 0150b46a9..7b17fa1a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java @@ -10,6 +10,6 @@ import org.thoughtcrime.securesms.BuildConfig; public class StandardUserAgentInterceptor extends UserAgentInterceptor { public StandardUserAgentInterceptor() { - super("Signal-Android " + BuildConfig.VERSION_NAME + " (API " + Build.VERSION.SDK_INT + ")"); + super("Signal-Android/" + BuildConfig.VERSION_NAME + " Android/" + Build.VERSION.SDK_INT); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index d9d4e2146..6942dce11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -7,6 +7,8 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.net.CustomDns; +import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor; +import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor; import org.thoughtcrime.securesms.net.SequentialDns; import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor; import org.thoughtcrime.securesms.util.Base64; @@ -21,6 +23,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -161,7 +164,7 @@ public class SignalServiceNetworkAccess { final SignalStorageUrl omanGoogleStorage = new SignalStorageUrl("https://www.google.com.om/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final List interceptors = Collections.singletonList(new StandardUserAgentInterceptor()); + final List interceptors = Arrays.asList(new StandardUserAgentInterceptor(), new RemoteDeprecationDetectorInterceptor(), new DeprecatedClientPreventionInterceptor()); final Optional dns = Optional.of(DNS); final byte[] zkGroupServerPublicParams; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java index 7f8bf1ff6..adc0dd8eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java @@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit; public class DirectoryRefreshListener extends PersistentAlarmManagerListener { - private static final long INTERVAL = TimeUnit.HOURS.toMillis(6); + private static final long INTERVAL = TimeUnit.HOURS.toMillis(24); @Override protected long getNextScheduledExecutionTime(Context context) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index 3e193f302..5e355866a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -16,13 +16,18 @@ */ package org.thoughtcrime.securesms.util; +import android.annotation.SuppressLint; import android.content.Context; +import android.os.Build; import android.text.format.DateFormat; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logging.Log; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -170,4 +175,33 @@ public class DateUtils extends android.text.format.DateUtils { private static String getLocalizedPattern(String template, Locale locale) { return DateFormat.getBestDateTimePattern(locale, template); } + + /** + * e.g. 2020-09-04T19:17:51Z + * https://www.iso.org/iso-8601-date-and-time-format.html + * + * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. + * + * @return The timestamp if able to be parsed, otherwise -1. + */ + @SuppressLint("ObsoleteSdkInt") + public static long parseIso8601(@Nullable String date) { + SimpleDateFormat format; + if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); + } else { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + } + + if (Util.isEmpty(date)) { + return -1; + } + + try { + return format.parse(date).getTime(); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse date.", e); + return -1; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 792d47412..6f2dc9e70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -64,6 +64,7 @@ public final class FeatureFlags { private static final String MENTIONS = "android.mentions"; private static final String VERIFY_V2 = "android.verifyV2"; private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion"; + private static final String CLIENT_EXPIRATION = "android.clientExpiration"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -82,7 +83,8 @@ public final class FeatureFlags { INTERNAL_USER, USERNAMES, MENTIONS, - VERIFY_V2 + VERIFY_V2, + CLIENT_EXPIRATION ); /** @@ -107,7 +109,8 @@ public final class FeatureFlags { GROUPS_V2_CREATE_VERSION, GROUPS_V2_JOIN_VERSION, VERIFY_V2, - CDS_VERSION + CDS_VERSION, + CLIENT_EXPIRATION ); /** @@ -280,6 +283,11 @@ public final class FeatureFlags { return getBoolean(VERIFY_V2, false); } + /** The raw client expiration JSON string. */ + public static String clientExpiration() { + return getString(CLIENT_EXPIRATION, null); + } + /** * Whether the user can choose phone number privacy settings, and; * Whether to fetch and store the secondary certificate @@ -463,6 +471,20 @@ public final class FeatureFlags { return defaultValue; } + private static String getString(@NonNull String key, String defaultValue) { + String forced = (String) FORCED_VALUES.get(key); + if (forced != null) { + return forced; + } + + Object remote = REMOTE_VALUES.get(key); + if (remote instanceof String) { + return (String) remote; + } + + return defaultValue; + } + private static Map parseStoredConfig(String stored) { Map parsed = new HashMap<>(); @@ -511,14 +533,11 @@ public final class FeatureFlags { } } - private static final class MissingFlagRequirementError extends Error { - } - @VisibleForTesting static final class UpdateResult { private final Map memory; private final Map disk; - private final Map memoryChanges; + private final Map memoryChanges; UpdateResult(@NonNull Map memory, @NonNull Map disk, @NonNull Map memoryChanges) { this.memory = memory; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java new file mode 100644 index 000000000..864687674 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.annimon.stream.Stream; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.logging.Log; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public final class RemoteDeprecation { + + private static final String TAG = Log.tag(RemoteDeprecation.class); + + private RemoteDeprecation() { } + + /** + * @return The amount of time (in milliseconds) until this client version expires, or -1 if + * there's no pending expiration. + */ + public static long getTimeUntilDeprecation() { + return getTimeUntilDeprecation(FeatureFlags.clientExpiration(), System.currentTimeMillis(), BuildConfig.VERSION_NAME); + } + + /** + * @return The amount of time (in milliseconds) until this client version expires, or -1 if + * there's no pending expiration. + */ + @VisibleForTesting + static long getTimeUntilDeprecation(String json, long currentTime, @NonNull String currentVersion) { + if (Util.isEmpty(json)) { + return -1; + } + + try { + SemanticVersion ourVersion = Objects.requireNonNull(SemanticVersion.parse(currentVersion)); + ClientExpiration[] expirations = JsonUtils.fromJson(json, ClientExpiration[].class); + + ClientExpiration expiration = Stream.of(expirations) + .filter(c -> c.getVersion() != null && c.getExpiration() != -1) + .filter(c -> c.requireVersion().compareTo(ourVersion) > 0) + .sortBy(ClientExpiration::getExpiration) + .findFirst() + .orElse(null); + + if (expiration != null) { + return Math.max(expiration.getExpiration() - currentTime, 0); + } + } catch (IOException e) { + Log.w(TAG, e); + } + + return -1; + } + + private static final class ClientExpiration { + @JsonProperty + private final String minVersion; + + @JsonProperty + private final String iso8601; + + ClientExpiration(@Nullable @JsonProperty("minVersion") String minVersion, + @Nullable @JsonProperty("iso8601") String iso8601) + { + this.minVersion = minVersion; + this.iso8601 = iso8601; + } + + public @Nullable SemanticVersion getVersion() { + return SemanticVersion.parse(minVersion); + } + + public @NonNull SemanticVersion requireVersion() { + return Objects.requireNonNull(getVersion()); + } + + public long getExpiration() { + return DateUtils.parseIso8601(iso8601); + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SemanticVersion.java b/app/src/main/java/org/thoughtcrime/securesms/util/SemanticVersion.java new file mode 100644 index 000000000..a34db6b0c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SemanticVersion.java @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.annimon.stream.ComparatorCompat; + +import java.util.Comparator; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class SemanticVersion implements Comparable { + + private static final Pattern VERSION_PATTERN = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)$"); + + private static final Comparator MAJOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.major, s2.major); + private static final Comparator MINOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.minor, s2.minor); + private static final Comparator PATCH_COMPARATOR = (s1, s2) -> Integer.compare(s1.patch, s2.patch); + private static final Comparator COMPARATOR = ComparatorCompat.chain(MAJOR_COMPARATOR) + .thenComparing(MINOR_COMPARATOR) + .thenComparing(PATCH_COMPARATOR); + + private final int major; + private final int minor; + private final int patch; + + public SemanticVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + public static @Nullable SemanticVersion parse(@Nullable String value) { + if (value == null) { + return null; + } + + Matcher matcher = VERSION_PATTERN.matcher(value); + if (Util.isEmpty(value) || !matcher.matches()) { + return null; + } + + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + int patch = Integer.parseInt(matcher.group(3)); + + return new SemanticVersion(major, minor, patch); + } + + @Override + public int compareTo(SemanticVersion other) { + return COMPARATOR.compare(this, other); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SemanticVersion that = (SemanticVersion) o; + return major == that.major && + minor == that.minor && + patch == that.patch; + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 583e0c9d8..cec591636 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -49,6 +49,7 @@ import com.google.i18n.phonenumbers.Phonenumber; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.components.ComposeText; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection; import org.whispersystems.libsignal.util.guava.Optional; @@ -77,6 +78,8 @@ import java.util.concurrent.TimeUnit; public class Util { private static final String TAG = Util.class.getSimpleName(); + private static final long BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90); + private static volatile Handler handler; public static List asList(T... elements) { @@ -458,9 +461,25 @@ public class Util { return secret; } - public static int getDaysTillBuildExpiry() { - int age = (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP); - return 90 - age; + /** + * @return The amount of time (in ms) until this build of Signal will be considered 'expired'. + * Takes into account both the build age as well as any remote deprecation values. + */ + public static long getTimeUntilBuildExpiry() { + if (SignalStore.misc().isClientDeprecated()) { + return 0; + } + + long buildAge = System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP; + long timeUntilBuildDeprecation = BUILD_LIFESPAN - buildAge; + long timeUntilRemoteDeprecation = RemoteDeprecation.getTimeUntilDeprecation(); + + if (timeUntilRemoteDeprecation != -1) { + long timeUntilDeprecation = Math.min(timeUntilBuildDeprecation, timeUntilRemoteDeprecation); + return Math.max(timeUntilDeprecation, 0); + } else { + return Math.max(timeUntilBuildDeprecation, 0); + } } @TargetApi(VERSION_CODES.LOLLIPOP) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java b/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java index 4c9a6ba97..e94710c02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java @@ -3,10 +3,15 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + import java.io.IOException; public class VersionTracker { + private static final String TAG = Log.tag(VersionTracker.class); + public static int getLastSeenVersion(@NonNull Context context) { return TextSecurePreferences.getLastVersionCode(context); } @@ -14,7 +19,13 @@ public class VersionTracker { public static void updateLastSeenVersion(@NonNull Context context) { try { int currentVersionCode = Util.getCanonicalVersionCode(); - TextSecurePreferences.setLastVersionCode(context, currentVersionCode); + int lastVersionCode = TextSecurePreferences.getLastVersionCode(context); + + if (currentVersionCode != lastVersionCode) { + Log.i(TAG, "Upgraded from " + lastVersionCode + " to " + currentVersionCode); + SignalStore.misc().clearClientDeprecated(); + TextSecurePreferences.setLastVersionCode(context, currentVersionCode); + } } catch (IOException ioe) { throw new AssertionError(ioe); } diff --git a/app/src/main/res/drawable/ic_signal_logo_large.xml b/app/src/main/res/drawable/ic_signal_logo_large.xml new file mode 100644 index 000000000..cab7595e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_logo_large.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/reminder_background_terminal.xml b/app/src/main/res/drawable/reminder_background_terminal.xml new file mode 100644 index 000000000..795dc3c25 --- /dev/null +++ b/app/src/main/res/drawable/reminder_background_terminal.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/layout/client_deprecated_activity.xml b/app/src/main/res/layout/client_deprecated_activity.xml new file mode 100644 index 000000000..31dbbba4f --- /dev/null +++ b/app/src/main/res/layout/client_deprecated_activity.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + +