diff --git a/CHANGELOG-upstream.md b/CHANGELOG-upstream.md index 523440fd8..784c5bbb3 100644 --- a/CHANGELOG-upstream.md +++ b/CHANGELOG-upstream.md @@ -6,6 +6,7 @@ * Unified date display in call bubbles * Explain at "Settings / Chats / Outgoing Media Quality" how to send original quality * Add a basic sticker picker +* Leave groups and channels before deletion * Fix: keep original sent timestamp for resent messages * Fix: make clicking on broadcast member-added messages work always * Fix: remove notification when a message is deleted by sender diff --git a/src/androidTest/java/com/b44t/messenger/TestUtils.java b/src/androidTest/java/com/b44t/messenger/TestUtils.java index 1953ee179..b7bbf0407 100644 --- a/src/androidTest/java/com/b44t/messenger/TestUtils.java +++ b/src/androidTest/java/com/b44t/messenger/TestUtils.java @@ -11,7 +11,6 @@ import android.content.Context; import android.content.Intent; import android.view.View; - import androidx.annotation.NonNull; import androidx.test.espresso.NoMatchingViewException; import androidx.test.espresso.UiController; @@ -19,7 +18,6 @@ import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.util.TreeIterables; import androidx.test.ext.junit.rules.ActivityScenarioRule; - import org.hamcrest.Matcher; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; @@ -60,10 +58,12 @@ public static void createOfflineAccount() { } @NonNull - public static ActivityScenarioRule getOfflineActivityRule(boolean useExistingChats) { + public static ActivityScenarioRule getOfflineActivityRule( + boolean useExistingChats) { Intent intent = - Intent.makeMainActivity( - new ComponentName(getInstrumentation().getTargetContext(), ConversationListActivity.class)); + Intent.makeMainActivity( + new ComponentName( + getInstrumentation().getTargetContext(), ConversationListActivity.class)); if (!useExistingChats) { createOfflineAccount(); } @@ -72,18 +72,22 @@ public static ActivityScenarioRule getOfflineActivityR } @NonNull - public static ActivityScenarioRule getOnlineActivityRule(Class activityClass) { + public static ActivityScenarioRule getOnlineActivityRule( + Class activityClass) { Context context = getInstrumentation().getTargetContext(); AccountManager.getInstance().beginAccountCreation(context); prepare(); - return new ActivityScenarioRule<>(new Intent(getInstrumentation().getTargetContext(), activityClass)); + return new ActivityScenarioRule<>( + new Intent(getInstrumentation().getTargetContext(), activityClass)); } private static void prepare() { - Prefs.setBooleanPreference(getInstrumentation().getTargetContext(), Prefs.DOZE_ASKED_DIRECTLY, true); + Prefs.setBooleanPreference( + getInstrumentation().getTargetContext(), Prefs.DOZE_ASKED_DIRECTLY, true); if (!AccessibilityUtil.areAnimationsDisabled(getInstrumentation().getTargetContext())) { - throw new RuntimeException("To run the tests, disable animations at Developer options' " + - "-> 'Window/Transition/Animator animation scale' -> Set all 3 to 'off'"); + throw new RuntimeException( + "To run the tests, disable animations at Developer options' " + + "-> 'Window/Transition/Animator animation scale' -> Set all 3 to 'off'"); } } @@ -116,26 +120,22 @@ public void perform(UiController uiController, View view) { } throw new NoMatchingViewException.Builder() - .withRootView(view) - .withViewMatcher(matcher) - .build(); + .withRootView(view) + .withViewMatcher(matcher) + .build(); } }; } /** - * Perform action of implicitly waiting for a certain view. - * This differs from EspressoExtensions.searchFor in that, - * upon failure to locate an element, it will fetch a new root view - * in which to traverse searching for our @param match + * Perform action of implicitly waiting for a certain view. This differs from + * EspressoExtensions.searchFor in that, upon failure to locate an element, it will fetch a new + * root view in which to traverse searching for our @param match * * @param viewMatcher ViewMatcher used to find our view */ public static ViewInteraction waitForView( - Matcher viewMatcher, - int waitMillis, - int waitMillisPerTry - ) { + Matcher viewMatcher, int waitMillis, int waitMillisPerTry) { // Derive the max tries int maxTries = (int) (waitMillis / waitMillisPerTry); @@ -164,12 +164,11 @@ public static ViewInteraction waitForView( } /** - * Normally, you would do - * onView(withId(R.id.send_button)).perform(click()); - * to send the draft message. However, in order to change the send button to the attach button - * while there is no draft, the send button is made invisible and the attach button is made - * visible instead. This confuses the test framework.

- * + * Normally, you would do onView(withId(R.id.send_button)).perform(click()); to send the draft + * message. However, in order to change the send button to the attach button while there is no + * draft, the send button is made invisible and the attach button is made visible instead. This + * confuses the test framework.
+ *
* So, this is a workaround for pressing the send button. */ public static void pressSend() { diff --git a/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java b/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java index 645585b5a..e9bbb6a03 100644 --- a/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java +++ b/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java @@ -10,14 +10,11 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText; import android.util.Log; - import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.TestUtils; - import org.junit.After; import org.junit.Ignore; import org.junit.Rule; @@ -34,18 +31,19 @@ public class EnterChatsBenchmark { // ============================================================================================== // Set this to true if you already have at least 10 chats on your existing DeltaChat installation // and want to traverse through them instead of 10 newly created chats - private final static boolean USE_EXISTING_CHATS = false; + private static final boolean USE_EXISTING_CHATS = false; // ============================================================================================== - private final static int GO_THROUGH_ALL_CHATS_N_TIMES = 8; + private static final int GO_THROUGH_ALL_CHATS_N_TIMES = 8; // ============================================================================================== // PLEASE BACKUP YOUR ACCOUNT BEFORE RUNNING THIS! // ============================================================================================== - private final static String TAG = EnterChatsBenchmark.class.getSimpleName(); + private static final String TAG = EnterChatsBenchmark.class.getSimpleName(); @Rule - public ActivityScenarioRule activityRule = TestUtils.getOfflineActivityRule(USE_EXISTING_CHATS); + public ActivityScenarioRule activityRule = + TestUtils.getOfflineActivityRule(USE_EXISTING_CHATS); @Test public void createAndEnter10FilledChats() { @@ -55,7 +53,9 @@ public void createAndEnter10FilledChats() { for (int i = 0; i < GO_THROUGH_ALL_CHATS_N_TIMES; i++) { times[i] = "" + timeGoToNChats(10); // 10 group chats were created } - Log.i(TAG, "MEASURED RESULTS (Benchmark) - Going thorough all 10 chats: " + String.join(",", times)); + Log.i( + TAG, + "MEASURED RESULTS (Benchmark) - Going thorough all 10 chats: " + String.join(",", times)); } @Test @@ -66,13 +66,17 @@ public void createAndEnterEmptyChats() { for (int i = 0; i < GO_THROUGH_ALL_CHATS_N_TIMES; i++) { times[i] = "" + timeGoToNChats(1); } - Log.i(TAG, "MEASURED RESULTS (Benchmark) - Entering and leaving 1 empty chat: " + String.join(",", times)); + Log.i( + TAG, + "MEASURED RESULTS (Benchmark) - Entering and leaving 1 empty chat: " + + String.join(",", times)); } @Test public void enterFilledChat() { if (!USE_EXISTING_CHATS) { - createChatAndGoBack("Group #1", true, "Hello!", "Some links: https://testrun.org", "And a command: /help"); + createChatAndGoBack( + "Group #1", true, "Hello!", "Some links: https://testrun.org", "And a command: /help"); } String[] times = new String[50]; @@ -82,7 +86,18 @@ public void enterFilledChat() { long end = System.currentTimeMillis(); long diff = end - start; pressBack(); - Log.i(TAG, "Measured (Benchmark) " + (i+1) + "/" + times.length + ": Entering 1 filled chat took " + diff + "ms " + "(going back took " + (System.currentTimeMillis() - end) + "ms)"); + Log.i( + TAG, + "Measured (Benchmark) " + + (i + 1) + + "/" + + times.length + + ": Entering 1 filled chat took " + + diff + + "ms " + + "(going back took " + + (System.currentTimeMillis() - end) + + "ms)"); times[i] = "" + diff; } @@ -91,16 +106,37 @@ public void enterFilledChat() { private void create10Chats(boolean fillWithMsgs) { if (!USE_EXISTING_CHATS) { - createChatAndGoBack("Group #1", fillWithMsgs, "Hello!", "Some links: https://testrun.org", "And a command: /help"); - createChatAndGoBack("Group #2", fillWithMsgs, "example.org, alice@example.org", "aaaaaaa", "bbbbbb"); - createChatAndGoBack("Group #3", fillWithMsgs, repeat("Some string ", 600), repeat("Another string", 200), "Hi!!!"); + createChatAndGoBack( + "Group #1", + fillWithMsgs, + "Hello!", + "Some links: https://testrun.org", + "And a command: /help"); + createChatAndGoBack( + "Group #2", fillWithMsgs, "example.org, alice@example.org", "aaaaaaa", "bbbbbb"); + createChatAndGoBack( + "Group #3", + fillWithMsgs, + repeat("Some string ", 600), + repeat("Another string", 200), + "Hi!!!"); createChatAndGoBack("Group #4", fillWithMsgs, "xyzabc", "Hi!!!!", "Let's meet!"); - createChatAndGoBack("Group #5", fillWithMsgs, repeat("aaaa", 40), "bbbbbbbbbbbbbbbbbb", "ccccccccccccccc"); - createChatAndGoBack("Group #6", fillWithMsgs, "aaaaaaaaaaa", repeat("Hi! ", 1000), "bbbbbbbbbb"); - createChatAndGoBack("Group #7", fillWithMsgs, repeat("abcdefg ", 500), repeat("xxxxx", 100), "yrrrrrrrrrrrrr"); - createChatAndGoBack("Group #8", fillWithMsgs, "and a number: 037362/384756", "ccccc", "Nice!"); - createChatAndGoBack("Group #9", fillWithMsgs, "ddddddddddddddddd", "zuuuuuuuuuuuuuuuu", "ccccc"); - createChatAndGoBack("Group #10", fillWithMsgs, repeat("xxxxxxyyyyy", 100), repeat("String!!", 10), "abcd"); + createChatAndGoBack( + "Group #5", fillWithMsgs, repeat("aaaa", 40), "bbbbbbbbbbbbbbbbbb", "ccccccccccccccc"); + createChatAndGoBack( + "Group #6", fillWithMsgs, "aaaaaaaaaaa", repeat("Hi! ", 1000), "bbbbbbbbbb"); + createChatAndGoBack( + "Group #7", + fillWithMsgs, + repeat("abcdefg ", 500), + repeat("xxxxx", 100), + "yrrrrrrrrrrrrr"); + createChatAndGoBack( + "Group #8", fillWithMsgs, "and a number: 037362/384756", "ccccc", "Nice!"); + createChatAndGoBack( + "Group #9", fillWithMsgs, "ddddddddddddddddd", "zuuuuuuuuuuuuuuuu", "ccccc"); + createChatAndGoBack( + "Group #10", fillWithMsgs, repeat("xxxxxxyyyyy", 100), repeat("String!!", 10), "abcd"); } } @@ -130,10 +166,10 @@ private void createChatAndGoBack(String groupName, boolean fillWithMsgs, String. onView(withContentDescription(R.string.group_create_button)).perform(click()); if (fillWithMsgs) { - for (String t: texts) { + for (String t : texts) { sendText(t); } - for (String t: texts) { + for (String t : texts) { sendText(t); } } diff --git a/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java b/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java index 5c6acacb1..aa2be24eb 100644 --- a/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java +++ b/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java @@ -14,11 +14,10 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.TestUtils; - +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -29,8 +28,6 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; -import java.util.concurrent.TimeUnit; - @RunWith(AndroidJUnit4.class) @LargeTest public class ForwardingTest { @@ -43,7 +40,8 @@ public static void beforeClass() { } @Rule - public final ActivityScenarioRule activityRule = TestUtils.getOfflineActivityRule(false); + public final ActivityScenarioRule activityRule = + TestUtils.getOfflineActivityRule(false); @Before public void createChats() { @@ -51,10 +49,13 @@ public void createChats() { dcContext.createChatByContactId(DcContact.DC_CONTACT_ID_SELF); // Disable bcc_self so that DC doesn't try to send messages to the server. // If we didn't do this, messages would stay in DC_STATE_OUT_PENDING forever. - // The thing is, DC_STATE_OUT_PENDING show a rotating circle animation, and Espresso doesn't work + // The thing is, DC_STATE_OUT_PENDING show a rotating circle animation, and Espresso doesn't + // work // with animations, and the tests would hang and never finish. dcContext.setConfig("bcc_self", "0"); - activityRule.getScenario().onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat( "group")); + activityRule + .getScenario() + .onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat("group")); } @After @@ -68,7 +69,8 @@ public void testSimpleForwarding() { // The group is at position 0, self chat is at position 1, device talk is at position 2 onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click())); onView(withId(R.id.title)).check(matches(withText(R.string.device_talk))); - onView(withId(android.R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); + onView(withId(android.R.id.list)) + .perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); onView(withId(R.id.menu_context_forward)).perform(click()); // Send it to self chat (which is sorted to the top because we're forwarding) onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); @@ -77,11 +79,13 @@ public void testSimpleForwarding() { pressBack(); - onView(withId(R.id.toolbar_title)).check(matches(withText(R.string.connectivity_not_connected))); + onView(withId(R.id.toolbar_title)) + .check(matches(withText(R.string.connectivity_not_connected))); // Self chat moved up because we sent a message there onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); onView(withId(R.id.title)).check(matches(withText(R.string.saved_messages))); - onView(withId(android.R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); + onView(withId(android.R.id.list)) + .perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); onView(withId(R.id.menu_context_forward)).perform(click()); // Send it to the group onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click())); @@ -89,6 +93,7 @@ public void testSimpleForwarding() { onView(withId(R.id.title)).check(matches(withText("group"))); pressBack(); - onView(withId(R.id.toolbar_title)).check(matches(withText(R.string.connectivity_not_connected))); + onView(withId(R.id.toolbar_title)) + .check(matches(withText(R.string.connectivity_not_connected))); } } diff --git a/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java b/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java index 02ada950c..11416cd1c 100644 --- a/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java +++ b/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java @@ -14,15 +14,13 @@ import android.content.ComponentName; import android.content.Intent; import android.net.Uri; - import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.DcContext; import com.b44t.messenger.TestUtils; - +import java.io.File; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -34,9 +32,6 @@ import org.thoughtcrime.securesms.ShareActivity; import org.thoughtcrime.securesms.connect.DcHelper; -import java.io.File; - - @RunWith(AndroidJUnit4.class) @LargeTest public class SharingTest { @@ -48,26 +43,34 @@ public class SharingTest { private static int createdSingleChatId; @Rule - public final ActivityScenarioRule activityRule = TestUtils.getOfflineActivityRule(false); + public final ActivityScenarioRule activityRule = + TestUtils.getOfflineActivityRule(false); @Before public void createGroup() { - activityRule.getScenario().onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat( "group")); + activityRule + .getScenario() + .onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat("group")); } @Before public void createSingleChat() { - activityRule.getScenario().onActivity(a -> { - int contactId = DcHelper.getContext(a).createContact("", "abc@example.org"); - createdSingleChatId = DcHelper.getContext(a).createChatByContactId(contactId); - }); + activityRule + .getScenario() + .onActivity( + a -> { + int contactId = DcHelper.getContext(a).createContact("", "abc@example.org"); + createdSingleChatId = DcHelper.getContext(a).createChatByContactId(contactId); + }); } @Test public void testNormalSharing() { Intent i = new Intent(Intent.ACTION_SEND); i.putExtra(Intent.EXTRA_TEXT, "Hello!"); - i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); + i.setComponent( + new ComponentName( + getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); activityRule.getScenario().onActivity(a -> a.startActivity(i)); onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); @@ -77,9 +80,9 @@ public void testNormalSharing() { } /** - * Test direct sharing from a screenshot. - * Also, this is the regression test for https://github.com/deltachat/deltachat-android/issues/2040 - * where network changes during sharing lead to a bug + * Test direct sharing from a screenshot. Also, this is the regression test for + * https://github.com/deltachat/deltachat-android/issues/2040 where network changes during sharing + * lead to a bug */ @Test public void testShareFromScreenshot() { @@ -92,20 +95,25 @@ public void testShareFromScreenshot() { pngImage = file; } } - Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(pngImage)); + Uri uri = + Uri.parse( + "content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(pngImage)); DcHelper.sharedFiles.put(pngImage, "image/png"); Intent i = new Intent(Intent.ACTION_SEND); i.setType("image/png"); i.putExtra(Intent.EXTRA_SUBJECT, "Screenshot (Sep 27, 2021 00:00:00"); - i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET + i.setFlags( + Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET | Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP | Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_GRANT_READ_URI_PERMISSION); i.putExtra(Intent.EXTRA_STREAM, uri); i.putExtra(ShareActivity.EXTRA_CHAT_ID, createdGroupId); - i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); + i.setComponent( + new ComponentName( + getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); activityRule.getScenario().onActivity(a -> a.startActivity(i)); TestUtils.waitForView(withId(R.id.send_button), 10000, 50); @@ -121,16 +129,32 @@ public void testShareFromScreenshot() { } /** - * Tests https://github.com/deltachat/interface/blob/master/user-testing/mailto-links.md#mailto-links: + * Tests + * https://github.com/deltachat/interface/blob/master/user-testing/mailto-links.md#mailto-links: * *
    - *
  • Just an email address - should open a chat with abc@example.org (and maybe ask whether a chat should be created if it does not exist already)
  • - *
  • email address with subject - should open a chat with abc@example.org and fill testing mailto uris; as we created the chat in the previous step, it should not ask Chat with … but directly open the chat
  • - *
  • email address with body - should open a chat with abc@example.org, draft this is a test
  • - *
  • email address with subject and body - should open a chat with abc@example.org, draft testing mailto uris <newline> this is a test
  • - *
  • HTML encoding - should open a chat with info@example.org
  • - *
  • more HTML encoding - should open a chat with simplebot@example.org, draft !web https://duckduckgo.com/lite?q=duck%20it
  • - *
  • no email, just subject&body - this should let you choose a chat and create a draft bla <newline> blub there
  • + *
  • Just an email address - should open a chat with + * abc@example.org (and maybe ask whether a chat should be created if it does + * not exist already) + *
  • email address with + * subject - should open a chat with abc@example.org and fill + * testing mailto uris; as we created the chat in the previous step, it should not + * ask Chat with … but directly open the chat + *
  • email address with body - + * should open a chat with abc@example.org, draft this is a test + *
  • email + * address with subject and body - should open a chat with abc@example.org, + * draft testing mailto uris <newline> this is a test + *
  • HTML encoding - should open a chat with + * info@example.org + *
  • more + * HTML encoding - should open a chat with simplebot@example.org, draft + * !web https://duckduckgo.com/lite?q=duck%20it + *
  • no email, just subject&body - this + * should let you choose a chat and create a draft bla <newline> + * blub there *
*/ @Test @@ -140,7 +164,8 @@ public void testShareFromLink() { openLink("mailto:abc@example.org?subject=testing%20mailto%20uris"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("testing mailto uris"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("testing mailto uris"))); openLink("mailto:abc@example.org?body=this%20is%20a%20test"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); @@ -148,17 +173,22 @@ public void testShareFromLink() { openLink("mailto:abc@example.org?subject=testing%20mailto%20uris&body=this%20is%20a%20test"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("testing mailto uris\nthis is a test"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("testing mailto uris\nthis is a test"))); openLink("mailto:%20abc@example.org"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - openLink("mailto:abc@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it"); + openLink( + "mailto:abc@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("!web https://duckduckgo.com/lite?q=duck%20it"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("!web https://duckduckgo.com/lite?q=duck%20it"))); openLink("mailto:?subject=bla&body=blub"); - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("bla\nblub"))); } @@ -170,49 +200,61 @@ private void openLink(String link) { } /** + * + * *
    - *
  • Open Saved Messages chat (could be any other chat too)
  • - *
  • Go to another app and share some text to DC
  • - *
  • In DC select Saved Messages. Edit the shared text if you like. Don't hit the Send button.
  • - *
  • Leave DC
  • - *
  • Open DC again from the "Recent apps"
  • - *
  • Check that your draft is still there
  • + *
  • Open Saved Messages chat (could be any other chat too) + *
  • Go to another app and share some text to DC + *
  • In DC select Saved Messages. Edit the shared text if you like. Don't hit the + * Send button. + *
  • Leave DC + *
  • Open DC again from the "Recent apps" + *
  • Check that your draft is still there *
*/ @Test public void testOpenAgainFromRecents() { // Open a chat - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); // Share some text to DC Intent i = new Intent(Intent.ACTION_SEND); i.putExtra(Intent.EXTRA_TEXT, "Veeery important draft"); - i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); + i.setComponent( + new ComponentName( + getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); activityRule.getScenario().onActivity(a -> a.startActivity(i)); // In DC, select the same chat you opened before - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); // Leave DC and go back to the previous activity pressBack(); // Here, we can't exactly replicate the "steps to reproduce". Previously, the other activity // stayed open in the background, but since it doesn't anymore, we need to open it again: - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); // Check that the draft is still there // Util.sleep(2000); // Uncomment for debugging - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("Veeery important draft"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("Veeery important draft"))); } /** * Regression test: * - * If you save your contacts's emails in the contacts app of the phone, there are buttons to call - * them and also to write an email to them. + *

If you save your contacts's emails in the contacts app of the phone, there are buttons to + * call them and also to write an email to them. * - * If you click the email button, ArcaneChat opened but instead of opening a chat with that contact, - * the chat list was show and "share with" was displayed at the top + *

If you click the email button, Delta Chat opened but instead of opening a chat with that + * contact, the chat list was show and "share with" was displayed at the top */ @Test public void testOpenChatFromContacts() { diff --git a/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java b/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java index 880de597e..e5cc22677 100644 --- a/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java +++ b/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java @@ -1,6 +1,5 @@ package com.b44t.messenger.uitests.online; - import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.replaceText; @@ -11,13 +10,10 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText; import android.text.TextUtils; - import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.TestUtils; - import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -30,14 +26,16 @@ @LargeTest public class OnboardingTest { @Rule - public ActivityScenarioRule activityRule = TestUtils.getOnlineActivityRule(WelcomeActivity.class); + public ActivityScenarioRule activityRule = + TestUtils.getOnlineActivityRule(WelcomeActivity.class); @Test public void testAccountCreation() { if (TextUtils.isEmpty(BuildConfig.TEST_ADDR) || TextUtils.isEmpty(BuildConfig.TEST_MAIL_PW)) { - throw new RuntimeException("You need to set TEST_ADDR and TEST_MAIL_PW; " + - "either in gradle.properties or via an environment variable. " + - "See README.md for more details."); + throw new RuntimeException( + "You need to set TEST_ADDR and TEST_MAIL_PW; " + + "either in gradle.properties or via an environment variable. " + + "See README.md for more details."); } onView(withText(R.string.scan_invitation_code)).check(matches(isClickable())); onView(withText(R.string.import_backup_title)).check(matches(isClickable())); diff --git a/src/debug/res/drawable/ic_launcher_foreground.xml b/src/debug/res/drawable/ic_launcher_foreground.xml index 6be6d473f..b1fe6fe18 100644 --- a/src/debug/res/drawable/ic_launcher_foreground.xml +++ b/src/debug/res/drawable/ic_launcher_foreground.xml @@ -1,17 +1,19 @@ - - - - - + + + + diff --git a/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml index 8111c6f84..0c42a4264 100644 --- a/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,6 @@ - - - + + + diff --git a/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml index 5005f3469..d6cd2ee93 100644 --- a/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + diff --git a/src/foss/AndroidManifest.xml b/src/foss/AndroidManifest.xml index 498c668a4..f81ce6d34 100644 --- a/src/foss/AndroidManifest.xml +++ b/src/foss/AndroidManifest.xml @@ -1,14 +1,16 @@ - + - + - - - + + + diff --git a/src/foss/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java b/src/foss/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java index dfb2f61c1..c14f2ce8e 100644 --- a/src/foss/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java +++ b/src/foss/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.notifications; import android.content.Context; - import androidx.annotation.Nullable; /* @@ -10,6 +9,11 @@ */ public class FcmReceiveService { public static void register(Context context) {} + public static void waitForRegisterFinished() {} - @Nullable public static String getToken() { return null; } + + @Nullable + public static String getToken() { + return null; + } } diff --git a/src/gplay/AndroidManifest.xml b/src/gplay/AndroidManifest.xml index b0d786db4..904f0fd56 100644 --- a/src/gplay/AndroidManifest.xml +++ b/src/gplay/AndroidManifest.xml @@ -1,34 +1,42 @@ - + - - + + - - - + + + - - + + - - - - - + + + + + - - - + + + diff --git a/src/gplay/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java b/src/gplay/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java index 376ffc9d5..9ff6e33c7 100644 --- a/src/gplay/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java +++ b/src/gplay/java/org/thoughtcrime/securesms/notifications/FcmReceiveService.java @@ -1,20 +1,16 @@ package org.thoughtcrime.securesms.notifications; import android.content.Context; -import android.os.Build; import android.text.TextUtils; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; - import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; - import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.service.FetchForegroundService; @@ -35,36 +31,38 @@ public static void register(Context context) { return; } - Util.runOnAnyBackgroundThread(() -> { - final String rawToken; - - try { - synchronized (INIT_LOCK) { - if (!initialized) { - // manual init: read tokens from `./google-services.json`; - // automatic init disabled in AndroidManifest.xml to skip FCM code completely. - FirebaseApp.initializeApp(context); + Util.runOnAnyBackgroundThread( + () -> { + final String rawToken; + + try { + synchronized (INIT_LOCK) { + if (!initialized) { + // manual init: read tokens from `./google-services.json`; + // automatic init disabled in AndroidManifest.xml to skip FCM code completely. + FirebaseApp.initializeApp(context); + } + initialized = true; + } + rawToken = Tasks.await(FirebaseMessaging.getInstance().getToken()); + } catch (Exception e) { + // we're here usually when FCM is not available and initializeApp() or getToken() + // failed. + Log.w(TAG, "cannot get FCM token for " + BuildConfig.APPLICATION_ID + ": " + e); + triedRegistering = true; + return; + } + if (TextUtils.isEmpty(rawToken)) { + Log.w(TAG, "got empty FCM token for " + BuildConfig.APPLICATION_ID); + triedRegistering = true; + return; } - initialized = true; - } - rawToken = Tasks.await(FirebaseMessaging.getInstance().getToken()); - } catch (Exception e) { - // we're here usually when FCM is not available and initializeApp() or getToken() failed. - Log.w(TAG, "cannot get FCM token for " + BuildConfig.APPLICATION_ID + ": " + e); - triedRegistering = true; - return; - } - if (TextUtils.isEmpty(rawToken)) { - Log.w(TAG, "got empty FCM token for " + BuildConfig.APPLICATION_ID); - triedRegistering = true; - return; - } - prefixedToken = addPrefix(rawToken); - Log.i(TAG, "FCM token: " + prefixedToken); - ApplicationContext.getDcAccounts().setPushDeviceToken(prefixedToken); - triedRegistering = true; - }); + prefixedToken = addPrefix(rawToken); + Log.i(TAG, "FCM token: " + prefixedToken); + ApplicationContext.getDcAccounts().setPushDeviceToken(prefixedToken); + triedRegistering = true; + }); } // wait a until FCM registration got a token or not. @@ -98,7 +96,8 @@ public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { // ForegroundServiceStartNotAllowedException. // So, it's recommended to check the result of RemoteMessage.getPriority() and // confirm it's PRIORITY_HIGH() before attempting to start a foreground service. - // source: https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start + // source: + // https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start if (remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH) { FetchForegroundService.start(this); } else { diff --git a/src/main/java/com/b44t/messenger/DcAccounts.java b/src/main/java/com/b44t/messenger/DcAccounts.java index 27ec0a981..253df4c7f 100644 --- a/src/main/java/com/b44t/messenger/DcAccounts.java +++ b/src/main/java/com/b44t/messenger/DcAccounts.java @@ -2,64 +2,92 @@ public class DcAccounts { - public DcAccounts(String dir, DcEventChannel channel) { - accountsCPtr = createAccountsCPtr(dir, channel); - if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer"); - } + public DcAccounts(String dir, DcEventChannel channel) { + accountsCPtr = createAccountsCPtr(dir, channel); + if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer"); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unref(); + } - @Override - protected void finalize() throws Throwable { - super.finalize(); - unref(); + public void unref() { + if (accountsCPtr != 0) { + unrefAccountsCPtr(); + accountsCPtr = 0; } + } + + public DcEventEmitter getEventEmitter() { + return new DcEventEmitter(getEventEmitterCPtr()); + } - public void unref() { - if (accountsCPtr != 0) { - unrefAccountsCPtr(); - accountsCPtr = 0; - } + public DcJsonrpcInstance getJsonrpcInstance() { + return new DcJsonrpcInstance(getJsonrpcInstanceCPtr()); + } + + public void startIo() { + for (int accountId : getAll()) { + DcContext acc = getAccount(accountId); + if (acc.isEnabled()) { + acc.startIo(); + } } + } + ; + + public native void startIo2(); + + public native void stopIo(); + + public native void maybeNetwork(); + + public native void setPushDeviceToken(String token); + + public native boolean backgroundFetch(int timeoutSeconds); + + public native void stopBackgroundFetch(); + + public native int migrateAccount(String dbfile); + + public native boolean removeAccount(int accountId); + + public native int[] getAll(); + + public DcContext getAccount(int accountId) { + return new DcContext(getAccountCPtr(accountId)); + } + + public DcContext getSelectedAccount() { + return new DcContext(getSelectedAccountCPtr()); + } + + public native boolean selectAccount(int accountId); + + // working with raw c-data + private long accountsCPtr; // CAVE: the name is referenced in the JNI + + private native long createAccountsCPtr(String dir, DcEventChannel channel); + + private native void unrefAccountsCPtr(); + + private native long getEventEmitterCPtr(); + + private native long getJsonrpcInstanceCPtr(); + + private native long getAccountCPtr(int accountId); + + private native long getSelectedAccountCPtr(); - public DcEventEmitter getEventEmitter () { return new DcEventEmitter(getEventEmitterCPtr()); } - public DcJsonrpcInstance getJsonrpcInstance () { return new DcJsonrpcInstance(getJsonrpcInstanceCPtr()); } - public void startIo () { - for (int accountId : getAll()) { - DcContext acc = getAccount(accountId); - if (acc.isEnabled()) { - acc.startIo(); - } - } - }; - public native void startIo2 (); - public native void stopIo (); - public native void maybeNetwork (); - public native void setPushDeviceToken (String token); - public native boolean backgroundFetch (int timeoutSeconds); - public native void stopBackgroundFetch (); - - public native int migrateAccount (String dbfile); - public native boolean removeAccount (int accountId); - public native int[] getAll (); - public DcContext getAccount (int accountId) { return new DcContext(getAccountCPtr(accountId)); } - public DcContext getSelectedAccount () { return new DcContext(getSelectedAccountCPtr()); } - public native boolean selectAccount (int accountId); - - // working with raw c-data - private long accountsCPtr; // CAVE: the name is referenced in the JNI - private native long createAccountsCPtr (String dir, DcEventChannel channel); - private native void unrefAccountsCPtr (); - private native long getEventEmitterCPtr (); - private native long getJsonrpcInstanceCPtr (); - private native long getAccountCPtr (int accountId); - private native long getSelectedAccountCPtr (); - - public boolean isAllChatmail() { - for (int accountId : getAll()) { - DcContext dcContext = getAccount(accountId); - if (!dcContext.isChatmail()) { - return false; - } - } - return true; + public boolean isAllChatmail() { + for (int accountId : getAll()) { + DcContext dcContext = getAccount(accountId); + if (!dcContext.isChatmail()) { + return false; + } } + return true; + } } diff --git a/src/main/java/com/b44t/messenger/DcBackupProvider.java b/src/main/java/com/b44t/messenger/DcBackupProvider.java index 459be4a36..2461357b3 100644 --- a/src/main/java/com/b44t/messenger/DcBackupProvider.java +++ b/src/main/java/com/b44t/messenger/DcBackupProvider.java @@ -2,31 +2,35 @@ public class DcBackupProvider { - public DcBackupProvider(long backupProviderCPtr) { - this.backupProviderCPtr = backupProviderCPtr; - } + public DcBackupProvider(long backupProviderCPtr) { + this.backupProviderCPtr = backupProviderCPtr; + } - public boolean isOk() { - return backupProviderCPtr != 0; - } + public boolean isOk() { + return backupProviderCPtr != 0; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unref(); - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unref(); + } - public void unref() { - if (backupProviderCPtr != 0) { - unrefBackupProviderCPtr(); - backupProviderCPtr = 0; - } + public void unref() { + if (backupProviderCPtr != 0) { + unrefBackupProviderCPtr(); + backupProviderCPtr = 0; } + } + + public native String getQr(); + + public native String getQrSvg(); + + public native void waitForReceiver(); - public native String getQr (); - public native String getQrSvg (); - public native void waitForReceiver (); + // working with raw c-data + private long backupProviderCPtr; // CAVE: the name is referenced in the JNI - // working with raw c-data - private long backupProviderCPtr; // CAVE: the name is referenced in the JNI - private native void unrefBackupProviderCPtr(); + private native void unrefBackupProviderCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcChat.java b/src/main/java/com/b44t/messenger/DcChat.java index 90497a797..8f32f9725 100644 --- a/src/main/java/com/b44t/messenger/DcChat.java +++ b/src/main/java/com/b44t/messenger/DcChat.java @@ -1,76 +1,109 @@ package com.b44t.messenger; +import org.thoughtcrime.securesms.util.Util; + public class DcChat { - public static final int DC_CHAT_TYPE_UNDEFINED = 0; - public static final int DC_CHAT_TYPE_SINGLE = 100; - public static final int DC_CHAT_TYPE_GROUP = 120; - public static final int DC_CHAT_TYPE_MAILINGLIST = 140; - public static final int DC_CHAT_TYPE_OUT_BROADCAST = 160; - public static final int DC_CHAT_TYPE_IN_BROADCAST = 165; + public static final int DC_CHAT_TYPE_UNDEFINED = 0; + public static final int DC_CHAT_TYPE_SINGLE = 100; + public static final int DC_CHAT_TYPE_GROUP = 120; + public static final int DC_CHAT_TYPE_MAILINGLIST = 140; + public static final int DC_CHAT_TYPE_OUT_BROADCAST = 160; + public static final int DC_CHAT_TYPE_IN_BROADCAST = 165; - public static final int DC_CHAT_NO_CHAT = 0; - public final static int DC_CHAT_ID_ARCHIVED_LINK = 6; - public final static int DC_CHAT_ID_ALLDONE_HINT = 7; - public final static int DC_CHAT_ID_LAST_SPECIAL = 9; + public static final int DC_CHAT_NO_CHAT = 0; + public static final int DC_CHAT_ID_ARCHIVED_LINK = 6; + public static final int DC_CHAT_ID_ALLDONE_HINT = 7; + public static final int DC_CHAT_ID_LAST_SPECIAL = 9; - public final static int DC_CHAT_VISIBILITY_NORMAL = 0; - public final static int DC_CHAT_VISIBILITY_ARCHIVED = 1; - public final static int DC_CHAT_VISIBILITY_PINNED = 2; + public static final int DC_CHAT_VISIBILITY_NORMAL = 0; + public static final int DC_CHAT_VISIBILITY_ARCHIVED = 1; + public static final int DC_CHAT_VISIBILITY_PINNED = 2; - private int accountId; + private int accountId; - public DcChat(int accountId, long chatCPtr) { - this.accountId = accountId; - this.chatCPtr = chatCPtr; - } + public DcChat(int accountId, long chatCPtr) { + this.accountId = accountId; + this.chatCPtr = chatCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefChatCPtr(); - chatCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefChatCPtr(); + chatCPtr = 0; + } - public int getAccountId () { return accountId; } - public native int getId (); - public native int getType (); - public native int getVisibility (); - public native String getName (); - public native String getMailinglistAddr(); - public native String getProfileImage (); - public native int getColor (); - public native boolean isEncrypted (); - public native boolean isUnpromoted (); - public native boolean isSelfTalk (); - public native boolean isDeviceTalk (); - public native boolean canSend (); - public native boolean isSendingLocations(); - public native boolean isMuted (); - public native boolean isContactRequest (); - - - // aliases and higher-level tools - - public boolean isMultiUser() { - int type = getType(); - return type != DC_CHAT_TYPE_SINGLE; - } + public int getAccountId() { + return accountId; + } - public boolean isMailingList() { - return getType() == DC_CHAT_TYPE_MAILINGLIST; - } + public native int getId(); - public boolean isInBroadcast() { - return getType() == DC_CHAT_TYPE_IN_BROADCAST; - } - public boolean isOutBroadcast() { - return getType() == DC_CHAT_TYPE_OUT_BROADCAST; + public native int getType(); + + public native int getVisibility(); + + public native String getName(); + + public native String getMailinglistAddr(); + + public native String getProfileImage(); + + public native int getColor(); + + public native boolean isEncrypted(); + + public native boolean isUnpromoted(); + + public native boolean isSelfTalk(); + + public native boolean isDeviceTalk(); + + public native boolean canSend(); + + public native boolean isSendingLocations(); + + public native boolean isMuted(); + + public native boolean isContactRequest(); + + // aliases and higher-level tools + + public boolean isMultiUser() { + int type = getType(); + return type != DC_CHAT_TYPE_SINGLE; + } + + public boolean shallLeaveBeforeDelete(DcContext dcContext) { + if (isInBroadcast()) { + final int[] members = dcContext.getChatContacts(getId()); + return Util.contains(members, DcContact.DC_CONTACT_ID_SELF); + } else if (isMultiUser() && isEncrypted() && canSend() && !isOutBroadcast()) { + return true; } + return false; + } + + public boolean isMailingList() { + return getType() == DC_CHAT_TYPE_MAILINGLIST; + } + + public boolean isInBroadcast() { + return getType() == DC_CHAT_TYPE_IN_BROADCAST; + } + + public boolean isOutBroadcast() { + return getType() == DC_CHAT_TYPE_OUT_BROADCAST; + } + + // working with raw c-data - // working with raw c-data + private long chatCPtr; // CAVE: the name is referenced in the JNI - private long chatCPtr; // CAVE: the name is referenced in the JNI - private native void unrefChatCPtr(); - public long getChatCPtr () { return chatCPtr; } + private native void unrefChatCPtr(); + public long getChatCPtr() { + return chatCPtr; + } } diff --git a/src/main/java/com/b44t/messenger/DcChatlist.java b/src/main/java/com/b44t/messenger/DcChatlist.java index 4a250d15d..7180758dd 100644 --- a/src/main/java/com/b44t/messenger/DcChatlist.java +++ b/src/main/java/com/b44t/messenger/DcChatlist.java @@ -2,45 +2,64 @@ public class DcChatlist { - private int accountId; - - public DcChatlist(int accountId, long chatlistCPtr) { - this.accountId = accountId; - this.chatlistCPtr = chatlistCPtr; - } - - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefChatlistCPtr(); - chatlistCPtr = 0; - } - - public int getAccountId() { return accountId; } - public native int getCnt (); - public native int getChatId (int index); - public DcChat getChat (int index) { return new DcChat(accountId, getChatCPtr(index)); } - public native int getMsgId (int index); - public DcMsg getMsg (int index) { return new DcMsg(getMsgCPtr(index)); } - public DcLot getSummary(int index, DcChat chat) { return new DcLot(getSummaryCPtr(index, chat==null? 0 : chat.getChatCPtr())); } - - public class Item { - public DcLot summary; - public int msgId; - public int chatId; - } - - public Item getItem(int index) { - Item item = new Item(); - item.summary = getSummary(index, null); - item.msgId = getMsgId(index); - item.chatId = getChatId(index); - return item; - } - - // working with raw c-data - private long chatlistCPtr; // CAVE: the name is referenced in the JNI - private native void unrefChatlistCPtr(); - private native long getChatCPtr (int index); - private native long getMsgCPtr (int index); - private native long getSummaryCPtr (int index, long chatCPtr); + private int accountId; + + public DcChatlist(int accountId, long chatlistCPtr) { + this.accountId = accountId; + this.chatlistCPtr = chatlistCPtr; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefChatlistCPtr(); + chatlistCPtr = 0; + } + + public int getAccountId() { + return accountId; + } + + public native int getCnt(); + + public native int getChatId(int index); + + public DcChat getChat(int index) { + return new DcChat(accountId, getChatCPtr(index)); + } + + public native int getMsgId(int index); + + public DcMsg getMsg(int index) { + return new DcMsg(getMsgCPtr(index)); + } + + public DcLot getSummary(int index, DcChat chat) { + return new DcLot(getSummaryCPtr(index, chat == null ? 0 : chat.getChatCPtr())); + } + + public class Item { + public DcLot summary; + public int msgId; + public int chatId; + } + + public Item getItem(int index) { + Item item = new Item(); + item.summary = getSummary(index, null); + item.msgId = getMsgId(index); + item.chatId = getChatId(index); + return item; + } + + // working with raw c-data + private long chatlistCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefChatlistCPtr(); + + private native long getChatCPtr(int index); + + private native long getMsgCPtr(int index); + + private native long getSummaryCPtr(int index, long chatCPtr); } diff --git a/src/main/java/com/b44t/messenger/DcContact.java b/src/main/java/com/b44t/messenger/DcContact.java index c1db787df..b0b358dec 100644 --- a/src/main/java/com/b44t/messenger/DcContact.java +++ b/src/main/java/com/b44t/messenger/DcContact.java @@ -2,67 +2,82 @@ public class DcContact { - public final static int DC_CONTACT_ID_SELF = 1; - public final static int DC_CONTACT_ID_INFO = 2; - public final static int DC_CONTACT_ID_DEVICE = 5; - public final static int DC_CONTACT_ID_LAST_SPECIAL = 9; - public final static int DC_CONTACT_ID_NEW_CLASSIC_CONTACT= -1; // used by the UI, not valid to the core - public final static int DC_CONTACT_ID_NEW_GROUP = -2; // - " - - public final static int DC_CONTACT_ID_ADD_MEMBER = -3; // - " - - public final static int DC_CONTACT_ID_QR_INVITE = -4; // - " - - public final static int DC_CONTACT_ID_NEW_BROADCAST = -5; // - " - - public final static int DC_CONTACT_ID_ADD_ACCOUNT = -6; // - " - - public final static int DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP = -7; // - " - - - public DcContact(long contactCPtr) { - this.contactCPtr = contactCPtr; - } + public static final int DC_CONTACT_ID_SELF = 1; + public static final int DC_CONTACT_ID_INFO = 2; + public static final int DC_CONTACT_ID_DEVICE = 5; + public static final int DC_CONTACT_ID_LAST_SPECIAL = 9; + public static final int DC_CONTACT_ID_NEW_CLASSIC_CONTACT = + -1; // used by the UI, not valid to the core + public static final int DC_CONTACT_ID_NEW_GROUP = -2; // - " - + public static final int DC_CONTACT_ID_ADD_MEMBER = -3; // - " - + public static final int DC_CONTACT_ID_QR_INVITE = -4; // - " - + public static final int DC_CONTACT_ID_NEW_BROADCAST = -5; // - " - + public static final int DC_CONTACT_ID_ADD_ACCOUNT = -6; // - " - + public static final int DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP = -7; // - " - + + public DcContact(long contactCPtr) { + this.contactCPtr = contactCPtr; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefContactCPtr(); + contactCPtr = 0; + } - @Override - protected void finalize() throws Throwable { - super.finalize(); - unrefContactCPtr(); - contactCPtr = 0; + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof DcContact)) { + return false; } + DcContact that = (DcContact) other; + return this.getId() == that.getId(); + } - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof DcContact)) { - return false; - } + @Override + public int hashCode() { + return this.getId(); + } - DcContact that = (DcContact) other; - return this.getId()==that.getId(); - } + @Override + public String toString() { + return getAddr(); + } - @Override - public int hashCode() { - return this.getId(); - } + public native int getId(); - @Override - public String toString() { - return getAddr(); - } + public native String getName(); + + public native String getAuthName(); + + public native String getDisplayName(); + + public native String getAddr(); + + public native String getProfileImage(); + + public native int getColor(); + + public native String getStatus(); + + public native long getLastSeen(); + + public native boolean wasSeenRecently(); + + public native boolean isBlocked(); + + public native boolean isVerified(); + + public native boolean isKeyContact(); + + public native int getVerifierId(); + + public native boolean isBot(); + + // working with raw c-data + private long contactCPtr; // CAVE: the name is referenced in the JNI - public native int getId (); - public native String getName (); - public native String getAuthName (); - public native String getDisplayName (); - public native String getAddr (); - public native String getProfileImage(); - public native int getColor (); - public native String getStatus (); - public native long getLastSeen (); - public native boolean wasSeenRecently(); - public native boolean isBlocked (); - public native boolean isVerified (); - public native boolean isKeyContact (); - public native int getVerifierId (); - public native boolean isBot (); - - // working with raw c-data - private long contactCPtr; // CAVE: the name is referenced in the JNI - private native void unrefContactCPtr(); + private native void unrefContactCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcContext.java b/src/main/java/com/b44t/messenger/DcContext.java index 27132c2a4..722b76c62 100644 --- a/src/main/java/com/b44t/messenger/DcContext.java +++ b/src/main/java/com/b44t/messenger/DcContext.java @@ -2,289 +2,415 @@ public class DcContext { - public final static int DC_EVENT_INFO = 100; - public final static int DC_EVENT_WARNING = 300; - public final static int DC_EVENT_ERROR = 400; - public final static int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410; - public final static int DC_EVENT_MSGS_CHANGED = 2000; - public final static int DC_EVENT_REACTIONS_CHANGED = 2001; - public final static int DC_EVENT_INCOMING_REACTION = 2002; - public final static int DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003; - public final static int DC_EVENT_INCOMING_MSG = 2005; - public final static int DC_EVENT_MSGS_NOTICED = 2008; - public final static int DC_EVENT_MSG_DELIVERED = 2010; - public final static int DC_EVENT_MSG_FAILED = 2012; - public final static int DC_EVENT_MSG_READ = 2015; - public final static int DC_EVENT_MSG_DELETED = 2016; - public final static int DC_EVENT_CHAT_MODIFIED = 2020; - public final static int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021; - public final static int DC_EVENT_CHAT_DELETED = 2023; - public final static int DC_EVENT_CONTACTS_CHANGED = 2030; - public final static int DC_EVENT_LOCATION_CHANGED = 2035; - public final static int DC_EVENT_CONFIGURE_PROGRESS = 2041; - public final static int DC_EVENT_IMEX_PROGRESS = 2051; - public final static int DC_EVENT_IMEX_FILE_WRITTEN = 2052; - public final static int DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060; - public final static int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061; - public final static int DC_EVENT_CONNECTIVITY_CHANGED = 2100; - public final static int DC_EVENT_SELFAVATAR_CHANGED = 2110; - public final static int DC_EVENT_WEBXDC_STATUS_UPDATE = 2120; - public final static int DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121; - public final static int DC_EVENT_WEBXDC_REALTIME_DATA = 2150; - public final static int DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200; - public final static int DC_EVENT_INCOMING_CALL = 2550; - public final static int DC_EVENT_INCOMING_CALL_ACCEPTED = 2560; - public final static int DC_EVENT_OUTGOING_CALL_ACCEPTED = 2570; - public final static int DC_EVENT_CALL_ENDED = 2580; - public final static int DC_EVENT_TRANSPORTS_MODIFIED = 2600; - - public final static int DC_IMEX_EXPORT_SELF_KEYS = 1; - public final static int DC_IMEX_IMPORT_SELF_KEYS = 2; - public final static int DC_IMEX_EXPORT_BACKUP = 11; - public final static int DC_IMEX_IMPORT_BACKUP = 12; - - public final static int DC_GCL_VERIFIED_ONLY = 1; - public final static int DC_GCL_ADD_SELF = 2; - public final static int DC_GCL_ADDRESS = 0x04; - public final static int DC_GCL_ARCHIVED_ONLY = 0x01; - public final static int DC_GCL_NO_SPECIALS = 0x02; - public final static int DC_GCL_ADD_ALLDONE_HINT = 0x04; - public final static int DC_GCL_FOR_FORWARDING = 0x08; - - public final static int DC_GCM_ADDDAYMARKER = 0x01; - - public final static int DC_QR_ASK_VERIFYCONTACT = 200; - public final static int DC_QR_ASK_VERIFYGROUP = 202; - public final static int DC_QR_ASK_JOIN_BROADCAST= 204; - public final static int DC_QR_FPR_OK = 210; - public final static int DC_QR_FPR_MISMATCH = 220; - public final static int DC_QR_FPR_WITHOUT_ADDR = 230; - public final static int DC_QR_ACCOUNT = 250; - public final static int DC_QR_BACKUP2 = 252; - public final static int DC_QR_BACKUP_TOO_NEW = 255; - public final static int DC_QR_WEBRTC = 260; - public final static int DC_QR_PROXY = 271; - public final static int DC_QR_ADDR = 320; - public final static int DC_QR_TEXT = 330; - public final static int DC_QR_URL = 332; - public final static int DC_QR_ERROR = 400; - public final static int DC_QR_WITHDRAW_VERIFYCONTACT = 500; - public final static int DC_QR_WITHDRAW_VERIFYGROUP = 502; - public final static int DC_QR_WITHDRAW_JOINBROADCAST = 504; - public final static int DC_QR_REVIVE_VERIFYCONTACT = 510; - public final static int DC_QR_REVIVE_VERIFYGROUP = 512; - public final static int DC_QR_REVIVE_JOINBROADCAST = 514; - public final static int DC_QR_LOGIN = 520; - - public final static int DC_SOCKET_AUTO = 0; - public final static int DC_SOCKET_SSL = 1; - public final static int DC_SOCKET_STARTTLS = 2; - public final static int DC_SOCKET_PLAIN = 3; - - public final static int DC_SHOW_EMAILS_OFF = 0; - public final static int DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1; - public final static int DC_SHOW_EMAILS_ALL = 2; - - public final static int DC_MEDIA_QUALITY_BALANCED = 0; - public final static int DC_MEDIA_QUALITY_WORSE = 1; - - public final static int DC_CONNECTIVITY_NOT_CONNECTED = 1000; - public final static int DC_CONNECTIVITY_CONNECTING = 2000; - public final static int DC_CONNECTIVITY_WORKING = 3000; - public final static int DC_CONNECTIVITY_CONNECTED = 4000; - - private static final String CONFIG_ACCOUNT_ENABLED = "ui.enabled"; - private static final String CONFIG_MUTE_MENTIONS_IF_MUTED = "ui.mute_mentions_if_muted"; - - // when using DcAccounts, use Rpc.addAccount() instead - public DcContext(String osName, String dbfile) { - contextCPtr = createContextCPtr(osName, dbfile); + public static final int DC_EVENT_INFO = 100; + public static final int DC_EVENT_WARNING = 300; + public static final int DC_EVENT_ERROR = 400; + public static final int DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410; + public static final int DC_EVENT_MSGS_CHANGED = 2000; + public static final int DC_EVENT_REACTIONS_CHANGED = 2001; + public static final int DC_EVENT_INCOMING_REACTION = 2002; + public static final int DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003; + public static final int DC_EVENT_INCOMING_MSG = 2005; + public static final int DC_EVENT_MSGS_NOTICED = 2008; + public static final int DC_EVENT_MSG_DELIVERED = 2010; + public static final int DC_EVENT_MSG_FAILED = 2012; + public static final int DC_EVENT_MSG_READ = 2015; + public static final int DC_EVENT_MSG_DELETED = 2016; + public static final int DC_EVENT_CHAT_MODIFIED = 2020; + public static final int DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021; + public static final int DC_EVENT_CHAT_DELETED = 2023; + public static final int DC_EVENT_CONTACTS_CHANGED = 2030; + public static final int DC_EVENT_LOCATION_CHANGED = 2035; + public static final int DC_EVENT_CONFIGURE_PROGRESS = 2041; + public static final int DC_EVENT_IMEX_PROGRESS = 2051; + public static final int DC_EVENT_IMEX_FILE_WRITTEN = 2052; + public static final int DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060; + public static final int DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061; + public static final int DC_EVENT_CONNECTIVITY_CHANGED = 2100; + public static final int DC_EVENT_SELFAVATAR_CHANGED = 2110; + public static final int DC_EVENT_WEBXDC_STATUS_UPDATE = 2120; + public static final int DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121; + public static final int DC_EVENT_WEBXDC_REALTIME_DATA = 2150; + public static final int DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200; + public static final int DC_EVENT_INCOMING_CALL = 2550; + public static final int DC_EVENT_INCOMING_CALL_ACCEPTED = 2560; + public static final int DC_EVENT_OUTGOING_CALL_ACCEPTED = 2570; + public static final int DC_EVENT_CALL_ENDED = 2580; + public static final int DC_EVENT_TRANSPORTS_MODIFIED = 2600; + + public static final int DC_IMEX_EXPORT_SELF_KEYS = 1; + public static final int DC_IMEX_IMPORT_SELF_KEYS = 2; + public static final int DC_IMEX_EXPORT_BACKUP = 11; + public static final int DC_IMEX_IMPORT_BACKUP = 12; + + public static final int DC_GCL_VERIFIED_ONLY = 1; + public static final int DC_GCL_ADD_SELF = 2; + public static final int DC_GCL_ADDRESS = 0x04; + public static final int DC_GCL_ARCHIVED_ONLY = 0x01; + public static final int DC_GCL_NO_SPECIALS = 0x02; + public static final int DC_GCL_ADD_ALLDONE_HINT = 0x04; + public static final int DC_GCL_FOR_FORWARDING = 0x08; + + public static final int DC_GCM_ADDDAYMARKER = 0x01; + + public static final int DC_QR_ASK_VERIFYCONTACT = 200; + public static final int DC_QR_ASK_VERIFYGROUP = 202; + public static final int DC_QR_ASK_JOIN_BROADCAST = 204; + public static final int DC_QR_FPR_OK = 210; + public static final int DC_QR_FPR_MISMATCH = 220; + public static final int DC_QR_FPR_WITHOUT_ADDR = 230; + public static final int DC_QR_ACCOUNT = 250; + public static final int DC_QR_BACKUP2 = 252; + public static final int DC_QR_BACKUP_TOO_NEW = 255; + public static final int DC_QR_WEBRTC = 260; + public static final int DC_QR_PROXY = 271; + public static final int DC_QR_ADDR = 320; + public static final int DC_QR_TEXT = 330; + public static final int DC_QR_URL = 332; + public static final int DC_QR_ERROR = 400; + public static final int DC_QR_WITHDRAW_VERIFYCONTACT = 500; + public static final int DC_QR_WITHDRAW_VERIFYGROUP = 502; + public static final int DC_QR_WITHDRAW_JOINBROADCAST = 504; + public static final int DC_QR_REVIVE_VERIFYCONTACT = 510; + public static final int DC_QR_REVIVE_VERIFYGROUP = 512; + public static final int DC_QR_REVIVE_JOINBROADCAST = 514; + public static final int DC_QR_LOGIN = 520; + + public static final int DC_SOCKET_AUTO = 0; + public static final int DC_SOCKET_SSL = 1; + public static final int DC_SOCKET_STARTTLS = 2; + public static final int DC_SOCKET_PLAIN = 3; + + public static final int DC_SHOW_EMAILS_OFF = 0; + public static final int DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1; + public static final int DC_SHOW_EMAILS_ALL = 2; + + public static final int DC_MEDIA_QUALITY_BALANCED = 0; + public static final int DC_MEDIA_QUALITY_WORSE = 1; + + public static final int DC_CONNECTIVITY_NOT_CONNECTED = 1000; + public static final int DC_CONNECTIVITY_CONNECTING = 2000; + public static final int DC_CONNECTIVITY_WORKING = 3000; + public static final int DC_CONNECTIVITY_CONNECTED = 4000; + + private static final String CONFIG_ACCOUNT_ENABLED = "ui.enabled"; + private static final String CONFIG_MUTE_MENTIONS_IF_MUTED = "ui.mute_mentions_if_muted"; + + // when using DcAccounts, use Rpc.addAccount() instead + public DcContext(String osName, String dbfile) { + contextCPtr = createContextCPtr(osName, dbfile); + } + + public DcContext(long contextCPtr) { + this.contextCPtr = contextCPtr; + } + + public boolean isOk() { + return contextCPtr != 0; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (contextCPtr != 0) { + unrefContextCPtr(); + contextCPtr = 0; } + } - public DcContext(long contextCPtr) { - this.contextCPtr = contextCPtr; - } + public native int getAccountId(); - public boolean isOk() { - return contextCPtr != 0; - } + // when using DcAccounts, use DcAccounts.getEventEmitter() instead + public DcEventEmitter getEventEmitter() { + return new DcEventEmitter(getEventEmitterCPtr()); + } - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (contextCPtr != 0) { - unrefContextCPtr(); - contextCPtr = 0; - } - } + public native void setStockTranslation(int stockId, String translation); - public native int getAccountId (); - - // when using DcAccounts, use DcAccounts.getEventEmitter() instead - public DcEventEmitter getEventEmitter () { return new DcEventEmitter(getEventEmitterCPtr()); } - - public native void setStockTranslation (int stockId, String translation); - public native String getBlobdir (); - public native String getLastError (); - public native void stopOngoingProcess (); - public native int isConfigured (); - public native boolean open (String passphrase); - public native boolean isOpen (); - - // when using DcAccounts, use DcAccounts.startIo() instead - public native void startIo (); - - // when using DcAccounts, use DcAccounts.stopIo() instead - public native void stopIo (); - - // when using DcAccounts, use DcAccounts.maybeNetwork() instead - public native void maybeNetwork (); - - public native void setConfig (String key, String value); - public void setConfigInt (String key, int value) { setConfig(key, Integer.toString(value)); } - public native boolean setConfigFromQr (String qr); - public native String getConfig (String key); - public int getConfigInt (String key) { return getConfigInt(key, 0); } - public int getConfigInt (String key, int defValue) { try{return Integer.parseInt(getConfig(key));} catch(Exception e) {} return defValue; } - public native String getInfo (); - public native int getConnectivity (); - public native String getConnectivityHtml (); - public native String initiateKeyTransfer (); - public native void imex (int what, String dir); - public native String imexHasBackup (String dir); - public DcBackupProvider newBackupProvider () { return new DcBackupProvider(newBackupProviderCPtr()); } - public native boolean receiveBackup (String qr); - public native boolean mayBeValidAddr (String addr); - public native int lookupContactIdByAddr(String addr); - public native int[] getContacts (int flags, String query); - public native int[] getBlockedContacts (); - public DcContact getContact (int contact_id) { return new DcContact(getContactCPtr(contact_id)); } - public native int createContact (String name, String addr); - public native void blockContact (int id, int block); - public native String getContactEncrInfo (int contact_id); - public native boolean deleteContact (int id); - public native int addAddressBook (String adrbook); - public DcChatlist getChatlist (int listflags, String query, int queryId) { return new DcChatlist(getAccountId(), getChatlistCPtr(listflags, query, queryId)); } - public DcChat getChat (int chat_id) { return new DcChat(getAccountId(), getChatCPtr(chat_id)); } - public native String getChatEncrInfo (int chat_id); - public native void markseenMsgs (int msg_ids[]); - public native void marknoticedChat (int chat_id); - public native void setChatVisibility (int chat_id, int visibility); - public native int getChatIdByContactId (int contact_id); - public native int createChatByContactId(int contact_id); - public native int createGroupChat (String name); - public native int createBroadcastList (); - public native boolean isContactInChat (int chat_id, int contact_id); - public native int addContactToChat (int chat_id, int contact_id); - public native int removeContactFromChat(int chat_id, int contact_id); - public native void setDraft (int chat_id, DcMsg msg/*null=delete*/); - public DcMsg getDraft (int chat_id) { return new DcMsg(getDraftCPtr(chat_id)); } - public native int setChatName (int chat_id, String name); - public native int setChatProfileImage (int chat_id, String name); - public native int[] getChatMsgs (int chat_id, int flags, int marker1before); - public native int[] searchMsgs (int chat_id, String query); - public native int[] getFreshMsgs (); - public native int[] getChatMedia (int chat_id, int type1, int type2, int type3); - public native int[] getChatContacts (int chat_id); - public native int getChatEphemeralTimer (int chat_id); - public native boolean setChatEphemeralTimer (int chat_id, int timer); - public native boolean setChatMuteDuration (int chat_id, long duration); - public native void deleteChat (int chat_id); - public native void blockChat (int chat_id); - public native void acceptChat (int chat_id); - public DcMsg getMsg (int msg_id) { return new DcMsg(getMsgCPtr(msg_id)); } - public native void sendEditRequest (int msg_id, String text); - public native String getMsgInfo (int id); - public native String getMsgHtml (int msg_id); - public native void downloadFullMsg (int msg_id); - public native int getFreshMsgCount (int chat_id); - public native int estimateDeletionCount(boolean from_server, long seconds); - public native void deleteMsgs (int msg_ids[]); - public native void sendDeleteRequest (int msg_ids[]); - public native void forwardMsgs (int msg_ids[], int chat_id); - public native void saveMsgs (int msg_ids[]); - public native boolean resendMsgs (int msg_ids[]); - public native int sendMsg (int chat_id, DcMsg msg); - public native int sendTextMsg (int chat_id, String text); - public native boolean sendWebxdcStatusUpdate(int msg_id, String payload); - public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial); - public native void setWebxdcIntegration (String file); - public native int initWebxdcIntegration(int chat_id); - public native int addDeviceMsg (String label, DcMsg msg); - public native boolean wasDeviceMsgEverAdded(String label); - public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); } - public native String getSecurejoinQr (int chat_id); - public native String getSecurejoinQrSvg (int chat_id); - public native String createQrSvg (String payload); - public native int joinSecurejoin (String qr); - public native void sendLocationsToChat (int chat_id, int seconds); - public native boolean isSendingLocationsToChat(int chat_id); - public DcProvider getProviderFromEmailWithDns (String email) { long cptr = getProviderFromEmailWithDnsCPtr(email); return cptr!=0 ? new DcProvider(cptr) : null; } - - public boolean isEnabled() { - return !"0".equals(getConfig(CONFIG_ACCOUNT_ENABLED)); - } + public native String getBlobdir(); - public void setEnabled(boolean enabled) { - setConfigInt(CONFIG_ACCOUNT_ENABLED, enabled? 1 : 0); - if (enabled) { - startIo(); - } else { - stopIo(); - } - } + public native String getLastError(); - public boolean isMentionsEnabled() { - return getConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED) != 1; - } + public native void stopOngoingProcess(); - public void setMentionsEnabled(boolean enabled) { - setConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED, enabled? 0 : 1); - } + public native int isConfigured(); - public String getName() { - String displayname = getConfig("displayname"); - if (displayname.isEmpty()) { - displayname = getContact(DcContact.DC_CONTACT_ID_SELF).getAddr(); - } - return displayname; - } + public native boolean open(String passphrase); - public boolean isChatmail() { - return getConfigInt("is_chatmail") == 1; - } + public native boolean isOpen(); - public boolean isMuted() { - return getConfigInt("is_muted") == 1; - } + // when using DcAccounts, use DcAccounts.startIo() instead + public native void startIo(); + + // when using DcAccounts, use DcAccounts.stopIo() instead + public native void stopIo(); + + // when using DcAccounts, use DcAccounts.maybeNetwork() instead + public native void maybeNetwork(); + + public native void setConfig(String key, String value); + + public void setConfigInt(String key, int value) { + setConfig(key, Integer.toString(value)); + } + + public native boolean setConfigFromQr(String qr); - public void setMuted(boolean muted) { - setConfigInt("is_muted", muted? 1 : 0); + public native String getConfig(String key); + + public int getConfigInt(String key) { + return getConfigInt(key, 0); + } + + public int getConfigInt(String key, int defValue) { + try { + return Integer.parseInt(getConfig(key)); + } catch (Exception e) { } + return defValue; + } - public void restartIo() { - if (!isEnabled()) return; - stopIo(); + public native String getInfo(); + + public native int getConnectivity(); + + public native String getConnectivityHtml(); + + public native String initiateKeyTransfer(); + + public native void imex(int what, String dir); + + public native String imexHasBackup(String dir); + + public DcBackupProvider newBackupProvider() { + return new DcBackupProvider(newBackupProviderCPtr()); + } + + public native boolean receiveBackup(String qr); + + public native boolean mayBeValidAddr(String addr); + + public native int lookupContactIdByAddr(String addr); + + public native int[] getContacts(int flags, String query); + + public native int[] getBlockedContacts(); + + public DcContact getContact(int contact_id) { + return new DcContact(getContactCPtr(contact_id)); + } + + public native int createContact(String name, String addr); + + public native void blockContact(int id, int block); + + public native String getContactEncrInfo(int contact_id); + + public native boolean deleteContact(int id); + + public native int addAddressBook(String adrbook); + + public DcChatlist getChatlist(int listflags, String query, int queryId) { + return new DcChatlist(getAccountId(), getChatlistCPtr(listflags, query, queryId)); + } + + public DcChat getChat(int chat_id) { + return new DcChat(getAccountId(), getChatCPtr(chat_id)); + } + + public native String getChatEncrInfo(int chat_id); + + public native void markseenMsgs(int msg_ids[]); + + public native void marknoticedChat(int chat_id); + + public native void setChatVisibility(int chat_id, int visibility); + + public native int getChatIdByContactId(int contact_id); + + public native int createChatByContactId(int contact_id); + + public native int createGroupChat(String name); + + public native int createBroadcastList(); + + public native boolean isContactInChat(int chat_id, int contact_id); + + public native int addContactToChat(int chat_id, int contact_id); + + public native int removeContactFromChat(int chat_id, int contact_id); + + public native void setDraft(int chat_id, DcMsg msg /*null=delete*/); + + public DcMsg getDraft(int chat_id) { + return new DcMsg(getDraftCPtr(chat_id)); + } + + public native int setChatName(int chat_id, String name); + + public native int setChatProfileImage(int chat_id, String name); + + public native int[] getChatMsgs(int chat_id, int flags, int marker1before); + + public native int[] searchMsgs(int chat_id, String query); + + public native int[] getFreshMsgs(); + + public native int[] getChatMedia(int chat_id, int type1, int type2, int type3); + + public native int[] getChatContacts(int chat_id); + + public native int getChatEphemeralTimer(int chat_id); + + public native boolean setChatEphemeralTimer(int chat_id, int timer); + + public native boolean setChatMuteDuration(int chat_id, long duration); + + public native void deleteChat(int chat_id); + + public native void blockChat(int chat_id); + + public native void acceptChat(int chat_id); + + public DcMsg getMsg(int msg_id) { + return new DcMsg(getMsgCPtr(msg_id)); + } + + public native void sendEditRequest(int msg_id, String text); + + public native String getMsgInfo(int id); + + public native String getMsgHtml(int msg_id); + + public native void downloadFullMsg(int msg_id); + + public native int getFreshMsgCount(int chat_id); + + public native int estimateDeletionCount(boolean from_server, long seconds); + + public native void deleteMsgs(int msg_ids[]); + + public native void sendDeleteRequest(int msg_ids[]); + + public native void forwardMsgs(int msg_ids[], int chat_id); + + public native void saveMsgs(int msg_ids[]); + + public native boolean resendMsgs(int msg_ids[]); + + public native int sendMsg(int chat_id, DcMsg msg); + + public native int sendTextMsg(int chat_id, String text); + + public native boolean sendWebxdcStatusUpdate(int msg_id, String payload); + + public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial); + + public native void setWebxdcIntegration(String file); + + public native int initWebxdcIntegration(int chat_id); + + public native int addDeviceMsg(String label, DcMsg msg); + + public native boolean wasDeviceMsgEverAdded(String label); + + public DcLot checkQr(String qr) { + return new DcLot(checkQrCPtr(qr)); + } + + public native String getSecurejoinQr(int chat_id); + + public native String getSecurejoinQrSvg(int chat_id); + + public native String createQrSvg(String payload); + + public native int joinSecurejoin(String qr); + + public native void sendLocationsToChat(int chat_id, int seconds); + + public native boolean isSendingLocationsToChat(int chat_id); + + public DcProvider getProviderFromEmailWithDns(String email) { + long cptr = getProviderFromEmailWithDnsCPtr(email); + return cptr != 0 ? new DcProvider(cptr) : null; + } + + public boolean isEnabled() { + return !"0".equals(getConfig(CONFIG_ACCOUNT_ENABLED)); + } + + public void setEnabled(boolean enabled) { + setConfigInt(CONFIG_ACCOUNT_ENABLED, enabled ? 1 : 0); + if (enabled) { startIo(); + } else { + stopIo(); } + } + + public boolean isMentionsEnabled() { + return getConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED) != 1; + } + + public void setMentionsEnabled(boolean enabled) { + setConfigInt(CONFIG_MUTE_MENTIONS_IF_MUTED, enabled ? 0 : 1); + } + + public String getName() { + String displayname = getConfig("displayname"); + if (displayname.isEmpty()) { + displayname = getConfig("addr"); + } + return displayname; + } + + public boolean isChatmail() { + return getConfigInt("is_chatmail") == 1; + } + + public boolean isMuted() { + return getConfigInt("is_muted") == 1; + } + + public void setMuted(boolean muted) { + setConfigInt("is_muted", muted ? 1 : 0); + } + + public void restartIo() { + if (!isEnabled()) return; + stopIo(); + startIo(); + } + + /** + * @return true if at least one chat has location streaming enabled + */ + public native boolean setLocation(float latitude, float longitude, float accuracy); + + // working with raw c-data + private long contextCPtr; // CAVE: the name is referenced in the JNI + + private native long createContextCPtr(String osName, String dbfile); + + private native void unrefContextCPtr(); + + private native long getEventEmitterCPtr(); + + public native long createMsgCPtr(int viewtype); + + private native long getChatlistCPtr(int listflags, String query, int queryId); + + private native long getChatCPtr(int chat_id); + + private native long getMsgCPtr(int id); + + private native long getDraftCPtr(int id); + + private native long getContactCPtr(int id); + + private native long checkQrCPtr(String qr); + + private native long getProviderFromEmailWithDnsCPtr(String addr); - /** - * @return true if at least one chat has location streaming enabled - */ - public native boolean setLocation (float latitude, float longitude, float accuracy); - - // working with raw c-data - private long contextCPtr; // CAVE: the name is referenced in the JNI - private native long createContextCPtr(String osName, String dbfile); - private native void unrefContextCPtr (); - private native long getEventEmitterCPtr(); - public native long createMsgCPtr (int viewtype); - private native long getChatlistCPtr (int listflags, String query, int queryId); - private native long getChatCPtr (int chat_id); - private native long getMsgCPtr (int id); - private native long getDraftCPtr (int id); - private native long getContactCPtr (int id); - private native long checkQrCPtr (String qr); - private native long getProviderFromEmailWithDnsCPtr (String addr); - private native long newBackupProviderCPtr(); + private native long newBackupProviderCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcEvent.java b/src/main/java/com/b44t/messenger/DcEvent.java index 1acfc2ee0..4180bd48c 100644 --- a/src/main/java/com/b44t/messenger/DcEvent.java +++ b/src/main/java/com/b44t/messenger/DcEvent.java @@ -2,24 +2,31 @@ public class DcEvent { - public DcEvent(long eventCPtr) { - this.eventCPtr = eventCPtr; - } - - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefEventCPtr(); - eventCPtr = 0; - } - - public native int getId (); - public native int getData1Int (); - public native int getData2Int (); - public native String getData2Str (); - public native byte[] getData2Blob(); - public native int getAccountId(); - - // working with raw c-data - private long eventCPtr; // CAVE: the name is referenced in the JNI - private native void unrefEventCPtr(); + public DcEvent(long eventCPtr) { + this.eventCPtr = eventCPtr; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefEventCPtr(); + eventCPtr = 0; + } + + public native int getId(); + + public native int getData1Int(); + + public native int getData2Int(); + + public native String getData2Str(); + + public native byte[] getData2Blob(); + + public native int getAccountId(); + + // working with raw c-data + private long eventCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefEventCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcEventChannel.java b/src/main/java/com/b44t/messenger/DcEventChannel.java index 2c3ece1dd..b5e9ee241 100644 --- a/src/main/java/com/b44t/messenger/DcEventChannel.java +++ b/src/main/java/com/b44t/messenger/DcEventChannel.java @@ -20,8 +20,11 @@ public DcEventEmitter getEventEmitter() { } // working with raw c-data - private long eventChannelCPtr; // CAVE: the name is referenced in the JNI - private native long createEventChannelCPtr (); - private native void unrefEventChannelCPtr (); - private native long getEventEmitterCPtr (); + private long eventChannelCPtr; // CAVE: the name is referenced in the JNI + + private native long createEventChannelCPtr(); + + private native void unrefEventChannelCPtr(); + + private native long getEventEmitterCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcEventEmitter.java b/src/main/java/com/b44t/messenger/DcEventEmitter.java index bd7c8ad54..b51715a7c 100644 --- a/src/main/java/com/b44t/messenger/DcEventEmitter.java +++ b/src/main/java/com/b44t/messenger/DcEventEmitter.java @@ -2,23 +2,26 @@ public class DcEventEmitter { - public DcEventEmitter(long eventEmitterCPtr) { - this.eventEmitterCPtr = eventEmitterCPtr; - } + public DcEventEmitter(long eventEmitterCPtr) { + this.eventEmitterCPtr = eventEmitterCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefEventEmitterCPtr(); - eventEmitterCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefEventEmitterCPtr(); + eventEmitterCPtr = 0; + } - public DcEvent getNextEvent () { - long eventCPtr = getNextEventCPtr(); - return eventCPtr == 0 ? null : new DcEvent(eventCPtr); - } + public DcEvent getNextEvent() { + long eventCPtr = getNextEventCPtr(); + return eventCPtr == 0 ? null : new DcEvent(eventCPtr); + } - // working with raw c-data - private long eventEmitterCPtr; // CAVE: the name is referenced in the JNI - private native long getNextEventCPtr (); - private native void unrefEventEmitterCPtr(); + // working with raw c-data + private long eventEmitterCPtr; // CAVE: the name is referenced in the JNI + + private native long getNextEventCPtr(); + + private native void unrefEventEmitterCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java b/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java index 57b353b44..6eded9ced 100644 --- a/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java +++ b/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java @@ -2,20 +2,23 @@ public class DcJsonrpcInstance { - public DcJsonrpcInstance(long jsonrpcInstanceCPtr) { - this.jsonrpcInstanceCPtr = jsonrpcInstanceCPtr; - } + public DcJsonrpcInstance(long jsonrpcInstanceCPtr) { + this.jsonrpcInstanceCPtr = jsonrpcInstanceCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefJsonrpcInstanceCPtr(); - jsonrpcInstanceCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefJsonrpcInstanceCPtr(); + jsonrpcInstanceCPtr = 0; + } - public native void request(String request); - public native String getNextResponse(); + public native void request(String request); - // working with raw c-data - private long jsonrpcInstanceCPtr; // CAVE: the name is referenced in the JNI - private native void unrefJsonrpcInstanceCPtr(); + public native String getNextResponse(); + + // working with raw c-data + private long jsonrpcInstanceCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefJsonrpcInstanceCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcLot.java b/src/main/java/com/b44t/messenger/DcLot.java index 97ab2f013..2d036e130 100644 --- a/src/main/java/com/b44t/messenger/DcLot.java +++ b/src/main/java/com/b44t/messenger/DcLot.java @@ -2,28 +2,35 @@ public class DcLot { - public final static int DC_TEXT1_DRAFT = 1; - public final static int DC_TEXT1_USERNAME = 2; - public final static int DC_TEXT1_SELF = 3; - - public DcLot(long lotCPtr) { - this.lotCPtr = lotCPtr; - } - - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefLotCPtr(); - lotCPtr = 0; - } - - public native String getText1 (); - public native int getText1Meaning(); - public native String getText2 (); - public native long getTimestamp (); - public native int getState (); - public native int getId (); - - // working with raw c-data - private long lotCPtr; // CAVE: the name is referenced in the JNI - private native void unrefLotCPtr(); + public static final int DC_TEXT1_DRAFT = 1; + public static final int DC_TEXT1_USERNAME = 2; + public static final int DC_TEXT1_SELF = 3; + + public DcLot(long lotCPtr) { + this.lotCPtr = lotCPtr; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefLotCPtr(); + lotCPtr = 0; + } + + public native String getText1(); + + public native int getText1Meaning(); + + public native String getText2(); + + public native long getTimestamp(); + + public native int getState(); + + public native int getId(); + + // working with raw c-data + private long lotCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefLotCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java b/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java index 288ae7f82..bb33319f0 100644 --- a/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java +++ b/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java @@ -1,14 +1,14 @@ package com.b44t.messenger; -/** - * Contains a list of media entries, their respective positions and ability to move through it. - */ +/** Contains a list of media entries, their respective positions and ability to move through it. */ public class DcMediaGalleryElement { final int[] mediaMsgs; int position; final DcContext context; - public DcMediaGalleryElement(int[] mediaMsgs, int position, DcContext context, boolean leftIsRecent) { + + public DcMediaGalleryElement( + int[] mediaMsgs, int position, DcContext context, boolean leftIsRecent) { this.mediaMsgs = mediaMsgs; this.position = position; this.context = context; @@ -33,7 +33,7 @@ public int getPosition() { } public void moveToPosition(int newPosition) { - if(newPosition < 0 || newPosition >= mediaMsgs.length) + if (newPosition < 0 || newPosition >= mediaMsgs.length) throw new IllegalArgumentException("can't move outside of known area."); position = newPosition; } diff --git a/src/main/java/com/b44t/messenger/DcMsg.java b/src/main/java/com/b44t/messenger/DcMsg.java index 66caf4915..be3126e88 100644 --- a/src/main/java/com/b44t/messenger/DcMsg.java +++ b/src/main/java/com/b44t/messenger/DcMsg.java @@ -1,267 +1,332 @@ package com.b44t.messenger; import android.text.TextUtils; - -import org.json.JSONObject; - import java.io.File; import java.util.Set; +import org.json.JSONObject; public class DcMsg { - public final static int DC_MSG_UNDEFINED = 0; - public final static int DC_MSG_TEXT = 10; - public final static int DC_MSG_IMAGE = 20; - public final static int DC_MSG_GIF = 21; - public final static int DC_MSG_STICKER = 23; - public final static int DC_MSG_AUDIO = 40; - public final static int DC_MSG_VOICE = 41; - public final static int DC_MSG_VIDEO = 50; - public final static int DC_MSG_FILE = 60; - public final static int DC_MSG_CALL = 71; - public final static int DC_MSG_WEBXDC = 80; - public final static int DC_MSG_VCARD = 90; - - public final static int DC_INFO_UNKNOWN = 0; - public final static int DC_INFO_GROUP_NAME_CHANGED = 2; - public final static int DC_INFO_GROUP_IMAGE_CHANGED = 3; - public final static int DC_INFO_MEMBER_ADDED_TO_GROUP = 4; - public final static int DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5; - public final static int DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6; - public final static int DC_INFO_SECURE_JOIN_MESSAGE = 7; - public final static int DC_INFO_LOCATIONSTREAMING_ENABLED = 8; - public final static int DC_INFO_LOCATION_ONLY = 9; - public final static int DC_INFO_EPHEMERAL_TIMER_CHANGED = 10; - public final static int DC_INFO_PROTECTION_ENABLED = 11; - public final static int DC_INFO_INVALID_UNENCRYPTED_MAIL = 13; - public final static int DC_INFO_WEBXDC_INFO_MESSAGE = 32; - public final static int DC_INFO_CHAT_E2EE = 50; - public final static int DC_INFO_CHAT_DESCRIPTION_CHANGED = 70; - - public final static int DC_STATE_UNDEFINED = 0; - public final static int DC_STATE_IN_FRESH = 10; - public final static int DC_STATE_IN_NOTICED = 13; - public final static int DC_STATE_IN_SEEN = 16; - public final static int DC_STATE_OUT_PREPARING = 18; - public final static int DC_STATE_OUT_DRAFT = 19; - public final static int DC_STATE_OUT_PENDING = 20; - public final static int DC_STATE_OUT_FAILED = 24; - public final static int DC_STATE_OUT_DELIVERED = 26; - public final static int DC_STATE_OUT_MDN_RCVD = 28; - - public final static int DC_DOWNLOAD_DONE = 0; - public final static int DC_DOWNLOAD_AVAILABLE = 10; - public final static int DC_DOWNLOAD_FAILURE = 20; - public final static int DC_DOWNLOAD_UNDECIPHERABLE = 30; - public final static int DC_DOWNLOAD_IN_PROGRESS = 1000; - - public static final int DC_MSG_NO_ID = 0; - public final static int DC_MSG_ID_MARKER1 = 1; - public final static int DC_MSG_ID_DAYMARKER = 9; - - public final static int DC_VIDEOCHATTYPE_UNKNOWN = 0; - public final static int DC_VIDEOCHATTYPE_BASICWEBRTC = 1; - - private static final String TAG = DcMsg.class.getSimpleName(); - - public DcMsg(DcContext context, int viewtype) { - msgCPtr = context.createMsgCPtr(viewtype); + public static final int DC_MSG_UNDEFINED = 0; + public static final int DC_MSG_TEXT = 10; + public static final int DC_MSG_IMAGE = 20; + public static final int DC_MSG_GIF = 21; + public static final int DC_MSG_STICKER = 23; + public static final int DC_MSG_AUDIO = 40; + public static final int DC_MSG_VOICE = 41; + public static final int DC_MSG_VIDEO = 50; + public static final int DC_MSG_FILE = 60; + public static final int DC_MSG_CALL = 71; + public static final int DC_MSG_WEBXDC = 80; + public static final int DC_MSG_VCARD = 90; + + public static final int DC_INFO_UNKNOWN = 0; + public static final int DC_INFO_GROUP_NAME_CHANGED = 2; + public static final int DC_INFO_GROUP_IMAGE_CHANGED = 3; + public static final int DC_INFO_MEMBER_ADDED_TO_GROUP = 4; + public static final int DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5; + public static final int DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6; + public static final int DC_INFO_SECURE_JOIN_MESSAGE = 7; + public static final int DC_INFO_LOCATIONSTREAMING_ENABLED = 8; + public static final int DC_INFO_LOCATION_ONLY = 9; + public static final int DC_INFO_EPHEMERAL_TIMER_CHANGED = 10; + public static final int DC_INFO_PROTECTION_ENABLED = 11; + public static final int DC_INFO_INVALID_UNENCRYPTED_MAIL = 13; + public static final int DC_INFO_WEBXDC_INFO_MESSAGE = 32; + public static final int DC_INFO_CHAT_E2EE = 50; + public static final int DC_INFO_CHAT_DESCRIPTION_CHANGED = 70; + + public static final int DC_STATE_UNDEFINED = 0; + public static final int DC_STATE_IN_FRESH = 10; + public static final int DC_STATE_IN_NOTICED = 13; + public static final int DC_STATE_IN_SEEN = 16; + public static final int DC_STATE_OUT_PREPARING = 18; + public static final int DC_STATE_OUT_DRAFT = 19; + public static final int DC_STATE_OUT_PENDING = 20; + public static final int DC_STATE_OUT_FAILED = 24; + public static final int DC_STATE_OUT_DELIVERED = 26; + public static final int DC_STATE_OUT_MDN_RCVD = 28; + + public static final int DC_DOWNLOAD_DONE = 0; + public static final int DC_DOWNLOAD_AVAILABLE = 10; + public static final int DC_DOWNLOAD_FAILURE = 20; + public static final int DC_DOWNLOAD_UNDECIPHERABLE = 30; + public static final int DC_DOWNLOAD_IN_PROGRESS = 1000; + + public static final int DC_MSG_NO_ID = 0; + public static final int DC_MSG_ID_MARKER1 = 1; + public static final int DC_MSG_ID_DAYMARKER = 9; + + public static final int DC_VIDEOCHATTYPE_UNKNOWN = 0; + public static final int DC_VIDEOCHATTYPE_BASICWEBRTC = 1; + + private static final String TAG = DcMsg.class.getSimpleName(); + + public DcMsg(DcContext context, int viewtype) { + msgCPtr = context.createMsgCPtr(viewtype); + } + + public DcMsg(long msgCPtr) { + this.msgCPtr = msgCPtr; + } + + public boolean isOk() { + return msgCPtr != 0; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefMsgCPtr(); + msgCPtr = 0; + } + + @Override + public int hashCode() { + return this.getId(); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof DcMsg)) { + return false; } - public DcMsg(long msgCPtr) { - this.msgCPtr = msgCPtr; + DcMsg that = (DcMsg) other; + return this.getId() == that.getId() && this.getId() != 0; + } + + /** If given a message, calculates the position of the message in the chat */ + public static int getMessagePosition(DcMsg msg, DcContext dcContext) { + int msgs[] = dcContext.getChatMsgs(msg.getChatId(), 0, 0); + int startingPosition = -1; + int msgId = msg.getId(); + for (int i = 0; i < msgs.length; i++) { + if (msgs[i] == msgId) { + startingPosition = msgs.length - 1 - i; + break; + } } + return startingPosition; + } - public boolean isOk() { - return msgCPtr != 0; - } + public native int getId(); - @Override - protected void finalize() throws Throwable { - super.finalize(); - unrefMsgCPtr(); - msgCPtr = 0; - } + public native String getText(); - @Override - public int hashCode() { - return this.getId(); - } + public native String getSubject(); - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof DcMsg)) { - return false; - } + public native long getTimestamp(); - DcMsg that = (DcMsg) other; - return this.getId()==that.getId() && this.getId()!=0; - } + public native long getSortTimestamp(); - /** - * If given a message, calculates the position of the message in the chat - */ - public static int getMessagePosition(DcMsg msg, DcContext dcContext) { - int msgs[] = dcContext.getChatMsgs(msg.getChatId(), 0, 0); - int startingPosition = -1; - int msgId = msg.getId(); - for (int i = 0; i < msgs.length; i++) { - if (msgs[i] == msgId) { - startingPosition = msgs.length - 1 - i; - break; - } - } - return startingPosition; - } + public native boolean hasDeviatingTimestamp(); - public native int getId (); - public native String getText (); - public native String getSubject (); - public native long getTimestamp (); - public native long getSortTimestamp (); - public native boolean hasDeviatingTimestamp(); - public native boolean hasLocation (); - private native int getViewType (); - public int getType () { return getDownloadState()==DC_DOWNLOAD_DONE? getViewType() : DC_MSG_TEXT; } - public native int getInfoType (); - public native int getInfoContactId (); - public native int getState (); - public native int getDownloadState (); - public native int getChatId (); - public native int getFromId (); - public native int getWidth (int def); - public native int getHeight (int def); - public native int getDuration (); - public native void lateFilingMediaSize(int width, int height, int duration); - public DcLot getSummary (DcChat chat) { return new DcLot(getSummaryCPtr(chat.getChatCPtr())); } - public native String getSummarytext (int approx_characters); - public native int showPadlock (); - public boolean hasFile () { String file = getFile(); return file!=null && !file.isEmpty(); } - public native String getFile (); - public native String getFilemime (); - public native String getFilename (); - public native long getFilebytes (); - public native byte[] getWebxdcBlob (String filename); - public JSONObject getWebxdcInfo () { - try { - String json = getWebxdcInfoJson(); - if (json != null && !json.isEmpty()) return new JSONObject(json); - } catch(Exception e) { - e.printStackTrace(); - } - return new JSONObject(); - } - public native String getWebxdcHref (); - public native boolean isForwarded (); - public native boolean isInfo (); - public native boolean hasHtml (); - public native String getSetupCodeBegin (); - public native void setText (String text); - public native void setSubject (String text); - public native void setHtml (String text); - public native void forceSticker (); - public native void setFileAndDeduplicate(String file, String name, String filemime); - public native void setDimension (int width, int height); - public native void setDuration (int duration); - public native void setLocation (float latitude, float longitude); - public native String getPOILocation (); - public void setQuote (DcMsg quote) { setQuoteCPtr(quote.msgCPtr); } - public native String getQuotedText (); - public native String getError (); - public native String getOverrideSenderName(); - public native boolean isEdited (); - - public String getSenderName(DcContact dcContact) { - String overrideName = getOverrideSenderName(); - if (overrideName != null) { - return "~" + overrideName; - } else { - return dcContact.getDisplayName(); - } - } + public native boolean hasLocation(); - public DcMsg getQuotedMsg () { - long cPtr = getQuotedMsgCPtr(); - return cPtr != 0 ? new DcMsg(cPtr) : null; - } + private native int getViewType(); - public DcMsg getParent() { - long cPtr = getParentCPtr(); - return cPtr != 0 ? new DcMsg(cPtr) : null; - } + public int getType() { + return getDownloadState() == DC_DOWNLOAD_DONE ? getViewType() : DC_MSG_TEXT; + } - public native int getOriginalMsgId (); - public native int getSavedMsgId (); + public native int getInfoType(); - public boolean canSave() { - // saving info-messages out of context results in confusion, see https://github.com/deltachat/deltachat-ios/issues/2567 - return !isInfo(); - } + public native int getInfoContactId(); - public File getFileAsFile() { - if(getFile()==null) - throw new AssertionError("expected a file to be present."); - return new File(getFile()); - } + public native int getState(); - // aliases and higher-level tools - public static int[] msgSetToIds(final Set dcMsgs) { - if (dcMsgs == null) { - return new int[0]; - } - int[] ids = new int[dcMsgs.size()]; - int i = 0; - for (DcMsg dcMsg : dcMsgs) { - ids[i++] = dcMsg.getId(); - } - return ids; - } + public native int getDownloadState(); - public boolean isOutgoing() { - return getFromId() == DcContact.DC_CONTACT_ID_SELF; - } + public native int getChatId(); - public String getDisplayBody() { - return getText(); - } + public native int getFromId(); - public String getBody() { - return getText(); - } + public native int getWidth(int def); - public long getDateReceived() { - return getTimestamp(); - } + public native int getHeight(int def); - public boolean isFailed() { - return (getState() == DC_STATE_OUT_FAILED) || (!TextUtils.isEmpty(getError())); - } - public boolean isPreparing() { - return getState() == DC_STATE_OUT_PREPARING; - } - public boolean isSecure() { - return showPadlock()!=0; - } - public boolean isPending() { - return getState() == DC_STATE_OUT_PENDING; + public native int getDuration(); + + public native void lateFilingMediaSize(int width, int height, int duration); + + public DcLot getSummary(DcChat chat) { + return new DcLot(getSummaryCPtr(chat.getChatCPtr())); + } + + public native String getSummarytext(int approx_characters); + + public native int showPadlock(); + + public boolean hasFile() { + String file = getFile(); + return file != null && !file.isEmpty(); + } + + public native String getFile(); + + public native String getFilemime(); + + public native String getFilename(); + + public native long getFilebytes(); + + public native byte[] getWebxdcBlob(String filename); + + public JSONObject getWebxdcInfo() { + try { + String json = getWebxdcInfoJson(); + if (json != null && !json.isEmpty()) return new JSONObject(json); + } catch (Exception e) { + e.printStackTrace(); } - public boolean isDelivered() { - return getState() == DC_STATE_OUT_DELIVERED; + return new JSONObject(); + } + + public native String getWebxdcHref(); + + public native boolean isForwarded(); + + public native boolean isInfo(); + + public native boolean hasHtml(); + + public native String getSetupCodeBegin(); + + public native void setText(String text); + + public native void setSubject(String text); + + public native void setHtml(String text); + + public native void forceSticker(); + + public native void setFileAndDeduplicate(String file, String name, String filemime); + + public native void setDimension(int width, int height); + + public native void setDuration(int duration); + + public native void setLocation(float latitude, float longitude); + + public native String getPOILocation(); + + public void setQuote(DcMsg quote) { + setQuoteCPtr(quote.msgCPtr); + } + + public native String getQuotedText(); + + public native String getError(); + + public native String getOverrideSenderName(); + + public native boolean isEdited(); + + public String getSenderName(DcContact dcContact) { + String overrideName = getOverrideSenderName(); + if (overrideName != null) { + return "~" + overrideName; + } else { + return dcContact.getDisplayName(); } - public boolean isRemoteRead() { - return getState() == DC_STATE_OUT_MDN_RCVD; + } + + public DcMsg getQuotedMsg() { + long cPtr = getQuotedMsgCPtr(); + return cPtr != 0 ? new DcMsg(cPtr) : null; + } + + public DcMsg getParent() { + long cPtr = getParentCPtr(); + return cPtr != 0 ? new DcMsg(cPtr) : null; + } + + public native int getOriginalMsgId(); + + public native int getSavedMsgId(); + + public boolean canSave() { + // saving info-messages out of context results in confusion, see + // https://github.com/deltachat/deltachat-ios/issues/2567 + return !isInfo(); + } + + public File getFileAsFile() { + if (getFile() == null) throw new AssertionError("expected a file to be present."); + return new File(getFile()); + } + + // aliases and higher-level tools + public static int[] msgSetToIds(final Set dcMsgs) { + if (dcMsgs == null) { + return new int[0]; } - public boolean isSeen() { - return getState() == DC_STATE_IN_SEEN; + int[] ids = new int[dcMsgs.size()]; + int i = 0; + for (DcMsg dcMsg : dcMsgs) { + ids[i++] = dcMsg.getId(); } + return ids; + } + + public boolean isOutgoing() { + return getFromId() == DcContact.DC_CONTACT_ID_SELF; + } + + public String getDisplayBody() { + return getText(); + } + + public String getBody() { + return getText(); + } + + public long getDateReceived() { + return getTimestamp(); + } + + public boolean isFailed() { + return (getState() == DC_STATE_OUT_FAILED) || (!TextUtils.isEmpty(getError())); + } + + public boolean isPreparing() { + return getState() == DC_STATE_OUT_PREPARING; + } + + public boolean isSecure() { + return showPadlock() != 0; + } + + public boolean isPending() { + return getState() == DC_STATE_OUT_PENDING; + } + + public boolean isDelivered() { + return getState() == DC_STATE_OUT_DELIVERED; + } + + public boolean isRemoteRead() { + return getState() == DC_STATE_OUT_MDN_RCVD; + } + + public boolean isSeen() { + return getState() == DC_STATE_IN_SEEN; + } + + // working with raw c-data + private long msgCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefMsgCPtr(); + + private native long getSummaryCPtr(long chatCPtr); + + private native void setQuoteCPtr(long quoteCPtr); + + private native long getQuotedMsgCPtr(); + private native long getParentCPtr(); - // working with raw c-data - private long msgCPtr; // CAVE: the name is referenced in the JNI - private native void unrefMsgCPtr (); - private native long getSummaryCPtr (long chatCPtr); - private native void setQuoteCPtr (long quoteCPtr); - private native long getQuotedMsgCPtr (); - private native long getParentCPtr (); - private native String getWebxdcInfoJson (); -}; + private native String getWebxdcInfoJson(); +} +; diff --git a/src/main/java/com/b44t/messenger/DcProvider.java b/src/main/java/com/b44t/messenger/DcProvider.java index 09941cfe4..336526302 100644 --- a/src/main/java/com/b44t/messenger/DcProvider.java +++ b/src/main/java/com/b44t/messenger/DcProvider.java @@ -2,25 +2,29 @@ public class DcProvider { - public final static int DC_PROVIDER_STATUS_OK = 1; - public final static int DC_PROVIDER_STATUS_PREPARATION = 2; - public final static int DC_PROVIDER_STATUS_BROKEN = 3; - - public DcProvider(long providerCPtr) { - this.providerCPtr = providerCPtr; - } - - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefProviderCPtr(); - providerCPtr = 0; - } - - public native int getStatus (); - public native String getBeforeLoginHint (); - public native String getOverviewPage (); - - // working with raw c-data - private long providerCPtr; // CAVE: the name is referenced in the JNI - private native void unrefProviderCPtr(); + public static final int DC_PROVIDER_STATUS_OK = 1; + public static final int DC_PROVIDER_STATUS_PREPARATION = 2; + public static final int DC_PROVIDER_STATUS_BROKEN = 3; + + public DcProvider(long providerCPtr) { + this.providerCPtr = providerCPtr; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefProviderCPtr(); + providerCPtr = 0; + } + + public native int getStatus(); + + public native String getBeforeLoginHint(); + + public native String getOverviewPage(); + + // working with raw c-data + private long providerCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefProviderCPtr(); } diff --git a/src/main/java/com/b44t/messenger/FFITransport.java b/src/main/java/com/b44t/messenger/FFITransport.java index 401048c52..7891a8809 100644 --- a/src/main/java/com/b44t/messenger/FFITransport.java +++ b/src/main/java/com/b44t/messenger/FFITransport.java @@ -19,4 +19,4 @@ protected void sendRequest(String jsonRequest) { protected String getResponse() { return dcJsonrpcInstance.getNextResponse(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java index fb409a73d..8121bbbc8 100644 --- a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java +++ b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java @@ -4,609 +4,590 @@ import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import java.security.InvalidParameterException; import java.util.ArrayList; -/** - * Created by Sergej Kravcenko on 4/24/2017. - * Copyright (c) 2017 Sergej Kravcenko - */ - +/** Created by Sergej Kravcenko on 4/24/2017. Copyright (c) 2017 Sergej Kravcenko */ @SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class StickyHeaderGridAdapter extends RecyclerView.Adapter { - public static final String TAG = "StickyHeaderGridAdapter"; - - public static final int TYPE_HEADER = 0; - public static final int TYPE_ITEM = 1; - - private ArrayList

mSections; - private int[] mSectionIndices; - private int mTotalItemNumber; - - @SuppressWarnings("WeakerAccess") - public static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(View itemView) { - super(itemView); - } - - public boolean isHeader() { - return false; - } - - - public int getSectionItemViewType() { - return StickyHeaderGridAdapter.externalViewType(getItemViewType()); - } - } - - public static class ItemViewHolder extends ViewHolder { - public ItemViewHolder(View itemView) { - super(itemView); - } - } - - public static class HeaderViewHolder extends ViewHolder { - public HeaderViewHolder(View itemView) { - super(itemView); - } - - @Override - public boolean isHeader() { - return true; - } - } - - private static class Section { - private int position; - private int itemNumber; - private int length; - } - - private void calculateSections() { - mSections = new ArrayList<>(); - - int total = 0; - int sectionCount = getSectionCount(); - for (int s = 0; s < sectionCount; s++) { - final Section section = new Section(); - section.position = total; - section.itemNumber = getSectionItemCount(s); - section.length = section.itemNumber + 1; - mSections.add(section); - - total += section.length; - } - mTotalItemNumber = total; - - total = 0; - mSectionIndices = new int[mTotalItemNumber]; - for (int s = 0; s < sectionCount; s++) { - final Section section = mSections.get(s); - for (int i = 0; i < section.length; i++) { - mSectionIndices[total + i] = s; - } - total += section.length; - } - } - - protected int getItemViewInternalType(int position) { - final int section = getAdapterPositionSection(position); +public abstract class StickyHeaderGridAdapter + extends RecyclerView.Adapter { + public static final String TAG = "StickyHeaderGridAdapter"; + + public static final int TYPE_HEADER = 0; + public static final int TYPE_ITEM = 1; + + private ArrayList
mSections; + private int[] mSectionIndices; + private int mTotalItemNumber; + + @SuppressWarnings("WeakerAccess") + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View itemView) { + super(itemView); + } + + public boolean isHeader() { + return false; + } + + public int getSectionItemViewType() { + return StickyHeaderGridAdapter.externalViewType(getItemViewType()); + } + } + + public static class ItemViewHolder extends ViewHolder { + public ItemViewHolder(View itemView) { + super(itemView); + } + } + + public static class HeaderViewHolder extends ViewHolder { + public HeaderViewHolder(View itemView) { + super(itemView); + } + + @Override + public boolean isHeader() { + return true; + } + } + + private static class Section { + private int position; + private int itemNumber; + private int length; + } + + private void calculateSections() { + mSections = new ArrayList<>(); + + int total = 0; + int sectionCount = getSectionCount(); + for (int s = 0; s < sectionCount; s++) { + final Section section = new Section(); + section.position = total; + section.itemNumber = getSectionItemCount(s); + section.length = section.itemNumber + 1; + mSections.add(section); + + total += section.length; + } + mTotalItemNumber = total; + + total = 0; + mSectionIndices = new int[mTotalItemNumber]; + for (int s = 0; s < sectionCount; s++) { + final Section section = mSections.get(s); + for (int i = 0; i < section.length; i++) { + mSectionIndices[total + i] = s; + } + total += section.length; + } + } + + protected int getItemViewInternalType(int position) { + final int section = getAdapterPositionSection(position); + final Section sectionObject = mSections.get(section); + final int sectionPosition = position - sectionObject.position; + + return getItemViewInternalType(section, sectionPosition); + } + + private int getItemViewInternalType(int section, int position) { + return position == 0 ? TYPE_HEADER : TYPE_ITEM; + } + + private static int internalViewType(int type) { + return type & 0xFF; + } + + private static int externalViewType(int type) { + return type >> 8; + } + + @Override + public final int getItemCount() { + if (mSections == null) { + calculateSections(); + } + return mTotalItemNumber; + } + + @NonNull + @Override + public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final int internalType = internalViewType(viewType); + final int externalType = externalViewType(viewType); + + switch (internalType) { + case TYPE_HEADER: + return onCreateHeaderViewHolder(parent, externalType); + case TYPE_ITEM: + return onCreateItemViewHolder(parent, externalType); + default: + throw new InvalidParameterException("Invalid viewType: " + viewType); + } + } + + @Override + public final void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (mSections == null) { + calculateSections(); + } + + final int section = mSectionIndices[position]; + final int internalType = internalViewType(holder.getItemViewType()); + final int externalType = externalViewType(holder.getItemViewType()); + + switch (internalType) { + case TYPE_HEADER: + onBindHeaderViewHolder((HeaderViewHolder) holder, section); + break; + case TYPE_ITEM: + final ItemViewHolder itemHolder = (ItemViewHolder) holder; + final int offset = getItemSectionOffset(section, position); + onBindItemViewHolder((ItemViewHolder) holder, section, offset); + break; + default: + throw new InvalidParameterException("invalid viewType: " + internalType); + } + } + + @Override + public final int getItemViewType(int position) { + final int section = getAdapterPositionSection(position); + final Section sectionObject = mSections.get(section); + final int sectionPosition = position - sectionObject.position; + final int internalType = getItemViewInternalType(section, sectionPosition); + int externalType = 0; + + switch (internalType) { + case TYPE_HEADER: + externalType = getSectionHeaderViewType(section); + break; + case TYPE_ITEM: + externalType = getSectionItemViewType(section, sectionPosition - 1); + break; + } + + return ((externalType & 0xFF) << 8) | (internalType & 0xFF); + } + + // Helpers + private int getItemSectionHeaderPosition(int position) { + return getSectionHeaderPosition(getAdapterPositionSection(position)); + } + + private int getAdapterPosition(int section, int offset) { + if (mSections == null) { + calculateSections(); + } + + if (section < 0) { + throw new IndexOutOfBoundsException("section " + section + " < 0"); + } + + if (section >= mSections.size()) { + throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); + } + + final Section sectionObject = mSections.get(section); + return sectionObject.position + offset; + } + + /** + * Given a section and an adapter position get the offset of an item + * inside section. + * + * @param section section to query + * @param position adapter position + * @return The item offset inside the section. + */ + public int getItemSectionOffset(int section, int position) { + if (mSections == null) { + calculateSections(); + } + + if (section < 0) { + throw new IndexOutOfBoundsException("section " + section + " < 0"); + } + + if (section >= mSections.size()) { + throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); + } + + final Section sectionObject = mSections.get(section); + final int localPosition = position - sectionObject.position; + if (localPosition >= sectionObject.length) { + throw new IndexOutOfBoundsException( + "localPosition: " + localPosition + " >=" + sectionObject.length); + } + + return localPosition - 1; + } + + /** + * Returns the section index having item or header with provided provider position. + * + * @param position adapter position + * @return The section containing provided adapter position. + */ + public int getAdapterPositionSection(int position) { + if (mSections == null) { + calculateSections(); + } + + if (getItemCount() == 0) { + return NO_POSITION; + } + + if (position < 0) { + throw new IndexOutOfBoundsException("position " + position + " < 0"); + } + + if (position >= getItemCount()) { + throw new IndexOutOfBoundsException("position " + position + " >=" + getItemCount()); + } + + return mSectionIndices[position]; + } + + /** + * Returns the adapter position for given section header. Use this only for {@link + * RecyclerView#scrollToPosition(int)} or similar functions. Never directly manipulate adapter + * items using this position. + * + * @param section section to query + * @return The adapter position. + */ + public int getSectionHeaderPosition(int section) { + return getAdapterPosition(section, 0); + } + + /** + * Returns the adapter position for given section and offset. Use this + * only for {@link RecyclerView#scrollToPosition(int)} or similar functions. Never directly + * manipulate adapter items using this position. + * + * @param section section to query + * @param position item position inside the section + * @return The adapter position. + */ + public int getSectionItemPosition(int section, int position) { + return getAdapterPosition(section, position + 1); + } + + // Overrides + /** + * Returns the total number of sections in the data set held by the adapter. + * + * @return The total number of section in this adapter. + */ + public int getSectionCount() { + return 0; + } + + /** + * Returns the number of items in the section. + * + * @param section section to query + * @return The total number of items in the section. + */ + public int getSectionItemCount(int section) { + return 0; + } + + /** + * Return the view type of the section header for the purposes of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of a single view + * type for the headers. Unlike ListView adapters, types need not be contiguous. Consider using id + * resources to uniquely identify item view types. + * + * @param section section to query + * @return integer value identifying the type of the view needed to represent the header in + * section. Type codes need not be contiguous. + */ + public int getSectionHeaderViewType(int section) { + return 0; + } + + /** + * Return the view type of the item at position in section for the + * purposes of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of a single view + * type for the adapter. Unlike ListView adapters, types need not be contiguous. Consider using id + * resources to uniquely identify item view types. + * + * @param section section to query + * @param offset section position to query + * @return integer value identifying the type of the view needed to represent the item at + * position in section. Type codes need not be contiguous. + */ + public int getSectionItemViewType(int section, int offset) { + return 0; + } + + /** + * Returns true if header in section is sticky. + * + * @param section section to query + * @return true if section header is sticky. + */ + public boolean isSectionHeaderSticky(int section) { + return true; + } + + /** + * Called when RecyclerView needs a new {@link HeaderViewHolder} of the given type to represent a + * header. + * + *

This new HeaderViewHolder should be constructed with a new View that can represent the + * headers of the given type. You can either create a new View manually or inflate it from an XML + * layout file. + * + *

The new HeaderViewHolder will be used to display items of the adapter using {@link + * #onBindHeaderViewHolder(HeaderViewHolder, int)}. Since it will be re-used to display different + * items in the data set, it is a good idea to cache references to sub views of the View to avoid + * unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to an + * adapter position. + * @param headerType The view type of the new View. + * @return A new ViewHolder that holds a View of the given view type. + * @see #getSectionHeaderViewType(int) + * @see #onBindHeaderViewHolder(HeaderViewHolder, int) + */ + public abstract HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType); + + /** + * Called when RecyclerView needs a new {@link ItemViewHolder} of the given type to represent an + * item. + * + *

This new ViewHolder should be constructed with a new View that can represent the items of + * the given type. You can either create a new View manually or inflate it from an XML layout + * file. + * + *

The new ViewHolder will be used to display items of the adapter using {@link + * #onBindItemViewHolder(ItemViewHolder, int, int)}. Since it will be re-used to display different + * items in the data set, it is a good idea to cache references to sub views of the View to avoid + * unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to an + * adapter position. + * @param itemType The view type of the new View. + * @return A new ViewHolder that holds a View of the given view type. + * @see #getSectionItemViewType(int, int) + * @see #onBindItemViewHolder(ItemViewHolder, int, int) + */ + public abstract ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType); + + /** + * Called by RecyclerView to display the data at the specified position. This method should update + * the contents of the {@link HeaderViewHolder#itemView} to reflect the header at the given + * position. + * + *

Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the header changes in the data set unless the header itself is + * invalidated or the new position cannot be determined. For this reason, you should only use the + * section parameter while acquiring the related header data inside this method and + * should not keep a copy of it. If you need the position of a header later on (e.g. in a click + * listener), use {@link HeaderViewHolder#getAdapterPosition()} which will have the updated + * adapter position. Then you can use {@link #getAdapterPositionSection(int)} to get section + * index. + * + * @param viewHolder The ViewHolder which should be updated to represent the contents of the + * header at the given position in the data set. + * @param section The index of the section. + */ + public abstract void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section); + + /** + * Called by RecyclerView to display the data at the specified position. This method should update + * the contents of the {@link ItemViewHolder#itemView} to reflect the item at the given position. + * + *

Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is invalidated + * or the new position cannot be determined. For this reason, you should only use the offset + * and section parameters while acquiring the related data item inside this + * method and should not keep a copy of it. If you need the position of an item later on (e.g. in + * a click listener), use {@link ItemViewHolder#getAdapterPosition()} which will have the updated + * adapter position. Then you can use {@link #getAdapterPositionSection(int)} and {@link + * #getItemSectionOffset(int, int)} + * + * @param viewHolder The ViewHolder which should be updated to represent the contents of the item + * at the given position in the data set. + * @param section The index of the section. + * @param offset The position of the item within the section. + */ + public abstract void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset); + + // Notify + /** + * Notify any registered observers that the data set has changed. + * + *

There are two different classes of data change events, item changes and structural changes. + * Item changes are when a single item has its data updated but no positional changes have + * occurred. Structural changes are when items are inserted, removed or moved within the data set. + * + *

This event does not specify what about the data set has changed, forcing any observers to + * assume that all existing items and structure may no longer be valid. LayoutManagers will be + * forced to fully rebind and relayout all visible views. + * + *

RecyclerView will attempt to synthesize visible structural change events for + * adapters that report that they have {@link #hasStableIds() stable IDs} when this method is + * used. This can help for the purposes of animation and visual object persistence but individual + * item views will still need to be rebound and relaid out. + * + *

If you are writing an adapter it will always be more efficient to use the more specific + * change events if you can. Rely on notifyDataSetChanged() as a last resort. + * + * @see #notifySectionDataSetChanged(int) + * @see #notifySectionHeaderChanged(int) + * @see #notifySectionItemChanged(int, int) + * @see #notifySectionInserted(int) + * @see #notifySectionItemInserted(int, int) + * @see #notifySectionItemRangeInserted(int, int, int) + * @see #notifySectionRemoved(int) + * @see #notifySectionItemRemoved(int, int) + * @see #notifySectionItemRangeRemoved(int, int, int) + */ + public void notifyAllSectionsDataSetChanged() { + calculateSections(); + notifyDataSetChanged(); + } + + public void notifySectionDataSetChanged(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { final Section sectionObject = mSections.get(section); - final int sectionPosition = position - sectionObject.position; - - return getItemViewInternalType(section, sectionPosition); - } - - private int getItemViewInternalType(int section, int position) { - return position == 0 ? TYPE_HEADER : TYPE_ITEM; - } - - static private int internalViewType(int type) { - return type & 0xFF; - } - - static private int externalViewType(int type) { - return type >> 8; - } - - @Override - final public int getItemCount() { - if (mSections == null) { - calculateSections(); - } - return mTotalItemNumber; - } - - @NonNull - @Override - final public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - final int internalType = internalViewType(viewType); - final int externalType = externalViewType(viewType); - - switch (internalType) { - case TYPE_HEADER: - return onCreateHeaderViewHolder(parent, externalType); - case TYPE_ITEM: - return onCreateItemViewHolder(parent, externalType); - default: - throw new InvalidParameterException("Invalid viewType: " + viewType); - } - } - - @Override - final public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - if (mSections == null) { - calculateSections(); - } - - final int section = mSectionIndices[position]; - final int internalType = internalViewType(holder.getItemViewType()); - final int externalType = externalViewType(holder.getItemViewType()); - - switch (internalType) { - case TYPE_HEADER: - onBindHeaderViewHolder((HeaderViewHolder)holder, section); - break; - case TYPE_ITEM: - final ItemViewHolder itemHolder = (ItemViewHolder)holder; - final int offset = getItemSectionOffset(section, position); - onBindItemViewHolder((ItemViewHolder)holder, section, offset); - break; - default: - throw new InvalidParameterException("invalid viewType: " + internalType); - } - } - - @Override - final public int getItemViewType(int position) { - final int section = getAdapterPositionSection(position); + notifyItemRangeChanged(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionHeaderChanged(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + notifyItemRangeChanged(sectionObject.position, 1); + } + } + + public void notifySectionItemChanged(int section, int position) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { final Section sectionObject = mSections.get(section); - final int sectionPosition = position - sectionObject.position; - final int internalType = getItemViewInternalType(section, sectionPosition); - int externalType = 0; - - switch (internalType) { - case TYPE_HEADER: - externalType = getSectionHeaderViewType(section); - break; - case TYPE_ITEM: - externalType = getSectionItemViewType(section, sectionPosition - 1); - break; - } - - return ((externalType & 0xFF) << 8) | (internalType & 0xFF); - } - - // Helpers - private int getItemSectionHeaderPosition(int position) { - return getSectionHeaderPosition(getAdapterPositionSection(position)); - } - - private int getAdapterPosition(int section, int offset) { - if (mSections == null) { - calculateSections(); - } - if (section < 0) { - throw new IndexOutOfBoundsException("section " + section + " < 0"); + if (position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); } - if (section >= mSections.size()) { - throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); - } + notifyItemChanged(sectionObject.position + position + 1); + } + } + public void notifySectionInserted(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + notifyItemRangeInserted(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionItemInserted(int section, int position) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { final Section sectionObject = mSections.get(section); - return sectionObject.position + offset; - } - - /** - * Given a section and an adapter position get the offset of an item - * inside section. - * - * @param section section to query - * @param position adapter position - * @return The item offset inside the section. - */ - public int getItemSectionOffset(int section, int position) { - if (mSections == null) { - calculateSections(); - } - if (section < 0) { - throw new IndexOutOfBoundsException("section " + section + " < 0"); + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); } - if (section >= mSections.size()) { - throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); - } + notifyItemInserted(sectionObject.position + position + 1); + } + } + public void notifySectionItemRangeInserted(int section, int position, int count) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { final Section sectionObject = mSections.get(section); - final int localPosition = position - sectionObject.position; - if (localPosition >= sectionObject.length) { - throw new IndexOutOfBoundsException("localPosition: " + localPosition + " >=" + sectionObject.length); - } - return localPosition - 1; - } - - /** - * Returns the section index having item or header with provided - * provider position. - * - * @param position adapter position - * @return The section containing provided adapter position. - */ - public int getAdapterPositionSection(int position) { - if (mSections == null) { - calculateSections(); + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); } - - if (getItemCount() == 0) { - return NO_POSITION; - } - - if (position < 0) { - throw new IndexOutOfBoundsException("position " + position + " < 0"); - } - - if (position >= getItemCount()) { - throw new IndexOutOfBoundsException("position " + position + " >=" + getItemCount()); + if (position + count > sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); } - return mSectionIndices[position]; - } - - /** - * Returns the adapter position for given section header. Use - * this only for {@link RecyclerView#scrollToPosition(int)} or similar functions. - * Never directly manipulate adapter items using this position. - * - * @param section section to query - * @return The adapter position. - */ - public int getSectionHeaderPosition(int section) { - return getAdapterPosition(section, 0); - } - - /** - * Returns the adapter position for given section and - * offset. Use this only for {@link RecyclerView#scrollToPosition(int)} - * or similar functions. Never directly manipulate adapter items using this position. - * - * @param section section to query - * @param position item position inside the section - * @return The adapter position. - */ - public int getSectionItemPosition(int section, int position) { - return getAdapterPosition(section, position + 1); - } - - // Overrides - /** - * Returns the total number of sections in the data set held by the adapter. - * - * @return The total number of section in this adapter. - */ - public int getSectionCount() { - return 0; - } - - /** - * Returns the number of items in the section. - * - * @param section section to query - * @return The total number of items in the section. - */ - public int getSectionItemCount(int section) { - return 0; - } - - /** - * Return the view type of the section header for the purposes - * of view recycling. - * - *

The default implementation of this method returns 0, making the assumption of - * a single view type for the headers. Unlike ListView adapters, types need not - * be contiguous. Consider using id resources to uniquely identify item view types. - * - * @param section section to query - * @return integer value identifying the type of the view needed to represent the header in - * section. Type codes need not be contiguous. - */ - public int getSectionHeaderViewType(int section) { - return 0; - } - - /** - * Return the view type of the item at position in section for - * the purposes of view recycling. - * - *

The default implementation of this method returns 0, making the assumption of - * a single view type for the adapter. Unlike ListView adapters, types need not - * be contiguous. Consider using id resources to uniquely identify item view types. - * - * @param section section to query - * @param offset section position to query - * @return integer value identifying the type of the view needed to represent the item at - * position in section. Type codes need not be - * contiguous. - */ - public int getSectionItemViewType(int section, int offset) { - return 0; - } - - /** - * Returns true if header in section is sticky. - * - * @param section section to query - * @return true if section header is sticky. - */ - public boolean isSectionHeaderSticky(int section) { - return true; - } - - /** - * Called when RecyclerView needs a new {@link HeaderViewHolder} of the given type to represent - * a header. - *

- * This new HeaderViewHolder should be constructed with a new View that can represent the headers - * of the given type. You can either create a new View manually or inflate it from an XML - * layout file. - *

- * The new HeaderViewHolder will be used to display items of the adapter using - * {@link #onBindHeaderViewHolder(HeaderViewHolder, int)}. Since it will be re-used to display - * different items in the data set, it is a good idea to cache references to sub views of - * the View to avoid unnecessary {@link View#findViewById(int)} calls. - * - * @param parent The ViewGroup into which the new View will be added after it is bound to - * an adapter position. - * @param headerType The view type of the new View. - * - * @return A new ViewHolder that holds a View of the given view type. - * @see #getSectionHeaderViewType(int) - * @see #onBindHeaderViewHolder(HeaderViewHolder, int) - */ - public abstract HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType); - - /** - * Called when RecyclerView needs a new {@link ItemViewHolder} of the given type to represent - * an item. - *

- * This new ViewHolder should be constructed with a new View that can represent the items - * of the given type. You can either create a new View manually or inflate it from an XML - * layout file. - *

- * The new ViewHolder will be used to display items of the adapter using - * {@link #onBindItemViewHolder(ItemViewHolder, int, int)}. Since it will be re-used to display - * different items in the data set, it is a good idea to cache references to sub views of - * the View to avoid unnecessary {@link View#findViewById(int)} calls. - * - * @param parent The ViewGroup into which the new View will be added after it is bound to - * an adapter position. - * @param itemType The view type of the new View. - * - * @return A new ViewHolder that holds a View of the given view type. - * @see #getSectionItemViewType(int, int) - * @see #onBindItemViewHolder(ItemViewHolder, int, int) - */ - public abstract ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType); - - /** - * Called by RecyclerView to display the data at the specified position. This method should - * update the contents of the {@link HeaderViewHolder#itemView} to reflect the header at the given - * position. - *

- * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method - * again if the position of the header changes in the data set unless the header itself is - * invalidated or the new position cannot be determined. For this reason, you should only - * use the section parameter while acquiring the - * related header data inside this method and should not keep a copy of it. If you need the - * position of a header later on (e.g. in a click listener), use - * {@link HeaderViewHolder#getAdapterPosition()} which will have the updated adapter - * position. Then you can use {@link #getAdapterPositionSection(int)} to get section index. - * - * - * @param viewHolder The ViewHolder which should be updated to represent the contents of the - * header at the given position in the data set. - * @param section The index of the section. - */ - public abstract void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section); - - /** - * Called by RecyclerView to display the data at the specified position. This method should - * update the contents of the {@link ItemViewHolder#itemView} to reflect the item at the given - * position. - *

- * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method - * again if the position of the item changes in the data set unless the item itself is - * invalidated or the new position cannot be determined. For this reason, you should only - * use the offset and section parameters while acquiring the - * related data item inside this method and should not keep a copy of it. If you need the - * position of an item later on (e.g. in a click listener), use - * {@link ItemViewHolder#getAdapterPosition()} which will have the updated adapter - * position. Then you can use {@link #getAdapterPositionSection(int)} and - * {@link #getItemSectionOffset(int, int)} - * - * - * @param viewHolder The ViewHolder which should be updated to represent the contents of the - * item at the given position in the data set. - * @param section The index of the section. - * @param offset The position of the item within the section. - */ - public abstract void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset); - - // Notify - /** - * Notify any registered observers that the data set has changed. - * - *

There are two different classes of data change events, item changes and structural - * changes. Item changes are when a single item has its data updated but no positional - * changes have occurred. Structural changes are when items are inserted, removed or moved - * within the data set.

- * - *

This event does not specify what about the data set has changed, forcing - * any observers to assume that all existing items and structure may no longer be valid. - * LayoutManagers will be forced to fully rebind and relayout all visible views.

- * - *

RecyclerView will attempt to synthesize visible structural change events - * for adapters that report that they have {@link #hasStableIds() stable IDs} when - * this method is used. This can help for the purposes of animation and visual - * object persistence but individual item views will still need to be rebound - * and relaid out.

- * - *

If you are writing an adapter it will always be more efficient to use the more - * specific change events if you can. Rely on notifyDataSetChanged() - * as a last resort.

- * - * @see #notifySectionDataSetChanged(int) - * @see #notifySectionHeaderChanged(int) - * @see #notifySectionItemChanged(int, int) - * @see #notifySectionInserted(int) - * @see #notifySectionItemInserted(int, int) - * @see #notifySectionItemRangeInserted(int, int, int) - * @see #notifySectionRemoved(int) - * @see #notifySectionItemRemoved(int, int) - * @see #notifySectionItemRangeRemoved(int, int, int) - */ - public void notifyAllSectionsDataSetChanged() { - calculateSections(); - notifyDataSetChanged(); - } + notifyItemRangeInserted(sectionObject.position + position + 1, count); + } + } - public void notifySectionDataSetChanged(int section) { + public void notifySectionRemoved(int section) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - notifyItemRangeChanged(sectionObject.position, sectionObject.length); - } - } - - public void notifySectionHeaderChanged(int section) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - notifyItemRangeChanged(sectionObject.position, 1); - } - } + notifyItemRangeRemoved(sectionObject.position, sectionObject.length); + } + } - public void notifySectionItemChanged(int section, int position) { + public void notifySectionItemRemoved(int section, int position) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - - if (position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - - notifyItemChanged(sectionObject.position + position + 1); - } - } + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); - public void notifySectionInserted(int section) { - calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); } - else { - final Section sectionObject = mSections.get(section); - notifyItemRangeInserted(sectionObject.position, sectionObject.length); - } - } - public void notifySectionItemInserted(int section, int position) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } + notifyItemRemoved(sectionObject.position + position + 1); + } + } - notifyItemInserted(sectionObject.position + position + 1); - } - } - - public void notifySectionItemRangeInserted(int section, int position, int count) { + public void notifySectionItemRangeRemoved(int section, int position, int count) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - if (position + count > sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); - } - - notifyItemRangeInserted(sectionObject.position + position + 1, count); - } - } - - public void notifySectionRemoved(int section) { - if (mSections == null) { - calculateSections(); - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - calculateSections(); - notifyItemRangeRemoved(sectionObject.position, sectionObject.length); - } - } + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); - public void notifySectionItemRemoved(int section, int position) { - if (mSections == null) { - calculateSections(); - notifyAllSectionsDataSetChanged(); + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); } - else { - final Section sectionObject = mSections.get(section); - - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - - calculateSections(); - notifyItemRemoved(sectionObject.position + position + 1); + if (position + count > sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); } - } - public void notifySectionItemRangeRemoved(int section, int position, int count) { - if (mSections == null) { - calculateSections(); - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - if (position + count > sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); - } - - calculateSections(); - notifyItemRangeRemoved(sectionObject.position + position + 1, count); - } - } + calculateSections(); + notifyItemRangeRemoved(sectionObject.position + position + 1, count); + } + } } diff --git a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java index 24aa99eea..2fd5c9887 100644 --- a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java +++ b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java @@ -12,276 +12,270 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; - import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; - import java.util.ArrayList; import java.util.Arrays; -/** - * Created by Sergej Kravcenko on 4/24/2017. - * Copyright (c) 2017 Sergej Kravcenko - */ - +/** Created by Sergej Kravcenko on 4/24/2017. Copyright (c) 2017 Sergej Kravcenko */ @SuppressWarnings({"unused", "WeakerAccess"}) -public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider { - public static final String TAG = "StickyLayoutManager"; - - private static final int DEFAULT_ROW_COUNT = 16; - - private int mSpanCount; - private SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); - - private StickyHeaderGridAdapter mAdapter; - - private int mHeadersStartPosition; - - private View mFloatingHeaderView; - private int mFloatingHeaderPosition; - private int mStickOffset; - private int mAverageHeaderHeight; - private int mHeaderOverlapMargin; - - private HeaderStateChangeListener mHeaderStateListener; - private int mStickyHeaderSection = NO_POSITION; - private View mStickyHeaderView; - private HeaderState mStickyHeadeState; - - private View mFillViewSet[]; - - private SavedState mPendingSavedState; - private int mPendingScrollPosition = NO_POSITION; - private int mPendingScrollPositionOffset; - private AnchorPosition mAnchor = new AnchorPosition(); - - private final FillResult mFillResult = new FillResult(); - private ArrayList mLayoutRows = new ArrayList<>(DEFAULT_ROW_COUNT); - - public enum HeaderState { - NORMAL, - STICKY, - PUSHED - } - - /** - * The interface to be implemented by listeners to header events from this - * LayoutManager. - */ - public interface HeaderStateChangeListener { - /** - * Called when a section header state changes. The position can be HeaderState.NORMAL, - * HeaderState.STICKY, HeaderState.PUSHED. - * - *

- *

    - *
  • NORMAL - the section header is invisible or has normal position
  • - *
  • STICKY - the section header is sticky at the top of RecyclerView
  • - *
  • PUSHED - the section header is sticky and pushed up by next header
  • - *
0) { - state.mAnchorSection = mAnchor.section; - state.mAnchorItem = mAnchor.item; - state.mAnchorOffset = mAnchor.offset; - } - else { - state.invalidateAnchor(); - } - - return state; - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (state instanceof SavedState) { - mPendingSavedState = (SavedState) state; - requestLayout(); - } - else { - Log.d(TAG, "invalid saved state class"); - } - } - - @Override - public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { - return lp instanceof LayoutParams; - } - - @Override - public boolean canScrollVertically() { - return true; - } - - /** - *

Scroll the RecyclerView to make the position visible.

- * - *

RecyclerView will scroll the minimum amount that is necessary to make the - * target position visible. - * - *

Note that scroll position change will not be reflected until the next layout call.

- * - * @param position Scroll to this adapter position - */ - @Override - public void scrollToPosition(int position) { - if (position < 0 || position > getItemCount()) { - throw new IndexOutOfBoundsException("adapter position out of range"); - } - - mPendingScrollPosition = position; - mPendingScrollPositionOffset = 0; - if (mPendingSavedState != null) { - mPendingSavedState.invalidateAnchor(); - } +public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager + implements RecyclerView.SmoothScroller.ScrollVectorProvider { + public static final String TAG = "StickyLayoutManager"; + + private static final int DEFAULT_ROW_COUNT = 16; + + private int mSpanCount; + private SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); + + private StickyHeaderGridAdapter mAdapter; + + private int mHeadersStartPosition; + + private View mFloatingHeaderView; + private int mFloatingHeaderPosition; + private int mStickOffset; + private int mAverageHeaderHeight; + private int mHeaderOverlapMargin; + + private HeaderStateChangeListener mHeaderStateListener; + private int mStickyHeaderSection = NO_POSITION; + private View mStickyHeaderView; + private HeaderState mStickyHeadeState; + + private View mFillViewSet[]; + + private SavedState mPendingSavedState; + private int mPendingScrollPosition = NO_POSITION; + private int mPendingScrollPositionOffset; + private AnchorPosition mAnchor = new AnchorPosition(); + + private final FillResult mFillResult = new FillResult(); + private ArrayList mLayoutRows = new ArrayList<>(DEFAULT_ROW_COUNT); + + public enum HeaderState { + NORMAL, + STICKY, + PUSHED + } + + /** The interface to be implemented by listeners to header events from this LayoutManager. */ + public interface HeaderStateChangeListener { + /** + * Called when a section header state changes. The position can be HeaderState.NORMAL, + * HeaderState.STICKY, HeaderState.PUSHED. + * + *

+ * + *

    + *
  • NORMAL - the section header is invisible or has normal position + *
  • STICKY - the section header is sticky at the top of RecyclerView + *
  • PUSHED - the section header is sticky and pushed up by next header
0) { + state.mAnchorSection = mAnchor.section; + state.mAnchorItem = mAnchor.item; + state.mAnchorOffset = mAnchor.offset; + } else { + state.invalidateAnchor(); + } + + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = (SavedState) state; requestLayout(); - } - - private int getExtraLayoutSpace(RecyclerView.State state) { - if (state.hasTargetScrollPosition()) { - return getHeight(); - } - else { - return 0; - } - } - - @Override - public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state, int position) { - final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { - @Override - public int calculateDyToMakeVisible(View view, int snapPreference) { + } else { + Log.d(TAG, "invalid saved state class"); + } + } + + @Override + public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { + return lp instanceof LayoutParams; + } + + @Override + public boolean canScrollVertically() { + return true; + } + + /** + * Scroll the RecyclerView to make the position visible. + * + *

RecyclerView will scroll the minimum amount that is necessary to make the target position + * visible. + * + *

Note that scroll position change will not be reflected until the next layout call. + * + * @param position Scroll to this adapter position + */ + @Override + public void scrollToPosition(int position) { + if (position < 0 || position > getItemCount()) { + throw new IndexOutOfBoundsException("adapter position out of range"); + } + + mPendingScrollPosition = position; + mPendingScrollPositionOffset = 0; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + private int getExtraLayoutSpace(RecyclerView.State state) { + if (state.hasTargetScrollPosition()) { + return getHeight(); + } else { + return 0; + } + } + + @Override + public void smoothScrollToPosition( + final RecyclerView recyclerView, RecyclerView.State state, int position) { + final LinearSmoothScroller linearSmoothScroller = + new LinearSmoothScroller(recyclerView.getContext()) { + @Override + public int calculateDyToMakeVisible(View view, int snapPreference) { final RecyclerView.LayoutManager layoutManager = getLayoutManager(); if (layoutManager == null || !layoutManager.canScrollVertically()) { - return 0; + return 0; } final int adapterPosition = getPosition(view); @@ -291,1082 +285,1096 @@ public int calculateDyToMakeVisible(View view, int snapPreference) { final int start = layoutManager.getPaddingTop() + topOffset; final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); return calculateDtToFit(top, bottom, start, end, snapPreference); - } - }; - linearSmoothScroller.setTargetPosition(position); - startSmoothScroll(linearSmoothScroller); - } - - @Override - public PointF computeScrollVectorForPosition(int targetPosition) { - if (getChildCount() == 0) { - return null; - } - - final LayoutRow firstRow = getFirstVisibleRow(); - if (firstRow == null) { - return null; - } - - return new PointF(0, targetPosition - firstRow.adapterPosition); - } - - private int getAdapterPositionFromAnchor(AnchorPosition anchor) { - if (anchor.section < 0 || anchor.section >= mAdapter.getSectionCount()) { - anchor.reset(); - return NO_POSITION; - } - else if (anchor.item < 0 || anchor.item >= mAdapter.getSectionItemCount(anchor.section)) { - anchor.offset = 0; - return mAdapter.getSectionHeaderPosition(anchor.section); - } - return mAdapter.getSectionItemPosition(anchor.section, anchor.item); - } - - private int getAdapterPositionChecked(int section, int offset) { - if (section < 0 || section >= mAdapter.getSectionCount()) { - return NO_POSITION; - } - else if (offset < 0 || offset >= mAdapter.getSectionItemCount(section)) { - return mAdapter.getSectionHeaderPosition(section); - } - return mAdapter.getSectionItemPosition(section, offset); - } - - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - if (mAdapter == null || state.getItemCount() == 0) { - removeAndRecycleAllViews(recycler); - clearState(); - return; - } - - int pendingAdapterPosition; - int pendingAdapterOffset; - if (mPendingScrollPosition >= 0) { - pendingAdapterPosition = mPendingScrollPosition; - pendingAdapterOffset = mPendingScrollPositionOffset; - } - else if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { - pendingAdapterPosition = getAdapterPositionChecked(mPendingSavedState.mAnchorSection, mPendingSavedState.mAnchorItem); - pendingAdapterOffset = mPendingSavedState.mAnchorOffset; - mPendingSavedState = null; - } - else { - pendingAdapterPosition = getAdapterPositionFromAnchor(mAnchor); - pendingAdapterOffset = mAnchor.offset; - } + } + }; + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + if (getChildCount() == 0) { + return null; + } - if (pendingAdapterPosition < 0 || pendingAdapterPosition >= state.getItemCount()) { - pendingAdapterPosition = 0; - pendingAdapterOffset = 0; - mPendingScrollPosition = NO_POSITION; - } + final LayoutRow firstRow = getFirstVisibleRow(); + if (firstRow == null) { + return null; + } - if (pendingAdapterOffset > 0) { - pendingAdapterOffset = 0; - } + return new PointF(0, targetPosition - firstRow.adapterPosition); + } - detachAndScrapAttachedViews(recycler); + private int getAdapterPositionFromAnchor(AnchorPosition anchor) { + if (anchor.section < 0 || anchor.section >= mAdapter.getSectionCount()) { + anchor.reset(); + return NO_POSITION; + } else if (anchor.item < 0 || anchor.item >= mAdapter.getSectionItemCount(anchor.section)) { + anchor.offset = 0; + return mAdapter.getSectionHeaderPosition(anchor.section); + } + return mAdapter.getSectionItemPosition(anchor.section, anchor.item); + } + + private int getAdapterPositionChecked(int section, int offset) { + if (section < 0 || section >= mAdapter.getSectionCount()) { + return NO_POSITION; + } else if (offset < 0 || offset >= mAdapter.getSectionItemCount(section)) { + return mAdapter.getSectionHeaderPosition(section); + } + return mAdapter.getSectionItemPosition(section, offset); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mAdapter == null || state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); clearState(); - - // Make sure mFirstViewPosition is the start of the row - pendingAdapterPosition = findFirstRowItem(pendingAdapterPosition); - - int left = getPaddingLeft(); - int right = getWidth() - getPaddingRight(); - final int recyclerBottom = getHeight() - getPaddingBottom(); - int totalHeight = 0; - - int adapterPosition = pendingAdapterPosition; - int top = getPaddingTop() + pendingAdapterOffset; - while (true) { - if (adapterPosition >= state.getItemCount()) { - break; - } - - int bottom; - final int viewType = mAdapter.getItemViewInternalType(adapterPosition); - if (viewType == TYPE_HEADER) { - final View v = recycler.getViewForPosition(adapterPosition); - addView(v); - measureChildWithMargins(v, 0, 0); - - int height = getDecoratedMeasuredHeight(v); - final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; - bottom = top + height; - layoutDecorated(v, left, top, right, bottom); - - bottom -= margin; - height -= margin; - mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, bottom)); - adapterPosition++; - mAverageHeaderHeight = height; - } - else { - final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); - bottom = top + result.height; - mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, bottom)); - adapterPosition += result.length; - } - top = bottom; - - if (bottom >= recyclerBottom + getExtraLayoutSpace(state)) { - break; - } - } - - if (getBottomRow().bottom < recyclerBottom) { - scrollVerticallyBy(getBottomRow().bottom - recyclerBottom, recycler, state); - } - else { - clearViewsAndStickHeaders(recycler, state, false); - } - - // If layout was caused by the pending scroll, adjust top item position and move it under sticky header - if (mPendingScrollPosition >= 0) { - mPendingScrollPosition = NO_POSITION; - - final int topOffset = getPositionSectionHeaderHeight(pendingAdapterPosition); - if (topOffset != 0) { - scrollVerticallyBy(-topOffset, recycler, state); - } - } - } - - @Override - public void onLayoutCompleted(RecyclerView.State state) { - super.onLayoutCompleted(state); + return; + } + + int pendingAdapterPosition; + int pendingAdapterOffset; + if (mPendingScrollPosition >= 0) { + pendingAdapterPosition = mPendingScrollPosition; + pendingAdapterOffset = mPendingScrollPositionOffset; + } else if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + pendingAdapterPosition = + getAdapterPositionChecked( + mPendingSavedState.mAnchorSection, mPendingSavedState.mAnchorItem); + pendingAdapterOffset = mPendingSavedState.mAnchorOffset; mPendingSavedState = null; - } - - private int getPositionSectionHeaderHeight(int adapterPosition) { - final int section = mAdapter.getAdapterPositionSection(adapterPosition); - if (section >= 0 && mAdapter.isSectionHeaderSticky(section)) { - final int offset = mAdapter.getItemSectionOffset(section, adapterPosition); - if (offset >= 0) { - final int headerAdapterPosition = mAdapter.getSectionHeaderPosition(section); - if (mFloatingHeaderView != null && headerAdapterPosition == mFloatingHeaderPosition) { - return Math.max(0, getDecoratedMeasuredHeight(mFloatingHeaderView) - mHeaderOverlapMargin); - } - else { - final LayoutRow header = getHeaderRow(headerAdapterPosition); - if (header != null) { - return header.getHeight(); - } - else { - // Fall back to cached header size, can be incorrect - return mAverageHeaderHeight; - } - } - } - } - - return 0; - } - - private int findFirstRowItem(int adapterPosition) { - final int section = mAdapter.getAdapterPositionSection(adapterPosition); - int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); - while (sectionPosition > 0 && mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount) != 0) { - sectionPosition--; - adapterPosition--; - } - - return adapterPosition; - } - - private int getSpanWidth(int recyclerWidth, int spanIndex, int spanSize) { - final int spanWidth = recyclerWidth / mSpanCount; - final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; - final int widthCorrection = Math.min(Math.max(0, spanWidthReminder - spanIndex), spanSize); - - return spanWidth * spanSize + widthCorrection; - } - - private int getSpanLeft(int recyclerWidth, int spanIndex) { - final int spanWidth = recyclerWidth / mSpanCount; - final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; - final int widthCorrection = Math.min(spanWidthReminder, spanIndex); - - return spanWidth * spanIndex + widthCorrection; - } - - private FillResult fillBottomRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { - final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); - final int section = mAdapter.getAdapterPositionSection(position); - int adapterPosition = position; - int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); - int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); - int count = 0; - int maxHeight = 0; - - // Create phase - Arrays.fill(mFillViewSet, null); - while (spanIndex + spanSize <= mSpanCount) { - // Create view and fill layout params - final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); - final View v = recycler.getViewForPosition(adapterPosition); - final LayoutParams params = (LayoutParams)v.getLayoutParams(); - params.mSpanIndex = spanIndex; - params.mSpanSize = spanSize; - - addView(v, mHeadersStartPosition); - mHeadersStartPosition++; - measureChildWithMargins(v, recyclerWidth - spanWidth, 0); - mFillViewSet[count] = v; - count++; - - final int height = getDecoratedMeasuredHeight(v); - if (maxHeight < height) { - maxHeight = height; - } - - // Check next - adapterPosition++; - sectionPosition++; - if (sectionPosition >= mAdapter.getSectionItemCount(section)) { - break; - } - - spanIndex += spanSize; - spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - } - - // Layout phase - int left = getPaddingLeft(); - for (int i = 0; i < count; ++i) { - final View v = mFillViewSet[i]; - final int height = getDecoratedMeasuredHeight(v); - final int width = getDecoratedMeasuredWidth(v); - layoutDecorated(v, left, top, left + width, top + height); - left += width; - } - - mFillResult.edgeView = mFillViewSet[count - 1]; - mFillResult.adapterPosition = position; - mFillResult.length = count; - mFillResult.height = maxHeight; - - return mFillResult; - } - - private FillResult fillTopRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { - final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); - final int section = mAdapter.getAdapterPositionSection(position); - int adapterPosition = position; - int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); - int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); - int count = 0; - int maxHeight = 0; - - Arrays.fill(mFillViewSet, null); - while (spanIndex >= 0) { - // Create view and fill layout params - final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); - final View v = recycler.getViewForPosition(adapterPosition); - final LayoutParams params = (LayoutParams)v.getLayoutParams(); - params.mSpanIndex = spanIndex; - params.mSpanSize = spanSize; - - addView(v, 0); - mHeadersStartPosition++; - measureChildWithMargins(v, recyclerWidth - spanWidth, 0); - mFillViewSet[count] = v; - count++; - - final int height = getDecoratedMeasuredHeight(v); - if (maxHeight < height) { - maxHeight = height; - } - - // Check next - adapterPosition--; - sectionPosition--; - if (sectionPosition < 0) { - break; - } - - spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - spanIndex -= spanSize; - } - - // Layout phase - int left = getPaddingLeft(); - for (int i = count - 1; i >= 0; --i) { - final View v = mFillViewSet[i]; - final int height = getDecoratedMeasuredHeight(v); - final int width = getDecoratedMeasuredWidth(v); - layoutDecorated(v, left, top - maxHeight, left + width, top - (maxHeight - height)); - left += width; - } - - mFillResult.edgeView = mFillViewSet[count - 1]; - mFillResult.adapterPosition = adapterPosition + 1; - mFillResult.length = count; - mFillResult.height = maxHeight; - - return mFillResult; - } - - private void clearHiddenRows(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { - if (mLayoutRows.size() <= 0) { - return; - } - - final int recyclerTop = getPaddingTop(); - final int recyclerBottom = getHeight() - getPaddingBottom(); - - if (top) { - LayoutRow row = getTopRow(); - while (row.bottom < recyclerTop - getExtraLayoutSpace(state) || row.top > recyclerBottom) { - if (row.header) { - removeAndRecycleViewAt(mHeadersStartPosition + (mFloatingHeaderView != null ? 1 : 0), recycler); - } - else { - for (int i = 0; i < row.length; ++i) { - removeAndRecycleViewAt(0, recycler); - mHeadersStartPosition--; - } - } - mLayoutRows.remove(0); - row = getTopRow(); - } - } - else { - LayoutRow row = getBottomRow(); - while (row.bottom < recyclerTop || row.top > recyclerBottom + getExtraLayoutSpace(state)) { - if (row.header) { - removeAndRecycleViewAt(getChildCount() - 1, recycler); - } - else { - for (int i = 0; i < row.length; ++i) { - removeAndRecycleViewAt(mHeadersStartPosition - 1, recycler); - mHeadersStartPosition--; - } - } - mLayoutRows.remove(mLayoutRows.size() - 1); - row = getBottomRow(); - } - } - } + } else { + pendingAdapterPosition = getAdapterPositionFromAnchor(mAnchor); + pendingAdapterOffset = mAnchor.offset; + } - private void clearViewsAndStickHeaders(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { - clearHiddenRows(recycler, state, top); - if (getChildCount() > 0) { - stickTopHeader(recycler); - } - updateTopPosition(); - } + if (pendingAdapterPosition < 0 || pendingAdapterPosition >= state.getItemCount()) { + pendingAdapterPosition = 0; + pendingAdapterOffset = 0; + mPendingScrollPosition = NO_POSITION; + } - private LayoutRow getBottomRow() { - return mLayoutRows.get(mLayoutRows.size() - 1); - } + if (pendingAdapterOffset > 0) { + pendingAdapterOffset = 0; + } - private LayoutRow getTopRow() { - return mLayoutRows.get(0); - } + detachAndScrapAttachedViews(recycler); + clearState(); - private void offsetRowsVertical(int offset) { - for (LayoutRow row : mLayoutRows) { - row.top += offset; - row.bottom += offset; - } - offsetChildrenVertical(offset); - } + // Make sure mFirstViewPosition is the start of the row + pendingAdapterPosition = findFirstRowItem(pendingAdapterPosition); - private void addRow(RecyclerView.Recycler recycler, RecyclerView.State state, boolean isTop, int adapterPosition, int top) { - final int left = getPaddingLeft(); - final int right = getWidth() - getPaddingRight(); + int left = getPaddingLeft(); + int right = getWidth() - getPaddingRight(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + int totalHeight = 0; - // Reattach floating header if needed - if (isTop && mFloatingHeaderView != null && adapterPosition == mFloatingHeaderPosition) { - removeFloatingHeader(recycler); + int adapterPosition = pendingAdapterPosition; + int top = getPaddingTop() + pendingAdapterOffset; + while (true) { + if (adapterPosition >= state.getItemCount()) { + break; } + int bottom; final int viewType = mAdapter.getItemViewInternalType(adapterPosition); if (viewType == TYPE_HEADER) { - final View v = recycler.getViewForPosition(adapterPosition); - if (isTop) { - addView(v, mHeadersStartPosition); - } - else { - addView(v); - } - measureChildWithMargins(v, 0, 0); - final int height = getDecoratedMeasuredHeight(v); - final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; - if (isTop) { - layoutDecorated(v, left, top - height + margin, right, top + margin); - mLayoutRows.add(0, new LayoutRow(v, adapterPosition, 1, top - height + margin, top)); - } - else { - layoutDecorated(v, left, top, right, top + height); - mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, top + height - margin)); - } - mAverageHeaderHeight = height - margin; - } - else { - if (isTop) { - final FillResult result = fillTopRow(recycler, state, adapterPosition, top); - mLayoutRows.add(0, new LayoutRow(result.adapterPosition, result.length, top - result.height, top)); - } - else { - final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); - mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, top + result.height)); - } - } - } - - private void addOffScreenRows(RecyclerView.Recycler recycler, RecyclerView.State state, int recyclerTop, int recyclerBottom, boolean bottom) { - if (bottom) { - // Bottom - while (true) { - final LayoutRow bottomRow = getBottomRow(); - final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; - if (bottomRow.bottom >= recyclerBottom + getExtraLayoutSpace(state) || adapterPosition >= state.getItemCount()) { - break; - } - addRow(recycler, state, false, adapterPosition, bottomRow.bottom); - } - } - else { - // Top - while (true) { - final LayoutRow topRow = getTopRow(); - final int adapterPosition = topRow.adapterPosition - 1; - if (topRow.top < recyclerTop - getExtraLayoutSpace(state) || adapterPosition < 0) { - break; - } - addRow(recycler, state, true, adapterPosition, topRow.top); - } - } - } - - @Override - public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { - if (getChildCount() == 0) { - return 0; - } - - int scrolled = 0; - int left = getPaddingLeft(); - int right = getWidth() - getPaddingRight(); - final int recyclerTop = getPaddingTop(); - final int recyclerBottom = getHeight() - getPaddingBottom(); - - // If we have simple header stick, offset it back - final int firstHeader = getFirstVisibleSectionHeader(); - if (firstHeader != NO_POSITION) { - mLayoutRows.get(firstHeader).headerView.offsetTopAndBottom(-mStickOffset); - } - - if (dy >= 0) { - // Up - while (scrolled < dy) { - final LayoutRow bottomRow = getBottomRow(); - final int scrollChunk = -Math.min(Math.max(bottomRow.bottom - recyclerBottom, 0), dy - scrolled); - - offsetRowsVertical(scrollChunk); - scrolled -= scrollChunk; - - final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; - if (scrolled >= dy || adapterPosition >= state.getItemCount()) { - break; - } - - addRow(recycler, state, false, adapterPosition, bottomRow.bottom); - } - } - else { - // Down - while (scrolled > dy) { - final LayoutRow topRow = getTopRow(); - final int scrollChunk = Math.min(Math.max(-topRow.top + recyclerTop, 0), scrolled - dy); - - offsetRowsVertical(scrollChunk); - scrolled -= scrollChunk; - - final int adapterPosition = topRow.adapterPosition - 1; - if (scrolled <= dy || adapterPosition >= state.getItemCount() || adapterPosition < 0) { - break; - } - - addRow(recycler, state, true, adapterPosition, topRow.top); - } - } - - // Fill extra offscreen rows for smooth scroll - if (scrolled == dy) { - addOffScreenRows(recycler, state, recyclerTop, recyclerBottom, dy >= 0); - } - - clearViewsAndStickHeaders(recycler, state, dy >= 0); - return scrolled; - } - - /** - * Returns first visible item excluding headers. - * - * @param visibleTop Whether item top edge should be visible or not - * @return The first visible item adapter position closest to top of the layout. - */ - public int getFirstVisibleItemPosition(boolean visibleTop) { - return getFirstVisiblePosition(TYPE_ITEM, visibleTop); - } - - /** - * Returns last visible item excluding headers. - * - * @return The last visible item adapter position closest to bottom of the layout. - */ - public int getLastVisibleItemPosition() { - return getLastVisiblePosition(TYPE_ITEM); - } - - /** - * Returns first visible header. - * - * @param visibleTop Whether header top edge should be visible or not - * @return The first visible header adapter position closest to top of the layout. - */ - public int getFirstVisibleHeaderPosition(boolean visibleTop) { - return getFirstVisiblePosition(TYPE_HEADER, visibleTop); - } - - /** - * Returns last visible header. - * - * @return The last visible header adapter position closest to bottom of the layout. - */ - public int getLastVisibleHeaderPosition() { - return getLastVisiblePosition(TYPE_HEADER); - } - - private int getFirstVisiblePosition(int type, boolean visibleTop) { - if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { - return NO_POSITION; - } - else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { - return NO_POSITION; - } - - int viewFrom = type == TYPE_ITEM ? 0 : mHeadersStartPosition; - int viewTo = type == TYPE_ITEM ? mHeadersStartPosition : getChildCount(); - final int recyclerTop = getPaddingTop(); - for (int i = viewFrom; i < viewTo; ++i) { - final View v = getChildAt(i); - final int adapterPosition = getPosition(v); - final int headerHeight = getPositionSectionHeaderHeight(adapterPosition); - final int top = getDecoratedTop(v); - final int bottom = getDecoratedBottom(v); - - if (visibleTop) { - if (top >= recyclerTop + headerHeight) { - return adapterPosition; - } - } - else { - if (bottom >= recyclerTop + headerHeight) { - return adapterPosition; - } - } - } - + final View v = recycler.getViewForPosition(adapterPosition); + addView(v); + measureChildWithMargins(v, 0, 0); + + int height = getDecoratedMeasuredHeight(v); + final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; + bottom = top + height; + layoutDecorated(v, left, top, right, bottom); + + bottom -= margin; + height -= margin; + mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, bottom)); + adapterPosition++; + mAverageHeaderHeight = height; + } else { + final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); + bottom = top + result.height; + mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, bottom)); + adapterPosition += result.length; + } + top = bottom; + + if (bottom >= recyclerBottom + getExtraLayoutSpace(state)) { + break; + } + } + + if (getBottomRow().bottom < recyclerBottom) { + scrollVerticallyBy(getBottomRow().bottom - recyclerBottom, recycler, state); + } else { + clearViewsAndStickHeaders(recycler, state, false); + } + + // If layout was caused by the pending scroll, adjust top item position and move it under sticky + // header + if (mPendingScrollPosition >= 0) { + mPendingScrollPosition = NO_POSITION; + + final int topOffset = getPositionSectionHeaderHeight(pendingAdapterPosition); + if (topOffset != 0) { + scrollVerticallyBy(-topOffset, recycler, state); + } + } + } + + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; + } + + private int getPositionSectionHeaderHeight(int adapterPosition) { + final int section = mAdapter.getAdapterPositionSection(adapterPosition); + if (section >= 0 && mAdapter.isSectionHeaderSticky(section)) { + final int offset = mAdapter.getItemSectionOffset(section, adapterPosition); + if (offset >= 0) { + final int headerAdapterPosition = mAdapter.getSectionHeaderPosition(section); + if (mFloatingHeaderView != null && headerAdapterPosition == mFloatingHeaderPosition) { + return Math.max( + 0, getDecoratedMeasuredHeight(mFloatingHeaderView) - mHeaderOverlapMargin); + } else { + final LayoutRow header = getHeaderRow(headerAdapterPosition); + if (header != null) { + return header.getHeight(); + } else { + // Fall back to cached header size, can be incorrect + return mAverageHeaderHeight; + } + } + } + } + + return 0; + } + + private int findFirstRowItem(int adapterPosition) { + final int section = mAdapter.getAdapterPositionSection(adapterPosition); + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + while (sectionPosition > 0 + && mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount) != 0) { + sectionPosition--; + adapterPosition--; + } + + return adapterPosition; + } + + private int getSpanWidth(int recyclerWidth, int spanIndex, int spanSize) { + final int spanWidth = recyclerWidth / mSpanCount; + final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; + final int widthCorrection = Math.min(Math.max(0, spanWidthReminder - spanIndex), spanSize); + + return spanWidth * spanSize + widthCorrection; + } + + private int getSpanLeft(int recyclerWidth, int spanIndex) { + final int spanWidth = recyclerWidth / mSpanCount; + final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; + final int widthCorrection = Math.min(spanWidthReminder, spanIndex); + + return spanWidth * spanIndex + widthCorrection; + } + + private FillResult fillBottomRow( + RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { + final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int section = mAdapter.getAdapterPositionSection(position); + int adapterPosition = position; + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); + int count = 0; + int maxHeight = 0; + + // Create phase + Arrays.fill(mFillViewSet, null); + while (spanIndex + spanSize <= mSpanCount) { + // Create view and fill layout params + final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); + final View v = recycler.getViewForPosition(adapterPosition); + final LayoutParams params = (LayoutParams) v.getLayoutParams(); + params.mSpanIndex = spanIndex; + params.mSpanSize = spanSize; + + addView(v, mHeadersStartPosition); + mHeadersStartPosition++; + measureChildWithMargins(v, recyclerWidth - spanWidth, 0); + mFillViewSet[count] = v; + count++; + + final int height = getDecoratedMeasuredHeight(v); + if (maxHeight < height) { + maxHeight = height; + } + + // Check next + adapterPosition++; + sectionPosition++; + if (sectionPosition >= mAdapter.getSectionItemCount(section)) { + break; + } + + spanIndex += spanSize; + spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + } + + // Layout phase + int left = getPaddingLeft(); + for (int i = 0; i < count; ++i) { + final View v = mFillViewSet[i]; + final int height = getDecoratedMeasuredHeight(v); + final int width = getDecoratedMeasuredWidth(v); + layoutDecorated(v, left, top, left + width, top + height); + left += width; + } + + mFillResult.edgeView = mFillViewSet[count - 1]; + mFillResult.adapterPosition = position; + mFillResult.length = count; + mFillResult.height = maxHeight; + + return mFillResult; + } + + private FillResult fillTopRow( + RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { + final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int section = mAdapter.getAdapterPositionSection(position); + int adapterPosition = position; + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); + int count = 0; + int maxHeight = 0; + + Arrays.fill(mFillViewSet, null); + while (spanIndex >= 0) { + // Create view and fill layout params + final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); + final View v = recycler.getViewForPosition(adapterPosition); + final LayoutParams params = (LayoutParams) v.getLayoutParams(); + params.mSpanIndex = spanIndex; + params.mSpanSize = spanSize; + + addView(v, 0); + mHeadersStartPosition++; + measureChildWithMargins(v, recyclerWidth - spanWidth, 0); + mFillViewSet[count] = v; + count++; + + final int height = getDecoratedMeasuredHeight(v); + if (maxHeight < height) { + maxHeight = height; + } + + // Check next + adapterPosition--; + sectionPosition--; + if (sectionPosition < 0) { + break; + } + + spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + spanIndex -= spanSize; + } + + // Layout phase + int left = getPaddingLeft(); + for (int i = count - 1; i >= 0; --i) { + final View v = mFillViewSet[i]; + final int height = getDecoratedMeasuredHeight(v); + final int width = getDecoratedMeasuredWidth(v); + layoutDecorated(v, left, top - maxHeight, left + width, top - (maxHeight - height)); + left += width; + } + + mFillResult.edgeView = mFillViewSet[count - 1]; + mFillResult.adapterPosition = adapterPosition + 1; + mFillResult.length = count; + mFillResult.height = maxHeight; + + return mFillResult; + } + + private void clearHiddenRows( + RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { + if (mLayoutRows.size() <= 0) { + return; + } + + final int recyclerTop = getPaddingTop(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + + if (top) { + LayoutRow row = getTopRow(); + while (row.bottom < recyclerTop - getExtraLayoutSpace(state) || row.top > recyclerBottom) { + if (row.header) { + removeAndRecycleViewAt( + mHeadersStartPosition + (mFloatingHeaderView != null ? 1 : 0), recycler); + } else { + for (int i = 0; i < row.length; ++i) { + removeAndRecycleViewAt(0, recycler); + mHeadersStartPosition--; + } + } + mLayoutRows.remove(0); + row = getTopRow(); + } + } else { + LayoutRow row = getBottomRow(); + while (row.bottom < recyclerTop || row.top > recyclerBottom + getExtraLayoutSpace(state)) { + if (row.header) { + removeAndRecycleViewAt(getChildCount() - 1, recycler); + } else { + for (int i = 0; i < row.length; ++i) { + removeAndRecycleViewAt(mHeadersStartPosition - 1, recycler); + mHeadersStartPosition--; + } + } + mLayoutRows.remove(mLayoutRows.size() - 1); + row = getBottomRow(); + } + } + } + + private void clearViewsAndStickHeaders( + RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { + clearHiddenRows(recycler, state, top); + if (getChildCount() > 0) { + stickTopHeader(recycler); + } + updateTopPosition(); + } + + private LayoutRow getBottomRow() { + return mLayoutRows.get(mLayoutRows.size() - 1); + } + + private LayoutRow getTopRow() { + return mLayoutRows.get(0); + } + + private void offsetRowsVertical(int offset) { + for (LayoutRow row : mLayoutRows) { + row.top += offset; + row.bottom += offset; + } + offsetChildrenVertical(offset); + } + + private void addRow( + RecyclerView.Recycler recycler, + RecyclerView.State state, + boolean isTop, + int adapterPosition, + int top) { + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + + // Reattach floating header if needed + if (isTop && mFloatingHeaderView != null && adapterPosition == mFloatingHeaderPosition) { + removeFloatingHeader(recycler); + } + + final int viewType = mAdapter.getItemViewInternalType(adapterPosition); + if (viewType == TYPE_HEADER) { + final View v = recycler.getViewForPosition(adapterPosition); + if (isTop) { + addView(v, mHeadersStartPosition); + } else { + addView(v); + } + measureChildWithMargins(v, 0, 0); + final int height = getDecoratedMeasuredHeight(v); + final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; + if (isTop) { + layoutDecorated(v, left, top - height + margin, right, top + margin); + mLayoutRows.add(0, new LayoutRow(v, adapterPosition, 1, top - height + margin, top)); + } else { + layoutDecorated(v, left, top, right, top + height); + mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, top + height - margin)); + } + mAverageHeaderHeight = height - margin; + } else { + if (isTop) { + final FillResult result = fillTopRow(recycler, state, adapterPosition, top); + mLayoutRows.add( + 0, new LayoutRow(result.adapterPosition, result.length, top - result.height, top)); + } else { + final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); + mLayoutRows.add( + new LayoutRow(result.adapterPosition, result.length, top, top + result.height)); + } + } + } + + private void addOffScreenRows( + RecyclerView.Recycler recycler, + RecyclerView.State state, + int recyclerTop, + int recyclerBottom, + boolean bottom) { + if (bottom) { + // Bottom + while (true) { + final LayoutRow bottomRow = getBottomRow(); + final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; + if (bottomRow.bottom >= recyclerBottom + getExtraLayoutSpace(state) + || adapterPosition >= state.getItemCount()) { + break; + } + addRow(recycler, state, false, adapterPosition, bottomRow.bottom); + } + } else { + // Top + while (true) { + final LayoutRow topRow = getTopRow(); + final int adapterPosition = topRow.adapterPosition - 1; + if (topRow.top < recyclerTop - getExtraLayoutSpace(state) || adapterPosition < 0) { + break; + } + addRow(recycler, state, true, adapterPosition, topRow.top); + } + } + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + + int scrolled = 0; + int left = getPaddingLeft(); + int right = getWidth() - getPaddingRight(); + final int recyclerTop = getPaddingTop(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + + // If we have simple header stick, offset it back + final int firstHeader = getFirstVisibleSectionHeader(); + if (firstHeader != NO_POSITION) { + mLayoutRows.get(firstHeader).headerView.offsetTopAndBottom(-mStickOffset); + } + + if (dy >= 0) { + // Up + while (scrolled < dy) { + final LayoutRow bottomRow = getBottomRow(); + final int scrollChunk = + -Math.min(Math.max(bottomRow.bottom - recyclerBottom, 0), dy - scrolled); + + offsetRowsVertical(scrollChunk); + scrolled -= scrollChunk; + + final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; + if (scrolled >= dy || adapterPosition >= state.getItemCount()) { + break; + } + + addRow(recycler, state, false, adapterPosition, bottomRow.bottom); + } + } else { + // Down + while (scrolled > dy) { + final LayoutRow topRow = getTopRow(); + final int scrollChunk = Math.min(Math.max(-topRow.top + recyclerTop, 0), scrolled - dy); + + offsetRowsVertical(scrollChunk); + scrolled -= scrollChunk; + + final int adapterPosition = topRow.adapterPosition - 1; + if (scrolled <= dy || adapterPosition >= state.getItemCount() || adapterPosition < 0) { + break; + } + + addRow(recycler, state, true, adapterPosition, topRow.top); + } + } + + // Fill extra offscreen rows for smooth scroll + if (scrolled == dy) { + addOffScreenRows(recycler, state, recyclerTop, recyclerBottom, dy >= 0); + } + + clearViewsAndStickHeaders(recycler, state, dy >= 0); + return scrolled; + } + + /** + * Returns first visible item excluding headers. + * + * @param visibleTop Whether item top edge should be visible or not + * @return The first visible item adapter position closest to top of the layout. + */ + public int getFirstVisibleItemPosition(boolean visibleTop) { + return getFirstVisiblePosition(TYPE_ITEM, visibleTop); + } + + /** + * Returns last visible item excluding headers. + * + * @return The last visible item adapter position closest to bottom of the layout. + */ + public int getLastVisibleItemPosition() { + return getLastVisiblePosition(TYPE_ITEM); + } + + /** + * Returns first visible header. + * + * @param visibleTop Whether header top edge should be visible or not + * @return The first visible header adapter position closest to top of the layout. + */ + public int getFirstVisibleHeaderPosition(boolean visibleTop) { + return getFirstVisiblePosition(TYPE_HEADER, visibleTop); + } + + /** + * Returns last visible header. + * + * @return The last visible header adapter position closest to bottom of the layout. + */ + public int getLastVisibleHeaderPosition() { + return getLastVisiblePosition(TYPE_HEADER); + } + + private int getFirstVisiblePosition(int type, boolean visibleTop) { + if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { return NO_POSITION; - } - - private int getLastVisiblePosition(int type) { - if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { - return NO_POSITION; - } - else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { - return NO_POSITION; - } - - int viewFrom = type == TYPE_ITEM ? mHeadersStartPosition - 1 : getChildCount() - 1; - int viewTo = type == TYPE_ITEM ? 0 : mHeadersStartPosition; - final int recyclerBottom = getHeight() - getPaddingBottom(); - for (int i = viewFrom; i >= viewTo; --i) { - final View v = getChildAt(i); - final int top = getDecoratedTop(v); - - if (top < recyclerBottom) { - return getPosition(v); - } - } - + } else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { return NO_POSITION; - } - - private LayoutRow getFirstVisibleRow() { - final int recyclerTop = getPaddingTop(); - for (LayoutRow row : mLayoutRows) { - if (row.bottom > recyclerTop) { - return row; - } - } - return null; - } - - private int getFirstVisibleSectionHeader() { - final int recyclerTop = getPaddingTop(); - - int header = NO_POSITION; - for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { - final LayoutRow row = mLayoutRows.get(i); - if (row.header) { - header = i; - } - if (row.bottom > recyclerTop) { - return header; - } - } + } + + int viewFrom = type == TYPE_ITEM ? 0 : mHeadersStartPosition; + int viewTo = type == TYPE_ITEM ? mHeadersStartPosition : getChildCount(); + final int recyclerTop = getPaddingTop(); + for (int i = viewFrom; i < viewTo; ++i) { + final View v = getChildAt(i); + final int adapterPosition = getPosition(v); + final int headerHeight = getPositionSectionHeaderHeight(adapterPosition); + final int top = getDecoratedTop(v); + final int bottom = getDecoratedBottom(v); + + if (visibleTop) { + if (top >= recyclerTop + headerHeight) { + return adapterPosition; + } + } else { + if (bottom >= recyclerTop + headerHeight) { + return adapterPosition; + } + } + } + + return NO_POSITION; + } + + private int getLastVisiblePosition(int type) { + if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { return NO_POSITION; - } - - private LayoutRow getNextVisibleSectionHeader(int headerFrom) { - for (int i = headerFrom + 1, n = mLayoutRows.size(); i < n; ++i) { - final LayoutRow row = mLayoutRows.get(i); - if (row.header) { - return row; - } - } - return null; - } - - private LayoutRow getHeaderRow(int adapterPosition) { - for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { - final LayoutRow row = mLayoutRows.get(i); - if (row.header && row.adapterPosition == adapterPosition) { - return row; - } - } - return null; - } - - private void removeFloatingHeader(RecyclerView.Recycler recycler) { - if (mFloatingHeaderView == null) { - return; - } - - final View view = mFloatingHeaderView; - mFloatingHeaderView = null; - mFloatingHeaderPosition = NO_POSITION; - removeAndRecycleView(view, recycler); - } - - private void onHeaderChanged(int section, View view, HeaderState state, int pushOffset) { - if (mStickyHeaderSection != NO_POSITION && section != mStickyHeaderSection) { - onHeaderUnstick(); - } - - final boolean headerStateChanged = mStickyHeaderSection != section || !mStickyHeadeState.equals(state) || state.equals(HeaderState.PUSHED); - - mStickyHeaderSection = section; - mStickyHeaderView = view; - mStickyHeadeState = state; - - if (headerStateChanged && mHeaderStateListener != null) { - mHeaderStateListener.onHeaderStateChanged(section, view, state, pushOffset); - } - } - - private void onHeaderUnstick() { - if (mStickyHeaderSection != NO_POSITION) { - if (mHeaderStateListener != null) { - mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); - } - mStickyHeaderSection = NO_POSITION; - mStickyHeaderView = null; - mStickyHeadeState = HeaderState.NORMAL; - } - } - - private void stickTopHeader(RecyclerView.Recycler recycler) { - final int firstHeader = getFirstVisibleSectionHeader(); - final int top = getPaddingTop(); - final int left = getPaddingLeft(); - final int right = getWidth() - getPaddingRight(); - - int notifySection = NO_POSITION; - View notifyView = null; - HeaderState notifyState = HeaderState.NORMAL; - int notifyOffset = 0; - - if (firstHeader != NO_POSITION) { - // Top row is header, floating header is not visible, remove - removeFloatingHeader(recycler); - - final LayoutRow firstHeaderRow = mLayoutRows.get(firstHeader); - final int section = mAdapter.getAdapterPositionSection(firstHeaderRow.adapterPosition); - if (mAdapter.isSectionHeaderSticky(section)) { - final LayoutRow nextHeaderRow = getNextVisibleSectionHeader(firstHeader); - int offset = 0; - if (nextHeaderRow != null) { - final int height = firstHeaderRow.getHeight(); - offset = Math.min(Math.max(top - nextHeaderRow.top, -height) + height, height); - } - - mStickOffset = top - firstHeaderRow.top - offset; - firstHeaderRow.headerView.offsetTopAndBottom(mStickOffset); - - onHeaderChanged(section, firstHeaderRow.headerView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); - } - else { - onHeaderUnstick(); - mStickOffset = 0; - } - } - else { - // We don't have first visible sector header in layout, create floating - final LayoutRow firstVisibleRow = getFirstVisibleRow(); - if (firstVisibleRow != null) { - final int section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); - if (mAdapter.isSectionHeaderSticky(section)) { - final int headerPosition = mAdapter.getSectionHeaderPosition(section); - if (mFloatingHeaderView == null || mFloatingHeaderPosition != headerPosition) { - removeFloatingHeader(recycler); - - // Create floating header - final View v = recycler.getViewForPosition(headerPosition); - addView(v, mHeadersStartPosition); - measureChildWithMargins(v, 0, 0); - mFloatingHeaderView = v; - mFloatingHeaderPosition = headerPosition; - } - - // Push floating header up, if needed - final int height = getDecoratedMeasuredHeight(mFloatingHeaderView); - int offset = 0; - if (getChildCount() - mHeadersStartPosition > 1) { - final View nextHeader = getChildAt(mHeadersStartPosition + 1); - final int contentHeight = Math.max(0, height - mHeaderOverlapMargin); - offset = Math.max(top - getDecoratedTop(nextHeader), -contentHeight) + contentHeight; - } - - layoutDecorated(mFloatingHeaderView, left, top - offset, right, top + height - offset); - onHeaderChanged(section, mFloatingHeaderView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); - } - else { - onHeaderUnstick(); - } - } - else { - onHeaderUnstick(); - } - } - } - - private void updateTopPosition() { - if (getChildCount() == 0) { - mAnchor.reset(); - } - + } else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { + return NO_POSITION; + } + + int viewFrom = type == TYPE_ITEM ? mHeadersStartPosition - 1 : getChildCount() - 1; + int viewTo = type == TYPE_ITEM ? 0 : mHeadersStartPosition; + final int recyclerBottom = getHeight() - getPaddingBottom(); + for (int i = viewFrom; i >= viewTo; --i) { + final View v = getChildAt(i); + final int top = getDecoratedTop(v); + + if (top < recyclerBottom) { + return getPosition(v); + } + } + + return NO_POSITION; + } + + private LayoutRow getFirstVisibleRow() { + final int recyclerTop = getPaddingTop(); + for (LayoutRow row : mLayoutRows) { + if (row.bottom > recyclerTop) { + return row; + } + } + return null; + } + + private int getFirstVisibleSectionHeader() { + final int recyclerTop = getPaddingTop(); + + int header = NO_POSITION; + for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header) { + header = i; + } + if (row.bottom > recyclerTop) { + return header; + } + } + return NO_POSITION; + } + + private LayoutRow getNextVisibleSectionHeader(int headerFrom) { + for (int i = headerFrom + 1, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header) { + return row; + } + } + return null; + } + + private LayoutRow getHeaderRow(int adapterPosition) { + for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header && row.adapterPosition == adapterPosition) { + return row; + } + } + return null; + } + + private void removeFloatingHeader(RecyclerView.Recycler recycler) { + if (mFloatingHeaderView == null) { + return; + } + + final View view = mFloatingHeaderView; + mFloatingHeaderView = null; + mFloatingHeaderPosition = NO_POSITION; + removeAndRecycleView(view, recycler); + } + + private void onHeaderChanged(int section, View view, HeaderState state, int pushOffset) { + if (mStickyHeaderSection != NO_POSITION && section != mStickyHeaderSection) { + onHeaderUnstick(); + } + + final boolean headerStateChanged = + mStickyHeaderSection != section + || !mStickyHeadeState.equals(state) + || state.equals(HeaderState.PUSHED); + + mStickyHeaderSection = section; + mStickyHeaderView = view; + mStickyHeadeState = state; + + if (headerStateChanged && mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged(section, view, state, pushOffset); + } + } + + private void onHeaderUnstick() { + if (mStickyHeaderSection != NO_POSITION) { + if (mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged( + mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); + } + mStickyHeaderSection = NO_POSITION; + mStickyHeaderView = null; + mStickyHeadeState = HeaderState.NORMAL; + } + } + + private void stickTopHeader(RecyclerView.Recycler recycler) { + final int firstHeader = getFirstVisibleSectionHeader(); + final int top = getPaddingTop(); + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + + int notifySection = NO_POSITION; + View notifyView = null; + HeaderState notifyState = HeaderState.NORMAL; + int notifyOffset = 0; + + if (firstHeader != NO_POSITION) { + // Top row is header, floating header is not visible, remove + removeFloatingHeader(recycler); + + final LayoutRow firstHeaderRow = mLayoutRows.get(firstHeader); + final int section = mAdapter.getAdapterPositionSection(firstHeaderRow.adapterPosition); + if (mAdapter.isSectionHeaderSticky(section)) { + final LayoutRow nextHeaderRow = getNextVisibleSectionHeader(firstHeader); + int offset = 0; + if (nextHeaderRow != null) { + final int height = firstHeaderRow.getHeight(); + offset = Math.min(Math.max(top - nextHeaderRow.top, -height) + height, height); + } + + mStickOffset = top - firstHeaderRow.top - offset; + firstHeaderRow.headerView.offsetTopAndBottom(mStickOffset); + + onHeaderChanged( + section, + firstHeaderRow.headerView, + offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, + offset); + } else { + onHeaderUnstick(); + mStickOffset = 0; + } + } else { + // We don't have first visible sector header in layout, create floating final LayoutRow firstVisibleRow = getFirstVisibleRow(); if (firstVisibleRow != null) { - mAnchor.section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); - mAnchor.item = mAdapter.getItemSectionOffset(mAnchor.section, firstVisibleRow.adapterPosition); - mAnchor.offset = Math.min(firstVisibleRow.top - getPaddingTop(), 0); - } - } - - private int getViewType(View view) { - return getItemViewType(view) & 0xFF; - } - - private int getViewType(int position) { - return mAdapter.getItemViewType(position) & 0xFF; - } - - private void clearState() { - mHeadersStartPosition = 0; - mStickOffset = 0; - mFloatingHeaderView = null; - mFloatingHeaderPosition = -1; - mAverageHeaderHeight = 0; - mLayoutRows.clear(); - - if (mStickyHeaderSection != NO_POSITION) { - if (mHeaderStateListener != null) { - mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); - } - mStickyHeaderSection = NO_POSITION; - mStickyHeaderView = null; - mStickyHeadeState = HeaderState.NORMAL; - } - } - - @Override - public int computeVerticalScrollExtent(RecyclerView.State state) { - if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { - return 0; - } - - final View startChild = getChildAt(0); - final View endChild = getChildAt(mHeadersStartPosition - 1); - if (startChild == null || endChild == null) { - return 0; - } - - return Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; - } - - @Override - public int computeVerticalScrollOffset(RecyclerView.State state) { - if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { - return 0; - } - - final View startChild = getChildAt(0); - final View endChild = getChildAt(mHeadersStartPosition - 1); - if (startChild == null || endChild == null) { - return 0; - } - - final int recyclerTop = getPaddingTop(); - final LayoutRow topRow = getTopRow(); - final int scrollChunk = Math.max(-topRow.top + recyclerTop, 0); - if (scrollChunk == 0) { - return 0; - } - - final int minPosition = Math.min(getPosition(startChild), getPosition(endChild)); - final int maxPosition = Math.max(getPosition(startChild), getPosition(endChild)); - return Math.max(0, minPosition); - } - - @Override - public int computeVerticalScrollRange(RecyclerView.State state) { - if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { - return 0; - } - - final View startChild = getChildAt(0); - final View endChild = getChildAt(mHeadersStartPosition - 1); - if (startChild == null || endChild == null) { - return 0; - } - - return state.getItemCount(); - } - - public static class LayoutParams extends RecyclerView.LayoutParams { - public static final int INVALID_SPAN_ID = -1; - - private int mSpanIndex = INVALID_SPAN_ID; - private int mSpanSize = 0; - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - } - - public LayoutParams(int width, int height) { - super(width, height); - } - - public LayoutParams(ViewGroup.MarginLayoutParams source) { - super(source); - } - - public LayoutParams(ViewGroup.LayoutParams source) { - super(source); - } - - public LayoutParams(RecyclerView.LayoutParams source) { - super(source); - } - - public int getSpanIndex() { - return mSpanIndex; - } - - public int getSpanSize() { - return mSpanSize; - } - } - - public static final class DefaultSpanSizeLookup extends SpanSizeLookup { - @Override - public int getSpanSize(int section, int position) { - return 1; - } - - @Override - public int getSpanIndex(int section, int position, int spanCount) { - return position % spanCount; - } - } - - /** - * An interface to provide the number of spans each item occupies. - *

- * Default implementation sets each item to occupy exactly 1 span. - * - * @see StickyHeaderGridLayoutManager#setSpanSizeLookup(StickyHeaderGridLayoutManager.SpanSizeLookup) - */ - public static abstract class SpanSizeLookup { - /** - * Returns the number of span occupied by the item in section at position. - * - * @param section The adapter section of the item - * @param position The adapter position of the item in section - * @return The number of spans occupied by the item at the provided section and position - */ - abstract public int getSpanSize(int section, int position); - - /** - * Returns the final span index of the provided position. - * - *

- * If you override this method, you need to make sure it is consistent with - * {@link #getSpanSize(int, int)}. StickyHeaderGridLayoutManager does not call this method for - * each item. It is called only for the reference item and rest of the items - * are assigned to spans based on the reference item. For example, you cannot assign a - * position to span 2 while span 1 is empty. - *

- * - * @param section The adapter section of the item - * @param position The adapter position of the item in section - * @param spanCount The total number of spans in the grid - * @return The final span position of the item. Should be between 0 (inclusive) and - * spanCount(exclusive) - */ - public int getSpanIndex(int section, int position, int spanCount) { - // TODO: cache them? - final int positionSpanSize = getSpanSize(section, position); - if (positionSpanSize >= spanCount) { - return 0; - } - - int spanIndex = 0; - for (int i = 0; i < position; ++i) { - final int spanSize = getSpanSize(section, i); - spanIndex += spanSize; - - if (spanIndex == spanCount) { - spanIndex = 0; - } - else if (spanIndex > spanCount) { - spanIndex = spanSize; - } - } - - if (spanIndex + positionSpanSize <= spanCount) { - return spanIndex; - } - - return 0; - } - } + final int section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); + if (mAdapter.isSectionHeaderSticky(section)) { + final int headerPosition = mAdapter.getSectionHeaderPosition(section); + if (mFloatingHeaderView == null || mFloatingHeaderPosition != headerPosition) { + removeFloatingHeader(recycler); + + // Create floating header + final View v = recycler.getViewForPosition(headerPosition); + addView(v, mHeadersStartPosition); + measureChildWithMargins(v, 0, 0); + mFloatingHeaderView = v; + mFloatingHeaderPosition = headerPosition; + } + + // Push floating header up, if needed + final int height = getDecoratedMeasuredHeight(mFloatingHeaderView); + int offset = 0; + if (getChildCount() - mHeadersStartPosition > 1) { + final View nextHeader = getChildAt(mHeadersStartPosition + 1); + final int contentHeight = Math.max(0, height - mHeaderOverlapMargin); + offset = Math.max(top - getDecoratedTop(nextHeader), -contentHeight) + contentHeight; + } + + layoutDecorated(mFloatingHeaderView, left, top - offset, right, top + height - offset); + onHeaderChanged( + section, + mFloatingHeaderView, + offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, + offset); + } else { + onHeaderUnstick(); + } + } else { + onHeaderUnstick(); + } + } + } + + private void updateTopPosition() { + if (getChildCount() == 0) { + mAnchor.reset(); + } + + final LayoutRow firstVisibleRow = getFirstVisibleRow(); + if (firstVisibleRow != null) { + mAnchor.section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); + mAnchor.item = + mAdapter.getItemSectionOffset(mAnchor.section, firstVisibleRow.adapterPosition); + mAnchor.offset = Math.min(firstVisibleRow.top - getPaddingTop(), 0); + } + } + + private int getViewType(View view) { + return getItemViewType(view) & 0xFF; + } + + private int getViewType(int position) { + return mAdapter.getItemViewType(position) & 0xFF; + } + + private void clearState() { + mHeadersStartPosition = 0; + mStickOffset = 0; + mFloatingHeaderView = null; + mFloatingHeaderPosition = -1; + mAverageHeaderHeight = 0; + mLayoutRows.clear(); + + if (mStickyHeaderSection != NO_POSITION) { + if (mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged( + mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); + } + mStickyHeaderSection = NO_POSITION; + mStickyHeaderView = null; + mStickyHeadeState = HeaderState.NORMAL; + } + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } - public static class SavedState implements Parcelable { - private int mAnchorSection; - private int mAnchorItem; - private int mAnchorOffset; + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } - public SavedState() { + return Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; + } - } + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } - SavedState(Parcel in) { - mAnchorSection = in.readInt(); - mAnchorItem = in.readInt(); - mAnchorOffset = in.readInt(); - } + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } - public SavedState(SavedState other) { - mAnchorSection = other.mAnchorSection; - mAnchorItem = other.mAnchorItem; - mAnchorOffset = other.mAnchorOffset; - } + final int recyclerTop = getPaddingTop(); + final LayoutRow topRow = getTopRow(); + final int scrollChunk = Math.max(-topRow.top + recyclerTop, 0); + if (scrollChunk == 0) { + return 0; + } - boolean hasValidAnchor() { - return mAnchorSection >= 0; - } + final int minPosition = Math.min(getPosition(startChild), getPosition(endChild)); + final int maxPosition = Math.max(getPosition(startChild), getPosition(endChild)); + return Math.max(0, minPosition); + } - void invalidateAnchor() { - mAnchorSection = NO_POSITION; - } - - @Override - public int describeContents() { - return 0; - } + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mAnchorSection); - dest.writeInt(mAnchorItem); - dest.writeInt(mAnchorOffset); + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } + + return state.getItemCount(); + } + + public static class LayoutParams extends RecyclerView.LayoutParams { + public static final int INVALID_SPAN_ID = -1; + + private int mSpanIndex = INVALID_SPAN_ID; + private int mSpanSize = 0; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(RecyclerView.LayoutParams source) { + super(source); + } + + public int getSpanIndex() { + return mSpanIndex; + } + + public int getSpanSize() { + return mSpanSize; + } + } + + public static final class DefaultSpanSizeLookup extends SpanSizeLookup { + @Override + public int getSpanSize(int section, int position) { + return 1; + } + + @Override + public int getSpanIndex(int section, int position, int spanCount) { + return position % spanCount; + } + } + + /** + * An interface to provide the number of spans each item occupies. + * + *

Default implementation sets each item to occupy exactly 1 span. + * + * @see + * StickyHeaderGridLayoutManager#setSpanSizeLookup(StickyHeaderGridLayoutManager.SpanSizeLookup) + */ + public abstract static class SpanSizeLookup { + /** + * Returns the number of span occupied by the item in section at position + * . + * + * @param section The adapter section of the item + * @param position The adapter position of the item in section + * @return The number of spans occupied by the item at the provided section and position + */ + public abstract int getSpanSize(int section, int position); + + /** + * Returns the final span index of the provided position. + * + *

If you override this method, you need to make sure it is consistent with {@link + * #getSpanSize(int, int)}. StickyHeaderGridLayoutManager does not call this method for each + * item. It is called only for the reference item and rest of the items are assigned to spans + * based on the reference item. For example, you cannot assign a position to span 2 while span 1 + * is empty. + * + *

+ * + * @param section The adapter section of the item + * @param position The adapter position of the item in section + * @param spanCount The total number of spans in the grid + * @return The final span position of the item. Should be between 0 (inclusive) and + * spanCount(exclusive) + */ + public int getSpanIndex(int section, int position, int spanCount) { + // TODO: cache them? + final int positionSpanSize = getSpanSize(section, position); + if (positionSpanSize >= spanCount) { + return 0; + } + + int spanIndex = 0; + for (int i = 0; i < position; ++i) { + final int spanSize = getSpanSize(section, i); + spanIndex += spanSize; + + if (spanIndex == spanCount) { + spanIndex = 0; + } else if (spanIndex > spanCount) { + spanIndex = spanSize; + } + } + + if (spanIndex + positionSpanSize <= spanCount) { + return spanIndex; } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel in) { + return 0; + } + } + + public static class SavedState implements Parcelable { + private int mAnchorSection; + private int mAnchorItem; + private int mAnchorOffset; + + public SavedState() {} + + SavedState(Parcel in) { + mAnchorSection = in.readInt(); + mAnchorItem = in.readInt(); + mAnchorOffset = in.readInt(); + } + + public SavedState(SavedState other) { + mAnchorSection = other.mAnchorSection; + mAnchorItem = other.mAnchorItem; + mAnchorOffset = other.mAnchorOffset; + } + + boolean hasValidAnchor() { + return mAnchorSection >= 0; + } + + void invalidateAnchor() { + mAnchorSection = NO_POSITION; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAnchorSection); + dest.writeInt(mAnchorItem); + dest.writeInt(mAnchorOffset); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { return new SavedState(in); - } + } - @Override - public SavedState[] newArray(int size) { + @Override + public SavedState[] newArray(int size) { return new SavedState[size]; - } - }; - } - - private static class LayoutRow { - private boolean header; - private View headerView; - private int adapterPosition; - private int length; - private int top; - private int bottom; - - public LayoutRow(int adapterPosition, int length, int top, int bottom) { - this.header = false; - this.headerView = null; - this.adapterPosition = adapterPosition; - this.length = length; - this.top = top; - this.bottom = bottom; - } - - public LayoutRow(View headerView, int adapterPosition, int length, int top, int bottom) { - this.header = true; - this.headerView = headerView; - this.adapterPosition = adapterPosition; - this.length = length; - this.top = top; - this.bottom = bottom; - } - - int getHeight() { - return bottom - top; - } - } - - private static class FillResult { - private View edgeView; - private int adapterPosition; - private int length; - private int height; - } - - private static class AnchorPosition { - private int section; - private int item; - private int offset; - - public AnchorPosition() { - reset(); - } - - public void reset() { - section = NO_POSITION; - item = 0; - offset = 0; - } - } + } + }; + } + + private static class LayoutRow { + private boolean header; + private View headerView; + private int adapterPosition; + private int length; + private int top; + private int bottom; + + public LayoutRow(int adapterPosition, int length, int top, int bottom) { + this.header = false; + this.headerView = null; + this.adapterPosition = adapterPosition; + this.length = length; + this.top = top; + this.bottom = bottom; + } + + public LayoutRow(View headerView, int adapterPosition, int length, int top, int bottom) { + this.header = true; + this.headerView = headerView; + this.adapterPosition = adapterPosition; + this.length = length; + this.top = top; + this.bottom = bottom; + } + + int getHeight() { + return bottom - top; + } + } + + private static class FillResult { + private View edgeView; + private int adapterPosition; + private int length; + private int height; + } + + private static class AnchorPosition { + private int section; + private int item; + private int offset; + + public AnchorPosition() { + reset(); + } + + public void reset() { + section = NO_POSITION; + item = 0; + offset = 0; + } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java b/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java index 56d2fdeb0..fa454b870 100644 --- a/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java @@ -5,7 +5,6 @@ import android.util.Log; import android.view.MenuItem; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -20,14 +19,13 @@ import androidx.media3.session.SessionCommand; import androidx.media3.session.SessionToken; import androidx.viewpager.widget.ViewPager; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; import com.google.android.material.tabs.TabLayout; import com.google.common.util.concurrent.ListenableFuture; - +import java.util.ArrayList; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -35,38 +33,37 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.ArrayList; - public class AllMediaActivity extends PassphraseRequiredActionBarActivity - implements DcEventCenter.DcEventDelegate -{ + implements DcEventCenter.DcEventDelegate { private static final String TAG = AllMediaActivity.class.getSimpleName(); - public static final String CHAT_ID_EXTRA = "chat_id"; + public static final String CHAT_ID_EXTRA = "chat_id"; public static final String CONTACT_ID_EXTRA = "contact_id"; - public static final String FORCE_GALLERY = "force_gallery"; + public static final String FORCE_GALLERY = "force_gallery"; static class TabData { final int title; final int type1; final int type2; final int type3; + TabData(int title, int type1, int type2, int type3) { this.title = title; this.type1 = type1; this.type2 = type2; this.type3 = type3; } - }; + } + ; - private DcContext dcContext; - private int chatId; - private int contactId; + private DcContext dcContext; + private int chatId; + private int contactId; private final ArrayList tabs = new ArrayList<>(); - private Toolbar toolbar; - private TabLayout tabLayout; - private ViewPager viewPager; + private Toolbar toolbar; + private TabLayout tabLayout; + private ViewPager viewPager; private @Nullable MediaController mediaController; private ListenableFuture mediaControllerFuture; @@ -82,7 +79,9 @@ protected void onPreCreate() { @Override protected void onCreate(Bundle bundle, boolean ready) { tabs.add(new TabData(R.string.webxdc_apps, DcMsg.DC_MSG_WEBXDC, 0, 0)); - tabs.add(new TabData(R.string.tab_gallery, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO)); + tabs.add( + new TabData( + R.string.tab_gallery, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO)); tabs.add(new TabData(R.string.audio, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_VOICE, 0)); tabs.add(new TabData(R.string.files, DcMsg.DC_MSG_FILE, 0, 0)); @@ -94,7 +93,8 @@ protected void onCreate(Bundle bundle, boolean ready) { ActionBar supportActionBar = getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(true); - supportActionBar.setTitle(isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media); + supportActionBar.setTitle( + isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media); } this.tabLayout.setupWithViewPager(viewPager); @@ -123,47 +123,44 @@ public void onDestroy() { } @Override - public void handleEvent(@NonNull DcEvent event) { - } + public void handleEvent(@NonNull DcEvent event) {} private void initializeResources() { - chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, 0); - contactId = getIntent().getIntExtra(CONTACT_ID_EXTRA, 0); + chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, 0); + contactId = getIntent().getIntExtra(CONTACT_ID_EXTRA, 0); - if (contactId!=0) { + if (contactId != 0) { chatId = dcContext.getChatIdByContactId(contactId); } - if(chatId!=0) { + if (chatId != 0) { DcChat dcChat = dcContext.getChat(chatId); - if(!dcChat.isMultiUser()) { + if (!dcChat.isMultiUser()) { final int[] members = dcContext.getChatContacts(chatId); - contactId = members.length>=1? members[0] : 0; + contactId = members.length >= 1 ? members[0] : 0; } } this.viewPager = ViewUtil.findById(this, R.id.pager); - this.toolbar = ViewUtil.findById(this, R.id.toolbar); + this.toolbar = ViewUtil.findById(this, R.id.toolbar); this.tabLayout = ViewUtil.findById(this, R.id.tab_layout); } private void initializeMediaController() { - SessionToken sessionToken = new SessionToken(this, - new ComponentName(this, AudioPlaybackService.class)); - mediaControllerFuture = new MediaController.Builder(this, sessionToken) - .buildAsync(); - mediaControllerFuture.addListener(() -> { - try { - mediaController = mediaControllerFuture.get(); - addActivityContext( - this.getIntent().getExtras(), - this.getClass().getName() - ); - playbackViewModel.setMediaController(mediaController); - } catch (Exception e) { - Log.e(TAG, "Error connecting to audio playback service", e); - } - }, ContextCompat.getMainExecutor(this)); + SessionToken sessionToken = + new SessionToken(this, new ComponentName(this, AudioPlaybackService.class)); + mediaControllerFuture = new MediaController.Builder(this, sessionToken).buildAsync(); + mediaControllerFuture.addListener( + () -> { + try { + mediaController = mediaControllerFuture.get(); + addActivityContext(this.getIntent().getExtras(), this.getClass().getName()); + playbackViewModel.setMediaController(mediaController); + } catch (Exception e) { + Log.e(TAG, "Error connecting to audio playback service", e); + } + }, + ContextCompat.getMainExecutor(this)); } private void addActivityContext(Bundle extras, String activityClassName) { @@ -176,13 +173,13 @@ private void addActivityContext(Bundle extras, String activityClassName) { } SessionCommand updateContextCommand = - new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY); + new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY); mediaController.sendCustomCommand(updateContextCommand, commandArgs); } private boolean isGlobalGallery() { - return contactId==0 && chatId==0; + return contactId == 0 && chatId == 0; } private class AllMediaPagerAdapter extends FragmentStatePagerAdapter { @@ -216,10 +213,14 @@ public Fragment getItem(int position) { if (data.type1 == DcMsg.DC_MSG_IMAGE) { fragment = new AllMediaGalleryFragment(); - args.putInt(AllMediaGalleryFragment.CHAT_ID_EXTRA, (chatId==0&&!isGlobalGallery())? -1 : chatId); + args.putInt( + AllMediaGalleryFragment.CHAT_ID_EXTRA, + (chatId == 0 && !isGlobalGallery()) ? -1 : chatId); } else { fragment = new AllMediaDocumentsFragment(); - args.putInt(AllMediaDocumentsFragment.CHAT_ID_EXTRA, (chatId==0&&!isGlobalGallery())? -1 : chatId); + args.putInt( + AllMediaDocumentsFragment.CHAT_ID_EXTRA, + (chatId == 0 && !isGlobalGallery()) ? -1 : chatId); args.putInt(AllMediaDocumentsFragment.VIEWTYPE1, data.type1); args.putInt(AllMediaDocumentsFragment.VIEWTYPE2, data.type2); } diff --git a/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsAdapter.java b/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsAdapter.java index 59b0cb4b1..79511538b 100644 --- a/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsAdapter.java @@ -5,12 +5,12 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcMsg; import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter; - +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.WebxdcView; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; @@ -22,31 +22,27 @@ import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MediaUtil; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter { - private final Context context; - private final ItemClickListener itemClickListener; - private final Set selected; + private final Context context; + private final ItemClickListener itemClickListener; + private final Set selected; - private BucketedThreadMedia media; - private AudioPlaybackViewModel playbackViewModel; + private BucketedThreadMedia media; + private AudioPlaybackViewModel playbackViewModel; private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder { private final DocumentView documentView; - private final AudioView audioView; - private final WebxdcView webxdcView; - private final TextView date; + private final AudioView audioView; + private final WebxdcView webxdcView; + private final TextView date; public ViewHolder(View v) { super(v); - documentView = v.findViewById(R.id.document_view); - audioView = v.findViewById(R.id.audio_view); - webxdcView = v.findViewById(R.id.webxdc_view); - date = v.findViewById(R.id.date); + documentView = v.findViewById(R.id.document_view); + audioView = v.findViewById(R.id.audio_view); + webxdcView = v.findViewById(R.id.webxdc_view); + date = v.findViewById(R.id.date); } } @@ -59,14 +55,12 @@ private static class HeaderHolder extends StickyHeaderGridAdapter.HeaderViewHold } } - AllMediaDocumentsAdapter(@NonNull Context context, - BucketedThreadMedia media, - ItemClickListener clickListener) - { - this.context = context; - this.media = media; + AllMediaDocumentsAdapter( + @NonNull Context context, BucketedThreadMedia media, ItemClickListener clickListener) { + this.context = context; + this.media = media; this.itemClickListener = clickListener; - this.selected = new HashSet<>(); + this.selected = new HashSet<>(); } public void setMedia(BucketedThreadMedia media) { @@ -78,25 +72,30 @@ public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) { } @Override - public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) { - return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false)); + public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder( + ViewGroup parent, int headerType) { + return new HeaderHolder( + LayoutInflater.from(context) + .inflate(R.layout.contact_selection_list_divider, parent, false)); } @Override public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) { - return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.profile_document_item, parent, false)); + return new ViewHolder( + LayoutInflater.from(context).inflate(R.layout.profile_document_item, parent, false)); } @Override - public void onBindHeaderViewHolder(StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) { - ((HeaderHolder)viewHolder).textView.setText(media.getName(section)); + public void onBindHeaderViewHolder( + StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) { + ((HeaderHolder) viewHolder).textView.setText(media.getName(section)); } @Override public void onBindItemViewHolder(ItemViewHolder itemViewHolder, int section, int offset) { - ViewHolder viewHolder = ((ViewHolder)itemViewHolder); - DcMsg dcMsg = media.get(section, offset); - Slide slide = MediaUtil.getSlideForMsg(context, dcMsg); + ViewHolder viewHolder = ((ViewHolder) itemViewHolder); + DcMsg dcMsg = media.get(section, offset); + Slide slide = MediaUtil.getSlideForMsg(context, dcMsg); if (slide != null && slide.hasAudio()) { viewHolder.documentView.setVisibility(View.GONE); @@ -104,46 +103,60 @@ public void onBindItemViewHolder(ItemViewHolder itemViewHolder, int section, int viewHolder.audioView.setVisibility(View.VISIBLE); viewHolder.audioView.setPlaybackViewModel(playbackViewModel); - viewHolder.audioView.setAudio((AudioSlide)slide); + viewHolder.audioView.setAudio((AudioSlide) slide); viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg)); - viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; }); + viewHolder.audioView.setOnLongClickListener( + view -> { + itemClickListener.onMediaLongClicked(dcMsg); + return true; + }); viewHolder.audioView.disablePlayer(!selected.isEmpty()); viewHolder.itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg)); viewHolder.date.setVisibility(View.VISIBLE); - } - else if (slide != null && slide.isWebxdcDocument()) { + } else if (slide != null && slide.isWebxdcDocument()) { viewHolder.audioView.setVisibility(View.GONE); viewHolder.documentView.setVisibility(View.GONE); viewHolder.webxdcView.setVisibility(View.VISIBLE); viewHolder.webxdcView.setWebxdc(dcMsg, ""); viewHolder.webxdcView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg)); - viewHolder.webxdcView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; }); + viewHolder.webxdcView.setOnLongClickListener( + view -> { + itemClickListener.onMediaLongClicked(dcMsg); + return true; + }); viewHolder.itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg)); viewHolder.date.setVisibility(View.GONE); - } - else if (slide != null && slide.hasDocument()) { + } else if (slide != null && slide.hasDocument()) { viewHolder.audioView.setVisibility(View.GONE); viewHolder.webxdcView.setVisibility(View.GONE); viewHolder.documentView.setVisibility(View.VISIBLE); - viewHolder.documentView.setDocument((DocumentSlide)slide); + viewHolder.documentView.setDocument((DocumentSlide) slide); viewHolder.documentView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg)); - viewHolder.documentView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; }); + viewHolder.documentView.setOnLongClickListener( + view -> { + itemClickListener.onMediaLongClicked(dcMsg); + return true; + }); viewHolder.itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg)); viewHolder.date.setVisibility(View.VISIBLE); - } - else { + } else { viewHolder.documentView.setVisibility(View.GONE); viewHolder.audioView.setVisibility(View.GONE); viewHolder.webxdcView.setVisibility(View.GONE); viewHolder.date.setVisibility(View.GONE); } - viewHolder.itemView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; }); + viewHolder.itemView.setOnLongClickListener( + view -> { + itemClickListener.onMediaLongClicked(dcMsg); + return true; + }); viewHolder.itemView.setSelected(selected.contains(dcMsg)); - viewHolder.date.setText(DateUtils.getBriefRelativeTimeSpanString(context, dcMsg.getTimestamp())); + viewHolder.date.setText( + DateUtils.getBriefRelativeTimeSpanString(context, dcMsg.getTimestamp())); } @Override @@ -184,6 +197,7 @@ public void clearSelection() { interface ItemClickListener { void onMediaClicked(@NonNull DcMsg mediaRecord); + void onMediaLongClicked(DcMsg mediaRecord); } } diff --git a/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsFragment.java b/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsFragment.java index 439400c40..d8b7fafd1 100644 --- a/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/AllMediaDocumentsFragment.java @@ -12,7 +12,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; @@ -20,25 +19,20 @@ import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; - +import java.util.Set; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.Set; - -public class AllMediaDocumentsFragment - extends MessageSelectorFragment +public class AllMediaDocumentsFragment extends MessageSelectorFragment implements LoaderManager.LoaderCallbacks, - AllMediaDocumentsAdapter.ItemClickListener -{ + AllMediaDocumentsAdapter.ItemClickListener { public static final String CHAT_ID_EXTRA = "chat_id"; public static final String VIEWTYPE1 = "viewtype1"; public static final String VIEWTYPE2 = "viewtype2"; @@ -50,7 +44,7 @@ public class AllMediaDocumentsFragment private int viewtype1; private int viewtype2; - protected int chatId; + protected int chatId; @Override public void onCreate(Bundle bundle) { @@ -64,21 +58,23 @@ public void onCreate(Bundle bundle) { } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.profile_documents_fragment, container, false); this.recyclerView = ViewUtil.findById(view, R.id.recycler_view); - this.noMedia = ViewUtil.findById(view, R.id.no_documents); - this.gridManager = new StickyHeaderGridLayoutManager(1); + this.noMedia = ViewUtil.findById(view, R.id.no_documents); + this.gridManager = new StickyHeaderGridLayoutManager(1); // add padding to avoid content hidden behind system bars ViewUtil.applyWindowInsets(recyclerView, true, false, true, true); - AllMediaDocumentsAdapter adapter = new AllMediaDocumentsAdapter(getContext(), - new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()), - this); + AllMediaDocumentsAdapter adapter = + new AllMediaDocumentsAdapter( + getContext(), new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()), this); this.recyclerView.setAdapter(adapter); - adapter.setPlaybackViewModel(new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class)); + adapter.setPlaybackViewModel( + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class)); this.recyclerView.setLayoutManager(gridManager); this.recyclerView.setHasFixedSize(true); @@ -109,12 +105,15 @@ public void onConfigurationChanged(Configuration newConfig) { } @Override - public Loader onCreateLoader(int i, Bundle bundle) { + public Loader onCreateLoader( + int i, Bundle bundle) { return new BucketedThreadMediaLoader(getContext(), chatId, viewtype1, viewtype2, 0); } @Override - public void onLoadFinished(Loader loader, BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) { + public void onLoadFinished( + Loader loader, + BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) { ((AllMediaDocumentsAdapter) recyclerView.getAdapter()).setMedia(bucketedThreadMedia); ((AllMediaDocumentsAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged(); @@ -122,7 +121,7 @@ public void onLoadFinished(Loader if (chatId == DC_CHAT_NO_CHAT) { if (viewtype1 == DcMsg.DC_MSG_WEBXDC) { noMedia.setText(R.string.all_apps_empty_hint); - } else if (viewtype1 == DcMsg.DC_MSG_FILE){ + } else if (viewtype1 == DcMsg.DC_MSG_FILE) { noMedia.setText(R.string.all_files_empty_hint); } else { noMedia.setText(R.string.tab_all_media_empty_hint); @@ -137,7 +136,8 @@ public void onLoadFinished(Loader @Override public void onLoaderReset(Loader cursorLoader) { - ((AllMediaDocumentsAdapter) recyclerView.getAdapter()).setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext())); + ((AllMediaDocumentsAdapter) recyclerView.getAdapter()) + .setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext())); } @Override @@ -168,7 +168,7 @@ private void handleMediaMultiSelectClick(@NonNull DcMsg mediaRecord) { private void handleMediaPreviewClick(@NonNull DcMsg dcMsg) { // audio is started by the play-button - if (dcMsg.getType()==DcMsg.DC_MSG_AUDIO || dcMsg.getType()==DcMsg.DC_MSG_VOICE) { + if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO || dcMsg.getType() == DcMsg.DC_MSG_VOICE) { return; } @@ -216,7 +216,8 @@ protected void setCorrectMenuVisibility(Menu menu) { } menu.findItem(R.id.menu_resend).setVisible(canResend); - boolean webxdcApp = singleSelection && messageRecords.iterator().next().getType() == DcMsg.DC_MSG_WEBXDC; + boolean webxdcApp = + singleSelection && messageRecords.iterator().next().getType() == DcMsg.DC_MSG_WEBXDC; menu.findItem(R.id.menu_add_to_home_screen).setVisible(webxdcApp); } @@ -243,20 +244,26 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) { int itemId = menuItem.getItemId(); - AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); + AudioPlaybackViewModel playbackViewModel = + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); if (itemId == R.id.details) { handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia())); mode.finish(); return true; } else if (itemId == R.id.delete) { - handleDeleteMessages(chatId, getListAdapter().getSelectedMedia(), playbackViewModel::stopByIds, playbackViewModel::stopByIds); + handleDeleteMessages( + chatId, + getListAdapter().getSelectedMedia(), + playbackViewModel::stopByIds, + playbackViewModel::stopByIds); mode.finish(); return true; } else if (itemId == R.id.share) { handleShare(getSelectedMessageRecord(getListAdapter().getSelectedMedia())); return true; } else if (itemId == R.id.menu_add_to_home_screen) { - WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedMedia()).getId()); + WebxdcActivity.addToHomeScreen( + getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedMedia()).getId()); mode.finish(); return true; } else if (itemId == R.id.show_in_chat) { diff --git a/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryAdapter.java b/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryAdapter.java index 85d940625..4773220db 100644 --- a/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryAdapter.java @@ -5,38 +5,34 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcMsg; import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter; - +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.util.MediaUtil; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - class AllMediaGalleryAdapter extends StickyHeaderGridAdapter { - private final Context context; - private final GlideRequests glideRequests; - private final ItemClickListener itemClickListener; - private final Set selected; + private final Context context; + private final GlideRequests glideRequests; + private final ItemClickListener itemClickListener; + private final Set selected; - private BucketedThreadMedia media; + private BucketedThreadMedia media; private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder { final ThumbnailView imageView; - final View selectedIndicator; + final View selectedIndicator; ViewHolder(View v) { super(v); - imageView = v.findViewById(R.id.image); + imageView = v.findViewById(R.id.image); selectedIndicator = v.findViewById(R.id.selected_indicator); } } @@ -50,16 +46,16 @@ private static class HeaderHolder extends StickyHeaderGridAdapter.HeaderViewHold } } - AllMediaGalleryAdapter(@NonNull Context context, - @NonNull GlideRequests glideRequests, - BucketedThreadMedia media, - ItemClickListener clickListener) - { - this.context = context; - this.glideRequests = glideRequests; - this.media = media; + AllMediaGalleryAdapter( + @NonNull Context context, + @NonNull GlideRequests glideRequests, + BucketedThreadMedia media, + ItemClickListener clickListener) { + this.context = context; + this.glideRequests = glideRequests; + this.media = media; this.itemClickListener = clickListener; - this.selected = new HashSet<>(); + this.selected = new HashSet<>(); } public void setMedia(BucketedThreadMedia media) { @@ -67,36 +63,42 @@ public void setMedia(BucketedThreadMedia media) { } @Override - public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) { - return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false)); + public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder( + ViewGroup parent, int headerType) { + return new HeaderHolder( + LayoutInflater.from(context) + .inflate(R.layout.contact_selection_list_divider, parent, false)); } @Override public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) { - return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.profile_gallery_item, parent, false)); + return new ViewHolder( + LayoutInflater.from(context).inflate(R.layout.profile_gallery_item, parent, false)); } @Override - public void onBindHeaderViewHolder(StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) { - ((HeaderHolder)viewHolder).textView.setText(media.getName(section)); + public void onBindHeaderViewHolder( + StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) { + ((HeaderHolder) viewHolder).textView.setText(media.getName(section)); } @Override public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) { - DcMsg mediaRecord = media.get(section, offset); - ThumbnailView thumbnailView = ((ViewHolder)viewHolder).imageView; - View selectedIndicator = ((ViewHolder)viewHolder).selectedIndicator; - Slide slide = MediaUtil.getSlideForMsg(context, mediaRecord); + DcMsg mediaRecord = media.get(section, offset); + ThumbnailView thumbnailView = ((ViewHolder) viewHolder).imageView; + View selectedIndicator = ((ViewHolder) viewHolder).selectedIndicator; + Slide slide = MediaUtil.getSlideForMsg(context, mediaRecord); if (slide != null) { thumbnailView.setImageResource(glideRequests, slide); } thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord)); - thumbnailView.setOnLongClickListener(view -> { - itemClickListener.onMediaLongClicked(mediaRecord); - return true; - }); + thumbnailView.setOnLongClickListener( + view -> { + itemClickListener.onMediaLongClicked(mediaRecord); + return true; + }); selectedIndicator.setVisibility(selected.contains(mediaRecord) ? View.VISIBLE : View.GONE); } @@ -139,6 +141,7 @@ public void clearSelection() { interface ItemClickListener { void onMediaClicked(@NonNull DcMsg mediaRecord); + void onMediaLongClicked(DcMsg mediaRecord); } } diff --git a/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryFragment.java b/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryFragment.java index 57103cf02..ef794b1a7 100644 --- a/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/AllMediaGalleryFragment.java @@ -12,19 +12,17 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; - +import java.util.Set; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.database.Address; @@ -32,13 +30,9 @@ import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.Set; - -public class AllMediaGalleryFragment - extends MessageSelectorFragment +public class AllMediaGalleryFragment extends MessageSelectorFragment implements LoaderManager.LoaderCallbacks, - AllMediaGalleryAdapter.ItemClickListener -{ + AllMediaGalleryAdapter.ItemClickListener { public static final String CHAT_ID_EXTRA = "chat_id"; protected TextView noMedia; @@ -46,7 +40,7 @@ public class AllMediaGalleryFragment private StickyHeaderGridLayoutManager gridManager; private final ActionModeCallback actionModeCallback = new ActionModeCallback(); - private int chatId; + private int chatId; @Override public void onCreate(Bundle bundle) { @@ -58,20 +52,23 @@ public void onCreate(Bundle bundle) { } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.profile_gallery_fragment, container, false); this.recyclerView = ViewUtil.findById(view, R.id.media_grid); - this.noMedia = ViewUtil.findById(view, R.id.no_images); - this.gridManager = new StickyHeaderGridLayoutManager(getCols()); + this.noMedia = ViewUtil.findById(view, R.id.no_images); + this.gridManager = new StickyHeaderGridLayoutManager(getCols()); // add padding to avoid content hidden behind system bars ViewUtil.applyWindowInsets(recyclerView, true, false, true, true); - this.recyclerView.setAdapter(new AllMediaGalleryAdapter(getContext(), - GlideApp.with(this), - new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()), - this)); + this.recyclerView.setAdapter( + new AllMediaGalleryAdapter( + getContext(), + GlideApp.with(this), + new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()), + this)); this.recyclerView.setLayoutManager(gridManager); this.recyclerView.setHasFixedSize(true); @@ -94,7 +91,9 @@ public void handleEvent(@NonNull DcEvent event) { } private int getCols() { - return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE? 5 : 3; + return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE + ? 5 + : 3; } @Override @@ -107,12 +106,16 @@ public void onConfigurationChanged(Configuration newConfig) { } @Override - public Loader onCreateLoader(int i, Bundle bundle) { - return new BucketedThreadMediaLoader(getContext(), chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO); + public Loader onCreateLoader( + int i, Bundle bundle) { + return new BucketedThreadMediaLoader( + getContext(), chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO); } @Override - public void onLoadFinished(Loader loader, BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) { + public void onLoadFinished( + Loader loader, + BucketedThreadMediaLoader.BucketedThreadMedia bucketedThreadMedia) { ((AllMediaGalleryAdapter) recyclerView.getAdapter()).setMedia(bucketedThreadMedia); ((AllMediaGalleryAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged(); @@ -125,7 +128,8 @@ public void onLoadFinished(Loader @Override public void onLoaderReset(Loader cursorLoader) { - ((AllMediaGalleryAdapter) recyclerView.getAdapter()).setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext())); + ((AllMediaGalleryAdapter) recyclerView.getAdapter()) + .setMedia(new BucketedThreadMediaLoader.BucketedThreadMedia(getContext())); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 8cd28b80b..a8320fd72 100644 --- a/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -21,7 +21,6 @@ import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -32,19 +31,17 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.Preference; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment; import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; -import org.thoughtcrime.securesms.preferences.PrivacyPreferenceFragment; import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment; import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment; +import org.thoughtcrime.securesms.preferences.PrivacyPreferenceFragment; import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference; import org.thoughtcrime.securesms.qr.BackupTransferActivity; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -57,24 +54,22 @@ * The Activity for application preference display and management. * * @author Moxie Marlinspike - * */ - public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarActivity - implements SharedPreferences.OnSharedPreferenceChangeListener -{ - private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile"; - private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications"; - private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance"; - private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats"; - private static final String PREFERENCE_CATEGORY_PRIVACY = "preference_category_privacy"; - private static final String PREFERENCE_CATEGORY_MULTIDEVICE = "preference_category_multidevice"; - private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced"; - private static final String PREFERENCE_CATEGORY_CONNECTIVITY = "preference_category_connectivity"; - private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate"; - private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help"; - - public static final int REQUEST_CODE_SET_BACKGROUND = 11; + implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile"; + private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = + "preference_category_notifications"; + private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance"; + private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats"; + private static final String PREFERENCE_CATEGORY_PRIVACY = "preference_category_privacy"; + private static final String PREFERENCE_CATEGORY_MULTIDEVICE = "preference_category_multidevice"; + private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced"; + private static final String PREFERENCE_CATEGORY_CONNECTIVITY = "preference_category_connectivity"; + private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate"; + private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help"; + + public static final int REQUEST_CODE_SET_BACKGROUND = 11; @Override protected void onCreate(Bundle icicle, boolean ready) { @@ -115,44 +110,51 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin public void showBackupProvider() { Intent intent = new Intent(this, BackupTransferActivity.class); - intent.putExtra(BackupTransferActivity.TRANSFER_MODE, BackupTransferActivity.TransferMode.SENDER_SHOW_QR.getInt()); + intent.putExtra( + BackupTransferActivity.TRANSFER_MODE, + BackupTransferActivity.TransferMode.SENDER_SHOW_QR.getInt()); startActivity(intent); - overridePendingTransition(0, 0); // let the activity appear in the same way as the other pages (which are mostly fragments) + overridePendingTransition( + 0, 0); // let the activity appear in the same way as the other pages (which are mostly + // fragments) finishAffinity(); // see comment (**2) in BackupTransferActivity.doFinish() } - public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment implements DcEventCenter.DcEventDelegate { + public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment + implements DcEventCenter.DcEventDelegate { private ActivityResultLauncher screenLockLauncher; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - screenLockLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK) { - ((ApplicationPreferencesActivity)getActivity()).showBackupProvider(); - } - } - ); + screenLockLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + ((ApplicationPreferencesActivity) getActivity()).showBackupProvider(); + } + }); this.findPreference(PREFERENCE_CATEGORY_PROFILE) .setOnPreferenceClickListener(new ProfileClickListener()); this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS) - .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS)); + .setOnPreferenceClickListener( + new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS)); this.findPreference(PREFERENCE_CATEGORY_CONNECTIVITY) - .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CONNECTIVITY)); + .setOnPreferenceClickListener( + new CategoryClickListener(PREFERENCE_CATEGORY_CONNECTIVITY)); this.findPreference(PREFERENCE_CATEGORY_APPEARANCE) - .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE)); + .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE)); this.findPreference(PREFERENCE_CATEGORY_CHATS) - .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS)); + .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS)); this.findPreference(PREFERENCE_CATEGORY_PRIVACY) - .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PRIVACY)); + .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PRIVACY)); this.findPreference(PREFERENCE_CATEGORY_MULTIDEVICE) - .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_MULTIDEVICE)); + .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_MULTIDEVICE)); this.findPreference(PREFERENCE_CATEGORY_ADVANCED) - .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED)); + .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED)); this.findPreference(PREFERENCE_CATEGORY_DONATE) .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE)); @@ -160,7 +162,8 @@ public void onCreate(Bundle icicle) { this.findPreference(PREFERENCE_CATEGORY_HELP) .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP)); - DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_CONNECTIVITY_CHANGED, this); + DcHelper.getEventCenter(getActivity()) + .addObserver(DcContext.DC_EVENT_CONNECTIVITY_CHANGED, this); } @Override @@ -172,7 +175,9 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, String root public void onResume() { super.onResume(); //noinspection ConstantConditions - ((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.menu_settings); + ((ApplicationPreferencesActivity) getActivity()) + .getSupportActionBar() + .setTitle(R.string.menu_settings); setCategorySummaries(); } @@ -186,12 +191,14 @@ public void onDestroy() { public void handleEvent(@NonNull DcEvent event) { if (event.getId() == DcContext.DC_EVENT_CONNECTIVITY_CHANGED) { this.findPreference(PREFERENCE_CATEGORY_CONNECTIVITY) - .setSummary(DcHelper.getConnectivitySummary(getActivity(), getString(R.string.connectivity_connected))); + .setSummary( + DcHelper.getConnectivitySummary( + getActivity(), getString(R.string.connectivity_connected))); } } private void setCategorySummaries() { - ((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh(); + ((ProfilePreference) this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh(); this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS) .setSummary(NotificationsPreferenceFragment.getSummary(getActivity())); @@ -202,7 +209,9 @@ private void setCategorySummaries() { this.findPreference(PREFERENCE_CATEGORY_PRIVACY) .setSummary(PrivacyPreferenceFragment.getSummary(getActivity())); this.findPreference(PREFERENCE_CATEGORY_CONNECTIVITY) - .setSummary(DcHelper.getConnectivitySummary(getActivity(), getString(R.string.connectivity_connected))); + .setSummary( + DcHelper.getConnectivitySummary( + getActivity(), getString(R.string.connectivity_connected))); this.findPreference(PREFERENCE_CATEGORY_HELP) .setSummary(AdvancedPreferenceFragment.getVersion(getActivity())); } @@ -219,63 +228,76 @@ public boolean onPreferenceClick(Preference preference) { Fragment fragment = null; switch (category) { - case PREFERENCE_CATEGORY_NOTIFICATIONS: - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getActivity()); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || notificationManager.areNotificationsEnabled()) { - fragment = new NotificationsPreferenceFragment(); - } else { - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.notifications_disabled) - .setMessage(R.string.perm_explain_access_to_notifications_denied) - .setPositiveButton(R.string.perm_continue, (dialog, which) -> getActivity().startActivity(Permissions.getApplicationSettingsIntent(getActivity()))) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - break; - case PREFERENCE_CATEGORY_CONNECTIVITY: - startActivity(new Intent(getActivity(), ConnectivityActivity.class)); - break; - case PREFERENCE_CATEGORY_APPEARANCE: - fragment = new AppearancePreferenceFragment(); - break; - case PREFERENCE_CATEGORY_CHATS: - fragment = new ChatsPreferenceFragment(); - break; - case PREFERENCE_CATEGORY_PRIVACY: - fragment = new PrivacyPreferenceFragment(); - break; - case PREFERENCE_CATEGORY_MULTIDEVICE: - if (!ScreenLockUtil.applyScreenLock(getActivity(), getString(R.string.multidevice_title), - getString(R.string.multidevice_this_creates_a_qr_code) + "\n\n" + getString(R.string.enter_system_secret_to_continue), - screenLockLauncher)) { - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.multidevice_title) - .setMessage(R.string.multidevice_this_creates_a_qr_code) - .setPositiveButton(R.string.perm_continue, - (dialog, which) -> ((ApplicationPreferencesActivity)getActivity()).showBackupProvider()) - .setNegativeButton(R.string.cancel, null) - .show(); - ; - } - break; - case PREFERENCE_CATEGORY_ADVANCED: - fragment = new AdvancedPreferenceFragment(); - break; - case PREFERENCE_CATEGORY_DONATE: - IntentUtils.showInBrowser(requireActivity(), "https://arcanechat.me/#contribute"); - break; - case PREFERENCE_CATEGORY_HELP: - startActivity(new Intent(getActivity(), LocalHelpActivity.class)); - break; - default: - throw new AssertionError(); + case PREFERENCE_CATEGORY_NOTIFICATIONS: + NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(getActivity()); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + || notificationManager.areNotificationsEnabled()) { + fragment = new NotificationsPreferenceFragment(); + } else { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.notifications_disabled) + .setMessage(R.string.perm_explain_access_to_notifications_denied) + .setPositiveButton( + R.string.perm_continue, + (dialog, which) -> + getActivity() + .startActivity( + Permissions.getApplicationSettingsIntent(getActivity()))) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + break; + case PREFERENCE_CATEGORY_CONNECTIVITY: + startActivity(new Intent(getActivity(), ConnectivityActivity.class)); + break; + case PREFERENCE_CATEGORY_APPEARANCE: + fragment = new AppearancePreferenceFragment(); + break; + case PREFERENCE_CATEGORY_CHATS: + fragment = new ChatsPreferenceFragment(); + break; + case PREFERENCE_CATEGORY_PRIVACY: + fragment = new PrivacyPreferenceFragment(); + break; + case PREFERENCE_CATEGORY_MULTIDEVICE: + if (!ScreenLockUtil.applyScreenLock( + getActivity(), + getString(R.string.multidevice_title), + getString(R.string.multidevice_this_creates_a_qr_code) + + "\n\n" + + getString(R.string.enter_system_secret_to_continue), + screenLockLauncher)) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.multidevice_title) + .setMessage(R.string.multidevice_this_creates_a_qr_code) + .setPositiveButton( + R.string.perm_continue, + (dialog, which) -> + ((ApplicationPreferencesActivity) getActivity()).showBackupProvider()) + .setNegativeButton(R.string.cancel, null) + .show(); + ; + } + break; + case PREFERENCE_CATEGORY_ADVANCED: + fragment = new AdvancedPreferenceFragment(); + break; + case PREFERENCE_CATEGORY_DONATE: + IntentUtils.showInBrowser(requireActivity(), "https://arcanechat.me/#contribute"); + break; + case PREFERENCE_CATEGORY_HELP: + startActivity(new Intent(getActivity(), LocalHelpActivity.class)); + break; + default: + throw new AssertionError(); } if (fragment != null) { Bundle args = new Bundle(); fragment.setArguments(args); - FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); + FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.replace(R.id.fragment, fragment); fragmentTransaction.addToBackStack(null); @@ -297,7 +319,8 @@ public boolean onPreferenceClick(Preference preference) { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } } diff --git a/src/main/java/org/thoughtcrime/securesms/AttachContactActivity.java b/src/main/java/org/thoughtcrime/securesms/AttachContactActivity.java index 4747254ea..a4b373ef5 100644 --- a/src/main/java/org/thoughtcrime/securesms/AttachContactActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/AttachContactActivity.java @@ -2,8 +2,6 @@ import android.content.Intent; -import org.thoughtcrime.securesms.connect.DcHelper; - public class AttachContactActivity extends ContactSelectionActivity { public static final String CONTACT_ID_EXTRA = "contact_id_extra"; diff --git a/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java index b75d76a42..beeae75aa 100644 --- a/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java @@ -1,13 +1,11 @@ package org.thoughtcrime.securesms; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.ViewConfiguration; import android.view.WindowManager; - import androidx.activity.EdgeToEdge; import androidx.annotation.IdRes; import androidx.annotation.NonNull; @@ -15,14 +13,11 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.WindowCompat; import androidx.fragment.app.Fragment; - +import java.lang.reflect.Field; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.ViewUtil; -import java.lang.reflect.Field; - - public abstract class BaseActionBarActivity extends AppCompatActivity { private static final String TAG = BaseActionBarActivity.class.getSimpleName(); @@ -40,11 +35,13 @@ protected void onCreate(Bundle savedInstanceState) { // Only enable Edge-to-Edge if it is well supported if (ViewUtil.isEdgeToEdgeSupported()) { // docs says to use: WindowCompat.enableEdgeToEdge(getWindow()); - // but it actually makes things worse, the next takes care of setting the 3-buttons navigation bar background + // but it actually makes things worse, the next takes care of setting the 3-buttons navigation + // bar background EdgeToEdge.enable(this); // force white text in status bar so it visible over background color - WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()).setAppearanceLightStatusBars(false); + WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()) + .setAppearanceLightStatusBars(false); } } @@ -68,14 +65,12 @@ private void initializeScreenshotSecurity() { } } - /** - * Modified from: http://stackoverflow.com/a/13098824 - */ + /** Modified from: http://stackoverflow.com/a/13098824 */ private void forceOverflowMenu() { try { - ViewConfiguration config = ViewConfiguration.get(this); - Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); - if(menuKeyField != null) { + ViewConfiguration config = ViewConfiguration.get(this); + Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); + if (menuKeyField != null) { menuKeyField.setAccessible(true); menuKeyField.setBoolean(config, false); } @@ -97,21 +92,18 @@ public void makeSearchMenuVisible(final Menu menu, final MenuItem searchItem, bo } else if (item == searchItem) { ; // searchItem is just always visible } else { - item.setVisible(!visible); // if search is shown, other items are hidden - and the other way round + item.setVisible( + !visible); // if search is shown, other items are hidden - and the other way round } } } - protected T initFragment(@IdRes int target, - @NonNull T fragment) - { + protected T initFragment(@IdRes int target, @NonNull T fragment) { return initFragment(target, fragment, null); } - protected T initFragment(@IdRes int target, - @NonNull T fragment, - @Nullable Bundle extras) - { + protected T initFragment( + @IdRes int target, @NonNull T fragment, @Nullable Bundle extras) { Bundle args = new Bundle(); if (extras != null) { @@ -119,9 +111,10 @@ protected T initFragment(@IdRes int target, } fragment.setArguments(args); - getSupportFragmentManager().beginTransaction() - .replace(target, fragment) - .commitAllowingStateLoss(); + getSupportFragmentManager() + .beginTransaction() + .replace(target, fragment) + .commitAllowingStateLoss(); return fragment; } } diff --git a/src/main/java/org/thoughtcrime/securesms/BaseConversationItem.java b/src/main/java/org/thoughtcrime/securesms/BaseConversationItem.java index 84526fc7b..652d496b1 100644 --- a/src/main/java/org/thoughtcrime/securesms/BaseConversationItem.java +++ b/src/main/java/org/thoughtcrime/securesms/BaseConversationItem.java @@ -6,42 +6,37 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; - +import chat.delta.rpc.Rpc; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - +import java.util.HashSet; +import java.util.Set; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.HashSet; -import java.util.Set; - -import chat.delta.rpc.Rpc; - public abstract class BaseConversationItem extends LinearLayout - implements BindableConversationItem -{ + implements BindableConversationItem { static final long PULSE_HIGHLIGHT_MILLIS = 500; - protected DcMsg messageRecord; - protected DcChat dcChat; - protected TextView bodyText; + protected DcMsg messageRecord; + protected DcChat dcChat; + protected TextView bodyText; - protected final Context context; - protected final DcContext dcContext; + protected final Context context; + protected final DcContext dcContext; protected final Rpc rpc; - protected Recipient conversationRecipient; + protected Recipient conversationRecipient; - protected @NonNull Set batchSelected = new HashSet<>(); + protected @NonNull Set batchSelected = new HashSet<>(); - protected final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); + protected final PassthroughClickListener passthroughClickListener = + new PassthroughClickListener(); public BaseConversationItem(Context context, AttributeSet attrs) { super(context, attrs); @@ -50,32 +45,32 @@ public BaseConversationItem(Context context, AttributeSet attrs) { this.rpc = DcHelper.getRpc(context); } - protected void bindPartial(@NonNull DcMsg messageRecord, - @NonNull DcChat dcChat, - @NonNull Set batchSelected, - boolean pulseHighlight, - @NonNull Recipient conversationRecipient) - { - this.messageRecord = messageRecord; - this.dcChat = dcChat; - this.batchSelected = batchSelected; - this.conversationRecipient = conversationRecipient; + protected void bindPartial( + @NonNull DcMsg messageRecord, + @NonNull DcChat dcChat, + @NonNull Set batchSelected, + boolean pulseHighlight, + @NonNull Recipient conversationRecipient) { + this.messageRecord = messageRecord; + this.dcChat = dcChat; + this.batchSelected = batchSelected; + this.conversationRecipient = conversationRecipient; setInteractionState(messageRecord, pulseHighlight); } protected void setInteractionState(DcMsg messageRecord, boolean pulseHighlight) { - final int[] attributes = new int[] { - R.attr.conversation_item_background, - R.attr.conversation_item_background_animated, - }; + final int[] attributes = + new int[] { + R.attr.conversation_item_background, R.attr.conversation_item_background_animated, + }; if (batchSelected.contains(messageRecord)) { - final TypedArray attrs = context.obtainStyledAttributes(attributes); + final TypedArray attrs = context.obtainStyledAttributes(attributes); ViewUtil.setBackground(this, attrs.getDrawable(0)); attrs.recycle(); setSelected(true); } else if (pulseHighlight) { - final TypedArray attrs = context.obtainStyledAttributes(attributes); + final TypedArray attrs = context.obtainStyledAttributes(attributes); ViewUtil.setBackground(this, attrs.getDrawable(1)); attrs.recycle(); setSelected(true); @@ -92,15 +87,16 @@ public void setOnClickListener(OnClickListener l) { protected boolean shouldInterceptClicks(DcMsg messageRecord) { return batchSelected.isEmpty() - && (messageRecord.isFailed() - || messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE - || messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED - || messageRecord.getInfoType() == DcMsg.DC_INFO_INVALID_UNENCRYPTED_MAIL); + && (messageRecord.isFailed() + || messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE + || messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED + || messageRecord.getInfoType() == DcMsg.DC_INFO_INVALID_UNENCRYPTED_MAIL); } protected void onAccessibilityClick() {} - protected class PassthroughClickListener implements View.OnLongClickListener, View.OnClickListener { + protected class PassthroughClickListener + implements View.OnLongClickListener, View.OnClickListener { @Override public boolean onLongClick(View v) { @@ -137,13 +133,15 @@ public void onClick(View v) { TextView detailsText = view.findViewById(R.id.details_text); detailsText.setText(messageRecord.getError()); - AlertDialog d = new AlertDialog.Builder(context) + AlertDialog d = + new AlertDialog.Builder(context) .setView(view) .setTitle(R.string.error) .setPositiveButton(R.string.ok, null) .create(); d.show(); - } else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE || messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED) { + } else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_E2EE + || messageRecord.getInfoType() == DcMsg.DC_INFO_PROTECTION_ENABLED) { DcHelper.showProtectionEnabledDialog(context); } else if (messageRecord.getInfoType() == DcMsg.DC_INFO_INVALID_UNENCRYPTED_MAIL) { DcHelper.showInvalidUnencryptedDialog(context); diff --git a/src/main/java/org/thoughtcrime/securesms/BaseConversationListAdapter.java b/src/main/java/org/thoughtcrime/securesms/BaseConversationListAdapter.java index 3114c3524..d74792c46 100644 --- a/src/main/java/org/thoughtcrime/securesms/BaseConversationListAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/BaseConversationListAdapter.java @@ -1,14 +1,14 @@ package org.thoughtcrime.securesms; import androidx.recyclerview.widget.RecyclerView; - import java.util.Collections; import java.util.HashSet; import java.util.Set; -public abstract class BaseConversationListAdapter extends RecyclerView.Adapter { - protected final Set batchSet = Collections.synchronizedSet(new HashSet()); - protected boolean batchMode = false; +public abstract class BaseConversationListAdapter + extends RecyclerView.Adapter { + protected final Set batchSet = Collections.synchronizedSet(new HashSet()); + protected boolean batchMode = false; public abstract void selectAllThreads(); diff --git a/src/main/java/org/thoughtcrime/securesms/BaseConversationListFragment.java b/src/main/java/org/thoughtcrime/securesms/BaseConversationListFragment.java index 9d74fc122..a7887863f 100644 --- a/src/main/java/org/thoughtcrime/securesms/BaseConversationListFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/BaseConversationListFragment.java @@ -17,7 +17,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.widget.Toast; - import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -27,36 +26,36 @@ import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import androidx.fragment.app.Fragment; - import com.b44t.messenger.DcChat; +import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.google.android.material.snackbar.Snackbar; - +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DirectShareUtil; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.ShareUtil; import org.thoughtcrime.securesms.util.SendRelayedMessageUtil; +import org.thoughtcrime.securesms.util.ShareUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask; import org.thoughtcrime.securesms.util.views.ProgressDialog; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - public abstract class BaseConversationListFragment extends Fragment implements ActionMode.Callback { protected ActionMode actionMode; protected PulsingFloatingActionButton fab; protected abstract boolean offerToArchive(); + protected abstract void setFabVisibility(boolean isActionMode); + protected abstract BaseConversationListAdapter getListAdapter(); protected void onItemClick(long chatId) { if (actionMode == null) { - ((ConversationSelectedListener) requireActivity()).onCreateConversation((int)chatId); + ((ConversationSelectedListener) requireActivity()).onCreateConversation((int) chatId); } else { BaseConversationListAdapter adapter = getListAdapter(); adapter.toggleThreadInBatchSet(chatId); @@ -71,8 +70,9 @@ protected void onItemClick(long chatId) { adapter.notifyDataSetChanged(); } } + public void onItemLongClick(long chatId) { - actionMode = ((AppCompatActivity)requireActivity()).startSupportActionMode(this); + actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(this); if (actionMode != null) { getListAdapter().initializeBatchMode(true); @@ -89,36 +89,54 @@ protected void initializeFabClickListener(boolean isActionMode) { Intent intent = new Intent(getActivity(), NewConversationActivity.class); if (isRelayingMessageContent(getActivity())) { if (isActionMode) { - fab.setOnClickListener(v -> { - final Set selectedChats = getListAdapter().getBatchSelections(); - ArrayList uris = getSharedUris(getActivity()); - String message; - if (isForwarding(getActivity())) { - message = String.format(Util.getLocale(), getString(R.string.ask_forward_multiple), selectedChats.size()); - } else if (!uris.isEmpty()) { - message = String.format(Util.getLocale(), getString(R.string.ask_send_files_to_selected_chats), uris.size(), selectedChats.size()); - } else { - message = String.format(Util.getLocale(), getString(R.string.share_text_multiple_chats), selectedChats.size(), getSharedText(getActivity())); - } - - Context context = getContext(); - if (context != null) { - if (SendRelayedMessageUtil.containsVideoType(context, uris)) { - message += "\n\n" + getString(R.string.videos_sent_without_recoding); - } - new AlertDialog.Builder(context) + fab.setOnClickListener( + v -> { + final Set selectedChats = getListAdapter().getBatchSelections(); + ArrayList uris = getSharedUris(getActivity()); + String message; + if (isForwarding(getActivity())) { + message = + String.format( + Util.getLocale(), + getString(R.string.ask_forward_multiple), + selectedChats.size()); + } else if (!uris.isEmpty()) { + message = + String.format( + Util.getLocale(), + getString(R.string.ask_send_files_to_selected_chats), + uris.size(), + selectedChats.size()); + } else { + message = + String.format( + Util.getLocale(), + getString(R.string.share_text_multiple_chats), + selectedChats.size(), + getSharedText(getActivity())); + } + + Context context = getContext(); + if (context != null) { + if (SendRelayedMessageUtil.containsVideoType(context, uris)) { + message += "\n\n" + getString(R.string.videos_sent_without_recoding); + } + new AlertDialog.Builder(context) .setMessage(message) .setCancelable(false) .setNegativeButton(android.R.string.cancel, ((dialog, which) -> {})) - .setPositiveButton(R.string.menu_send, (dialog, which) -> { - SendRelayedMessageUtil.immediatelyRelay(getActivity(), selectedChats.toArray(new Long[selectedChats.size()])); - actionMode.finish(); - actionMode = null; - getActivity().finish(); - }) + .setPositiveButton( + R.string.menu_send, + (dialog, which) -> { + SendRelayedMessageUtil.immediatelyRelay( + getActivity(), selectedChats.toArray(new Long[selectedChats.size()])); + actionMode.finish(); + actionMode = null; + getActivity().finish(); + }) .show(); - } - }); + } + }); } else { acquireRelayMessageContent(getActivity(), intent); fab.setOnClickListener(v -> requireActivity().startActivity(intent)); @@ -132,8 +150,8 @@ private boolean areSomeSelectedChatsUnpinned() { DcContext dcContext = DcHelper.getContext(requireActivity()); final Set selectedChats = getListAdapter().getBatchSelections(); for (long chatId : selectedChats) { - DcChat dcChat = dcContext.getChat((int)chatId); - if (dcChat.getVisibility()!=DcChat.DC_CHAT_VISIBILITY_PINNED) { + DcChat dcChat = dcContext.getChat((int) chatId); + if (dcChat.getVisibility() != DcChat.DC_CHAT_VISIBILITY_PINNED) { return true; } } @@ -144,7 +162,7 @@ private boolean areSomeSelectedChatsUnmuted() { DcContext dcContext = DcHelper.getContext(requireActivity()); final Set selectedChats = getListAdapter().getBatchSelections(); for (long chatId : selectedChats) { - DcChat dcChat = dcContext.getChat((int)chatId); + DcChat dcChat = dcContext.getChat((int) chatId); if (!dcChat.isMuted()) { return true; } @@ -153,12 +171,14 @@ private boolean areSomeSelectedChatsUnmuted() { } private void handlePinAllSelected() { - final DcContext dcContext = DcHelper.getContext(requireActivity()); - final Set selectedConversations = new HashSet(getListAdapter().getBatchSelections()); + final DcContext dcContext = DcHelper.getContext(requireActivity()); + final Set selectedConversations = + new HashSet(getListAdapter().getBatchSelections()); boolean doPin = areSomeSelectedChatsUnpinned(); for (long chatId : selectedConversations) { - dcContext.setChatVisibility((int)chatId, - doPin? DcChat.DC_CHAT_VISIBILITY_PINNED : DcChat.DC_CHAT_VISIBILITY_NORMAL); + dcContext.setChatVisibility( + (int) chatId, + doPin ? DcChat.DC_CHAT_VISIBILITY_PINNED : DcChat.DC_CHAT_VISIBILITY_NORMAL); } if (actionMode != null) { actionMode.finish(); @@ -167,23 +187,26 @@ private void handlePinAllSelected() { } private void handleMuteAllSelected() { - final DcContext dcContext = DcHelper.getContext(requireActivity()); - final Set selectedConversations = new HashSet(getListAdapter().getBatchSelections()); + final DcContext dcContext = DcHelper.getContext(requireActivity()); + final Set selectedConversations = + new HashSet(getListAdapter().getBatchSelections()); if (areSomeSelectedChatsUnmuted()) { - MuteDialog.show(getActivity(), duration -> { - for (long chatId : selectedConversations) { - dcContext.setChatMuteDuration((int)chatId, duration); - } + MuteDialog.show( + getActivity(), + duration -> { + for (long chatId : selectedConversations) { + dcContext.setChatMuteDuration((int) chatId, duration); + } - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } - }); + if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + }); } else { // unmute for (long chatId : selectedConversations) { - dcContext.setChatMuteDuration((int)chatId, 0); + dcContext.setChatMuteDuration((int) chatId, 0); } if (actionMode != null) { @@ -194,10 +217,11 @@ private void handleMuteAllSelected() { } private void handleMarknoticedSelected() { - final DcContext dcContext = DcHelper.getContext(requireActivity()); - final Set selectedConversations = new HashSet(getListAdapter().getBatchSelections()); + final DcContext dcContext = DcHelper.getContext(requireActivity()); + final Set selectedConversations = + new HashSet(getListAdapter().getBatchSelections()); for (long chatId : selectedConversations) { - dcContext.marknoticedChat((int)chatId); + dcContext.marknoticedChat((int) chatId); } if (actionMode != null) { actionMode.finish(); @@ -207,22 +231,21 @@ private void handleMarknoticedSelected() { @SuppressLint("StaticFieldLeak") private void handleArchiveAllSelected() { - final DcContext dcContext = DcHelper.getContext(requireActivity()); - final Set selectedConversations = new HashSet(getListAdapter().getBatchSelections()); - final boolean archive = offerToArchive(); + final DcContext dcContext = DcHelper.getContext(requireActivity()); + final Set selectedConversations = + new HashSet(getListAdapter().getBatchSelections()); + final boolean archive = offerToArchive(); int snackBarTitleId; if (archive) snackBarTitleId = R.plurals.chat_archived; - else snackBarTitleId = R.plurals.chat_unarchived; + else snackBarTitleId = R.plurals.chat_unarchived; - int count = selectedConversations.size(); + int count = selectedConversations.size(); String snackBarTitle = getResources().getQuantityString(snackBarTitleId, count, count); - new SnackbarAsyncTask(getView(), snackBarTitle, - getString(R.string.undo), - Snackbar.LENGTH_LONG, true) - { + new SnackbarAsyncTask( + getView(), snackBarTitle, getString(R.string.undo), Snackbar.LENGTH_LONG, true) { @Override protected void onPostExecute(Void result) { @@ -237,16 +260,18 @@ protected void onPostExecute(Void result) { @Override protected void executeAction(@Nullable Void parameter) { for (long chatId : selectedConversations) { - dcContext.setChatVisibility((int)chatId, - archive? DcChat.DC_CHAT_VISIBILITY_ARCHIVED : DcChat.DC_CHAT_VISIBILITY_NORMAL); + dcContext.setChatVisibility( + (int) chatId, + archive ? DcChat.DC_CHAT_VISIBILITY_ARCHIVED : DcChat.DC_CHAT_VISIBILITY_NORMAL); } } @Override protected void reverseAction(@Nullable Void parameter) { for (long threadId : selectedConversations) { - dcContext.setChatVisibility((int)threadId, - archive? DcChat.DC_CHAT_VISIBILITY_NORMAL : DcChat.DC_CHAT_VISIBILITY_ARCHIVED); + dcContext.setChatVisibility( + (int) threadId, + archive ? DcChat.DC_CHAT_VISIBILITY_NORMAL : DcChat.DC_CHAT_VISIBILITY_ARCHIVED); } } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -262,51 +287,73 @@ private void handleDeleteAllSelected() { final String alertText; if (chatsCount == 1) { long chatId = selectedChats.iterator().next(); - alertText = activity.getResources().getString(R.string.ask_delete_named_chat, dcContext.getChat((int)chatId).getName()); + alertText = + activity + .getResources() + .getString(R.string.ask_delete_named_chat, dcContext.getChat((int) chatId).getName()); } else { - alertText = activity.getResources().getQuantityString(R.plurals.ask_delete_chat, chatsCount, chatsCount); + alertText = + activity + .getResources() + .getQuantityString(R.plurals.ask_delete_chat, chatsCount, chatsCount); + } + + String alertButton = getString(R.string.delete_for_me); + for (long chatId : selectedChats) { + if (dcContext.getChat((int) chatId).shallLeaveBeforeDelete(dcContext)) { + alertButton = getString(R.string.menu_leave_and_delete); + break; + } } AlertDialog.Builder alert = new AlertDialog.Builder(activity); alert.setMessage(alertText); alert.setCancelable(true); - alert.setPositiveButton(R.string.delete, (dialog, which) -> { - - if (!selectedChats.isEmpty()) { - new AsyncTask() { - private ProgressDialog dialog; - - @Override - protected void onPreExecute() { - dialog = ProgressDialog.show(getActivity(), - "", - requireActivity().getString(R.string.one_moment), - true, false); - } - - @Override - protected Void doInBackground(Void... params) { - int accountId = dcContext.getAccountId(); - for (long chatId : selectedChats) { - DcHelper.getNotificationCenter(requireContext()).removeNotifications(accountId, (int) chatId); - dcContext.deleteChat((int) chatId); - DirectShareUtil.clearShortcut(requireContext(), (int) chatId); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - dialog.dismiss(); - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } + alert.setPositiveButton( + alertButton, + (dialog, which) -> { + if (!selectedChats.isEmpty()) { + new AsyncTask() { + private ProgressDialog dialog; + + @Override + protected void onPreExecute() { + dialog = + ProgressDialog.show( + getActivity(), + "", + requireActivity().getString(R.string.one_moment), + true, + false); + } + + @Override + protected Void doInBackground(Void... params) { + int accountId = dcContext.getAccountId(); + for (long chatId : selectedChats) { + DcHelper.getNotificationCenter(requireContext()) + .removeNotifications(accountId, (int) chatId); + if (dcContext.getChat((int) chatId).shallLeaveBeforeDelete(dcContext)) { + dcContext.removeContactFromChat((int) chatId, DcContact.DC_CONTACT_ID_SELF); + } + dcContext.deleteChat((int) chatId); + DirectShareUtil.clearShortcut(requireContext(), (int) chatId); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + dialog.dismiss(); + if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - }); + }); alert.setNegativeButton(android.R.string.cancel, null); AlertDialog dialog = alert.show(); @@ -331,22 +378,30 @@ private void handleAddToHomeScreen() { intent.putExtra(ShareActivity.EXTRA_CHAT_ID, chat.getId()); Recipient recipient = new Recipient(activity, chat); - Util.runOnAnyBackgroundThread(() -> { - Bitmap avatar = DirectShareUtil.getIconForShortcut(activity, recipient); - ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(activity, "chat-" + dcContext.getAccountId() + "-" + chat.getId()) - .setShortLabel(chat.getName()) - .setIcon(IconCompat.createWithAdaptiveBitmap(avatar)) - .setIntent(intent) - .build(); - Util.runOnMain(() -> { - if (!ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null)) { - Toast.makeText(activity, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG).show(); - } else if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } - }); - }); + Util.runOnAnyBackgroundThread( + () -> { + Bitmap avatar = DirectShareUtil.getIconForShortcut(activity, recipient); + ShortcutInfoCompat shortcutInfoCompat = + new ShortcutInfoCompat.Builder( + activity, "chat-" + dcContext.getAccountId() + "-" + chat.getId()) + .setShortLabel(chat.getName()) + .setIcon(IconCompat.createWithAdaptiveBitmap(avatar)) + .setIntent(intent) + .build(); + Util.runOnMain( + () -> { + if (!ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfoCompat, null)) { + Toast.makeText( + activity, + "ErrAddToHomescreen: requestPinShortcut() failed", + Toast.LENGTH_LONG) + .show(); + } else if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + }); + }); } private void updateActionModeItems(Menu menu) { @@ -356,11 +411,11 @@ private void updateActionModeItems(Menu menu) { menu.findItem(R.id.menu_add_to_home_screen).setVisible(selectedCount == 1); MenuItem archiveItem = menu.findItem(R.id.menu_archive_selected); if (offerToArchive()) { - archiveItem.setIcon(R.drawable.ic_archive_white_24dp); - archiveItem.setTitle(R.string.menu_archive_chat); + archiveItem.setIcon(R.drawable.ic_archive_white_24dp); + archiveItem.setTitle(R.string.menu_archive_chat); } else { - archiveItem.setIcon(R.drawable.ic_unarchive_white_24dp); - archiveItem.setTitle(R.string.menu_unarchive_chat); + archiveItem.setIcon(R.drawable.ic_unarchive_white_24dp); + archiveItem.setTitle(R.string.menu_unarchive_chat); } MenuItem pinItem = menu.findItem(R.id.menu_pin_selected); if (areSomeSelectedChatsUnpinned()) { @@ -383,7 +438,8 @@ private void updateActionModeItems(Menu menu) { public boolean onCreateActionMode(ActionMode mode, Menu menu) { if (isRelayingMessageContent(getActivity())) { if (ShareUtil.getSharedContactId(getActivity()) != 0) { - return false; // no sharing of a contact to multiple recipients at the same time, we can reconsider when that becomes a real-world need + return false; // no sharing of a contact to multiple recipients at the same time, we can + // reconsider when that becomes a real-world need } Context context = getContext(); if (context != null) { @@ -451,6 +507,7 @@ public void onDestroyActionMode(ActionMode mode) { public interface ConversationSelectedListener { void onCreateConversation(int chatId); + void onSwitchToArchive(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 863cf2c17..7516c9bc4 100644 --- a/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -2,26 +2,24 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcMsg; - +import java.util.Set; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.components.audioplay.AudioView; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; -import java.util.Set; - public interface BindableConversationItem extends Unbindable { - void bind(@NonNull DcMsg messageRecord, - @NonNull DcChat dcChat, - @NonNull GlideRequests glideRequests, - @NonNull Set batchSelected, - @NonNull Recipient recipients, - boolean pulseHighlight, - @Nullable AudioPlaybackViewModel playbackViewModel, - AudioView.OnActionListener audioPlayPauseListener); + void bind( + @NonNull DcMsg messageRecord, + @NonNull DcChat dcChat, + @NonNull GlideRequests glideRequests, + @NonNull Set batchSelected, + @NonNull Recipient recipients, + boolean pulseHighlight, + @Nullable AudioPlaybackViewModel playbackViewModel, + AudioView.OnActionListener audioPlayPauseListener); DcMsg getMessageRecord(); @@ -29,10 +27,15 @@ void bind(@NonNull DcMsg messageRecord, interface EventListener { void onQuoteClicked(DcMsg messageRecord); + void onJumpToOriginalClicked(DcMsg messageRecord); + void onShowFullClicked(DcMsg messageRecord); + void onDownloadClicked(DcMsg messageRecord); + void onReactionClicked(DcMsg messageRecord); + void onStickerClicked(DcMsg messageRecord); } } diff --git a/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java b/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java index 188a38797..9e6d1f4f8 100644 --- a/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java +++ b/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java @@ -1,19 +1,18 @@ package org.thoughtcrime.securesms; import androidx.annotation.NonNull; - import com.b44t.messenger.DcLot; - +import java.util.Set; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.mms.GlideRequests; -import java.util.Set; - public interface BindableConversationListItem extends Unbindable { - public void bind(@NonNull ThreadRecord thread, - int msgId, - @NonNull DcLot dcSummary, - @NonNull GlideRequests glideRequests, - @NonNull Set selectedThreads, boolean batchMode); + public void bind( + @NonNull ThreadRecord thread, + int msgId, + @NonNull DcLot dcSummary, + @NonNull GlideRequests glideRequests, + @NonNull Set selectedThreads, + boolean batchMode); } diff --git a/src/main/java/org/thoughtcrime/securesms/BlockedContactsActivity.java b/src/main/java/org/thoughtcrime/securesms/BlockedContactsActivity.java index e5ca41b4f..8a9e048a1 100644 --- a/src/main/java/org/thoughtcrime/securesms/BlockedContactsActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/BlockedContactsActivity.java @@ -6,7 +6,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; @@ -14,10 +13,8 @@ import androidx.loader.content.Loader; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - import org.thoughtcrime.securesms.connect.DcContactsLoader; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -39,17 +36,18 @@ public void onCreate(Bundle bundle, boolean ready) { @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: finish(); return true; + case android.R.id.home: + finish(); + return true; } return false; } - public static class BlockedAndShareContactsFragment - extends Fragment - implements LoaderManager.LoaderCallbacks, - DcEventCenter.DcEventDelegate, ContactSelectionListAdapter.ItemClickListener { - + public static class BlockedAndShareContactsFragment extends Fragment + implements LoaderManager.LoaderCallbacks, + DcEventCenter.DcEventDelegate, + ContactSelectionListAdapter.ItemClickListener { private RecyclerView recyclerView; private TextView emptyStateView; @@ -57,7 +55,7 @@ public static class BlockedAndShareContactsFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false); - recyclerView = ViewUtil.findById(view, R.id.recycler_view); + recyclerView = ViewUtil.findById(view, R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); // add padding to avoid content hidden behind system bars @@ -81,11 +79,8 @@ public void onActivityCreated(Bundle bundle) { } private void initializeAdapter() { - ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), - GlideApp.with(this), - this, - false, - false); + ContactSelectionListAdapter adapter = + new ContactSelectionListAdapter(getActivity(), GlideApp.with(this), this, false, false); recyclerView.setAdapter(adapter); } @@ -112,7 +107,7 @@ public void onLoaderReset(Loader loader) { @Override public void handleEvent(@NonNull DcEvent event) { - if (event.getId()==DcContext.DC_EVENT_CONTACTS_CHANGED) { + if (event.getId() == DcContext.DC_EVENT_CONTACTS_CHANGED) { restartLoader(); } } @@ -128,10 +123,12 @@ private ContactSelectionListAdapter getContactSelectionListAdapter() { @Override public void onItemClick(ContactSelectionListItem item, boolean handleActionMode) { new AlertDialog.Builder(getActivity()) - .setMessage(R.string.ask_unblock_contact) - .setCancelable(true) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.menu_unblock_contact, (dialog, which) -> unblockContact(item.getContactId())).show(); + .setMessage(R.string.ask_unblock_contact) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton( + R.string.menu_unblock_contact, (dialog, which) -> unblockContact(item.getContactId())) + .show(); } private void unblockContact(int contactId) { @@ -143,5 +140,4 @@ private void unblockContact(int contactId) { @Override public void onItemLongClick(ContactSelectionListItem view) {} } - } diff --git a/src/main/java/org/thoughtcrime/securesms/ConnectivityActivity.java b/src/main/java/org/thoughtcrime/securesms/ConnectivityActivity.java index ebf0cc06f..e0db0d9e8 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConnectivityActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConnectivityActivity.java @@ -2,12 +2,9 @@ import android.os.Bundle; import android.view.Menu; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -35,8 +32,10 @@ public boolean onPrepareOptionsMenu(Menu menu) { } private void refresh() { - final String connectivityHtml = DcHelper.getContext(this).getConnectivityHtml() - .replace("", " html { color-scheme: dark light; }"); + final String connectivityHtml = + DcHelper.getContext(this) + .getConnectivityHtml() + .replace("", " html { color-scheme: dark light; }"); webView.loadDataWithBaseURL(null, connectivityHtml, "text/html", "utf-8", null); } diff --git a/src/main/java/org/thoughtcrime/securesms/ContactMultiSelectionActivity.java b/src/main/java/org/thoughtcrime/securesms/ContactMultiSelectionActivity.java index 8767ea736..9b38daf30 100644 --- a/src/main/java/org/thoughtcrime/securesms/ContactMultiSelectionActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ContactMultiSelectionActivity.java @@ -21,7 +21,6 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; - import java.util.ArrayList; import java.util.List; @@ -29,7 +28,6 @@ * Activity container for selecting a list of contacts. * * @author Moxie Marlinspike - * */ public class ContactMultiSelectionActivity extends ContactSelectionActivity { @@ -74,7 +72,8 @@ private void saveSelection() { List selectedContacts = contactsFragment.getSelectedContacts(); List deselectedContacts = contactsFragment.getDeselectedContacts(); resultIntent.putIntegerArrayListExtra(CONTACTS_EXTRA, new ArrayList<>(selectedContacts)); - resultIntent.putIntegerArrayListExtra(DESELECTED_CONTACTS_EXTRA, new ArrayList<>(deselectedContacts)); + resultIntent.putIntegerArrayListExtra( + DESELECTED_CONTACTS_EXTRA, new ArrayList<>(deselectedContacts)); setResult(RESULT_OK, resultIntent); } } diff --git a/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java b/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java index a877218f2..dd4b156d6 100644 --- a/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java @@ -18,7 +18,6 @@ import android.os.Bundle; import android.view.MenuItem; - import org.thoughtcrime.securesms.components.ContactFilterToolbar; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.ViewUtil; @@ -27,11 +26,9 @@ * Base activity container for selecting a list of contacts. * * @author Moxie Marlinspike - * */ public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity - implements ContactSelectionListFragment.OnContactSelectedListener -{ + implements ContactSelectionListFragment.OnContactSelectedListener { private static final String TAG = ContactSelectionActivity.class.getSimpleName(); protected ContactSelectionListFragment contactsFragment; @@ -61,7 +58,7 @@ private void initializeToolbar() { this.toolbar = ViewUtil.findById(this, R.id.toolbar); setSupportActionBar(toolbar); - assert getSupportActionBar() != null; + assert getSupportActionBar() != null; getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(false); getSupportActionBar().setIcon(null); @@ -69,7 +66,9 @@ private void initializeToolbar() { } private void initializeResources() { - contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment); + contactsFragment = + (ContactSelectionListFragment) + getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment); contactsFragment.setOnContactSelectedListener(this); } diff --git a/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 1043def73..9a0d5718b 100644 --- a/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -16,7 +16,6 @@ */ package org.thoughtcrime.securesms; - import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent; import android.app.Activity; @@ -31,7 +30,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -43,11 +41,14 @@ import androidx.loader.content.Loader; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; import org.thoughtcrime.securesms.connect.DcContactsLoader; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -59,39 +60,30 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - /** * Fragment for selecting a one or more contacts from a list. * * @author Moxie Marlinspike - * */ -public class ContactSelectionListFragment extends Fragment - implements LoaderManager.LoaderCallbacks, - DcEventCenter.DcEventDelegate -{ +public class ContactSelectionListFragment extends Fragment + implements LoaderManager.LoaderCallbacks, DcEventCenter.DcEventDelegate { private static final String TAG = ContactSelectionListFragment.class.getSimpleName(); - public static final String MULTI_SELECT = "multi_select"; + public static final String MULTI_SELECT = "multi_select"; public static final String SELECT_UNENCRYPTED_EXTRA = "select_unencrypted_extra"; public static final String ALLOW_CREATION = "allow_creation"; public static final String PRESELECTED_CONTACTS = "preselected_contacts"; private DcContext dcContext; - private Set selectedContacts; - private Set deselectedContacts; + private Set selectedContacts; + private Set deselectedContacts; private OnContactSelectedListener onContactSelectedListener; - private String cursorFilter; - private RecyclerView recyclerView; - private TextView emptyView; - private ActionMode actionMode; - private ActionMode.Callback actionModeCallback; + private String cursorFilter; + private RecyclerView recyclerView; + private TextView emptyView; + private ActionMode actionMode; + private ActionMode.Callback actionModeCallback; private ActivityResultLauncher newContactLauncher; @Override @@ -99,19 +91,21 @@ public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); dcContext = DcHelper.getContext(requireContext()); - newContactLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - int contactId = result.getData().getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0); - if (contactId != 0) { - selectedContacts.add(contactId); - deselectedContacts.remove(contactId); - } - LoaderManager.getInstance(this).restartLoader(0, null, ContactSelectionListFragment.this); - } - } - ); + newContactLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + int contactId = + result.getData().getIntExtra(NewContactActivity.CONTACT_ID_EXTRA, 0); + if (contactId != 0) { + selectedContacts.add(contactId); + deselectedContacts.remove(contactId); + } + LoaderManager.getInstance(this) + .restartLoader(0, null, ContactSelectionListFragment.this); + } + }); } @Override @@ -127,55 +121,58 @@ public void onStart() { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false); - recyclerView = ViewUtil.findById(view, R.id.recycler_view); - emptyView = ViewUtil.findById(view, android.R.id.empty); + recyclerView = ViewUtil.findById(view, R.id.recycler_view); + emptyView = ViewUtil.findById(view, android.R.id.empty); // add padding to avoid content hidden behind system bars ViewUtil.applyWindowInsets(recyclerView, true, false, true, true); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - actionModeCallback = new ActionMode.Callback() { - @Override - public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { - MenuInflater inflater = getActivity().getMenuInflater(); - inflater.inflate(R.menu.contact_list, menu); - menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti()); - updateActionModeState(actionMode); - return true; - } + actionModeCallback = + new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.contact_list, menu); + menu.findItem(R.id.menu_delete_selected).setVisible(!isMulti()); + updateActionModeState(actionMode); + return true; + } - @Override - public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { - return false; - } + @Override + public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { + return false; + } - @Override - public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { - int itemId = menuItem.getItemId(); - if (itemId == R.id.menu_select_all) { - handleSelectAll(); - return true; - } else if (itemId == R.id.menu_view_profile) { - handleViewProfile(); - return true; - } else if (itemId == R.id.menu_delete_selected) { - handleDeleteSelected(); - return true; - } - return false; - } + @Override + public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { + int itemId = menuItem.getItemId(); + if (itemId == R.id.menu_select_all) { + handleSelectAll(); + return true; + } else if (itemId == R.id.menu_view_profile) { + handleViewProfile(); + return true; + } else if (itemId == R.id.menu_delete_selected) { + handleDeleteSelected(); + return true; + } + return false; + } - @Override - public void onDestroyActionMode(ActionMode actionMode) { - ContactSelectionListFragment.this.actionMode = null; - getContactSelectionListAdapter().resetActionModeSelection(); - } - }; + @Override + public void onDestroyActionMode(ActionMode actionMode) { + ContactSelectionListFragment.this.actionMode = null; + getContactSelectionListAdapter().resetActionModeSelection(); + } + }; - DcHelper.getEventCenter(requireActivity()).addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this); + DcHelper.getEventCenter(requireActivity()) + .addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this); initializeCursor(); return view; @@ -208,22 +205,28 @@ private void handleViewProfile() { } private void handleDeleteSelected() { - AlertDialog dialog = new AlertDialog.Builder(getActivity()) - .setMessage(R.string.ask_delete_contacts) - .setPositiveButton(R.string.delete, (d, i) -> { - ContactSelectionListAdapter adapter = getContactSelectionListAdapter(); - final SparseIntArray actionModeSelection = adapter.getActionModeSelection().clone(); - new Thread(() -> { - for (int index = 0; index < actionModeSelection.size(); index++) { - int contactId = actionModeSelection.valueAt(index); - dcContext.deleteContact(contactId); - } - }).start(); - adapter.resetActionModeSelection(); - actionMode.finish(); - }) - .setNegativeButton(R.string.cancel, null) - .show(); + AlertDialog dialog = + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.ask_delete_contacts) + .setPositiveButton( + R.string.delete, + (d, i) -> { + ContactSelectionListAdapter adapter = getContactSelectionListAdapter(); + final SparseIntArray actionModeSelection = + adapter.getActionModeSelection().clone(); + new Thread( + () -> { + for (int index = 0; index < actionModeSelection.size(); index++) { + int contactId = actionModeSelection.valueAt(index); + dcContext.deleteContact(contactId); + } + }) + .start(); + adapter.resetActionModeSelection(); + actionMode.finish(); + }) + .setNegativeButton(R.string.cancel, null) + .show(); Util.redPositiveButton(dialog); } @@ -232,7 +235,8 @@ private ContactSelectionListAdapter getContactSelectionListAdapter() { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } @@ -263,15 +267,14 @@ private boolean isUnencrypted() { } private void initializeCursor() { - ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), - GlideApp.with(this), - new ListClickListener(), - isMulti(), - true); + ContactSelectionListAdapter adapter = + new ContactSelectionListAdapter( + getActivity(), GlideApp.with(this), new ListClickListener(), isMulti(), true); selectedContacts = adapter.getSelectedContacts(); deselectedContacts = new HashSet<>(); - ArrayList preselectedContacts = getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS); - if(preselectedContacts!=null) { + ArrayList preselectedContacts = + getActivity().getIntent().getIntegerArrayListExtra(PRESELECTED_CONTACTS); + if (preselectedContacts != null) { selectedContacts.addAll(preselectedContacts); } recyclerView.setAdapter(adapter); @@ -286,11 +289,20 @@ public void setQueryFilter(String filter) { public Loader onCreateLoader(int id, Bundle args) { final boolean allowCreation = getActivity().getIntent().getBooleanExtra(ALLOW_CREATION, true); final boolean addCreateContactLink = allowCreation && isUnencrypted(); - final boolean addCreateGroupLinks = allowCreation && !isRelayingMessageContent(getActivity()) && !isMulti(); + final boolean addCreateGroupLinks = + allowCreation && !isRelayingMessageContent(getActivity()) && !isMulti(); final boolean addScanQRLink = allowCreation && !isMulti(); - final int listflags = DcContext.DC_GCL_ADD_SELF | (isUnencrypted()? DcContext.DC_GCL_ADDRESS : 0); - return new DcContactsLoader(getActivity(), listflags, cursorFilter, addCreateGroupLinks, addCreateContactLink, addScanQRLink, false); + final int listflags = + DcContext.DC_GCL_ADD_SELF | (isUnencrypted() ? DcContext.DC_GCL_ADDRESS : 0); + return new DcContactsLoader( + getActivity(), + listflags, + cursorFilter, + addCreateGroupLinks, + addCreateContactLink, + addScanQRLink, + false); } @Override @@ -314,15 +326,14 @@ public void onLoaderReset(Loader loader) { private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener { @Override - public void onItemClick(ContactSelectionListItem contact, boolean handleActionMode) - { + public void onItemClick(ContactSelectionListItem contact, boolean handleActionMode) { if (handleActionMode) { if (actionMode != null) { updateActionModeState(actionMode); } return; } - int contactId = contact.getSpecialId(); + int contactId = contact.getSpecialId(); if (!isMulti() || !selectedContacts.contains(contactId)) { if (contactId == DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT) { Intent intent = new Intent(getContext(), NewContactActivity.class); @@ -356,27 +367,27 @@ public void onItemClick(ContactSelectionListItem contact, boolean handleActionMo @Override public void onItemLongClick(ContactSelectionListItem view) { if (actionMode == null) { - actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); + actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback); } else { updateActionModeState(actionMode); } } } - public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) { + public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) { this.onContactSelectedListener = onContactSelectedListener; } public interface OnContactSelectedListener { void onContactSelected(int contactId); + void onContactDeselected(int contactId); } @Override public void handleEvent(@NonNull DcEvent event) { - if (event.getId()==DcContext.DC_EVENT_CONTACTS_CHANGED) { + if (event.getId() == DcContext.DC_EVENT_CONTACTS_CHANGED) { getLoaderManager().restartLoader(0, null, ContactSelectionListFragment.this); } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java index 608ee4053..9f148bd40 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java @@ -531,15 +531,12 @@ public boolean onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.menu_ephemeral_messages).setVisible(false); } - if (isMultiUser()) { - if (dcChat.isInBroadcast() && !dcChat.isContactRequest()) { - menu.findItem(R.id.menu_leave).setTitle(R.string.menu_leave_channel).setVisible(true); - } else if (dcChat.isEncrypted() - && dcChat.canSend() - && !dcChat.isOutBroadcast() - && !dcChat.isMailingList()) { - menu.findItem(R.id.menu_leave).setVisible(true); + if (dcChat.shallLeaveBeforeDelete(DcHelper.getContext(context))) { + if (dcChat.isInBroadcast()) { + menu.findItem(R.id.menu_leave).setTitle(R.string.menu_leave_channel); } + menu.findItem(R.id.menu_leave).setVisible(true); + menu.findItem(R.id.menu_delete_chat).setVisible(false); } if (isArchived()) { @@ -732,9 +729,16 @@ private void handleLeaveGroup() { DcHelper.getContext(context).removeContactFromChat(chatId, DcContact.DC_CONTACT_ID_SELF); Toast.makeText(this, getString(R.string.done), Toast.LENGTH_SHORT).show(); }) - .setNegativeButton(R.string.cancel, null) + .setNegativeButton(R.string.menu_leave_and_delete, (d, which) -> { + DcHelper.getContext(context).removeContactFromChat(chatId, DcContact.DC_CONTACT_ID_SELF); + DcHelper.getContext(context).deleteChat(chatId); + DirectShareUtil.clearShortcut(this, chatId); + finish(); + }) + .setNeutralButton(R.string.cancel, null) .show(); Util.redPositiveButton(dialog); + Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE); } private void handleArchiveChat() { @@ -753,7 +757,7 @@ private void handleArchiveChat() { private void handleDeleteChat() { AlertDialog dialog = new AlertDialog.Builder(this) .setMessage(getResources().getString(R.string.ask_delete_named_chat, dcChat.getName())) - .setPositiveButton(R.string.delete, (d, which) -> { + .setPositiveButton(R.string.delete_for_me, (d, which) -> { DcHelper.getContext(context).deleteChat(chatId); DirectShareUtil.clearShortcut(this, chatId); finish(); diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java b/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java index 5a8c66a6c..c738cfbfe 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationAdapter.java @@ -23,16 +23,20 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - +import java.lang.ref.SoftReference; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.components.audioplay.AudioView; @@ -45,61 +49,50 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.lang.ref.SoftReference; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - /** - * A DC adapter for a conversation thread. Ultimately - * used by ConversationActivity to display a conversation - * thread in a ListActivity. + * A DC adapter for a conversation thread. Ultimately used by ConversationActivity to display a + * conversation thread in a ListActivity. * * @author Moxie Marlinspike - * */ // FIXME: this breaks type checks, that is why there are so many casts. -public class ConversationAdapter +public class ConversationAdapter extends RecyclerView.Adapter - implements StickyHeaderDecoration.StickyHeaderAdapter -{ + implements StickyHeaderDecoration.StickyHeaderAdapter { private static final int MAX_CACHE_SIZE = 40; - private final Map> recordCache = - Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); - - private static final int MESSAGE_TYPE_OUTGOING = 0; - private static final int MESSAGE_TYPE_INCOMING = 1; - private static final int MESSAGE_TYPE_INFO = 2; - private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3; - private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4; + private final Map> recordCache = + Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); + + private static final int MESSAGE_TYPE_OUTGOING = 0; + private static final int MESSAGE_TYPE_INCOMING = 1; + private static final int MESSAGE_TYPE_INFO = 2; + private static final int MESSAGE_TYPE_AUDIO_OUTGOING = 3; + private static final int MESSAGE_TYPE_AUDIO_INCOMING = 4; private static final int MESSAGE_TYPE_THUMBNAIL_OUTGOING = 5; private static final int MESSAGE_TYPE_THUMBNAIL_INCOMING = 6; - private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7; - private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8; - private static final int MESSAGE_TYPE_STICKER_INCOMING = 9; - private static final int MESSAGE_TYPE_STICKER_OUTGOING = 10; + private static final int MESSAGE_TYPE_DOCUMENT_OUTGOING = 7; + private static final int MESSAGE_TYPE_DOCUMENT_INCOMING = 8; + private static final int MESSAGE_TYPE_STICKER_INCOMING = 9; + private static final int MESSAGE_TYPE_STICKER_OUTGOING = 10; private final Set batchSelected = Collections.synchronizedSet(new HashSet()); private final @Nullable ItemClickListener clickListener; - private final @NonNull GlideRequests glideRequests; - private final @NonNull Recipient recipient; - private final @NonNull LayoutInflater inflater; - private final @NonNull Context context; - private final @NonNull Calendar calendar; - - private final DcContext dcContext; - private @NonNull DcChat dcChat; - private @NonNull int[] dcMsgList = new int[0]; - private int positionToPulseHighlight = -1; - private int positionCurrentlyPulseHighlighting = -1; - private long pulseHighlightingSince = -1; - private int lastSeenPosition = -1; - private long lastSeen = -1; + private final @NonNull GlideRequests glideRequests; + private final @NonNull Recipient recipient; + private final @NonNull LayoutInflater inflater; + private final @NonNull Context context; + private final @NonNull Calendar calendar; + + private final DcContext dcContext; + private @NonNull DcChat dcChat; + private @NonNull int[] dcMsgList = new int[0]; + private int positionToPulseHighlight = -1; + private int positionCurrentlyPulseHighlighting = -1; + private long pulseHighlightingSince = -1; + private int lastSeenPosition = -1; + private long lastSeen = -1; private AudioPlaybackViewModel playbackViewModel; private AudioView.OnActionListener audioPlayPauseListener; @@ -110,7 +103,7 @@ public ViewHolder(final @NonNull V i @SuppressWarnings("unchecked") public V getView() { - return (V)itemView; + return (V) itemView; } public BindableConversationItem getItem() { @@ -118,16 +111,14 @@ public BindableConversationItem getItem() { } } - public boolean isActive() { return dcMsgList.length > 0; } - public @NonNull DcChat getChat(){ + public @NonNull DcChat getChat() { return dcChat; } - public void setLastSeen(long timestamp) { lastSeen = timestamp; } @@ -151,14 +142,14 @@ public int getItemCount() { @Override public long getItemId(int position) { - if (position<0 || position>=dcMsgList.length) { + if (position < 0 || position >= dcMsgList.length) { return 0; } - return dcMsgList[dcMsgList.length-1-position]; + return dcMsgList[dcMsgList.length - 1 - position]; } public @NonNull DcMsg getMsg(int position) { - if(position<0 || position>=dcMsgList.length) { + if (position < 0 || position >= dcMsgList.length) { return new DcMsg(0); } @@ -170,7 +161,7 @@ public long getItemId(int position) { } } - final DcMsg fromDb = dcContext.getMsg((int)getItemId(position)); + final DcMsg fromDb = dcContext.getMsg((int) getItemId(position)); recordCache.put(position, new SoftReference<>(fromDb)); return fromDb; } @@ -183,12 +174,10 @@ public void setAudioPlayPauseListener(AudioView.OnActionListener audioPlayPauseL this.audioPlayPauseListener = audioPlayPauseListener; } - /** - * Returns the position of the message with msgId in the chat list, counted from the top - */ + /** Returns the position of the message with msgId in the chat list, counted from the top */ public int msgIdToPosition(int msgId) { - for(int i=0; i { - if (clickListener != null) { - clickListener.onItemClick(itemView.getMessageRecord()); - } - }); - itemView.setOnLongClickListener(view -> { - if (clickListener != null) { - clickListener.onItemLongClick(itemView.getMessageRecord(), view); - } - return true; - }); + itemView.setOnClickListener( + view -> { + if (clickListener != null) { + clickListener.onItemClick(itemView.getMessageRecord()); + } + }); + itemView.setOnLongClickListener( + view -> { + if (clickListener != null) { + clickListener.onItemLongClick(itemView.getMessageRecord(), view); + } + return true; + }); itemView.setEventListener(clickListener); return new ViewHolder(itemView); } @@ -284,14 +287,19 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType case MESSAGE_TYPE_THUMBNAIL_OUTGOING: case MESSAGE_TYPE_DOCUMENT_OUTGOING: case MESSAGE_TYPE_STICKER_OUTGOING: - case MESSAGE_TYPE_OUTGOING: return R.layout.conversation_item_sent; + case MESSAGE_TYPE_OUTGOING: + return R.layout.conversation_item_sent; case MESSAGE_TYPE_AUDIO_INCOMING: case MESSAGE_TYPE_THUMBNAIL_INCOMING: case MESSAGE_TYPE_DOCUMENT_INCOMING: case MESSAGE_TYPE_STICKER_INCOMING: - case MESSAGE_TYPE_INCOMING: return R.layout.conversation_item_received; - case MESSAGE_TYPE_INFO: return R.layout.conversation_item_update; - default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter"); + case MESSAGE_TYPE_INCOMING: + return R.layout.conversation_item_received; + case MESSAGE_TYPE_INFO: + return R.layout.conversation_item_update; + default: + throw new IllegalArgumentException( + "unsupported item view type given to ConversationAdapter"); } } @@ -301,21 +309,18 @@ public int getItemViewType(int i) { int type = dcMsg.getType(); if (dcMsg.isInfo()) { return MESSAGE_TYPE_INFO; - } - else if (type==DcMsg.DC_MSG_AUDIO || type==DcMsg.DC_MSG_VOICE) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_AUDIO_OUTGOING : MESSAGE_TYPE_AUDIO_INCOMING; - } - else if (type==DcMsg.DC_MSG_FILE) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_DOCUMENT_OUTGOING : MESSAGE_TYPE_DOCUMENT_INCOMING; - } - else if (type==DcMsg.DC_MSG_IMAGE || type==DcMsg.DC_MSG_GIF || type==DcMsg.DC_MSG_VIDEO) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_THUMBNAIL_OUTGOING : MESSAGE_TYPE_THUMBNAIL_INCOMING; - } - else if (type == DcMsg.DC_MSG_STICKER) { - return dcMsg.isOutgoing()? MESSAGE_TYPE_STICKER_OUTGOING : MESSAGE_TYPE_STICKER_INCOMING; - } - else { - return dcMsg.isOutgoing()? MESSAGE_TYPE_OUTGOING : MESSAGE_TYPE_INCOMING; + } else if (type == DcMsg.DC_MSG_AUDIO || type == DcMsg.DC_MSG_VOICE) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_AUDIO_OUTGOING : MESSAGE_TYPE_AUDIO_INCOMING; + } else if (type == DcMsg.DC_MSG_FILE) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_DOCUMENT_OUTGOING : MESSAGE_TYPE_DOCUMENT_INCOMING; + } else if (type == DcMsg.DC_MSG_IMAGE + || type == DcMsg.DC_MSG_GIF + || type == DcMsg.DC_MSG_VIDEO) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_THUMBNAIL_OUTGOING : MESSAGE_TYPE_THUMBNAIL_INCOMING; + } else if (type == DcMsg.DC_MSG_STICKER) { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_STICKER_OUTGOING : MESSAGE_TYPE_STICKER_INCOMING; + } else { + return dcMsg.isOutgoing() ? MESSAGE_TYPE_OUTGOING : MESSAGE_TYPE_INCOMING; } } @@ -338,16 +343,16 @@ public int[] getMessageIds() { } public void pulseHighlightItem(int position) { - if (position>=0 && position < getItemCount()) { + if (position >= 0 && position < getItemCount()) { positionToPulseHighlight = position; notifyItemChanged(position); } } public long getSortTimestamp(int position) { - if (!isActive()) return 0; + if (!isActive()) return 0; if (position >= getItemCount()) return 0; - if (position < 0) return 0; + if (position < 0) return 0; DcMsg msg = getMsg(position); return msg.getSortTimestamp(); @@ -361,7 +366,7 @@ public Context getContext() { @Override public long getHeaderId(int position) { if (position >= getItemCount()) return -1; - if (position < 0) return -1; + if (position < 0) return -1; calendar.setTime(new Date(getSortTimestamp(position))); return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR)); @@ -369,18 +374,17 @@ public long getHeaderId(int position) { @Override public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { - return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false)); + return new HeaderViewHolder( + LayoutInflater.from(getContext()) + .inflate(R.layout.conversation_item_header, parent, false)); } - /** - * date header view - */ + /** date header view */ @Override public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { viewHolder.setText(DateUtils.getRelativeDate(getContext(), getSortTimestamp(position))); } - public void changeData(@Nullable int[] dcMsgList) { // should be called when there are new messages this.dcMsgList = dcMsgList == null ? new int[0] : dcMsgList; @@ -400,12 +404,11 @@ private void reloadData() { } private int findLastSeenPosition(long lastSeen) { - if (lastSeen <= 0) return -1; - if (!isActive()) return -1; + if (lastSeen <= 0) return -1; + if (!isActive()) return -1; int count = getItemCount(); - for (int i = 0; i < count; i++) { DcMsg msg = getMsg(i); if (msg.isOutgoing() || msg.getTimestamp() <= lastSeen) { @@ -417,11 +420,16 @@ private int findLastSeenPosition(long lastSeen) { } public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) { - return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false)); + return new HeaderViewHolder( + LayoutInflater.from(getContext()) + .inflate(R.layout.conversation_item_last_seen, parent, false)); } public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) { - viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.chat_n_new_messages, (position + 1), (position + 1))); + viewHolder.setText( + getContext() + .getResources() + .getQuantityString(R.plurals.chat_n_new_messages, (position + 1), (position + 1))); } static class LastSeenHeader extends StickyHeaderDecoration { @@ -429,32 +437,45 @@ static class LastSeenHeader extends StickyHeaderDecoration { LastSeenHeader(ConversationAdapter adapter) { super(adapter, false, false); - this.adapter = adapter; + this.adapter = adapter; } @Override - protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { - return adapter.isActive() && position == adapter.getLastSeenPosition(); + protected boolean hasHeader( + RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { + return adapter.isActive() && position == adapter.getLastSeenPosition(); } @Override - protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) { + protected int getHeaderTop( + RecyclerView parent, View child, View header, int adapterPos, int layoutPos) { return parent.getLayoutManager().getDecoratedTop(child); } @Override - protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { + protected HeaderViewHolder getHeader( + RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent); adapter.onBindLastSeenViewHolder(viewHolder, position); - int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); - int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); - - int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width); - int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height); + int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); + int heightSpec = + View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); + + int childWidth = + ViewGroup.getChildMeasureSpec( + widthSpec, + parent.getPaddingLeft() + parent.getPaddingRight(), + viewHolder.itemView.getLayoutParams().width); + int childHeight = + ViewGroup.getChildMeasureSpec( + heightSpec, + parent.getPaddingTop() + parent.getPaddingBottom(), + viewHolder.itemView.getLayoutParams().height); viewHolder.itemView.measure(childWidth, childHeight); - viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight()); + viewHolder.itemView.layout( + 0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight()); return viewHolder; } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java index beecb4160..46e5de0b1 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java @@ -37,7 +37,6 @@ import android.view.animation.AnimationUtils; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -46,13 +45,21 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnScrollListener; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; - +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; import org.thoughtcrime.securesms.components.reminder.DozeReminder; @@ -71,1064 +78,1125 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - @SuppressLint("StaticFieldLeak") -public class ConversationFragment extends MessageSelectorFragment -{ - private static final String TAG = ConversationFragment.class.getSimpleName(); - - private static final int SCROLL_ANIMATION_THRESHOLD = 50; - - private final ActionModeCallback actionModeCallback = new ActionModeCallback(); - private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener(); - - private ConversationFragmentListener listener; - - private Recipient recipient; - private long chatId; - private int startingPosition; - private boolean firstLoad; - private RecyclerView list; - private RecyclerView.ItemDecoration lastSeenDecoration; - private StickyHeaderDecoration dateDecoration; - private View scrollToBottomButton; - private View floatingLocationButton; - private View bottomDivider; - private AddReactionView addReactionView; - private TextView noMessageTextView; - private Timer reloadTimer; - - public boolean isPaused; - private Debouncer markseenDebouncer; - private Rpc rpc; - private boolean pendingAddBottomInsets; - private boolean pendingRemoveBottomInsets; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - rpc = DcHelper.getRpc(getContext()); - - DcEventCenter eventCenter = DcHelper.getEventCenter(getContext()); - eventCenter.addObserver(DcContext.DC_EVENT_INCOMING_MSG, this); - eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this); - eventCenter.addObserver(DcContext.DC_EVENT_REACTIONS_CHANGED, this); - eventCenter.addObserver(DcContext.DC_EVENT_MSG_DELIVERED, this); - eventCenter.addObserver(DcContext.DC_EVENT_MSG_FAILED, this); - eventCenter.addObserver(DcContext.DC_EVENT_MSG_READ, this); - eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this); - - markseenDebouncer = new Debouncer(800); - reloadTimer = new Timer("reloadTimer", false); - reloadTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - Util.runOnMain(ConversationFragment.this::reloadList); - } - }, 60 * 1000, 60 * 1000); +public class ConversationFragment extends MessageSelectorFragment { + private static final String TAG = ConversationFragment.class.getSimpleName(); + + private static final int SCROLL_ANIMATION_THRESHOLD = 50; + + private final ActionModeCallback actionModeCallback = new ActionModeCallback(); + private final ItemClickListener selectionClickListener = + new ConversationFragmentItemClickListener(); + + private ConversationFragmentListener listener; + + private Recipient recipient; + private long chatId; + private int startingPosition; + private boolean firstLoad; + private RecyclerView list; + private RecyclerView.ItemDecoration lastSeenDecoration; + private StickyHeaderDecoration dateDecoration; + private View scrollToBottomButton; + private View floatingLocationButton; + private View bottomDivider; + private AddReactionView addReactionView; + private TextView noMessageTextView; + private Timer reloadTimer; + + public boolean isPaused; + private Debouncer markseenDebouncer; + private Rpc rpc; + private boolean pendingAddBottomInsets; + private boolean pendingRemoveBottomInsets; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + rpc = DcHelper.getRpc(getContext()); + + DcEventCenter eventCenter = DcHelper.getEventCenter(getContext()); + eventCenter.addObserver(DcContext.DC_EVENT_INCOMING_MSG, this); + eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this); + eventCenter.addObserver(DcContext.DC_EVENT_REACTIONS_CHANGED, this); + eventCenter.addObserver(DcContext.DC_EVENT_MSG_DELIVERED, this); + eventCenter.addObserver(DcContext.DC_EVENT_MSG_FAILED, this); + eventCenter.addObserver(DcContext.DC_EVENT_MSG_READ, this); + eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this); + + markseenDebouncer = new Debouncer(800); + reloadTimer = new Timer("reloadTimer", false); + reloadTimer.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + Util.runOnMain(ConversationFragment.this::reloadList); + } + }, + 60 * 1000, + 60 * 1000); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) { + final View view = inflater.inflate(R.layout.conversation_fragment, container, false); + list = ViewUtil.findById(view, android.R.id.list); + scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button); + floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button); + addReactionView = ViewUtil.findById(view, R.id.add_reaction_view); + noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view); + bottomDivider = ViewUtil.findById(view, R.id.bottom_divider); + + scrollToBottomButton.setOnClickListener(v -> scrollToBottom()); + + final SetStartingPositionLinearLayoutManager layoutManager = + new SetStartingPositionLinearLayoutManager( + getActivity(), LinearLayoutManager.VERTICAL, true); + + list.setHasFixedSize(false); + list.setLayoutManager(layoutManager); + list.setItemAnimator(null); + + new ConversationItemSwipeCallback(msg -> actionMode == null, this::handleReplyMessage) + .attachToRecyclerView(list); + + // setLayerType() is needed to allow larger items (long texts in our case) + // with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be + // rendered into a texture" + list.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + if (pendingAddBottomInsets) { + bottomDivider.setVisibility(View.GONE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, true); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true); + pendingAddBottomInsets = false; } - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) { - final View view = inflater.inflate(R.layout.conversation_fragment, container, false); - list = ViewUtil.findById(view, android.R.id.list); - scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button); - floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button); - addReactionView = ViewUtil.findById(view, R.id.add_reaction_view); - noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view); - bottomDivider = ViewUtil.findById(view, R.id.bottom_divider); - - scrollToBottomButton.setOnClickListener(v -> scrollToBottom()); - - final SetStartingPositionLinearLayoutManager layoutManager = new SetStartingPositionLinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, true); - - list.setHasFixedSize(false); - list.setLayoutManager(layoutManager); - list.setItemAnimator(null); - - new ConversationItemSwipeCallback( - msg -> actionMode == null, - this::handleReplyMessage - ).attachToRecyclerView(list); - - // setLayerType() is needed to allow larger items (long texts in our case) - // with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture" - list.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - - if (pendingAddBottomInsets) { - bottomDivider.setVisibility(View.GONE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, true); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true); - pendingAddBottomInsets = false; - } - - if (pendingRemoveBottomInsets) { - bottomDivider.setVisibility(View.VISIBLE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, false); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false); - pendingRemoveBottomInsets = false; - } - - return view; + if (pendingRemoveBottomInsets) { + bottomDivider.setVisibility(View.VISIBLE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, false); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false); + pendingRemoveBottomInsets = false; } - @Override - public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - initializeResources(); - initializeListAdapter(); - } + return view; + } - private void setNoMessageText() { - DcChat dcChat = getListAdapter().getChat(); - if(dcChat.isMultiUser()){ - if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) { - noMessageTextView.setText(R.string.chat_new_channel_hint); - } else if (dcChat.isUnpromoted()) { - noMessageTextView.setText(R.string.chat_new_group_hint); - } - else { - noMessageTextView.setText(R.string.chat_no_messages); - } - } - else if(dcChat.isSelfTalk()) { - noMessageTextView.setText(R.string.saved_messages_explain); - } - else if(dcChat.isDeviceTalk()) { - noMessageTextView.setText(R.string.device_talk_explain); - } - else if(!dcChat.isEncrypted()) { - noMessageTextView.setText(R.string.chat_unencrypted_explanation); - } - else { - String message = getString(R.string.chat_new_one_to_one_hint, dcChat.getName()); - noMessageTextView.setText(message); - } - } + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); - public void handleAddBottomInsets() { - if (bottomDivider != null) { - bottomDivider.setVisibility(View.GONE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, true); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true); - pendingAddBottomInsets = false; - } else { - pendingAddBottomInsets = true; - } - } + initializeResources(); + initializeListAdapter(); + } - public void handleRemoveBottomInsets() { - if (bottomDivider != null) { - bottomDivider.setVisibility(View.VISIBLE); - ViewUtil.forceApplyWindowInsets(list, false, true, false, false); - ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false); - pendingRemoveBottomInsets = false; + private void setNoMessageText() { + DcChat dcChat = getListAdapter().getChat(); + if (dcChat.isMultiUser()) { + if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) { + noMessageTextView.setText(R.string.chat_new_channel_hint); + } else if (dcChat.isUnpromoted()) { + noMessageTextView.setText(R.string.chat_new_group_hint); } else { - pendingRemoveBottomInsets = true; + noMessageTextView.setText(R.string.chat_no_messages); } + } else if (dcChat.isSelfTalk()) { + noMessageTextView.setText(R.string.saved_messages_explain); + } else if (dcChat.isDeviceTalk()) { + noMessageTextView.setText(R.string.device_talk_explain); + } else if (!dcChat.isEncrypted()) { + noMessageTextView.setText(R.string.chat_unencrypted_explanation); + } else { + String message = getString(R.string.chat_new_one_to_one_hint, dcChat.getName()); + noMessageTextView.setText(message); } - - @Override - public void onDestroy() { - DcHelper.getEventCenter(getContext()).removeObservers(this); - reloadTimer.cancel(); - super.onDestroy(); + } + + public void handleAddBottomInsets() { + if (bottomDivider != null) { + bottomDivider.setVisibility(View.GONE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, true); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true); + pendingAddBottomInsets = false; + } else { + pendingAddBottomInsets = true; } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - this.listener = (ConversationFragmentListener)activity; + } + + public void handleRemoveBottomInsets() { + if (bottomDivider != null) { + bottomDivider.setVisibility(View.VISIBLE); + ViewUtil.forceApplyWindowInsets(list, false, true, false, false); + ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false); + pendingRemoveBottomInsets = false; + } else { + pendingRemoveBottomInsets = true; } - - @Override - public void onResume() { - super.onResume(); - Util.runOnBackground(() -> { + } + + @Override + public void onDestroy() { + DcHelper.getEventCenter(getContext()).removeObservers(this); + reloadTimer.cancel(); + super.onDestroy(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + this.listener = (ConversationFragmentListener) activity; + } + + @Override + public void onResume() { + super.onResume(); + Util.runOnBackground( + () -> { try { rpc.marknoticedChat(rpc.getSelectedAccountId(), (int) chatId); } catch (RpcException e) { - Log.e(TAG, "RPC error", e); + Log.e(TAG, "RPC error", e); } }); - if (list.getAdapter() != null) { - list.getAdapter().notifyDataSetChanged(); - } + if (list.getAdapter() != null) { + list.getAdapter().notifyDataSetChanged(); + } - if (isPaused) { - isPaused = false; - markseenDebouncer.publish(() -> manageMessageSeenState()); - } + if (isPaused) { + isPaused = false; + markseenDebouncer.publish(() -> manageMessageSeenState()); + } + } + + @Override + public void onPause() { + super.onPause(); + setLastSeen(System.currentTimeMillis()); + isPaused = true; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + dateDecoration.onConfigurationChanged(newConfig); + } + + public void onNewIntent() { + if (actionMode != null) { + actionMode.finish(); } + initializeResources(); + initializeListAdapter(); - @Override - public void onPause() { - super.onPause(); - setLastSeen(System.currentTimeMillis()); - isPaused = true; + if (chatId == -1) { + reloadList(); + updateLocationButton(); } + } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - dateDecoration.onConfigurationChanged(newConfig); + public void moveToLastSeen() { + if (list == null || getListAdapter() == null) { + Log.w(TAG, "Tried to move to last seen position, but we hadn't initialized the view yet."); + return; } - public void onNewIntent() { - if (actionMode != null) { - actionMode.finish(); - } + if (getListAdapter().getLastSeenPosition() < 0) { + return; + } + final int lastSeenPosition = getListAdapter().getLastSeenPosition() + 1; + if (lastSeenPosition > 0) { + list.post( + () -> + ((LinearLayoutManager) list.getLayoutManager()) + .scrollToPositionWithOffset(lastSeenPosition, list.getHeight())); + } + } + + public void hideAddReactionView() { + addReactionView.hide(); + } + + private void initializeResources() { + this.chatId = + this.getActivity().getIntent().getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1); + this.recipient = Recipient.from(getActivity(), Address.fromChat((int) this.chatId)); + this.startingPosition = + this.getActivity() + .getIntent() + .getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1); + this.firstLoad = true; + + OnScrollListener scrollListener = new ConversationScrollListener(getActivity()); + list.addOnScrollListener(scrollListener); + } + + private void initializeListAdapter() { + if (this.recipient != null && this.chatId != -1) { + ConversationAdapter adapter = + new ConversationAdapter( + getActivity(), + this.recipient.getChat(), + GlideApp.with(this), + selectionClickListener, + this.recipient); + list.setAdapter(adapter); + AudioPlaybackViewModel playbackViewModel = + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); + adapter.setPlaybackViewModel(playbackViewModel); + adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity())); - initializeResources(); - initializeListAdapter(); + if (dateDecoration != null) { + list.removeItemDecoration(dateDecoration); + } + dateDecoration = new StickyHeaderDecoration(adapter, false, false); + list.addItemDecoration(dateDecoration); + + int freshMsgs = 0; + try { + freshMsgs = rpc.getFreshMsgCnt(rpc.getSelectedAccountId(), (int) chatId); + } catch (RpcException e) { + Log.e(TAG, "RPC error", e); + } + SetStartingPositionLinearLayoutManager layoutManager = + (SetStartingPositionLinearLayoutManager) list.getLayoutManager(); + if (startingPosition > -1) { + layoutManager.setStartingPosition(startingPosition); + } else if (freshMsgs > 0) { + layoutManager.setStartingPosition(freshMsgs - 1); + } - if (chatId == -1) { - reloadList(); - updateLocationButton(); - } + reloadList(); + updateLocationButton(); + + if (lastSeenDecoration != null) { + list.removeItemDecoration(lastSeenDecoration); + } + if (freshMsgs > 0) { + getListAdapter().setLastSeenPosition(freshMsgs - 1); + lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter()); + list.addItemDecoration(lastSeenDecoration); + } } + } - public void moveToLastSeen() { - if (list == null || getListAdapter() == null) { - Log.w(TAG, "Tried to move to last seen position, but we hadn't initialized the view yet."); - return; - } + @Override + protected void setCorrectMenuVisibility(Menu menu) { + Set messageRecords = getListAdapter().getSelectedItems(); - if (getListAdapter().getLastSeenPosition() < 0) { - return; - } - final int lastSeenPosition = getListAdapter().getLastSeenPosition() + 1; - if (lastSeenPosition > 0) { - list.post(() -> ((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(lastSeenPosition, list.getHeight())); - } + if (actionMode != null && messageRecords.size() == 0) { + actionMode.finish(); + return; } - public void hideAddReactionView() { - addReactionView.hide(); - } - - private void initializeResources() { - this.chatId = this.getActivity().getIntent().getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1); - this.recipient = Recipient.from(getActivity(), Address.fromChat((int)this.chatId)); - this.startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1); - this.firstLoad = true; - - OnScrollListener scrollListener = new ConversationScrollListener(getActivity()); - list.addOnScrollListener(scrollListener); - } - - private void initializeListAdapter() { - if (this.recipient != null && this.chatId != -1) { - ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient); - list.setAdapter(adapter); - AudioPlaybackViewModel playbackViewModel = - new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); - adapter.setPlaybackViewModel(playbackViewModel); - adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity())); - - if (dateDecoration != null) { - list.removeItemDecoration(dateDecoration); - } - dateDecoration = new StickyHeaderDecoration(adapter, false, false); - list.addItemDecoration(dateDecoration); - - int freshMsgs = 0; - try { - freshMsgs = rpc.getFreshMsgCnt(rpc.getSelectedAccountId(), (int) chatId); - } catch (RpcException e) { - Log.e(TAG, "RPC error", e); - } - SetStartingPositionLinearLayoutManager layoutManager = (SetStartingPositionLinearLayoutManager) list.getLayoutManager(); - if (startingPosition > -1) { - layoutManager.setStartingPosition(startingPosition); - } else if (freshMsgs > 0) { - layoutManager.setStartingPosition(freshMsgs - 1); - } - - reloadList(); - updateLocationButton(); - - if (lastSeenDecoration != null) { - list.removeItemDecoration(lastSeenDecoration); - } - if (freshMsgs > 0) { - getListAdapter().setLastSeenPosition(freshMsgs - 1); - lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter()); - list.addItemDecoration(lastSeenDecoration); - } - } + menu.findItem(R.id.menu_toggle_save).setVisible(false); + + if (messageRecords.size() > 1) { + menu.findItem(R.id.menu_context_details).setVisible(false); + menu.findItem(R.id.menu_context_share).setVisible(false); + menu.findItem(R.id.menu_context_reply).setVisible(false); + menu.findItem(R.id.menu_context_edit).setVisible(false); + menu.findItem(R.id.menu_context_reply_privately).setVisible(false); + menu.findItem(R.id.menu_add_to_home_screen).setVisible(false); + // menu.findItem(R.id.menu_toggle_save).setVisible(false); + } else { + DcMsg messageRecord = messageRecords.iterator().next(); + DcChat chat = getListAdapter().getChat(); + menu.findItem(R.id.menu_context_details).setVisible(true); + menu.findItem(R.id.menu_context_share).setVisible(messageRecord.hasFile()); + boolean canReply = canReplyToMsg(messageRecord); + menu.findItem(R.id.menu_context_reply).setVisible(chat.canSend() && canReply); + menu.findItem(R.id.menu_context_edit) + .setVisible(chat.isEncrypted() && chat.canSend() && canEditMsg(messageRecord)); + boolean showReplyPrivately = chat.isMultiUser() && !messageRecord.isOutgoing() && canReply; + menu.findItem(R.id.menu_context_reply_privately).setVisible(showReplyPrivately); + menu.findItem(R.id.menu_add_to_home_screen) + .setVisible(messageRecord.getType() == DcMsg.DC_MSG_WEBXDC); + + /* + boolean saved = messageRecord.getSavedMsgId() != 0; + MenuItem toggleSave = menu.findItem(R.id.menu_toggle_save); + toggleSave.setVisible(messageRecord.canSave() && !chat.isSelfTalk()); + toggleSave.setIcon(saved? R.drawable.baseline_bookmark_remove_24 : R.drawable.baseline_bookmark_border_24); + toggleSave.setTitle(saved? R.string.unsave : R.string.save); + */ } - @Override - protected void setCorrectMenuVisibility(Menu menu) { - Set messageRecords = getListAdapter().getSelectedItems(); + // if one of the selected items cannot be saved, disable saving. + boolean canSave = true; + // if one of the selected items is not from self, disable resending. + boolean canResend = true; + for (DcMsg messageRecord : messageRecords) { + if (canSave && !messageRecord.hasFile()) { + canSave = false; + } + if (canResend && !messageRecord.isOutgoing()) { + canResend = false; + } + if (!canSave && !canResend) { + break; + } + } + menu.findItem(R.id.menu_context_save_attachment).setVisible(canSave); + menu.findItem(R.id.menu_resend).setVisible(canResend); + } + + static boolean canReplyToMsg(DcMsg dcMsg) { + if (dcMsg.isInfo()) { + switch (dcMsg.getInfoType()) { + case DcMsg.DC_INFO_GROUP_NAME_CHANGED: + case DcMsg.DC_INFO_GROUP_IMAGE_CHANGED: + case DcMsg.DC_INFO_MEMBER_ADDED_TO_GROUP: + case DcMsg.DC_INFO_MEMBER_REMOVED_FROM_GROUP: + case DcMsg.DC_INFO_LOCATIONSTREAMING_ENABLED: + case DcMsg.DC_INFO_EPHEMERAL_TIMER_CHANGED: + case DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE: + return true; + default: + return false; + } + } + return true; + } + + static boolean canEditMsg(DcMsg dcMsg) { + return dcMsg.isOutgoing() + && !dcMsg.isInfo() + && dcMsg.getType() != DcMsg.DC_MSG_CALL + && !dcMsg.hasHtml() + && !dcMsg.getText().isEmpty(); + } + + public void handleClearChat() { + AudioPlaybackViewModel playbackViewModel = + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); + + handleDeleteMessages( + (int) chatId, + getListAdapter().getMessageIds(), + playbackViewModel::stopByIds, + playbackViewModel::stopByIds); + } + + private ConversationAdapter getListAdapter() { + return (ConversationAdapter) list.getAdapter(); + } + + public void reload(Recipient recipient, long chatId) { + this.recipient = recipient; + + if (this.chatId != chatId) { + this.chatId = chatId; + initializeListAdapter(); + } + } + + public void scrollToTop() { + ConversationAdapter adapter = (ConversationAdapter) list.getAdapter(); + if (adapter.getItemCount() > 0) { + final int pos = adapter.getItemCount() - 1; + list.post( + () -> { + list.getLayoutManager().scrollToPosition(pos); + }); + } + } + + public void scrollToBottom() { + if (((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition() + < SCROLL_ANIMATION_THRESHOLD + && !AccessibilityUtil.areAnimationsDisabled(getContext())) { + list.smoothScrollToPosition(0); + } else { + list.scrollToPosition(0); + } + } + + void setLastSeen(long lastSeen) { + ConversationAdapter adapter = getListAdapter(); + if (adapter != null) { + adapter.setLastSeen(lastSeen); + if (lastSeenDecoration != null) { + list.removeItemDecoration(lastSeenDecoration); + } + if (lastSeen > 0) { + lastSeenDecoration = new ConversationAdapter.LastSeenHeader(adapter); + list.addItemDecoration(lastSeenDecoration); + } + } + } - if (actionMode != null && messageRecords.size() == 0) { - actionMode.finish(); - return; - } + private void handleCopyMessage(final Set dcMsgsSet) { + List dcMsgsList = new LinkedList<>(dcMsgsSet); + Collections.sort( + dcMsgsList, (lhs, rhs) -> Long.compare(lhs.getDateReceived(), rhs.getDateReceived())); + boolean singleMsg = dcMsgsList.size() == 1; - menu.findItem(R.id.menu_toggle_save).setVisible(false); + StringBuilder result = new StringBuilder(); - if (messageRecords.size() > 1) { - menu.findItem(R.id.menu_context_details).setVisible(false); - menu.findItem(R.id.menu_context_share).setVisible(false); - menu.findItem(R.id.menu_context_reply).setVisible(false); - menu.findItem(R.id.menu_context_edit).setVisible(false); - menu.findItem(R.id.menu_context_reply_privately).setVisible(false); - menu.findItem(R.id.menu_add_to_home_screen).setVisible(false); - //menu.findItem(R.id.menu_toggle_save).setVisible(false); - } else { - DcMsg messageRecord = messageRecords.iterator().next(); - DcChat chat = getListAdapter().getChat(); - menu.findItem(R.id.menu_context_details).setVisible(true); - menu.findItem(R.id.menu_context_share).setVisible(messageRecord.hasFile()); - boolean canReply = canReplyToMsg(messageRecord); - menu.findItem(R.id.menu_context_reply).setVisible(chat.canSend() && canReply); - menu.findItem(R.id.menu_context_edit).setVisible(chat.isEncrypted() && chat.canSend() && canEditMsg(messageRecord)); - boolean showReplyPrivately = chat.isMultiUser() && !messageRecord.isOutgoing() && canReply; - menu.findItem(R.id.menu_context_reply_privately).setVisible(showReplyPrivately); - menu.findItem(R.id.menu_add_to_home_screen).setVisible(messageRecord.getType() == DcMsg.DC_MSG_WEBXDC); - - /* - boolean saved = messageRecord.getSavedMsgId() != 0; - MenuItem toggleSave = menu.findItem(R.id.menu_toggle_save); - toggleSave.setVisible(messageRecord.canSave() && !chat.isSelfTalk()); - toggleSave.setIcon(saved? R.drawable.baseline_bookmark_remove_24 : R.drawable.baseline_bookmark_border_24); - toggleSave.setTitle(saved? R.string.unsave : R.string.save); - */ - } + DcContext dcContext = DcHelper.getContext(getContext()); + DcMsg prevMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); + for (DcMsg msg : dcMsgsList) { + if (result.length() > 0) { + result.append("\n\n"); + } - // if one of the selected items cannot be saved, disable saving. - boolean canSave = true; - // if one of the selected items is not from self, disable resending. - boolean canResend = true; - for (DcMsg messageRecord : messageRecords) { - if (canSave && !messageRecord.hasFile()) { - canSave = false; - } - if (canResend && !messageRecord.isOutgoing()) { - canResend = false; - } - if (!canSave && !canResend) { - break; - } - } - menu.findItem(R.id.menu_context_save_attachment).setVisible(canSave); - menu.findItem(R.id.menu_resend).setVisible(canResend); - } - - static boolean canReplyToMsg(DcMsg dcMsg) { - if (dcMsg.isInfo()) { - switch (dcMsg.getInfoType()) { - case DcMsg.DC_INFO_GROUP_NAME_CHANGED: - case DcMsg.DC_INFO_GROUP_IMAGE_CHANGED: - case DcMsg.DC_INFO_MEMBER_ADDED_TO_GROUP: - case DcMsg.DC_INFO_MEMBER_REMOVED_FROM_GROUP: - case DcMsg.DC_INFO_LOCATIONSTREAMING_ENABLED: - case DcMsg.DC_INFO_EPHEMERAL_TIMER_CHANGED: - case DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE: - return true; - default: - return false; - } - } - return true; - } + if (msg.getFromId() != prevMsg.getFromId() && !singleMsg) { + DcContact contact = dcContext.getContact(msg.getFromId()); + result.append(msg.getSenderName(contact)).append(":\n"); + } + if (msg.getType() == DcMsg.DC_MSG_TEXT || (singleMsg && !msg.getText().isEmpty())) { + result.append(msg.getText()); + } else { + result.append(msg.getSummarytext(10000000)); + } - static boolean canEditMsg(DcMsg dcMsg) { - return dcMsg.isOutgoing() && !dcMsg.isInfo() && dcMsg.getType() != DcMsg.DC_MSG_CALL && !dcMsg.hasHtml() && !dcMsg.getText().isEmpty(); + prevMsg = msg; } - public void handleClearChat() { - AudioPlaybackViewModel playbackViewModel = - new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); - - handleDeleteMessages( - (int) chatId, - getListAdapter().getMessageIds(), - playbackViewModel::stopByIds, - playbackViewModel::stopByIds); + if (result.length() > 0) { + Util.writeTextToClipboard(getActivity(), result.toString()); + Toast.makeText( + getActivity(), + getActivity().getResources().getString(R.string.copied_to_clipboard), + Toast.LENGTH_LONG) + .show(); + } + } + + private void handleForwardMessage(final Set messageRecords) { + Intent composeIntent = new Intent(); + int[] msgIds = DcMsg.msgSetToIds(messageRecords); + try { + setForwardingMessageIds(composeIntent, msgIds, rpc.getSelectedAccountId()); + ConversationListRelayingActivity.start(this, composeIntent); + getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); + } catch (RpcException e) { + Log.e(TAG, "RPC error", e); } + } - private ConversationAdapter getListAdapter() { - return (ConversationAdapter) list.getAdapter(); + private void handleSaveSticker(final DcMsg message) { + if (message.getType() != DcMsg.DC_MSG_STICKER) { + return; } - public void reload(Recipient recipient, long chatId) { - this.recipient = recipient; + File stickerFile = message.getFileAsFile(); + if (stickerFile == null || !stickerFile.exists()) { + Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); + return; + } - if (this.chatId != chatId) { - this.chatId = chatId; - initializeListAdapter(); - } + // Create stickers directory in internal storage + File stickersDir = new File(getContext().getFilesDir(), "stickers"); + if (!stickersDir.exists()) { + stickersDir.mkdirs(); } - public void scrollToTop() { - ConversationAdapter adapter = (ConversationAdapter)list.getAdapter(); - if (adapter.getItemCount()>0) { - final int pos = adapter.getItemCount()-1; - list.post(() -> { - list.getLayoutManager().scrollToPosition(pos); - }); - } + // Copy sticker to stickers directory + String extension = stickerFile.getName(); + int dotPos = extension.lastIndexOf('.'); + if (dotPos >= 0) { + extension = extension.substring(dotPos + 1); } + String fileName = System.currentTimeMillis() + "." + extension; + File destFile = new File(stickersDir, fileName); + + try (java.io.FileInputStream in = new java.io.FileInputStream(stickerFile); + java.io.FileOutputStream out = new java.io.FileOutputStream(destFile)) { + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + Toast.makeText(getContext(), R.string.saved, Toast.LENGTH_SHORT).show(); - public void scrollToBottom() { - if (((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition() < SCROLL_ANIMATION_THRESHOLD - && !AccessibilityUtil.areAnimationsDisabled(getContext())) { - list.smoothScrollToPosition(0); - } else { - list.scrollToPosition(0); - } + // Refresh sticker picker to show the newly added sticker + if (getActivity() instanceof ConversationActivity) { + ((ConversationActivity) getActivity()).refreshStickerPicker(); + } + } catch (Exception e) { + Log.e(TAG, "Error saving sticker", e); + Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); } + } - void setLastSeen(long lastSeen) { - ConversationAdapter adapter = getListAdapter(); - if (adapter != null) { - adapter.setLastSeen(lastSeen); - if (lastSeenDecoration != null) { - list.removeItemDecoration(lastSeenDecoration); - } - if (lastSeen > 0) { - lastSeenDecoration = new ConversationAdapter.LastSeenHeader(adapter); - list.addItemDecoration(lastSeenDecoration); - } - } + @SuppressLint("RestrictedApi") + private void handleReplyMessage(final DcMsg message) { + if (getActivity() != null) { + //noinspection ConstantConditions + ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); } - private void handleCopyMessage(final Set dcMsgsSet) { - List dcMsgsList = new LinkedList<>(dcMsgsSet); - Collections.sort(dcMsgsList, (lhs, rhs) -> Long.compare(lhs.getDateReceived(), rhs.getDateReceived())); - boolean singleMsg = dcMsgsList.size() == 1; + listener.handleReplyMessage(message); + } - StringBuilder result = new StringBuilder(); + @SuppressLint("RestrictedApi") + private void handleEditMessage(final DcMsg message) { + if (getActivity() != null) { + //noinspection ConstantConditions + ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); + } - DcContext dcContext = DcHelper.getContext(getContext()); - DcMsg prevMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); - for (DcMsg msg : dcMsgsList) { - if (result.length() > 0) { - result.append("\n\n"); - } + listener.handleEditMessage(message); + } - if (msg.getFromId() != prevMsg.getFromId() && !singleMsg) { - DcContact contact = dcContext.getContact(msg.getFromId()); - result.append(msg.getSenderName(contact)).append(":\n"); - } - if (msg.getType() == DcMsg.DC_MSG_TEXT || (singleMsg && !msg.getText().isEmpty())) { - result.append(msg.getText()); - } else { - result.append(msg.getSummarytext(10000000)); - } + private void handleReplyMessagePrivately(final DcMsg msg) { - prevMsg = msg; - } + if (getActivity() != null) { + DcContext dcContext = DcHelper.getContext(getActivity()); + int privateChatId = dcContext.createChatByContactId(msg.getFromId()); + DcMsg replyMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); + replyMsg.setQuote(msg); + dcContext.setDraft(privateChatId, replyMsg); - if (result.length() > 0) { - Util.writeTextToClipboard(getActivity(), result.toString()); - Toast.makeText(getActivity(), getActivity().getResources().getString(R.string.copied_to_clipboard), Toast.LENGTH_LONG).show(); - } + Intent intent = new Intent(getActivity(), ConversationActivity.class); + intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, privateChatId); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + getActivity().startActivity(intent); + } else { + Log.e(TAG, "Activity was null"); } - - private void handleForwardMessage(final Set messageRecords) { - Intent composeIntent = new Intent(); - int[] msgIds = DcMsg.msgSetToIds(messageRecords); - try { - setForwardingMessageIds(composeIntent, msgIds, rpc.getSelectedAccountId()); - ConversationListRelayingActivity.start(this, composeIntent); - getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); - } catch (RpcException e) { - Log.e(TAG, "RPC error", e); - } + } + + private void handleToggleSave(final Set messageRecords) { + DcMsg msg = getSelectedMessageRecord(messageRecords); + try { + if (msg.getSavedMsgId() != 0) { + rpc.deleteMessages( + rpc.getSelectedAccountId(), Collections.singletonList(msg.getSavedMsgId())); + } else { + rpc.saveMsgs(rpc.getSelectedAccountId(), Collections.singletonList(msg.getId())); + } + } catch (RpcException e) { + Log.e(TAG, "RPC error", e); } + } - private void handleSaveSticker(final DcMsg message) { - if (message.getType() != DcMsg.DC_MSG_STICKER) { - return; - } - - File stickerFile = message.getFileAsFile(); - if (stickerFile == null || !stickerFile.exists()) { - Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); - return; - } + private void reloadList() { + reloadList(false); + } - // Create stickers directory in internal storage - File stickersDir = new File(getContext().getFilesDir(), "stickers"); - if (!stickersDir.exists()) { - stickersDir.mkdirs(); - } + private void reloadList(boolean chatModified) { + ConversationAdapter adapter = getListAdapter(); + if (adapter == null) { + return; + } - // Copy sticker to stickers directory - String extension = stickerFile.getName(); - int dotPos = extension.lastIndexOf('.'); - if (dotPos >= 0) { - extension = extension.substring(dotPos + 1); - } - String fileName = System.currentTimeMillis() + "." + extension; - File destFile = new File(stickersDir, fileName); - - try (java.io.FileInputStream in = new java.io.FileInputStream(stickerFile); - java.io.FileOutputStream out = new java.io.FileOutputStream(destFile)) { - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - Toast.makeText(getContext(), R.string.saved, Toast.LENGTH_SHORT).show(); - - // Refresh sticker picker to show the newly added sticker - if (getActivity() instanceof ConversationActivity) { - ((ConversationActivity) getActivity()).refreshStickerPicker(); - } - } catch (Exception e) { - Log.e(TAG, "Error saving sticker", e); - Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show(); - } + // if chat is a contact request and is accepted/blocked, the DcChat object must be reloaded, + // otherwise DcChat.canSend() returns wrong values + if (chatModified) { + adapter.reloadChat(); } - @SuppressLint("RestrictedApi") - private void handleReplyMessage(final DcMsg message) { - if (getActivity() != null) { - //noinspection ConstantConditions - ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); - } + int oldCount = 0; + int oldIndex = 0; + int pixelOffset = 0; + if (!firstLoad) { + oldCount = adapter.getItemCount(); + oldIndex = + ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition(); + View firstView = list.getLayoutManager().findViewByPosition(oldIndex); + pixelOffset = + (firstView == null) + ? 0 + : list.getBottom() - firstView.getBottom() - list.getPaddingBottom(); + } - listener.handleReplyMessage(message); + if (getContext() == null) { + Log.e(TAG, "reloadList: getContext() was null"); + return; } - @SuppressLint("RestrictedApi") - private void handleEditMessage(final DcMsg message) { - if (getActivity() != null) { - //noinspection ConstantConditions - ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); - } + DcContext dcContext = DcHelper.getContext(getContext()); - listener.handleEditMessage(message); - } + long startMs = System.currentTimeMillis(); + int[] msgs = dcContext.getChatMsgs((int) chatId, 0, 0); + Log.i(TAG, "⏰ getChatMsgs(" + chatId + "): " + (System.currentTimeMillis() - startMs) + "ms"); - private void handleReplyMessagePrivately(final DcMsg msg) { + adapter.changeData(msgs); - if (getActivity() != null) { - DcContext dcContext = DcHelper.getContext(getActivity()); - int privateChatId = dcContext.createChatByContactId(msg.getFromId()); - DcMsg replyMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); - replyMsg.setQuote(msg); - dcContext.setDraft(privateChatId, replyMsg); - - Intent intent = new Intent(getActivity(), ConversationActivity.class); - intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, privateChatId); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - getActivity().startActivity(intent); - } else { - Log.e(TAG, "Activity was null"); - } + if (firstLoad) { + if (startingPosition >= 0) { + getListAdapter().pulseHighlightItem(startingPosition); + } + firstLoad = false; + } else if (oldIndex > 0) { + int newIndex = oldIndex + msgs.length - oldCount; + + if (newIndex < 0) { + newIndex = 0; + pixelOffset = 0; + } else if (newIndex >= msgs.length) { + newIndex = msgs.length - 1; + pixelOffset = 0; + } + + ((LinearLayoutManager) list.getLayoutManager()) + .scrollToPositionWithOffset(newIndex, pixelOffset); + } + + if (!adapter.isActive()) { + setNoMessageText(); + noMessageTextView.setVisibility(View.VISIBLE); + } else { + noMessageTextView.setVisibility(View.GONE); } - private void handleToggleSave(final Set messageRecords) { - DcMsg msg = getSelectedMessageRecord(messageRecords); - try { - if (msg.getSavedMsgId() != 0) { - rpc.deleteMessages(rpc.getSelectedAccountId(), Collections.singletonList(msg.getSavedMsgId())); + if (!isPaused) { + markseenDebouncer.publish(() -> manageMessageSeenState()); + } + } + + private void updateLocationButton() { + floatingLocationButton.setVisibility( + DcHelper.getContext(getContext()).isSendingLocationsToChat((int) chatId) + ? View.VISIBLE + : View.GONE); + } + + private void scrollAndHighlight(final int pos, boolean smooth) { + list.post( + () -> { + if (smooth && !AccessibilityUtil.areAnimationsDisabled(getContext())) { + list.smoothScrollToPosition(pos); } else { - rpc.saveMsgs(rpc.getSelectedAccountId(), Collections.singletonList(msg.getId())); + list.scrollToPosition(pos); } - } catch (RpcException e) { - Log.e(TAG, "RPC error", e); - } + getListAdapter().pulseHighlightItem(pos); + }); + } + + public void scrollToMsgId(final int msgId) { + ConversationAdapter adapter = (ConversationAdapter) list.getAdapter(); + int position = adapter.msgIdToPosition(msgId); + if (position != -1) { + scrollAndHighlight(position, false); + } else { + Log.e(TAG, "msgId {} not found for scrolling"); + } + } + + private void scrollMaybeSmoothToMsgId(final int msgId) { + LinearLayoutManager layout = ((LinearLayoutManager) list.getLayoutManager()); + boolean smooth = false; + ConversationAdapter adapter = (ConversationAdapter) list.getAdapter(); + if (adapter == null) return; + int position = adapter.msgIdToPosition(msgId); + if (layout != null) { + int distance1 = Math.abs(position - layout.findFirstVisibleItemPosition()); + int distance2 = Math.abs(position - layout.findLastVisibleItemPosition()); + int distance = Math.min(distance1, distance2); + smooth = distance < 15; + Log.i(TAG, "Scrolling to destMsg, smoth: " + smooth + ", distance: " + distance); } - private void reloadList() { - reloadList(false); + if (position != -1) { + scrollAndHighlight(position, smooth); + } else { + Log.e(TAG, "msgId not found for scrolling: " + msgId); } + } - private void reloadList(boolean chatModified) { - ConversationAdapter adapter = getListAdapter(); - if (adapter == null) { - return; - } + public interface ConversationFragmentListener { + void handleReplyMessage(DcMsg messageRecord); - // if chat is a contact request and is accepted/blocked, the DcChat object must be reloaded, otherwise DcChat.canSend() returns wrong values - if (chatModified) { - adapter.reloadChat(); - } + void handleEditMessage(DcMsg messageRecord); + } - int oldCount = 0; - int oldIndex = 0; - int pixelOffset = 0; - if (!firstLoad) { - oldCount = adapter.getItemCount(); - oldIndex = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition(); - View firstView = list.getLayoutManager().findViewByPosition(oldIndex); - pixelOffset = (firstView == null) ? 0 : list.getBottom() - firstView.getBottom() - list.getPaddingBottom(); - } + private class ConversationScrollListener extends OnScrollListener { - if (getContext() == null) { - Log.e(TAG, "reloadList: getContext() was null"); - return; - } + private final Animation scrollButtonInAnimation; + private final Animation scrollButtonOutAnimation; - DcContext dcContext = DcHelper.getContext(getContext()); + private boolean wasAtBottom = true; + private boolean wasAtZoomScrollHeight = false; - long startMs = System.currentTimeMillis(); - int[] msgs = dcContext.getChatMsgs((int) chatId, 0, 0); - Log.i(TAG, "⏰ getChatMsgs(" + chatId + "): " + (System.currentTimeMillis() - startMs) + "ms"); + // private long lastPositionId = -1; - adapter.changeData(msgs); + ConversationScrollListener(@NonNull Context context) { + this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in); + this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out); - if (firstLoad) { - if (startingPosition >= 0) { - getListAdapter().pulseHighlightItem(startingPosition); - } - firstLoad = false; - } else if(oldIndex > 0) { - int newIndex = oldIndex + msgs.length - oldCount; + this.scrollButtonInAnimation.setDuration(100); + this.scrollButtonOutAnimation.setDuration(50); + } - if (newIndex < 0) { newIndex = 0; pixelOffset = 0; } - else if (newIndex >= msgs.length) { newIndex = msgs.length - 1; pixelOffset = 0; } + @Override + public void onScrolled(final RecyclerView rv, final int dx, final int dy) { + boolean currentlyAtBottom = isAtBottom(); + boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight(); + // int positionId = getHeaderPositionId(); + + if (currentlyAtZoomScrollHeight && !wasAtZoomScrollHeight) { + ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation); + } else if (currentlyAtBottom && !wasAtBottom) { + ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE); + } - ((LinearLayoutManager) list.getLayoutManager()).scrollToPositionWithOffset(newIndex, pixelOffset); - } + // if (positionId != lastPositionId) { + // bindScrollHeader(conversationDateHeader, positionId); + // } - if(!adapter.isActive()){ - setNoMessageText(); - noMessageTextView.setVisibility(View.VISIBLE); - } - else{ - noMessageTextView.setVisibility(View.GONE); - } + wasAtBottom = currentlyAtBottom; + wasAtZoomScrollHeight = currentlyAtZoomScrollHeight; + // lastPositionId = positionId; - if (!isPaused) { - markseenDebouncer.publish(() -> manageMessageSeenState()); - } - } + markseenDebouncer.publish(() -> manageMessageSeenState()); - private void updateLocationButton() { - floatingLocationButton.setVisibility(DcHelper.getContext(getContext()).isSendingLocationsToChat((int) chatId)? View.VISIBLE : View.GONE); + ConversationFragment.this.addReactionView.move(dy); } - private void scrollAndHighlight(final int pos, boolean smooth) { - list.post(() -> { - if (smooth && !AccessibilityUtil.areAnimationsDisabled(getContext())) { - list.smoothScrollToPosition(pos); - } else { - list.scrollToPosition(pos); - } - getListAdapter().pulseHighlightItem(pos); - }); + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + // if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + // conversationDateHeader.show(); + // } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { + // conversationDateHeader.hide(); + // } } - public void scrollToMsgId(final int msgId) { - ConversationAdapter adapter = (ConversationAdapter)list.getAdapter(); - int position = adapter.msgIdToPosition(msgId); - if (position!=-1) { - scrollAndHighlight(position, false); - } else { - Log.e(TAG, "msgId {} not found for scrolling"); - } - } - - private void scrollMaybeSmoothToMsgId(final int msgId) { - LinearLayoutManager layout = ((LinearLayoutManager) list.getLayoutManager()); - boolean smooth = false; - ConversationAdapter adapter = (ConversationAdapter) list.getAdapter(); - if (adapter == null) return; - int position = adapter.msgIdToPosition(msgId); - if (layout != null) { - int distance1 = Math.abs(position - layout.findFirstVisibleItemPosition()); - int distance2 = Math.abs(position - layout.findLastVisibleItemPosition()); - int distance = Math.min(distance1, distance2); - smooth = distance < 15; - Log.i(TAG, "Scrolling to destMsg, smoth: " + smooth + ", distance: " + distance); - } + private boolean isAtBottom() { + if (list.getChildCount() == 0) return true; - if (position != -1) { - scrollAndHighlight(position, smooth); - } else { - Log.e(TAG, "msgId not found for scrolling: " + msgId); - } - } + View bottomView = list.getChildAt(0); + int firstVisibleItem = + ((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition(); + boolean isAtBottom = (firstVisibleItem == 0); - public interface ConversationFragmentListener { - void handleReplyMessage(DcMsg messageRecord); - void handleEditMessage(DcMsg messageRecord); + return isAtBottom && bottomView.getBottom() <= list.getHeight(); } - private class ConversationScrollListener extends OnScrollListener { - - private final Animation scrollButtonInAnimation; - private final Animation scrollButtonOutAnimation; - - private boolean wasAtBottom = true; - private boolean wasAtZoomScrollHeight = false; - //private long lastPositionId = -1; - - ConversationScrollListener(@NonNull Context context) { - this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in); - this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out); - - this.scrollButtonInAnimation.setDuration(100); - this.scrollButtonOutAnimation.setDuration(50); - } - - @Override - public void onScrolled(final RecyclerView rv, final int dx, final int dy) { - boolean currentlyAtBottom = isAtBottom(); - boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight(); -// int positionId = getHeaderPositionId(); - - if (currentlyAtZoomScrollHeight && !wasAtZoomScrollHeight) { - ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation); - } else if (currentlyAtBottom && !wasAtBottom) { - ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE); - } - -// if (positionId != lastPositionId) { -// bindScrollHeader(conversationDateHeader, positionId); -// } - - wasAtBottom = currentlyAtBottom; - wasAtZoomScrollHeight = currentlyAtZoomScrollHeight; -// lastPositionId = positionId; - - markseenDebouncer.publish(() -> manageMessageSeenState()); - - ConversationFragment.this.addReactionView.move(dy); - } - - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { -// if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { -// conversationDateHeader.show(); -// } else if (newState == RecyclerView.SCROLL_STATE_IDLE) { -// conversationDateHeader.hide(); -// } - } - - private boolean isAtBottom() { - if (list.getChildCount() == 0) return true; + private boolean isAtZoomScrollHeight() { + return ((LinearLayoutManager) list.getLayoutManager()) + .findFirstCompletelyVisibleItemPosition() + > 4; + } - View bottomView = list.getChildAt(0); - int firstVisibleItem = ((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition(); - boolean isAtBottom = (firstVisibleItem == 0); + // private int getHeaderPositionId() { + // return + // ((LinearLayoutManager)list.getLayoutManager()).findLastVisibleItemPosition(); + // } - return isAtBottom && bottomView.getBottom() <= list.getHeight(); - } + // private void bindScrollHeader(HeaderViewHolder headerViewHolder, int positionId) { + // if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) { + // ((ConversationAdapter) + // list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId); + // } + // } + } - private boolean isAtZoomScrollHeight() { - return ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition() > 4; - } + private void manageMessageSeenState() { - // private int getHeaderPositionId() { - // return ((LinearLayoutManager)list.getLayoutManager()).findLastVisibleItemPosition(); - // } + LinearLayoutManager layoutManager = (LinearLayoutManager) list.getLayoutManager(); - // private void bindScrollHeader(HeaderViewHolder headerViewHolder, int positionId) { - // if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) { - // ((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId); - // } - // } + int firstPos = layoutManager.findFirstVisibleItemPosition(); + int lastPos = layoutManager.findLastVisibleItemPosition(); + if (firstPos == RecyclerView.NO_POSITION || lastPos == RecyclerView.NO_POSITION) { + return; } - private void manageMessageSeenState() { - - LinearLayoutManager layoutManager = (LinearLayoutManager)list.getLayoutManager(); - - int firstPos = layoutManager.findFirstVisibleItemPosition(); - int lastPos = layoutManager.findLastVisibleItemPosition(); - if(firstPos == RecyclerView.NO_POSITION || lastPos == RecyclerView.NO_POSITION) { - return; - } - - ArrayList ids = new ArrayList<>(lastPos - firstPos + 1); - for(int pos = firstPos; pos <= lastPos; pos++) { - DcMsg message = ((ConversationAdapter) list.getAdapter()).getMsg(pos); - if (message.getFromId() != DC_CONTACT_ID_SELF) { - ids.add(message.getId()); - } - } - Util.runOnAnyBackgroundThread(() -> { + ArrayList ids = new ArrayList<>(lastPos - firstPos + 1); + for (int pos = firstPos; pos <= lastPos; pos++) { + DcMsg message = ((ConversationAdapter) list.getAdapter()).getMsg(pos); + if (message.getFromId() != DC_CONTACT_ID_SELF) { + ids.add(message.getId()); + } + } + Util.runOnAnyBackgroundThread( + () -> { try { rpc.markseenMsgs(rpc.getSelectedAccountId(), ids); } catch (RpcException e) { Log.e(TAG, "RPC error", e); } }); - } + } - private class ConversationFragmentItemClickListener implements ItemClickListener { - - @Override - public void onItemClick(DcMsg messageRecord) { - if (actionMode != null) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); - list.getAdapter().notifyDataSetChanged(); - - if (getListAdapter().getSelectedItems().size() == 0) { - actionMode.finish(); - } else { - hideAddReactionView(); - Menu menu = actionMode.getMenu(); - setCorrectMenuVisibility(menu); - ConversationAdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth()); - actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size())); - actionMode.setTitleOptionalHint(false); // the title represents important information, also indicating implicitly, more items can be selected - } - } - else if(DozeReminder.isDozeReminderMsg(getContext(), messageRecord)) { - DozeReminder.dozeReminderTapped(getContext()); - } - else if(StatsSending.isStatsSendingDeviceMsg(getContext(), messageRecord)) { - StatsSending.statsDeviceMsgTapped(getActivity()); - } - else if(messageRecord.getInfoType() == DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE) { - if (messageRecord.getParent() != null) { - // if the parent webxdc message still exists - WebxdcActivity.openWebxdcActivity(getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref()); - } - } - else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_DESCRIPTION_CHANGED) { - Intent intent = new Intent(getContext(), ProfileActivity.class); - intent.putExtra(ProfileActivity.CHAT_ID_EXTRA, (int) chatId); - startActivity(intent); - } - else if (!TextUtils.isEmpty(messageRecord.getPOILocation()) && messageRecord.getType() == DcMsg.DC_MSG_TEXT && !messageRecord.hasHtml()) { - WebxdcActivity.openMaps(getContext(), getListAdapter().getChat().getId(), "index.html#"+messageRecord.getPOILocation()); - } - else { - int infoContactId = messageRecord.getInfoContactId(); - if (infoContactId != 0 && infoContactId != DC_CONTACT_ID_SELF) { - Intent intent = new Intent(getContext(), ProfileActivity.class); - intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, infoContactId); - startActivity(intent); - } - else { - String self_mail = DcHelper.getContext(getContext()).getConfig("configured_mail_user"); - if (self_mail != null && !self_mail.isEmpty() - && messageRecord.getText().contains(self_mail) - && getListAdapter().getChat().isDeviceTalk()) { - // This is a device message informing the user that the password is wrong - Intent intent = new Intent(getActivity(), EditRelayActivity.class); - intent.putExtra(EditRelayActivity.EXTRA_ADDR, self_mail); - startActivity(intent); - } - } - } - } + private class ConversationFragmentItemClickListener implements ItemClickListener { - @Override - public void onItemLongClick(DcMsg messageRecord, View view) { - if (actionMode == null) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); - list.getAdapter().notifyDataSetChanged(); - - actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); - addReactionView.show(messageRecord, view, () -> { - if (actionMode != null) { - actionMode.finish(); - } - }); - } - } + @Override + public void onItemClick(DcMsg messageRecord) { + if (actionMode != null) { + ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); + list.getAdapter().notifyDataSetChanged(); - private void jumpToOriginal(DcMsg original) { - if (original == null) { - Log.i(TAG, "Clicked on a quote or jump-to-original whose original message was deleted/non-existing."); - Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT).show(); - return; - } - - int foreignChatId = original.getChatId(); - if (foreignChatId != 0 && foreignChatId != chatId) { - Intent intent = new Intent(getActivity(), ConversationActivity.class); - intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId); - int start = DcMsg.getMessagePosition(original, DcHelper.getContext(getContext())); - intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, start); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - ((ConversationActivity) getActivity()).hideSoftKeyboard(); - if (getActivity() != null) { - getActivity().startActivity(intent); - } else { - Log.e(TAG, "Activity was null"); - } - } else { - scrollMaybeSmoothToMsgId(original.getId()); - } + if (getListAdapter().getSelectedItems().size() == 0) { + actionMode.finish(); + } else { + hideAddReactionView(); + Menu menu = actionMode.getMenu(); + setCorrectMenuVisibility(menu); + ConversationAdaptiveActionsToolbar.adjustMenuActions( + menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth()); + actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size())); + actionMode.setTitleOptionalHint( + false); // the title represents important information, also indicating implicitly, + // more items can be selected } - - @Override - public void onJumpToOriginalClicked(DcMsg messageRecord) { - jumpToOriginal(DcHelper.getContext(getContext()).getMsg(messageRecord.getOriginalMsgId())); + } else if (DozeReminder.isDozeReminderMsg(getContext(), messageRecord)) { + DozeReminder.dozeReminderTapped(getContext()); + } else if (StatsSending.isStatsSendingDeviceMsg(getContext(), messageRecord)) { + StatsSending.statsDeviceMsgTapped(getActivity()); + } else if (messageRecord.getInfoType() == DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE) { + if (messageRecord.getParent() != null) { + // if the parent webxdc message still exists + WebxdcActivity.openWebxdcActivity( + getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref()); } - - @Override - public void onQuoteClicked(DcMsg messageRecord) { - jumpToOriginal(messageRecord.getQuotedMsg()); + } else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_DESCRIPTION_CHANGED) { + Intent intent = new Intent(getContext(), ProfileActivity.class); + intent.putExtra(ProfileActivity.CHAT_ID_EXTRA, (int) chatId); + startActivity(intent); + } else if (!TextUtils.isEmpty(messageRecord.getPOILocation()) + && messageRecord.getType() == DcMsg.DC_MSG_TEXT + && !messageRecord.hasHtml()) { + WebxdcActivity.openMaps( + getContext(), + getListAdapter().getChat().getId(), + "index.html#" + messageRecord.getPOILocation()); + } else { + int infoContactId = messageRecord.getInfoContactId(); + if (infoContactId != 0 && infoContactId != DC_CONTACT_ID_SELF) { + Intent intent = new Intent(getContext(), ProfileActivity.class); + intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, infoContactId); + startActivity(intent); + } else { + String self_mail = DcHelper.getContext(getContext()).getConfig("configured_mail_user"); + if (self_mail != null + && !self_mail.isEmpty() + && messageRecord.getText().contains(self_mail) + && getListAdapter().getChat().isDeviceTalk()) { + // This is a device message informing the user that the password is wrong + Intent intent = new Intent(getActivity(), EditRelayActivity.class); + intent.putExtra(EditRelayActivity.EXTRA_ADDR, self_mail); + startActivity(intent); + } } + } + } - @Override - public void onShowFullClicked(DcMsg messageRecord) { - Intent intent = new Intent(getActivity(), FullMsgActivity.class); - intent.putExtra(FullMsgActivity.MSG_ID_EXTRA, messageRecord.getId()); - intent.putExtra(FullMsgActivity.BLOCK_LOADING_REMOTE, getListAdapter().getChat().isContactRequest()); - startActivity(intent); - getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); + @Override + public void onItemLongClick(DcMsg messageRecord, View view) { + if (actionMode == null) { + ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); + list.getAdapter().notifyDataSetChanged(); + + actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback); + addReactionView.show( + messageRecord, + view, + () -> { + if (actionMode != null) { + actionMode.finish(); + } + }); } + } - @Override - public void onDownloadClicked(DcMsg messageRecord) { - DcHelper.getContext(getContext()).downloadFullMsg(messageRecord.getId()); + private void jumpToOriginal(DcMsg original) { + if (original == null) { + Log.i( + TAG, + "Clicked on a quote or jump-to-original whose original message was deleted/non-existing."); + Toast.makeText( + getContext(), + R.string.ConversationFragment_quoted_message_not_found, + Toast.LENGTH_SHORT) + .show(); + return; } - @Override - public void onReactionClicked(DcMsg messageRecord) { - ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId()); - dialog.show(getActivity().getSupportFragmentManager(), null); + int foreignChatId = original.getChatId(); + if (foreignChatId != 0 && foreignChatId != chatId) { + Intent intent = new Intent(getActivity(), ConversationActivity.class); + intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId); + int start = DcMsg.getMessagePosition(original, DcHelper.getContext(getContext())); + intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, start); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + ((ConversationActivity) getActivity()).hideSoftKeyboard(); + if (getActivity() != null) { + getActivity().startActivity(intent); + } else { + Log.e(TAG, "Activity was null"); + } + } else { + scrollMaybeSmoothToMsgId(original.getId()); } + } + + @Override + public void onJumpToOriginalClicked(DcMsg messageRecord) { + jumpToOriginal(DcHelper.getContext(getContext()).getMsg(messageRecord.getOriginalMsgId())); + } + + @Override + public void onQuoteClicked(DcMsg messageRecord) { + jumpToOriginal(messageRecord.getQuotedMsg()); + } + + @Override + public void onShowFullClicked(DcMsg messageRecord) { + Intent intent = new Intent(getActivity(), FullMsgActivity.class); + intent.putExtra(FullMsgActivity.MSG_ID_EXTRA, messageRecord.getId()); + intent.putExtra( + FullMsgActivity.BLOCK_LOADING_REMOTE, getListAdapter().getChat().isContactRequest()); + startActivity(intent); + getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out); + } - @Override - public void onStickerClicked(DcMsg messageRecord) { - new AlertDialog.Builder(getContext()) + @Override + public void onDownloadClicked(DcMsg messageRecord) { + DcHelper.getContext(getContext()).downloadFullMsg(messageRecord.getId()); + } + + @Override + public void onReactionClicked(DcMsg messageRecord) { + ReactionsDetailsFragment dialog = ReactionsDetailsFragment.newInstance(messageRecord.getId()); + dialog.show(getActivity().getSupportFragmentManager(), null); + } + + @Override + public void onStickerClicked(DcMsg messageRecord) { + new AlertDialog.Builder(getContext()) .setTitle(R.string.add_to_sticker_collection) .setMessage(R.string.ask_add_sticker_to_collection) - .setPositiveButton(R.string.ok, (dialog, which) -> { - handleSaveSticker(messageRecord); - }) + .setPositiveButton( + R.string.ok, + (dialog, which) -> { + handleSaveSticker(messageRecord); + }) .setNegativeButton(R.string.cancel, null) .show(); - } } + } - private class ActionModeCallback implements ActionMode.Callback { + private class ActionModeCallback implements ActionMode.Callback { - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.conversation_context, menu); - - mode.setTitle("1"); + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.conversation_context, menu); - Util.redMenuItem(menu, R.id.menu_context_delete_message); - setCorrectMenuVisibility(menu); - ConversationAdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth()); - return true; - } + mode.setTitle("1"); - @Override - public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { - return false; - } + Util.redMenuItem(menu, R.id.menu_context_delete_message); + setCorrectMenuVisibility(menu); + ConversationAdaptiveActionsToolbar.adjustMenuActions( + menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth()); + return true; + } - @Override - public void onDestroyActionMode(ActionMode mode) { - ((ConversationAdapter)list.getAdapter()).clearSelection(); - list.getAdapter().notifyDataSetChanged(); + @Override + public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { + return false; + } - actionMode = null; - hideAddReactionView(); - } + @Override + public void onDestroyActionMode(ActionMode mode) { + ((ConversationAdapter) list.getAdapter()).clearSelection(); + list.getAdapter().notifyDataSetChanged(); - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - hideAddReactionView(); - int itemId = item.getItemId(); - AudioPlaybackViewModel playbackViewModel = - new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); - - if (itemId == R.id.menu_context_copy) { - handleCopyMessage(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_delete_message) { - handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems(), playbackViewModel::stopByIds, playbackViewModel::stopByIds); - return true; - } else if (itemId == R.id.menu_context_share) { - DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND); - return true; - } else if (itemId == R.id.menu_context_details) { - handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_forward) { - handleForwardMessage(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_add_to_home_screen) { - WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId()); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_save_attachment) { - handleSaveAttachment(getListAdapter().getSelectedItems()); - return true; - } else if (itemId == R.id.menu_context_reply) { - handleReplyMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_edit) { - handleEditMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - actionMode.finish(); - return true; - } else if (itemId == R.id.menu_context_reply_privately) { - handleReplyMessagePrivately(getSelectedMessageRecord(getListAdapter().getSelectedItems())); - return true; - } else if (itemId == R.id.menu_resend) { - handleResendMessage(getListAdapter().getSelectedItems()); - return true; - } else if (itemId == R.id.menu_toggle_save) { - handleToggleSave(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - } - return false; - } + actionMode = null; + hideAddReactionView(); } @Override - public void handleEvent(@NonNull DcEvent event) { - switch (event.getId()) { - case DcContext.DC_EVENT_MSGS_CHANGED: - if (event.getData1Int() == 0 // deleted messages or batch insert - || event.getData1Int() == chatId) { - reloadList(); - } - break; - - case DcContext.DC_EVENT_REACTIONS_CHANGED: - case DcContext.DC_EVENT_INCOMING_MSG: - case DcContext.DC_EVENT_MSG_DELIVERED: - case DcContext.DC_EVENT_MSG_FAILED: - case DcContext.DC_EVENT_MSG_READ: - if (event.getData1Int() == chatId) { - reloadList(); - } - break; - - case DcContext.DC_EVENT_CHAT_MODIFIED: - if (event.getData1Int() == chatId) { - updateLocationButton(); - reloadList(true); - } - break; + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + hideAddReactionView(); + int itemId = item.getItemId(); + AudioPlaybackViewModel playbackViewModel = + new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class); + + if (itemId == R.id.menu_context_copy) { + handleCopyMessage(getListAdapter().getSelectedItems()); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_delete_message) { + handleDeleteMessages( + (int) chatId, + getListAdapter().getSelectedItems(), + playbackViewModel::stopByIds, + playbackViewModel::stopByIds); + return true; + } else if (itemId == R.id.menu_context_share) { + DcHelper.openForViewOrShare( + getContext(), + getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), + Intent.ACTION_SEND); + return true; + } else if (itemId == R.id.menu_context_details) { + handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_forward) { + handleForwardMessage(getListAdapter().getSelectedItems()); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_add_to_home_screen) { + WebxdcActivity.addToHomeScreen( + getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId()); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_save_attachment) { + handleSaveAttachment(getListAdapter().getSelectedItems()); + return true; + } else if (itemId == R.id.menu_context_reply) { + handleReplyMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_edit) { + handleEditMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + actionMode.finish(); + return true; + } else if (itemId == R.id.menu_context_reply_privately) { + handleReplyMessagePrivately(getSelectedMessageRecord(getListAdapter().getSelectedItems())); + return true; + } else if (itemId == R.id.menu_resend) { + handleResendMessage(getListAdapter().getSelectedItems()); + return true; + } else if (itemId == R.id.menu_toggle_save) { + handleToggleSave(getListAdapter().getSelectedItems()); + actionMode.finish(); + return true; + } + return false; + } + } + + @Override + public void handleEvent(@NonNull DcEvent event) { + switch (event.getId()) { + case DcContext.DC_EVENT_MSGS_CHANGED: + if (event.getData1Int() == 0 // deleted messages or batch insert + || event.getData1Int() == chatId) { + reloadList(); + } + break; + + case DcContext.DC_EVENT_REACTIONS_CHANGED: + case DcContext.DC_EVENT_INCOMING_MSG: + case DcContext.DC_EVENT_MSG_DELIVERED: + case DcContext.DC_EVENT_MSG_FAILED: + case DcContext.DC_EVENT_MSG_READ: + if (event.getData1Int() == chatId) { + reloadList(); } + break; - // removing the "new message" marker on incoming messages may be a bit unexpected, - // esp. when a series of message is coming in and after the first, the screen is turned on, - // the "new message" marker will flash for a short moment and disappear. - /*if (eventId == DcContext.DC_EVENT_INCOMING_MSG && isResumed()) { - setLastSeen(-1); - }*/ + case DcContext.DC_EVENT_CHAT_MODIFIED: + if (event.getData1Int() == chatId) { + updateLocationButton(); + reloadList(true); + } + break; } + + // removing the "new message" marker on incoming messages may be a bit unexpected, + // esp. when a series of message is coming in and after the first, the screen is turned on, + // the "new message" marker will flash for a short moment and disappear. + /*if (eventId == DcContext.DC_EVENT_INCOMING_MSG && isResumed()) { + setLastSeen(-1); + }*/ + } } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationItemSwipeCallback.java b/src/main/java/org/thoughtcrime/securesms/ConversationItemSwipeCallback.java index fd2f7cb84..f53fd297b 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationItemSwipeCallback.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationItemSwipeCallback.java @@ -6,40 +6,37 @@ import android.os.Vibrator; import android.view.MotionEvent; import android.view.View; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.util.AccessibilityUtil; import org.thoughtcrime.securesms.util.ServiceUtil; class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback { - private static final float SWIPE_SUCCESS_DX = ConversationSwipeAnimationHelper.TRIGGER_DX; - private static final long SWIPE_SUCCESS_VIBE_TIME_MS = 10; + private static final float SWIPE_SUCCESS_DX = ConversationSwipeAnimationHelper.TRIGGER_DX; + private static final long SWIPE_SUCCESS_VIBE_TIME_MS = 10; private boolean swipeBack; private boolean shouldTriggerSwipeFeedback; private boolean canTriggerSwipe; - private float latestDownX; - private float latestDownY; + private float latestDownX; + private float latestDownY; - private final SwipeAvailabilityProvider swipeAvailabilityProvider; + private final SwipeAvailabilityProvider swipeAvailabilityProvider; private final ConversationItemTouchListener itemTouchListener; - private final OnSwipeListener onSwipeListener; + private final OnSwipeListener onSwipeListener; - ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider, - @NonNull OnSwipeListener onSwipeListener) - { + ConversationItemSwipeCallback( + @NonNull SwipeAvailabilityProvider swipeAvailabilityProvider, + @NonNull OnSwipeListener onSwipeListener) { super(0, ItemTouchHelper.END); - this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate); - this.swipeAvailabilityProvider = swipeAvailabilityProvider; - this.onSwipeListener = onSwipeListener; + this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate); + this.swipeAvailabilityProvider = swipeAvailabilityProvider; + this.onSwipeListener = onSwipeListener; this.shouldTriggerSwipeFeedback = true; - this.canTriggerSwipe = true; + this.canTriggerSwipe = true; } void attachToRecyclerView(@NonNull RecyclerView recyclerView) { @@ -48,21 +45,19 @@ void attachToRecyclerView(@NonNull RecyclerView recyclerView) { } @Override - public boolean onMove(@NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, - @NonNull RecyclerView.ViewHolder target) - { + public boolean onMove( + @NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { return false; } @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - } + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {} @Override - public int getSwipeDirs(@NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder) - { + public int getSwipeDirs( + @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { if (cannotSwipeViewHolder(viewHolder)) return 0; return super.getSwipeDirs(recyclerView, viewHolder); } @@ -78,18 +73,21 @@ public int convertToAbsoluteDirection(int flags, int layoutDirection) { @Override public void onChildDraw( - @NonNull Canvas c, - @NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, - float dx, float dy, int actionState, boolean isCurrentlyActive) - { + @NonNull Canvas c, + @NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + float dx, + float dy, + int actionState, + boolean isCurrentlyActive) { if (cannotSwipeViewHolder(viewHolder)) return; - float sign = getSignFromDirection(viewHolder.itemView); + float sign = getSignFromDirection(viewHolder.itemView); boolean isCorrectSwipeDir = sameSign(dx, sign); if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && isCorrectSwipeDir) { - ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, Math.abs(dx), sign); + ConversationSwipeAnimationHelper.update( + (ConversationItem) viewHolder.itemView, Math.abs(dx), sign); handleSwipeFeedback((ConversationItem) viewHolder.itemView, Math.abs(dx)); if (canTriggerSwipe) { setTouchListener(recyclerView, viewHolder, Math.abs(dx)); @@ -100,7 +98,7 @@ public void onChildDraw( if (dx == 0) { shouldTriggerSwipeFeedback = true; - canTriggerSwipe = true; + canTriggerSwipe = true; } } @@ -122,50 +120,50 @@ private void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder) { } @SuppressLint("ClickableViewAccessibility") - private void setTouchListener(@NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, - float dx) - { - recyclerView.setOnTouchListener(new View.OnTouchListener() { - - // This variable is necessary to make sure that the handleTouchActionUp() and therefore onSwiped() is called only once. - // Otherwise, any subsequent little swipe would invoke onSwiped(). - // We can't call recyclerView.setOnTouchListener(null) because another ConversationItem might have set its own - // on touch listener in the meantime and we don't want to cancel it - private boolean listenerCalled = false; - - @Override - public boolean onTouch(View v, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - shouldTriggerSwipeFeedback = true; - break; - case MotionEvent.ACTION_UP: - if (!listenerCalled) { - listenerCalled = true; - ConversationItemSwipeCallback.this.handleTouchActionUp(recyclerView, viewHolder, dx); + private void setTouchListener( + @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dx) { + recyclerView.setOnTouchListener( + new View.OnTouchListener() { + + // This variable is necessary to make sure that the handleTouchActionUp() and therefore + // onSwiped() is called only once. + // Otherwise, any subsequent little swipe would invoke onSwiped(). + // We can't call recyclerView.setOnTouchListener(null) because another ConversationItem + // might have set its own + // on touch listener in the meantime and we don't want to cancel it + private boolean listenerCalled = false; + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + shouldTriggerSwipeFeedback = true; + break; + case MotionEvent.ACTION_UP: + if (!listenerCalled) { + listenerCalled = true; + ConversationItemSwipeCallback.this.handleTouchActionUp( + recyclerView, viewHolder, dx); + } + // fallthrough + case MotionEvent.ACTION_CANCEL: + swipeBack = true; + shouldTriggerSwipeFeedback = false; + // Sometimes the view does not go back correctly, so make sure that after 2s the + // progress is reset: + viewHolder.itemView.postDelayed(() -> resetProgress(viewHolder), 2000); + if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) { + resetProgress(viewHolder); + } + break; } - //fallthrough - case MotionEvent.ACTION_CANCEL: - swipeBack = true; - shouldTriggerSwipeFeedback = false; - // Sometimes the view does not go back correctly, so make sure that after 2s the progress is reset: - viewHolder.itemView.postDelayed(() -> resetProgress(viewHolder), 2000); - if (AccessibilityUtil.areAnimationsDisabled(viewHolder.itemView.getContext())) { - resetProgress(viewHolder); - } - break; - } - return false; - } - - }); + return false; + } + }); } - private void handleTouchActionUp(@NonNull RecyclerView recyclerView, - @NonNull RecyclerView.ViewHolder viewHolder, - float dx) - { + private void handleTouchActionUp( + @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dx) { if (dx > SWIPE_SUCCESS_DX) { canTriggerSwipe = false; onSwiped(viewHolder); @@ -177,17 +175,16 @@ private void handleTouchActionUp(@NonNull RecyclerView recyclerView, } private static void resetProgress(RecyclerView.ViewHolder viewHolder) { - ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, - 0f, - getSignFromDirection(viewHolder.itemView)); + ConversationSwipeAnimationHelper.update( + (ConversationItem) viewHolder.itemView, 0f, getSignFromDirection(viewHolder.itemView)); } private boolean cannotSwipeViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) { if (!(viewHolder.itemView instanceof ConversationItem)) return true; ConversationItem item = ((ConversationItem) viewHolder.itemView); - return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) || - item.disallowSwipe(latestDownX, latestDownY); + return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) + || item.disallowSwipe(latestDownX, latestDownY); } private void updateLatestDownCoordinate(float x, float y) { diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationItemTouchListener.java b/src/main/java/org/thoughtcrime/securesms/ConversationItemTouchListener.java index 72dda4bde..93bbfa839 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationItemTouchListener.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationItemTouchListener.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms; import android.view.MotionEvent; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java index cd2106809..4c8cbf158 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListActivity.java @@ -20,10 +20,10 @@ import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_URL; import static org.thoughtcrime.securesms.util.ShareUtil.acquireRelayMessageContent; import static org.thoughtcrime.securesms.util.ShareUtil.getDirectSharingChatId; +import static org.thoughtcrime.securesms.util.ShareUtil.getForwardedMessageAccountId; import static org.thoughtcrime.securesms.util.ShareUtil.getSharedTitle; import static org.thoughtcrime.securesms.util.ShareUtil.isDirectSharing; import static org.thoughtcrime.securesms.util.ShareUtil.isForwarding; -import static org.thoughtcrime.securesms.util.ShareUtil.getForwardedMessageAccountId; import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent; import static org.thoughtcrime.securesms.util.ShareUtil.resetRelayingMessageContent; @@ -45,7 +45,6 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; - import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -54,7 +53,8 @@ import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.TooltipCompat; import androidx.core.view.MenuCompat; - +import chat.delta.rpc.types.SecurejoinSource; +import chat.delta.rpc.types.SecurejoinUiPath; import com.amulyakhare.textdrawable.TextDrawable; import com.b44t.messenger.DcAccounts; import com.b44t.messenger.DcContact; @@ -62,7 +62,8 @@ import com.b44t.messenger.DcMsg; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - +import java.util.ArrayList; +import java.util.Date; import org.thoughtcrime.securesms.components.AvatarView; import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.connect.AccountManager; @@ -79,47 +80,49 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.Prefs; -import org.thoughtcrime.securesms.util.ScreenLockUtil; -import org.thoughtcrime.securesms.util.ShareUtil; import org.thoughtcrime.securesms.util.SaveAttachmentTask; +import org.thoughtcrime.securesms.util.ScreenLockUtil; import org.thoughtcrime.securesms.util.SendRelayedMessageUtil; +import org.thoughtcrime.securesms.util.ShareUtil; import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import chat.delta.rpc.types.SecurejoinSource; -import chat.delta.rpc.types.SecurejoinUiPath; - -import java.util.ArrayList; -import java.util.Date; public class ConversationListActivity extends PassphraseRequiredActionBarActivity - implements ConversationListFragment.ConversationSelectedListener -{ + implements ConversationListFragment.ConversationSelectedListener { private static final String TAG = ConversationListActivity.class.getSimpleName(); private static final String OPENPGP4FPR = "openpgp4fpr"; private static final String NDK_ARCH_WARNED = "ndk_arch_warned"; public static final String CLEAR_NOTIFICATIONS = "clear_notifications"; public static final String ACCOUNT_ID_EXTRA = "account_id"; - public static final String FROM_WELCOME = "from_welcome"; - public static final String FROM_WELCOME_RAW_QR = "from_welcome_raw_qr"; + public static final String FROM_WELCOME = "from_welcome"; + public static final String FROM_WELCOME_RAW_QR = "from_welcome_raw_qr"; private ConversationListFragment conversationListFragment; - public TextView title; - private AvatarView selfAvatar; - private ImageView unreadIndicator; - private SearchFragment searchFragment; - private SearchToolbar searchToolbar; - private ImageView searchAction; - private ViewGroup fragmentContainer; - private ViewGroup selfAvatarContainer; - - /** used to store temporarily scanned QR to pass it back to QrCodeHandler when ScreenLockUtil is used */ + public TextView title; + private AvatarView selfAvatar; + private ImageView unreadIndicator; + private SearchFragment searchFragment; + private SearchToolbar searchToolbar; + private ImageView searchAction; + private ViewGroup fragmentContainer; + private ViewGroup selfAvatarContainer; + + /** + * used to store temporarily scanned QR to pass it back to QrCodeHandler when ScreenLockUtil is + * used + */ private String qrData = null; + private ActivityResultLauncher relayLockLauncher; private ActivityResultLauncher qrScannerLauncher; - /** used to store temporarily profile ID to delete after authorization is granted via ScreenLockUtil */ + /** + * used to store temporarily profile ID to delete after authorization is granted via + * ScreenLockUtil + */ private int deleteProfileId = 0; + private ActivityResultLauncher deleteProfileLockLauncher; @Override @@ -130,40 +133,43 @@ protected void onPreCreate() { @Override protected void onCreate(Bundle icicle, boolean ready) { - relayLockLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK) { - // QrCodeHandler requested user authorization before adding a relay - // and it was granted, so proceed to add the relay - if (qrData != null) { - new QrCodeHandler(this).addRelay(qrData); - qrData = null; - } - } - } - ); - deleteProfileLockLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK) { - if (deleteProfileId != 0) { - deleteProfile(deleteProfileId); - deleteProfileId = 0; - } - } - } - ); - qrScannerLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(result.getResultCode(), result.getData()); - qrData = scanResult.getContents(); - new QrCodeHandler(this).handleQrData(qrData, SecurejoinSource.Scan, SecurejoinUiPath.QrIcon, relayLockLauncher); - } - } - ); + relayLockLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + // QrCodeHandler requested user authorization before adding a relay + // and it was granted, so proceed to add the relay + if (qrData != null) { + new QrCodeHandler(this).addRelay(qrData); + qrData = null; + } + } + }); + deleteProfileLockLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + if (deleteProfileId != 0) { + deleteProfile(deleteProfileId); + deleteProfileId = 0; + } + } + }); + qrScannerLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + IntentResult scanResult = + IntentIntegrator.parseActivityResult(result.getResultCode(), result.getData()); + qrData = scanResult.getContents(); + new QrCodeHandler(this) + .handleQrData( + qrData, SecurejoinSource.Scan, SecurejoinUiPath.QrIcon, relayLockLauncher); + } + }); addDeviceMessages(getIntent().getBooleanExtra(FROM_WELCOME, false)); if (getIntent().getIntExtra(ACCOUNT_ID_EXTRA, -1) <= 0) { @@ -175,59 +181,68 @@ protected void onCreate(Bundle icicle, boolean ready) { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - selfAvatar = findViewById(R.id.self_avatar); - selfAvatarContainer = findViewById(R.id.self_avatar_container); - unreadIndicator = findViewById(R.id.unread_indicator); - title = findViewById(R.id.toolbar_title); - searchToolbar = findViewById(R.id.search_toolbar); - searchAction = findViewById(R.id.search_action); - fragmentContainer = findViewById(R.id.fragment_container); + selfAvatar = findViewById(R.id.self_avatar); + selfAvatarContainer = findViewById(R.id.self_avatar_container); + unreadIndicator = findViewById(R.id.unread_indicator); + title = findViewById(R.id.toolbar_title); + searchToolbar = findViewById(R.id.search_toolbar); + searchAction = findViewById(R.id.search_action); + fragmentContainer = findViewById(R.id.fragment_container); // add margin to avoid content hidden behind system bars ViewUtil.applyWindowInsetsAsMargin(searchToolbar, true, true, true, false); Bundle bundle = new Bundle(); - conversationListFragment = initFragment(R.id.fragment_container, new ConversationListFragment(), bundle); + conversationListFragment = + initFragment(R.id.fragment_container, new ConversationListFragment(), bundle); initializeSearchListener(); - getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - if (searchToolbar.isVisible()) { - searchToolbar.collapse(); - } else { - Activity activity = ConversationListActivity.this; - if (isRelayingMessageContent(activity)) { - int selectedAccId = DcHelper.getContext(activity).getAccountId(); - int initialAccId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, selectedAccId); - if (initialAccId != selectedAccId) { - // allowing to go back is dangerous, it could be activity on previously selected account, - // instead of figuring out account rollback in onResume in each activity (conversation, gallery, media preview, webxdc, etc.) - // just clear the back stack and stay in newly selected account - finishAffinity(); - startActivity(new Intent(activity, ConversationListActivity.class)); - return; - } else { - handleResetRelaying(); - } - } - - setEnabled(false); - getOnBackPressedDispatcher().onBackPressed(); - } - } - }); + getOnBackPressedDispatcher() + .addCallback( + this, + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (searchToolbar.isVisible()) { + searchToolbar.collapse(); + } else { + Activity activity = ConversationListActivity.this; + if (isRelayingMessageContent(activity)) { + int selectedAccId = DcHelper.getContext(activity).getAccountId(); + int initialAccId = getIntent().getIntExtra(ACCOUNT_ID_EXTRA, selectedAccId); + if (initialAccId != selectedAccId) { + // allowing to go back is dangerous, it could be activity on previously + // selected account, + // instead of figuring out account rollback in onResume in each activity + // (conversation, gallery, media preview, webxdc, etc.) + // just clear the back stack and stay in newly selected account + finishAffinity(); + startActivity(new Intent(activity, ConversationListActivity.class)); + return; + } else { + handleResetRelaying(); + } + } + + setEnabled(false); + getOnBackPressedDispatcher().onBackPressed(); + } + } + }); TooltipCompat.setTooltipText(searchAction, getText(R.string.search_explain)); TooltipCompat.setTooltipText(selfAvatar, getText(R.string.switch_account)); - selfAvatar.setOnClickListener(v -> AccountManager.getInstance().showSwitchAccountMenu(this, false)); - findViewById(R.id.avatar_and_title).setOnClickListener(v -> { - if (!isRelayingMessageContent(this)) { - AccountManager.getInstance().showSwitchAccountMenu(this, false); - } - }); + selfAvatar.setOnClickListener( + v -> AccountManager.getInstance().showSwitchAccountMenu(this, false)); + findViewById(R.id.avatar_and_title) + .setOnClickListener( + v -> { + if (!isRelayingMessageContent(this)) { + AccountManager.getInstance().showSwitchAccountMenu(this, false); + } + }); refresh(); @@ -247,7 +262,7 @@ public void handleOnBackPressed() { * If the build script is invoked with a specific architecture (e.g.`ndk-make.sh arm64-v8a`), it * will compile the core only for this arch. This method checks if the arch was correct. * - * In order to do this, `ndk-make.sh` writes its argument into the file `ndkArch`. + *

In order to do this, `ndk-make.sh` writes its argument into the file `ndkArch`. * `getNdkArch()` in `build.gradle` then reads this file and its content is assigned to * `BuildConfig.NDK_ARCH`. */ @@ -260,7 +275,8 @@ private void checkNdkArchitecture() { String arch; // armv8l is 32 bit mode in 64 bit CPU: - if (archProperty.startsWith("armv7") || archProperty.startsWith("armv8l")) arch = "armeabi-v7a"; + if (archProperty.startsWith("armv7") || archProperty.startsWith("armv8l")) + arch = "armeabi-v7a"; else if (archProperty.equals("aarch64")) arch = "arm64-v8a"; else if (archProperty.equals("i686")) arch = "x86"; else if (archProperty.equals("x86_64")) arch = "x86_64"; @@ -274,23 +290,35 @@ private void checkNdkArchitecture() { String message; if (arch.equals("")) { - message = "This phone has the unknown architecture " + archProperty + ".\n\n"+ - "Please open an issue at https://github.com/deltachat/deltachat-android/issues."; + message = + "This phone has the unknown architecture " + + archProperty + + ".\n\n" + + "Please open an issue at https://github.com/deltachat/deltachat-android/issues."; } else { - message = "Apparently you used `ndk-make.sh " + BuildConfig.NDK_ARCH + "`, but this device is " + arch + ".\n\n" + - "You can use the app, but changes you made to the Rust code were not applied.\n\n" + - "To compile in your changes, you can:\n" + - "- Either run `ndk-make.sh " + arch + "` to build only for " + arch + " in debug mode\n" + - "- Or run `ndk-make.sh` without argument to build for all architectures in release mode\n\n" + - "If something doesn't work, please open an issue at https://github.com/deltachat/deltachat-android/issues!!"; + message = + "Apparently you used `ndk-make.sh " + + BuildConfig.NDK_ARCH + + "`, but this device is " + + arch + + ".\n\n" + + "You can use the app, but changes you made to the Rust code were not applied.\n\n" + + "To compile in your changes, you can:\n" + + "- Either run `ndk-make.sh " + + arch + + "` to build only for " + + arch + + " in debug mode\n" + + "- Or run `ndk-make.sh` without argument to build for all architectures in release mode\n\n" + + "If something doesn't work, please open an issue at https://github.com/deltachat/deltachat-android/issues!!"; } Log.e(TAG, message); if (!Prefs.getBooleanPreference(this, NDK_ARCH_WARNED, false)) { new AlertDialog.Builder(this) - .setMessage(message) - .setPositiveButton(android.R.string.ok, null) - .show(); + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); Prefs.setBooleanPreference(this, NDK_ARCH_WARNED, true); } } @@ -351,7 +379,8 @@ public void refreshTitle() { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } else { title.setText(DcHelper.getContext(this).getName()); - // refreshTitle is called by ConversationListFragment when connectivity changes so update connectivity dot here + // refreshTitle is called by ConversationListFragment when connectivity changes so update + // connectivity dot here selfAvatar.setConnectivity(DcHelper.getContext(this).getConnectivity()); getSupportActionBar().setDisplayHomeAsUpEnabled(false); } @@ -387,23 +416,26 @@ public void refreshUnreadIndicator() { } } - if(unreadCount == 0) { + if (unreadCount == 0) { unreadIndicator.setVisibility(View.GONE); } else { boolean isDarkTheme = DynamicTheme.isDarkTheme(this); int badgeColor = Color.WHITE; if (isDarkTheme) { - final TypedArray attrs = obtainStyledAttributes(new int[] { - R.attr.conversation_list_item_unreadcount_color, - }); + final TypedArray attrs = + obtainStyledAttributes( + new int[] { + R.attr.conversation_list_item_unreadcount_color, + }); badgeColor = attrs.getColor(0, Color.BLACK); } - String label = unreadCount>99? "99+" : String.valueOf(unreadCount); - unreadIndicator.setImageDrawable(TextDrawable.builder() + String label = unreadCount > 99 ? "99+" : String.valueOf(unreadCount); + unreadIndicator.setImageDrawable( + TextDrawable.builder() .beginConfig() .width(ViewUtil.dpToPx(this, 20)) .height(ViewUtil.dpToPx(this, 20)) - .textColor(isDarkTheme? Color.WHITE : Color.BLACK) + .textColor(isDarkTheme ? Color.WHITE : Color.BLACK) .bold() .endConfig() .buildRound(label, badgeColor)); @@ -426,9 +458,8 @@ public boolean onPrepareOptionsMenu(Menu menu) { if (isRelayingMessageContent(this)) { inflater.inflate(R.menu.forwarding_menu, menu); - menu.findItem(R.id.menu_export_attachment).setVisible( - ShareUtil.isFromWebxdc(this) && ShareUtil.getSharedUris(this).size() == 1 - ); + menu.findItem(R.id.menu_export_attachment) + .setVisible(ShareUtil.isFromWebxdc(this) && ShareUtil.getSharedUris(this).size() == 1); } else { inflater.inflate(R.menu.text_secure_normal, menu); menu.findItem(R.id.menu_global_map).setVisible(Prefs.isLocationStreamingEnabled(this)); @@ -437,7 +468,8 @@ public boolean onPrepareOptionsMenu(Menu menu) { proxyItem.setVisible(false); } else { boolean proxyEnabled = DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1; - proxyItem.setIcon(proxyEnabled? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24); + proxyItem.setIcon( + proxyEnabled ? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24); proxyItem.setVisible(true); } MenuCompat.setGroupDividerEnabled(menu, true); @@ -448,42 +480,42 @@ public boolean onPrepareOptionsMenu(Menu menu) { } private void initializeSearchListener() { - searchAction.setOnClickListener(v -> { - searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2), - searchAction.getY() + (searchAction.getHeight() / 2)); - }); - - searchToolbar.setListener(new SearchToolbar.SearchListener() { - @Override - public void onSearchTextChange(String text) { - String trimmed = text.trim(); - - if (trimmed.length() > 0) { - if (searchFragment == null) { - searchFragment = SearchFragment.newInstance(); - getSupportFragmentManager().beginTransaction() - .add(R.id.fragment_container, searchFragment, null) - .commit(); + searchAction.setOnClickListener( + v -> { + searchToolbar.display( + searchAction.getX() + (searchAction.getWidth() / 2), + searchAction.getY() + (searchAction.getHeight() / 2)); + }); + + searchToolbar.setListener( + new SearchToolbar.SearchListener() { + @Override + public void onSearchTextChange(String text) { + String trimmed = text.trim(); + + if (trimmed.length() > 0) { + if (searchFragment == null) { + searchFragment = SearchFragment.newInstance(); + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, searchFragment, null) + .commit(); + } + searchFragment.updateSearchQuery(trimmed); + } else if (searchFragment != null) { + getSupportFragmentManager().beginTransaction().remove(searchFragment).commit(); + searchFragment = null; + } } - searchFragment.updateSearchQuery(trimmed); - } else if (searchFragment != null) { - getSupportFragmentManager().beginTransaction() - .remove(searchFragment) - .commit(); - searchFragment = null; - } - } - @Override - public void onSearchClosed() { - if (searchFragment != null) { - getSupportFragmentManager().beginTransaction() - .remove(searchFragment) - .commit(); - searchFragment = null; - } - } - }); + @Override + public void onSearchClosed() { + if (searchFragment != null) { + getSupportFragmentManager().beginTransaction().remove(searchFragment).commit(); + searchFragment = null; + } + } + }); } @Override @@ -498,9 +530,8 @@ public boolean onOptionsItemSelected(MenuItem item) { startActivity(new Intent(this, ApplicationPreferencesActivity.class)); return true; } else if (itemId == R.id.menu_qr) { - Intent intent = new IntentIntegrator(this) - .setCaptureActivity(QrActivity.class) - .createScanIntent(); + Intent intent = + new IntentIntegrator(this).setCaptureActivity(QrActivity.class).createScanIntent(); qrScannerLauncher.launch(intent); return true; } else if (itemId == R.id.menu_global_map) { @@ -526,38 +557,40 @@ public boolean onOptionsItemSelected(MenuItem item) { } private void handleSaveAttachment() { - SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> { - if (StorageUtil.canWriteToMediaStore(this)) { - performSave(); - return; - } + SaveAttachmentTask.showWarningDialog( + this, + (dialogInterface, i) -> { + if (StorageUtil.canWriteToMediaStore(this)) { + performSave(); + return; + } - Permissions.with(this) + Permissions.with(this) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .alwaysGrantOnSdk30() .ifNecessary() .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) .onAllGranted(this::performSave) .execute(); - }); + }); } private void performSave() { - ArrayList uriList = ShareUtil.getSharedUris(this); + ArrayList uriList = ShareUtil.getSharedUris(this); Uri uri = uriList.get(0); String mimeType = PersistentBlobProvider.getMimeType(this, uri); String fileName = PersistentBlobProvider.getFileName(this, uri); - SaveAttachmentTask.Attachment[] attachments = new SaveAttachmentTask.Attachment[]{ - new SaveAttachmentTask.Attachment(uri, mimeType, new Date().getTime(), fileName) - }; + SaveAttachmentTask.Attachment[] attachments = + new SaveAttachmentTask.Attachment[] { + new SaveAttachmentTask.Attachment(uri, mimeType, new Date().getTime(), fileName) + }; SaveAttachmentTask saveTask = new SaveAttachmentTask(this); saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments); getOnBackPressedDispatcher().onBackPressed(); } private void handleOpenpgp4fpr() { - if (getIntent() != null && - Intent.ACTION_VIEW.equals(getIntent().getAction())) { + if (getIntent() != null && Intent.ACTION_VIEW.equals(getIntent().getAction())) { Uri uri = getIntent().getData(); if (uri == null) { return; @@ -590,7 +623,11 @@ public void openConversation(int chatId, int startingPosition) { int fwdAccId = getForwardedMessageAccountId(this); if (fwdAccId == dcContext.getAccountId() && dcContext.getChat(chatId).isSelfTalk()) { SendRelayedMessageUtil.immediatelyRelay(this, chatId); - Toast.makeText(this, DynamicTheme.getCheckmarkEmoji(this) + " " + getString(R.string.saved), Toast.LENGTH_SHORT).show(); + Toast.makeText( + this, + DynamicTheme.getCheckmarkEmoji(this) + " " + getString(R.string.saved), + Toast.LENGTH_SHORT) + .show(); handleResetRelaying(); finish(); } else { @@ -635,7 +672,8 @@ private void shareInvite() { private void addDeviceMessages(boolean fromWelcome) { // update messages - for new messages, do not reuse or modify strings but create new ones. - // it is not needed to keep all past update messages, however, when deleted, also the strings should be deleted. + // it is not needed to keep all past update messages, however, when deleted, also the strings + // should be deleted. try { DcContext dcContext = DcHelper.getContext(this); final String deviceMsgLabel = "update_2_33_1_android"; @@ -644,7 +682,8 @@ private void addDeviceMessages(boolean fromWelcome) { if (!getIntent().getBooleanExtra(FROM_WELCOME, false)) { msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); - // InputStream inputStream = getResources().getAssets().open("device-messages/green-checkmark.jpg"); + // InputStream inputStream = + // getResources().getAssets().open("device-messages/green-checkmark.jpg"); // String outputFile = DcHelper.getBlobdirFile(dcContext, "green-checkmark", ".jpg"); // Util.copy(inputStream, new FileOutputStream(outputFile)); // msg.setFile(outputFile, "image/jpeg"); @@ -653,7 +692,8 @@ private void addDeviceMessages(boolean fromWelcome) { } dcContext.addDeviceMsg(deviceMsgLabel, msg); - if (Prefs.getStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, "").equals(deviceMsgLabel)) { + if (Prefs.getStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, "") + .equals(deviceMsgLabel)) { int deviceChatId = dcContext.getChatIdByContactId(DcContact.DC_CONTACT_ID_DEVICE); if (deviceChatId != 0) { dcContext.marknoticedChat(deviceChatId); @@ -662,7 +702,7 @@ private void addDeviceMessages(boolean fromWelcome) { Prefs.setStringPreference(this, Prefs.LAST_DEVICE_MSG_LABEL, deviceMsgLabel); } - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -677,7 +717,12 @@ public void onProfileSwitched(int profileId) { public void onDeleteProfile(int profileId) { deleteProfileId = profileId; - boolean result = ScreenLockUtil.applyScreenLock(this, getString(R.string.delete_account), getString(R.string.enter_system_secret_to_continue), deleteProfileLockLauncher); + boolean result = + ScreenLockUtil.applyScreenLock( + this, + getString(R.string.delete_account), + getString(R.string.enter_system_secret_to_continue), + deleteProfileLockLauncher); if (!result) { deleteProfile(profileId); } @@ -705,7 +750,8 @@ private void deleteProfile(int profileId) { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/main/java/org/thoughtcrime/securesms/ConversationListAdapter.java index 3cfe66cce..19fc51b16 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListAdapter.java @@ -20,48 +20,44 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcLot; - +import java.lang.ref.WeakReference; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.ViewUtil; -import java.lang.ref.WeakReference; - /** * A CursorAdapter for building a list of conversation threads. * * @author Moxie Marlinspike */ -class ConversationListAdapter extends BaseConversationListAdapter { +class ConversationListAdapter + extends BaseConversationListAdapter { private static final int MESSAGE_TYPE_SWITCH_ARCHIVE = 1; - private static final int MESSAGE_TYPE_THREAD = 2; - private static final int MESSAGE_TYPE_INBOX_ZERO = 3; + private static final int MESSAGE_TYPE_THREAD = 2; + private static final int MESSAGE_TYPE_INBOX_ZERO = 3; - private final WeakReference context; - private @NonNull DcContext dcContext; - private @NonNull DcChatlist dcChatlist; - private final @NonNull GlideRequests glideRequests; - private final @NonNull LayoutInflater inflater; - private final @Nullable ItemClickListener clickListener; + private final WeakReference context; + private @NonNull DcContext dcContext; + private @NonNull DcChatlist dcChatlist; + private final @NonNull GlideRequests glideRequests; + private final @NonNull LayoutInflater inflater; + private final @Nullable ItemClickListener clickListener; protected static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(final @NonNull V itemView) - { + public ViewHolder(final @NonNull V itemView) { super(itemView); } public BindableConversationListItem getItem() { - return (BindableConversationListItem)itemView; + return (BindableConversationListItem) itemView; } } @@ -75,17 +71,17 @@ public long getItemId(int i) { return dcChatlist.getChatId(i); } - ConversationListAdapter(@NonNull Context context, - @NonNull GlideRequests glideRequests, - @Nullable ItemClickListener clickListener) - { + ConversationListAdapter( + @NonNull Context context, + @NonNull GlideRequests glideRequests, + @Nullable ItemClickListener clickListener) { super(); - this.context = new WeakReference<>(context); - this.glideRequests = glideRequests; - this.dcContext = DcHelper.getContext(context); - this.dcChatlist = new DcChatlist(0, 0); - this.inflater = LayoutInflater.from(context); - this.clickListener = clickListener; + this.context = new WeakReference<>(context); + this.glideRequests = glideRequests; + this.dcContext = DcHelper.getContext(context); + this.dcChatlist = new DcChatlist(0, 0); + this.inflater = LayoutInflater.from(context); + this.clickListener = clickListener; setHasStableIds(true); } @@ -93,29 +89,37 @@ public long getItemId(int i) { @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == MESSAGE_TYPE_SWITCH_ARCHIVE) { - final ConversationListItem item = (ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view, parent, false); + final ConversationListItem item = + (ConversationListItem) + inflater.inflate(R.layout.conversation_list_item_view, parent, false); item.getLayoutParams().height = ViewUtil.dpToPx(54); item.findViewById(R.id.subject).setVisibility(View.GONE); item.findViewById(R.id.date).setVisibility(View.GONE); - item.setOnClickListener(v -> { - if (clickListener != null) clickListener.onSwitchToArchive(); - }); + item.setOnClickListener( + v -> { + if (clickListener != null) clickListener.onSwitchToArchive(); + }); return new ViewHolder(item); } else if (viewType == MESSAGE_TYPE_INBOX_ZERO) { - return new ViewHolder((ConversationListItemInboxZero)inflater.inflate(R.layout.conversation_list_item_inbox_zero, parent, false)); + return new ViewHolder( + (ConversationListItemInboxZero) + inflater.inflate(R.layout.conversation_list_item_inbox_zero, parent, false)); } else { - final ConversationListItem item = (ConversationListItem)inflater.inflate(R.layout.conversation_list_item_view, - parent, false); + final ConversationListItem item = + (ConversationListItem) + inflater.inflate(R.layout.conversation_list_item_view, parent, false); - item.setOnClickListener(view -> { - if (clickListener != null) clickListener.onItemClick(item); - }); + item.setOnClickListener( + view -> { + if (clickListener != null) clickListener.onItemClick(item); + }); - item.setOnLongClickListener(view -> { - if (clickListener != null) clickListener.onItemLongClick(item); - return true; - }); + item.setOnLongClickListener( + view -> { + if (clickListener != null) clickListener.onItemLongClick(item); + return true; + }); return new ViewHolder(item); } @@ -130,7 +134,15 @@ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { DcChat chat = dcContext.getChat(dcChatlist.getChatId(i)); DcLot summary = dcChatlist.getSummary(i, chat); - viewHolder.getItem().bind(DcHelper.getThreadRecord(context, summary, chat), dcChatlist.getMsgId(i), summary, glideRequests, batchSet, batchMode); + viewHolder + .getItem() + .bind( + DcHelper.getThreadRecord(context, summary, chat), + dcChatlist.getMsgId(i), + summary, + glideRequests, + batchSet, + batchMode); } @Override @@ -139,7 +151,7 @@ public int getItemViewType(int i) { if (chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK) { return MESSAGE_TYPE_SWITCH_ARCHIVE; - } else if(chatId == DcChat.DC_CHAT_ID_ALLDONE_HINT) { + } else if (chatId == DcChat.DC_CHAT_ID_ALLDONE_HINT) { return MESSAGE_TYPE_INBOX_ZERO; } else { return MESSAGE_TYPE_THREAD; @@ -159,7 +171,9 @@ public void selectAllThreads() { interface ItemClickListener { void onItemClick(ConversationListItem item); + void onItemLongClick(ConversationListItem item); + void onSwitchToArchive(); } @@ -169,7 +183,7 @@ void changeData(@Nullable DcChatlist chatlist) { return; } if (chatlist == null) { - dcChatlist = new DcChatlist(0, 0); + dcChatlist = new DcChatlist(0, 0); } else { dcChatlist = chatlist; dcContext = DcHelper.getContext(context); diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListArchiveActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationListArchiveActivity.java index 3a59c615f..b39024ec7 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListArchiveActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListArchiveActivity.java @@ -11,22 +11,19 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; - import androidx.activity.OnBackPressedCallback; - import com.b44t.messenger.DcChat; - import org.thoughtcrime.securesms.connect.DcHelper; public class ConversationListArchiveActivity extends PassphraseRequiredActionBarActivity - implements ConversationListFragment.ConversationSelectedListener -{ + implements ConversationListFragment.ConversationSelectedListener { @Override protected void onCreate(Bundle icicle, boolean ready) { setContentView(R.layout.activity_conversation_list_archive); getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (isRelayingMessageContent(this)) { - getSupportActionBar().setTitle(isSharing(this) ? R.string.chat_share_with_title : R.string.forward_to); + getSupportActionBar() + .setTitle(isSharing(this) ? R.string.chat_share_with_title : R.string.forward_to); getSupportActionBar().setSubtitle(R.string.chat_archived_label); } else { getSupportActionBar().setTitle(R.string.chat_archived_label); @@ -36,20 +33,25 @@ protected void onCreate(Bundle icicle, boolean ready) { bundle.putBoolean(ConversationListFragment.ARCHIVE, true); initFragment(R.id.fragment, new ConversationListFragment(), bundle); - getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - if (!isRelayingMessageContent(ConversationListArchiveActivity.this)) { - // Load the ConversationListActivity in case it's not existent for some reason - Intent intent = new Intent(ConversationListArchiveActivity.this, ConversationListActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - } - - setEnabled(false); - getOnBackPressedDispatcher().onBackPressed(); - } - }); + getOnBackPressedDispatcher() + .addCallback( + this, + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (!isRelayingMessageContent(ConversationListArchiveActivity.this)) { + // Load the ConversationListActivity in case it's not existent for some reason + Intent intent = + new Intent( + ConversationListArchiveActivity.this, ConversationListActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + + setEnabled(false); + getOnBackPressedDispatcher().onBackPressed(); + } + }); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java index 6621d694b..4d8cc81ab 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListFragment.java @@ -29,16 +29,15 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; - +import java.util.Timer; +import java.util.TimerTask; import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener; import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator; import org.thoughtcrime.securesms.components.reminder.DozeReminder; @@ -52,25 +51,21 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.Timer; -import java.util.TimerTask; - - public class ConversationListFragment extends BaseConversationListFragment - implements ItemClickListener, DcEventCenter.DcEventDelegate { + implements ItemClickListener, DcEventCenter.DcEventDelegate { public static final String ARCHIVE = "archive"; public static final String RELOAD_LIST = "reload_list"; private static final String TAG = ConversationListFragment.class.getSimpleName(); - private RecyclerView list; - private View emptyState; - private TextView emptySearch; - private final String queryFilter = ""; - private boolean archive; - private Timer reloadTimer; - private boolean chatlistJustLoaded; - private boolean reloadTimerInstantly; + private RecyclerView list; + private View emptyState; + private TextView emptySearch; + private final String queryFilter = ""; + private boolean archive; + private Timer reloadTimer; + private boolean chatlistJustLoaded; + private boolean reloadTimerInstantly; @Override public void onCreate(Bundle icicle) { @@ -103,10 +98,10 @@ public void onDestroy() { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) { final View view = inflater.inflate(R.layout.conversation_list_fragment, container, false); - list = ViewUtil.findById(view, R.id.list); - fab = ViewUtil.findById(view, R.id.fab); - emptyState = ViewUtil.findById(view, R.id.empty_state); - emptySearch = ViewUtil.findById(view, R.id.empty_search); + list = ViewUtil.findById(view, R.id.list); + fab = ViewUtil.findById(view, R.id.fab); + emptyState = ViewUtil.findById(view, R.id.empty_state); + emptySearch = ViewUtil.findById(view, R.id.empty_search); // add padding to avoid content hidden behind system bars ViewUtil.applyWindowInsets(list, true, archive, true, true); @@ -145,22 +140,25 @@ public void onResume() { updateReminders(); - if (requireActivity().getIntent().getIntExtra(RELOAD_LIST, 0) == 1 - && !chatlistJustLoaded) { + if (requireActivity().getIntent().getIntExtra(RELOAD_LIST, 0) == 1 && !chatlistJustLoaded) { loadChatlist(); reloadTimerInstantly = false; } chatlistJustLoaded = false; reloadTimer = new Timer(); - reloadTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - Util.runOnMain(() -> { - list.getAdapter().notifyDataSetChanged(); - }); - } - }, reloadTimerInstantly? 0 : 60 * 1000, 60 * 1000); + reloadTimer.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + Util.runOnMain( + () -> { + list.getAdapter().notifyDataSetChanged(); + }); + } + }, + reloadTimerInstantly ? 0 : 60 * 1000, + 60 * 1000); } @Override @@ -184,7 +182,8 @@ public BaseConversationListAdapter getListAdapter() { @SuppressLint({"StaticFieldLeak", "NewApi"}) private void updateReminders() { - // by the time onPostExecute() is asynchronously run, getActivity() might return null, so get the activity here: + // by the time onPostExecute() is asynchronously run, getActivity() might return null, so get + // the activity here: Activity activity = requireActivity(); new AsyncTask() { @Override @@ -204,22 +203,29 @@ protected Void doInBackground(Context... params) { @Override protected void onPostExecute(Void result) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (!Prefs.getBooleanPreference(activity, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) { + if (!Prefs.getBooleanPreference( + activity, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) { Prefs.setBooleanPreference(activity, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, true); Permissions.with(activity) - .request(Manifest.permission.POST_NOTIFICATIONS) - .ifNecessary() - .onAllGranted(() -> { - DozeReminder.maybeAskDirectly(activity); - }) - .onAnyDenied(() -> { - final DcContext dcContext = DcHelper.getContext(activity); - DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); - msg.setText("\uD83D\uDC49 "+activity.getString(R.string.notifications_disabled)+" \uD83D\uDC48\n\n" - +activity.getString(R.string.perm_explain_access_to_notifications_denied)); - dcContext.addDeviceMsg("android.notifications-disabled", msg); - }) - .execute(); + .request(Manifest.permission.POST_NOTIFICATIONS) + .ifNecessary() + .onAllGranted( + () -> { + DozeReminder.maybeAskDirectly(activity); + }) + .onAnyDenied( + () -> { + final DcContext dcContext = DcHelper.getContext(activity); + DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); + msg.setText( + "\uD83D\uDC49 " + + activity.getString(R.string.notifications_disabled) + + " \uD83D\uDC48\n\n" + + activity.getString( + R.string.perm_explain_access_to_notifications_denied)); + dcContext.addDeviceMsg("android.notifications-disabled", msg); + }) + .execute(); } else { DozeReminder.maybeAskDirectly(activity); } @@ -233,6 +239,7 @@ protected void onPostExecute(Void result) { private final Object loadChatlistLock = new Object(); private boolean inLoadChatlist; private boolean needsAnotherLoad; + public void loadChatlistAsync() { synchronized (loadChatlistLock) { needsAnotherLoad = true; @@ -243,21 +250,22 @@ public void loadChatlistAsync() { inLoadChatlist = true; } - Util.runOnAnyBackgroundThread(() -> { - while(true) { - synchronized (loadChatlistLock) { - if (!needsAnotherLoad) { - inLoadChatlist = false; - return; + Util.runOnAnyBackgroundThread( + () -> { + while (true) { + synchronized (loadChatlistLock) { + if (!needsAnotherLoad) { + inLoadChatlist = false; + return; + } + needsAnotherLoad = false; + } + + Log.i(TAG, "executing debounced chatlist loading"); + loadChatlist(); + Util.sleep(100); } - needsAnotherLoad = false; - } - - Log.i(TAG, "executing debounced chatlist loading"); - loadChatlist(); - Util.sleep(100); - } - }); + }); } private void loadChatlist() { @@ -272,32 +280,36 @@ private void loadChatlist() { Context context = getContext(); if (context == null) { - // can't load chat list at this time, see: https://github.com/deltachat/deltachat-android/issues/2012 + // can't load chat list at this time, see: + // https://github.com/deltachat/deltachat-android/issues/2012 Log.w(TAG, "Ignoring call to loadChatlist()"); return; } - DcChatlist chatlist = DcHelper.getContext(context).getChatlist(listflags, queryFilter.isEmpty() ? null : queryFilter, 0); - - Util.runOnMain(() -> { - if (chatlist.getCnt() <= 0 && TextUtils.isEmpty(queryFilter)) { - list.setVisibility(View.INVISIBLE); - emptyState.setVisibility(View.VISIBLE); - emptySearch.setVisibility(View.INVISIBLE); - fab.startPulse(3 * 1000); - } else if (chatlist.getCnt() <= 0 && !TextUtils.isEmpty(queryFilter)) { - list.setVisibility(View.INVISIBLE); - emptyState.setVisibility(View.GONE); - emptySearch.setVisibility(View.VISIBLE); - emptySearch.setText(getString(R.string.search_no_result_for_x, queryFilter)); - } else { - list.setVisibility(View.VISIBLE); - emptyState.setVisibility(View.GONE); - emptySearch.setVisibility(View.INVISIBLE); - fab.stopPulse(); - } + DcChatlist chatlist = + DcHelper.getContext(context) + .getChatlist(listflags, queryFilter.isEmpty() ? null : queryFilter, 0); + + Util.runOnMain( + () -> { + if (chatlist.getCnt() <= 0 && TextUtils.isEmpty(queryFilter)) { + list.setVisibility(View.INVISIBLE); + emptyState.setVisibility(View.VISIBLE); + emptySearch.setVisibility(View.INVISIBLE); + fab.startPulse(3 * 1000); + } else if (chatlist.getCnt() <= 0 && !TextUtils.isEmpty(queryFilter)) { + list.setVisibility(View.INVISIBLE); + emptyState.setVisibility(View.GONE); + emptySearch.setVisibility(View.VISIBLE); + emptySearch.setText(getString(R.string.search_no_result_for_x, queryFilter)); + } else { + list.setVisibility(View.VISIBLE); + emptyState.setVisibility(View.GONE); + emptySearch.setVisibility(View.INVISIBLE); + fab.stopPulse(); + } - ((ConversationListAdapter)list.getAdapter()).changeData(chatlist); - }); + ((ConversationListAdapter) list.getAdapter()).changeData(chatlist); + }); } @Override @@ -307,12 +319,12 @@ protected boolean offerToArchive() { @Override protected void setFabVisibility(boolean isActionMode) { - fab.setVisibility((isActionMode || !archive)? View.VISIBLE : View.GONE); + fab.setVisibility((isActionMode || !archive) ? View.VISIBLE : View.GONE); } @Override public void onItemClick(ConversationListItem item) { - onItemClick(item.getChatId()); + onItemClick(item.getChatId()); } @Override @@ -322,14 +334,15 @@ public void onItemLongClick(ConversationListItem item) { @Override public void onSwitchToArchive() { - ((ConversationSelectedListener)requireActivity()).onSwitchToArchive(); + ((ConversationSelectedListener) requireActivity()).onSwitchToArchive(); } @Override public void handleEvent(@NonNull DcEvent event) { final int accId = event.getAccountId(); if (event.getId() == DcContext.DC_EVENT_CHAT_DELETED) { - DcHelper.getNotificationCenter(requireActivity()).removeNotifications(accId, event.getData1Int()); + DcHelper.getNotificationCenter(requireActivity()) + .removeNotifications(accId, event.getData1Int()); } else if (accId != DcHelper.getContext(requireActivity()).getAccountId()) { Activity activity = getActivity(); if (activity instanceof ConversationListActivity) { @@ -352,7 +365,4 @@ public void handleEvent(@NonNull DcEvent event) { loadChatlistAsync(); } } - } - - diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java b/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java index 8547e6457..ef949724b 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListItem.java @@ -30,17 +30,16 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.amulyakhare.textdrawable.TextDrawable; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcLot; import com.b44t.messenger.DcMsg; - +import java.util.Collections; +import java.util.Set; import org.thoughtcrime.securesms.components.AvatarView; import org.thoughtcrime.securesms.components.DeliveryStatusView; import org.thoughtcrime.securesms.components.FromTextView; @@ -53,25 +52,22 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.Collections; -import java.util.Set; - public class ConversationListItem extends RelativeLayout - implements BindableConversationListItem, Unbindable -{ - private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL); - private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL); - - private Set selectedThreads; - private long chatId; - private int msgId; - private TextView subjectView; - private FromTextView fromView; - private TextView dateView; - private TextView archivedBadgeView; - private TextView requestBadgeView; + implements BindableConversationListItem, Unbindable { + private static final Typeface BOLD_TYPEFACE = + Typeface.create("sans-serif-medium", Typeface.NORMAL); + private static final Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL); + + private Set selectedThreads; + private long chatId; + private int msgId; + private TextView subjectView; + private FromTextView fromView; + private TextView dateView; + private TextView archivedBadgeView; + private TextView requestBadgeView; private DeliveryStatusView deliveryStatusIndicator; - private ImageView unreadIndicator; + private ImageView unreadIndicator; private AvatarView avatar; @@ -86,69 +82,72 @@ public ConversationListItem(Context context, AttributeSet attrs) { @Override protected void onFinishInflate() { super.onFinishInflate(); - this.subjectView = findViewById(R.id.subject); - this.fromView = findViewById(R.id.from_text); - this.dateView = findViewById(R.id.date); + this.subjectView = findViewById(R.id.subject); + this.fromView = findViewById(R.id.from_text); + this.dateView = findViewById(R.id.date); this.deliveryStatusIndicator = new DeliveryStatusView(findViewById(R.id.delivery_indicator)); - this.avatar = findViewById(R.id.avatar); - this.archivedBadgeView = findViewById(R.id.archived_badge); - this.requestBadgeView = findViewById(R.id.request_badge); - this.unreadIndicator = findViewById(R.id.unread_indicator); + this.avatar = findViewById(R.id.avatar); + this.archivedBadgeView = findViewById(R.id.archived_badge); + this.requestBadgeView = findViewById(R.id.request_badge); + this.unreadIndicator = findViewById(R.id.unread_indicator); ViewUtil.setTextViewGravityStart(this.fromView, getContext()); ViewUtil.setTextViewGravityStart(this.subjectView, getContext()); } @Override - public void bind(@NonNull ThreadRecord thread, - int msgId, - @NonNull DcLot dcSummary, - @NonNull GlideRequests glideRequests, - @NonNull Set selectedThreads, - boolean batchMode) - { + public void bind( + @NonNull ThreadRecord thread, + int msgId, + @NonNull DcLot dcSummary, + @NonNull GlideRequests glideRequests, + @NonNull Set selectedThreads, + boolean batchMode) { bind(thread, msgId, dcSummary, glideRequests, selectedThreads, batchMode, null); } - public void bind(@NonNull ThreadRecord thread, - int msgId, - @NonNull DcLot dcSummary, - @NonNull GlideRequests glideRequests, - @NonNull Set selectedThreads, - boolean batchMode, - @Nullable String highlightSubstring) - { - this.selectedThreads = selectedThreads; - Recipient recipient = thread.getRecipient(); - this.chatId = thread.getThreadId(); - this.msgId = msgId; - - int state = dcSummary.getState(); + public void bind( + @NonNull ThreadRecord thread, + int msgId, + @NonNull DcLot dcSummary, + @NonNull GlideRequests glideRequests, + @NonNull Set selectedThreads, + boolean batchMode, + @Nullable String highlightSubstring) { + this.selectedThreads = selectedThreads; + Recipient recipient = thread.getRecipient(); + this.chatId = thread.getThreadId(); + this.msgId = msgId; + + int state = dcSummary.getState(); if (highlightSubstring != null) { this.fromView.setText(getHighlightedSpan(recipient.getName(), highlightSubstring)); } else { - this.fromView.setText(recipient, state!=DcMsg.DC_STATE_IN_FRESH); + this.fromView.setText(recipient, state != DcMsg.DC_STATE_IN_FRESH); } - subjectView.setVisibility(chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK? GONE : VISIBLE); + subjectView.setVisibility(chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK ? GONE : VISIBLE); this.subjectView.setText(thread.getDisplayBody()); - this.subjectView.setTypeface(state==DcMsg.DC_STATE_IN_FRESH ? BOLD_TYPEFACE : LIGHT_TYPEFACE); - this.subjectView.setTextColor(state==DcMsg.DC_STATE_IN_FRESH ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color) - : ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)); + this.subjectView.setTypeface(state == DcMsg.DC_STATE_IN_FRESH ? BOLD_TYPEFACE : LIGHT_TYPEFACE); + this.subjectView.setTextColor( + state == DcMsg.DC_STATE_IN_FRESH + ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color) + : ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color)); if (thread.getDate() > 0) { CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), thread.getDate()); dateView.setText(date); - } - else { + } else { dateView.setText(""); } dateView.setCompoundDrawablesWithIntrinsicBounds( - thread.isSendingLocations()? R.drawable.ic_location_chatlist : 0, 0, - thread.getVisibility()==DcChat.DC_CHAT_VISIBILITY_PINNED? R.drawable.ic_pinned_chatlist : 0, 0 - ); + thread.isSendingLocations() ? R.drawable.ic_location_chatlist : 0, 0, + thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_PINNED + ? R.drawable.ic_pinned_chatlist + : 0, + 0); setStatusIcons(thread, state); setBatchState(batchMode); @@ -159,22 +158,22 @@ public void bind(@NonNull ThreadRecord thread, DcContact contact = recipient.getDcContact(); avatar.setSeenRecently(contact != null && contact.wasSeenRecently()); - DcChat dcChat = DcHelper.getContext(getContext()).getChat((int)chatId); + DcChat dcChat = DcHelper.getContext(getContext()).getChat((int) chatId); boolean isProtected = dcChat.isDeviceTalk() || dcChat.isSelfTalk(); fromView.setCompoundDrawablesWithIntrinsicBounds( - thread.isMuted()? R.drawable.ic_volume_off_grey600_18dp : 0, + thread.isMuted() ? R.drawable.ic_volume_off_grey600_18dp : 0, 0, - isProtected? R.drawable.ic_verified : 0, + isProtected ? R.drawable.ic_verified : 0, 0); } - public void bind(@NonNull DcContact contact, - @NonNull GlideRequests glideRequests, - @Nullable String highlightSubstring) - { + public void bind( + @NonNull DcContact contact, + @NonNull GlideRequests glideRequests, + @Nullable String highlightSubstring) { this.selectedThreads = Collections.emptySet(); - Recipient recipient = new Recipient(getContext(), contact); + Recipient recipient = new Recipient(getContext(), contact); fromView.setText(getHighlightedSpan(contact.getDisplayName(), highlightSubstring)); fromView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); @@ -191,14 +190,14 @@ public void bind(@NonNull DcContact contact, avatar.setSeenRecently(contact.wasSeenRecently()); } - public void bind(@NonNull DcMsg messageResult, - @NonNull GlideRequests glideRequests, - @Nullable String highlightSubstring) - { + public void bind( + @NonNull DcMsg messageResult, + @NonNull GlideRequests glideRequests, + @Nullable String highlightSubstring) { DcContext dcContext = DcHelper.getContext(getContext()); DcContact sender = dcContext.getContact(messageResult.getFromId()); this.selectedThreads = Collections.emptySet(); - Recipient recipient = new Recipient(getContext(), sender); + Recipient recipient = new Recipient(getContext(), sender); fromView.setText(recipient, true); fromView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); @@ -206,10 +205,10 @@ public void bind(@NonNull DcMsg messageResult, subjectView.setText(getHighlightedSpan(messageResult.getSummarytext(512), highlightSubstring)); long timestamp = messageResult.getTimestamp(); - if(timestamp>0) { - dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), messageResult.getTimestamp())); - } - else { + if (timestamp > 0) { + dateView.setText( + DateUtils.getBriefRelativeTimeSpanString(getContext(), messageResult.getTimestamp())); + } else { dateView.setText(""); } dateView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); @@ -224,8 +223,7 @@ public void bind(@NonNull DcMsg messageResult, } @Override - public void unbind() { - } + public void unbind() {} private void setBatchState(boolean batch) { setSelected(batch && selectedThreads.contains(chatId)); @@ -240,19 +238,15 @@ public int getMsgId() { } private void setStatusIcons(ThreadRecord thread, int state) { - if (thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED) - { + if (thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_ARCHIVED) { archivedBadgeView.setVisibility(View.VISIBLE); requestBadgeView.setVisibility(thread.isContactRequest() ? View.VISIBLE : View.GONE); deliveryStatusIndicator.setNone(); - } - else if (thread.isContactRequest()) { + } else if (thread.isContactRequest()) { requestBadgeView.setVisibility(View.VISIBLE); archivedBadgeView.setVisibility(View.GONE); deliveryStatusIndicator.setNone(); - } - else - { + } else { requestBadgeView.setVisibility(View.GONE); archivedBadgeView.setVisibility(View.GONE); @@ -274,43 +268,50 @@ else if (thread.isContactRequest()) { } int unreadCount = thread.getUnreadCount(); - if(unreadCount==0 || thread.isContactRequest()) { + if (unreadCount == 0 || thread.isContactRequest()) { unreadIndicator.setVisibility(View.GONE); } else { final int color; - if(thread.isMuted() || chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK){ - color = getResources().getColor(ThemeUtil.isDarkTheme(getContext()) ? R.color.unread_count_muted_dark : R.color.unread_count_muted); + if (thread.isMuted() || chatId == DcChat.DC_CHAT_ID_ARCHIVED_LINK) { + color = + getResources() + .getColor( + ThemeUtil.isDarkTheme(getContext()) + ? R.color.unread_count_muted_dark + : R.color.unread_count_muted); } else { - final TypedArray attrs = getContext().obtainStyledAttributes(new int[] { - R.attr.conversation_list_item_unreadcount_color, - }); + final TypedArray attrs = + getContext() + .obtainStyledAttributes( + new int[] { + R.attr.conversation_list_item_unreadcount_color, + }); color = attrs.getColor(0, Color.BLACK); } - unreadIndicator.setImageDrawable(TextDrawable.builder() - .beginConfig() - .width(ViewUtil.dpToPx(getContext(), 24)) - .height(ViewUtil.dpToPx(getContext(), 24)) - .textColor(Color.WHITE) - .bold() - .endConfig() - .buildRound(String.valueOf(unreadCount), color)); + unreadIndicator.setImageDrawable( + TextDrawable.builder() + .beginConfig() + .width(ViewUtil.dpToPx(getContext(), 24)) + .height(ViewUtil.dpToPx(getContext(), 24)) + .textColor(Color.WHITE) + .bold() + .endConfig() + .buildRound(String.valueOf(unreadCount), color)); unreadIndicator.setVisibility(View.VISIBLE); } } private void setBgColor(ThreadRecord thread) { int bg = R.attr.conversation_list_item_background; - if (thread!=null && thread.getVisibility()==DcChat.DC_CHAT_VISIBILITY_PINNED) { - bg = R.attr.pinned_list_item_background; + if (thread != null && thread.getVisibility() == DcChat.DC_CHAT_VISIBILITY_PINNED) { + bg = R.attr.pinned_list_item_background; } - try (TypedArray ta = getContext().obtainStyledAttributes(new int[]{bg})) { + try (TypedArray ta = getContext().obtainStyledAttributes(new int[] {bg})) { ViewUtil.setBackground(this, ta.getDrawable(0)); } } - private Spanned getHighlightedSpan(@Nullable String value, - @Nullable String highlight) - { + private Spanned getHighlightedSpan(@Nullable String value, @Nullable String highlight) { if (TextUtils.isEmpty(value)) { return new SpannableString(""); } @@ -321,11 +322,11 @@ private Spanned getHighlightedSpan(@Nullable String value, return new SpannableString(value); } - String normalizedValue = value.toLowerCase(Util.getLocale()); - String normalizedTest = highlight.toLowerCase(Util.getLocale()); + String normalizedValue = value.toLowerCase(Util.getLocale()); + String normalizedTest = highlight.toLowerCase(Util.getLocale()); - Spannable spanned = new SpannableString(value); - int searchStartIndex = 0; + Spannable spanned = new SpannableString(value); + int searchStartIndex = 0; for (String token : normalizedTest.split(" ")) { if (token.trim().isEmpty()) continue; @@ -337,7 +338,8 @@ private Spanned getHighlightedSpan(@Nullable String value, if (start >= 0) { int end = Math.min(start + token.length(), spanned.length()); - spanned.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + spanned.setSpan( + new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); searchStartIndex = end; } } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListItemInboxZero.java b/src/main/java/org/thoughtcrime/securesms/ConversationListItemInboxZero.java index 76a146d84..c6ba7a1ca 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListItemInboxZero.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListItemInboxZero.java @@ -1,21 +1,17 @@ package org.thoughtcrime.securesms; - import android.content.Context; import android.util.AttributeSet; import android.widget.LinearLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcLot; - +import java.util.Set; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.mms.GlideRequests; -import java.util.Set; - -public class ConversationListItemInboxZero extends LinearLayout implements BindableConversationListItem{ +public class ConversationListItemInboxZero extends LinearLayout + implements BindableConversationListItem { public ConversationListItemInboxZero(Context context) { super(context); } @@ -24,21 +20,25 @@ public ConversationListItemInboxZero(Context context, @Nullable AttributeSet att super(context, attrs); } - public ConversationListItemInboxZero(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public ConversationListItemInboxZero( + Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - public ConversationListItemInboxZero(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public ConversationListItemInboxZero( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override - public void unbind() { - - } + public void unbind() {} @Override - public void bind(@NonNull ThreadRecord thread, int msgId, @NonNull DcLot dcSummary, @NonNull GlideRequests glideRequests, @NonNull Set selectedThreads, boolean batchMode) { - - } + public void bind( + @NonNull ThreadRecord thread, + int msgId, + @NonNull DcLot dcSummary, + @NonNull GlideRequests glideRequests, + @NonNull Set selectedThreads, + boolean batchMode) {} } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationListRelayingActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationListRelayingActivity.java index 369bcf1d9..bb8b713e7 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationListRelayingActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationListRelayingActivity.java @@ -4,27 +4,24 @@ import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; - import androidx.fragment.app.Fragment; - import java.lang.ref.WeakReference; /** * "Relaying" means "Forwarding or Sharing". * - * When forwarding or sharing, we show the ConversationListActivity to the user. - * However, ConversationListActivity has `launchMode="singleTask"`, which means that this will - * destroy the existing ConversationListActivity. + *

When forwarding or sharing, we show the ConversationListActivity to the user. However, + * ConversationListActivity has `launchMode="singleTask"`, which means that this will destroy the + * existing ConversationListActivity. * - * In API 20-29, `startActivityForResult()` could be used instead of `startActivity()` - * to override this behavior and get two instances of ConversationListActivity. + *

In API 20-29, `startActivityForResult()` could be used instead of `startActivity()` to + * override this behavior and get two instances of ConversationListActivity. * - * As this is not possible anymore starting with API 30, we needed another solution, and created + *

As this is not possible anymore starting with API 30, we needed another solution, and created * this activity here. * - * See https://github.com/deltachat/deltachat-android/issues/1704. + *

See https://github.com/deltachat/deltachat-android/issues/1704. */ - public class ConversationListRelayingActivity extends ConversationListActivity { static WeakReference INSTANCE = null; @@ -42,7 +39,8 @@ protected void onDestroy() { // =================== Static Methods =================== public static void start(Fragment fragment, Intent intent) { - intent.setComponent(new ComponentName(fragment.getContext(), ConversationListRelayingActivity.class)); + intent.setComponent( + new ComponentName(fragment.getContext(), ConversationListRelayingActivity.class)); fragment.startActivity(intent); } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationSwipeAnimationHelper.java b/src/main/java/org/thoughtcrime/securesms/ConversationSwipeAnimationHelper.java index 2e69f4ba2..7440f779f 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationSwipeAnimationHelper.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationSwipeAnimationHelper.java @@ -4,30 +4,32 @@ import android.content.res.Resources; import android.view.View; import android.view.animation.Interpolator; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.util.Util; final class ConversationSwipeAnimationHelper { static final float TRIGGER_DX = dpToPx(64); - static final float MAX_DX = dpToPx(96); - - private static final float REPLY_SCALE_OVERSHOOT = 1.8f; - private static final float REPLY_SCALE_MAX = 1.2f; - private static final float REPLY_SCALE_MIN = 1f; - private static final long REPLY_SCALE_OVERSHOOT_DURATION = 200; - - private static final Interpolator BUBBLE_INTERPOLATOR = new BubblePositionInterpolator(0f, TRIGGER_DX, MAX_DX); - private static final Interpolator REPLY_ALPHA_INTERPOLATOR = new ClampingLinearInterpolator(0f, 1f, 1f); - private static final Interpolator REPLY_TRANSITION_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(10)); - private static final Interpolator AVATAR_INTERPOLATOR = new ClampingLinearInterpolator(0f, dpToPx(8)); - private static final Interpolator REPLY_SCALE_INTERPOLATOR = new ClampingLinearInterpolator(REPLY_SCALE_MIN, REPLY_SCALE_MAX); - - private ConversationSwipeAnimationHelper() { - } + static final float MAX_DX = dpToPx(96); + + private static final float REPLY_SCALE_OVERSHOOT = 1.8f; + private static final float REPLY_SCALE_MAX = 1.2f; + private static final float REPLY_SCALE_MIN = 1f; + private static final long REPLY_SCALE_OVERSHOOT_DURATION = 200; + + private static final Interpolator BUBBLE_INTERPOLATOR = + new BubblePositionInterpolator(0f, TRIGGER_DX, MAX_DX); + private static final Interpolator REPLY_ALPHA_INTERPOLATOR = + new ClampingLinearInterpolator(0f, 1f, 1f); + private static final Interpolator REPLY_TRANSITION_INTERPOLATOR = + new ClampingLinearInterpolator(0f, dpToPx(10)); + private static final Interpolator AVATAR_INTERPOLATOR = + new ClampingLinearInterpolator(0f, dpToPx(8)); + private static final Interpolator REPLY_SCALE_INTERPOLATOR = + new ClampingLinearInterpolator(REPLY_SCALE_MIN, REPLY_SCALE_MAX); + + private ConversationSwipeAnimationHelper() {} public static void update(@NonNull ConversationItem conversationItem, float dx, float sign) { float progress = dx / TRIGGER_DX; @@ -46,11 +48,13 @@ private static void updateBodyBubbleTransition(@NonNull View bodyBubble, float d bodyBubble.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign); } - private static void updateReactionsTransition(@NonNull View reactionsContainer, float dx, float sign) { + private static void updateReactionsTransition( + @NonNull View reactionsContainer, float dx, float sign) { reactionsContainer.setTranslationX(BUBBLE_INTERPOLATOR.getInterpolation(dx) * sign); } - private static void updateReplyIconTransition(@NonNull View replyIcon, float dx, float progress, float sign) { + private static void updateReplyIconTransition( + @NonNull View replyIcon, float dx, float progress, float sign) { if (progress > 0.05f) { replyIcon.setAlpha(REPLY_ALPHA_INTERPOLATOR.getInterpolation(progress)); } else replyIcon.setAlpha(0f); @@ -64,21 +68,21 @@ private static void updateReplyIconTransition(@NonNull View replyIcon, float dx, } } - private static void updateContactPhotoHolderTransition(@Nullable View contactPhotoHolder, - float progress, - float sign) - { + private static void updateContactPhotoHolderTransition( + @Nullable View contactPhotoHolder, float progress, float sign) { if (contactPhotoHolder == null) return; contactPhotoHolder.setTranslationX(AVATAR_INTERPOLATOR.getInterpolation(progress) * sign); } private static void triggerReplyIcon(@NonNull View replyIcon) { - ValueAnimator animator = ValueAnimator.ofFloat(REPLY_SCALE_MAX, REPLY_SCALE_OVERSHOOT, REPLY_SCALE_MAX); + ValueAnimator animator = + ValueAnimator.ofFloat(REPLY_SCALE_MAX, REPLY_SCALE_OVERSHOOT, REPLY_SCALE_MAX); animator.setDuration(REPLY_SCALE_OVERSHOOT_DURATION); - animator.addUpdateListener(animation -> { - replyIcon.setScaleX((float) animation.getAnimatedValue()); - replyIcon.setScaleY((float) animation.getAnimatedValue()); - }); + animator.addUpdateListener( + animation -> { + replyIcon.setScaleX((float) animation.getAnimatedValue()); + replyIcon.setScaleY((float) animation.getAnimatedValue()); + }); animator.start(); } @@ -93,9 +97,9 @@ private static final class BubblePositionInterpolator implements Interpolator { private final float end; private BubblePositionInterpolator(float start, float middle, float end) { - this.start = start; + this.start = start; this.middle = middle; - this.end = end; + this.end = end; } @Override @@ -105,11 +109,11 @@ public float getInterpolation(float input) { } else if (input < middle) { return input; } else { - float segmentLength = end - middle; - float segmentTraveled = input - middle; + float segmentLength = end - middle; + float segmentTraveled = input - middle; float segmentCompletion = segmentTraveled / segmentLength; - float scaleDownFactor = middle / (input * 2); - float output = middle + (segmentLength * segmentCompletion * scaleDownFactor); + float scaleDownFactor = middle / (input * 2); + float output = middle + (segmentLength * segmentCompletion * scaleDownFactor); return Math.min(output, end); } @@ -128,10 +132,10 @@ private static final class ClampingLinearInterpolator implements Interpolator { } ClampingLinearInterpolator(float start, float end, float scale) { - slope = (end - start) * scale; + slope = (end - start) * scale; yIntercept = start; - max = Math.max(start, end); - min = Math.min(start, end); + max = Math.max(start, end); + min = Math.min(start, end); } @Override @@ -139,5 +143,4 @@ public float getInterpolation(float input) { return Util.clamp(slope * input + yIntercept, min, max); } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationTitleView.java b/src/main/java/org/thoughtcrime/securesms/ConversationTitleView.java index b1c3a7203..b687a842f 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationTitleView.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationTitleView.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms; -import android.app.Activity; import android.content.Context; import android.text.TextUtils; import android.util.AttributeSet; @@ -8,14 +7,11 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.components.AvatarView; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -24,16 +20,14 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.Locale; - public class ConversationTitleView extends RelativeLayout { - private View content; - private ImageView back; - private AvatarView avatar; - private TextView title; - private TextView subtitle; - private ImageView ephemeralIcon; + private View content; + private ImageView back; + private AvatarView avatar; + private TextView title; + private TextView subtitle; + private ImageView ephemeralIcon; public ConversationTitleView(Context context) { this(context, null); @@ -41,18 +35,17 @@ public ConversationTitleView(Context context) { public ConversationTitleView(Context context, AttributeSet attrs) { super(context, attrs); - } @Override public void onFinishInflate() { super.onFinishInflate(); - this.back = ViewUtil.findById(this, R.id.up_button); - this.content = ViewUtil.findById(this, R.id.content); - this.title = ViewUtil.findById(this, R.id.title); - this.subtitle = ViewUtil.findById(this, R.id.subtitle); - this.avatar = ViewUtil.findById(this, R.id.avatar); + this.back = ViewUtil.findById(this, R.id.up_button); + this.content = ViewUtil.findById(this, R.id.content); + this.title = ViewUtil.findById(this, R.id.title); + this.subtitle = ViewUtil.findById(this, R.id.subtitle); + this.avatar = ViewUtil.findById(this, R.id.avatar); this.ephemeralIcon = ViewUtil.findById(this, R.id.ephemeral_icon); ViewUtil.setTextViewGravityStart(this.title, getContext()); @@ -75,21 +68,25 @@ public void setTitle(@NonNull GlideRequests glideRequests, @NonNull DcChat dcCha } else if (dcChat.isInBroadcast()) { subtitleStr = context.getString(R.string.channel); } else if (dcChat.isOutBroadcast()) { - subtitleStr = context.getResources().getQuantityString(R.plurals.n_recipients, chatContacts.length, chatContacts.length); - } else if( dcChat.isMultiUser() ) { + subtitleStr = + context + .getResources() + .getQuantityString(R.plurals.n_recipients, chatContacts.length, chatContacts.length); + } else if (dcChat.isMultiUser()) { if (chatContacts.length > 1 || Util.contains(chatContacts, DcContact.DC_CONTACT_ID_SELF)) { - subtitleStr = context.getResources().getQuantityString(R.plurals.n_members, chatContacts.length, chatContacts.length); + subtitleStr = + context + .getResources() + .getQuantityString(R.plurals.n_members, chatContacts.length, chatContacts.length); } else { subtitleStr = "…"; } - } else if( chatContacts.length>=1 ) { - if( dcChat.isSelfTalk() ) { + } else if (chatContacts.length >= 1) { + if (dcChat.isSelfTalk()) { subtitleStr = context.getString(R.string.chat_self_talk_subtitle); - } - else if( dcChat.isDeviceTalk() ) { + } else if (dcChat.isDeviceTalk()) { subtitleStr = context.getString(R.string.device_talk_subtitle); - } - else { + } else { DcContact dcContact = dcContext.getContact(chatContacts[0]); isOnline = dcContact.wasSeenRecently(); if (!dcChat.isEncrypted()) { @@ -101,7 +98,9 @@ else if( dcChat.isDeviceTalk() ) { } else { long timestamp = dcContact.getLastSeen(); if (timestamp >= 0) { - subtitleStr = context.getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(context, timestamp)); + subtitleStr = + context.getString( + R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(context, timestamp)); } } } @@ -109,8 +108,8 @@ else if( dcChat.isDeviceTalk() ) { avatar.setAvatar(glideRequests, new Recipient(getContext(), dcChat), false); avatar.setSeenRecently(isOnline); - int imgLeft = dcChat.isMuted()? R.drawable.ic_volume_off_white_18dp : 0; - int imgRight = dcChat.isSelfTalk() || dcChat.isDeviceTalk()? R.drawable.ic_verified : 0; + int imgLeft = dcChat.isMuted() ? R.drawable.ic_volume_off_white_18dp : 0; + int imgRight = dcChat.isSelfTalk() || dcChat.isDeviceTalk() ? R.drawable.ic_verified : 0; title.setCompoundDrawablesWithIntrinsicBounds(imgLeft, 0, imgRight, 0); if (!TextUtils.isEmpty(subtitleStr)) { subtitle.setText(subtitleStr); @@ -119,7 +118,7 @@ else if( dcChat.isDeviceTalk() ) { subtitle.setVisibility(View.GONE); } boolean isEphemeral = dcContext.getChatEphemeralTimer(chatId) != 0; - ephemeralIcon.setVisibility(isEphemeral? View.VISIBLE : View.GONE); + ephemeralIcon.setVisibility(isEphemeral ? View.VISIBLE : View.GONE); } public void setTitle(@NonNull GlideRequests glideRequests, @NonNull DcContact contact) { diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationUpdateItem.java b/src/main/java/org/thoughtcrime/securesms/ConversationUpdateItem.java index d0fd32e1e..cfda90618 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationUpdateItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationUpdateItem.java @@ -5,14 +5,13 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.util.AttributeSet; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcMsg; - +import java.io.ByteArrayInputStream; +import java.util.Set; import org.json.JSONObject; import org.thoughtcrime.securesms.components.DeliveryStatusView; import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel; @@ -21,14 +20,10 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.JsonUtils; -import java.io.ByteArrayInputStream; -import java.util.Set; - -public class ConversationUpdateItem extends BaseConversationItem -{ - private DeliveryStatusView deliveryStatusView; - private AppCompatImageView appIcon; - private int textColor; +public class ConversationUpdateItem extends BaseConversationItem { + private DeliveryStatusView deliveryStatusView; + private AppCompatImageView appIcon; + private int textColor; public ConversationUpdateItem(Context context) { this(context, null); @@ -44,10 +39,9 @@ public void onFinishInflate() { initializeAttributes(); - bodyText = findViewById(R.id.conversation_update_body); + bodyText = findViewById(R.id.conversation_update_body); deliveryStatusView = new DeliveryStatusView(findViewById(R.id.delivery_indicator)); - appIcon = findViewById(R.id.app_icon); - + appIcon = findViewById(R.id.app_icon); bodyText.setOnLongClickListener(passthroughClickListener); bodyText.setOnClickListener(passthroughClickListener); @@ -58,24 +52,25 @@ public void onFinishInflate() { } @Override - public void bind(@NonNull DcMsg messageRecord, - @NonNull DcChat dcChat, - @NonNull GlideRequests glideRequests, - @NonNull Set batchSelected, - @NonNull Recipient conversationRecipient, - boolean pulseUpdate, - @Nullable AudioPlaybackViewModel playbackViewModel, - AudioView.OnActionListener audioPlayPauseListener) - { + public void bind( + @NonNull DcMsg messageRecord, + @NonNull DcChat dcChat, + @NonNull GlideRequests glideRequests, + @NonNull Set batchSelected, + @NonNull Recipient conversationRecipient, + boolean pulseUpdate, + @Nullable AudioPlaybackViewModel playbackViewModel, + AudioView.OnActionListener audioPlayPauseListener) { bindPartial(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient); setGenericInfoRecord(messageRecord); } private void initializeAttributes() { - final int[] attributes = new int[] { - R.attr.conversation_item_update_text_color, - }; - final TypedArray attrs = context.obtainStyledAttributes(attributes); + final int[] attributes = + new int[] { + R.attr.conversation_item_update_text_color, + }; + final TypedArray attrs = context.obtainStyledAttributes(attributes); textColor = attrs.getColor(0, Color.WHITE); attrs.recycle(); @@ -118,14 +113,13 @@ private void setGenericInfoRecord(DcMsg messageRecord) { bodyText.setText(messageRecord.getDisplayBody()); bodyText.setVisibility(VISIBLE); - if (messageRecord.isFailed()) deliveryStatusView.setFailed(); - else if (!messageRecord.isOutgoing()) deliveryStatusView.setNone(); - else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing(); - else if (messageRecord.isPending()) deliveryStatusView.setPending(); - else deliveryStatusView.setNone(); + if (messageRecord.isFailed()) deliveryStatusView.setFailed(); + else if (!messageRecord.isOutgoing()) deliveryStatusView.setNone(); + else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing(); + else if (messageRecord.isPending()) deliveryStatusView.setPending(); + else deliveryStatusView.setNone(); } @Override - public void unbind() { - } + public void unbind() {} } diff --git a/src/main/java/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/main/java/org/thoughtcrime/securesms/CreateProfileActivity.java index f878d57f9..f8ff97640 100644 --- a/src/main/java/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms; - import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; @@ -15,22 +14,19 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; - import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.loader.app.LoaderManager; - -import com.b44t.messenger.DcContext; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.transition.Transition; -import com.google.android.material.textfield.TextInputLayout; - +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; import org.thoughtcrime.securesms.components.AvatarSelector; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.connect.DcHelper; @@ -43,11 +39,6 @@ import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.ViewUtil; -import java.io.File; -import java.io.IOException; -import java.security.SecureRandom; - - @SuppressLint("StaticFieldLeak") public class CreateProfileActivity extends BaseActionBarActivity { @@ -55,10 +46,10 @@ public class CreateProfileActivity extends BaseActionBarActivity { private static final int REQUEST_CODE_AVATAR = 1; - private InputAwareLayout container; - private ImageView avatar; - private EditText name; - private EditText statusView; + private InputAwareLayout container; + private ImageView avatar; + private EditText name; + private EditText statusView; private boolean avatarChanged; private boolean imageLoaded; @@ -66,7 +57,6 @@ public class CreateProfileActivity extends BaseActionBarActivity { private Bitmap avatarBmp; private AttachmentManager attachmentManager; - @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); @@ -84,17 +74,20 @@ public void onCreate(Bundle bundle) { initializeProfileAvatar(); initializeStatusText(); - getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - if (container.isInputOpen()) { - container.hideCurrentInput(name); - } else { - setEnabled(false); - getOnBackPressedDispatcher().onBackPressed(); - } - } - }); + getOnBackPressedDispatcher() + .addCallback( + this, + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (container.isInputOpen()) { + container.hideCurrentInput(name); + } else { + setEnabled(false); + getOnBackPressedDispatcher().onBackPressed(); + } + } + }); } @Override @@ -119,7 +112,8 @@ public boolean onOptionsItemSelected(MenuItem item) { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } @@ -128,12 +122,12 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != Activity.RESULT_OK) { - return; + return; } switch (requestCode) { case REQUEST_CODE_AVATAR: - Uri inputFile = (data != null ? data.getData() : null); + Uri inputFile = (data != null ? data.getData() : null); onFileSelected(inputFile); break; @@ -145,26 +139,28 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { private void setAvatarView(Uri output) { GlideApp.with(this) - .asBitmap() - .load(output) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE) - .into(new SimpleTarget() { + .asBitmap() + .load(output) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE) + .into( + new SimpleTarget() { @Override - public void onResourceReady(@NonNull Bitmap resource, Transition transition) { + public void onResourceReady( + @NonNull Bitmap resource, Transition transition) { avatarChanged = true; imageLoaded = true; avatarBmp = resource; } }); GlideApp.with(this) - .load(output) - .circleCrop() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(avatar); + .load(output) + .circleCrop() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(avatar); } private void onFileSelected(Uri inputFile) { @@ -176,11 +172,11 @@ private void onFileSelected(Uri inputFile) { } private void initializeResources() { - TextView loginSuccessText = ViewUtil.findById(this, R.id.login_success_text); - this.avatar = ViewUtil.findById(this, R.id.avatar); - this.name = ViewUtil.findById(this, R.id.name_text); - this.container = ViewUtil.findById(this, R.id.container); - this.statusView = ViewUtil.findById(this, R.id.status_text); + TextView loginSuccessText = ViewUtil.findById(this, R.id.login_success_text); + this.avatar = ViewUtil.findById(this, R.id.avatar); + this.name = ViewUtil.findById(this, R.id.name_text); + this.container = ViewUtil.findById(this, R.id.container); + this.statusView = ViewUtil.findById(this, R.id.status_text); // add padding to avoid content hidden behind system bars ViewUtil.applyWindowInsets(container); @@ -189,7 +185,7 @@ private void initializeResources() { } private void initializeProfileName() { - String profileName = DcHelper.get(this, DcHelper.CONFIG_DISPLAY_NAME); + String profileName = DcHelper.get(this, DcHelper.CONFIG_DISPLAY_NAME); if (!TextUtils.isEmpty(profileName)) { name.setText(profileName); name.setSelection(profileName.length(), profileName.length()); @@ -200,18 +196,21 @@ private void initializeProfileAvatar() { File avatarFile = AvatarHelper.getSelfAvatarFile(this); if (avatarFile.exists() && avatarFile.length() > 0) { imageLoaded = true; - GlideApp.with(this) - .load(avatarFile) - .circleCrop() - .into(avatar); + GlideApp.with(this).load(avatarFile).circleCrop().into(avatar); } else { imageLoaded = false; - avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(this, getResources().getColor(R.color.grey_400))); + avatar.setImageDrawable( + new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp) + .asDrawable(this, getResources().getColor(R.color.grey_400))); } - avatar.setOnClickListener(view -> - new AvatarSelector(this, LoaderManager.getInstance(this), new AvatarSelectedListener(), imageLoaded) - .show(this, avatar) - ); + avatar.setOnClickListener( + view -> + new AvatarSelector( + this, + LoaderManager.getInstance(this), + new AvatarSelectedListener(), + imageLoaded) + .show(this, avatar)); } private void initializeStatusText() { @@ -229,7 +228,7 @@ private void updateProfile() { new AsyncTask() { @Override protected Boolean doInBackground(Void... params) { - Context context = CreateProfileActivity.this; + Context context = CreateProfileActivity.this; DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name); setStatusText(); @@ -253,7 +252,7 @@ public void onPostExecute(Boolean result) { if (result) { attachmentManager.cleanup(); finish(); - } else { + } else { Toast.makeText(CreateProfileActivity.this, R.string.error, Toast.LENGTH_LONG).show(); } } @@ -276,7 +275,10 @@ public void onClick(int type) { avatarBmp = null; imageLoaded = false; avatarChanged = true; - avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(CreateProfileActivity.this, getResources().getColor(R.color.grey_400))); + avatar.setImageDrawable( + new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp) + .asDrawable( + CreateProfileActivity.this, getResources().getColor(R.color.grey_400))); break; case AvatarSelector.TAKE_PHOTO: attachmentManager.capturePhoto(CreateProfileActivity.this, REQUEST_CODE_AVATAR); diff --git a/src/main/java/org/thoughtcrime/securesms/DummyActivity.java b/src/main/java/org/thoughtcrime/securesms/DummyActivity.java index d2a108c92..08a6c03bd 100644 --- a/src/main/java/org/thoughtcrime/securesms/DummyActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/DummyActivity.java @@ -4,7 +4,8 @@ import android.os.Bundle; // dummy activity that just pushes the app to foreground when fired. -// can also be used to work around android bug https://code.google.com/p/android/issues/detail?id=53313 +// can also be used to work around android bug +// https://code.google.com/p/android/issues/detail?id=53313 public class DummyActivity extends Activity { @Override public void onCreate(Bundle bundle) { diff --git a/src/main/java/org/thoughtcrime/securesms/EphemeralMessagesDialog.java b/src/main/java/org/thoughtcrime/securesms/EphemeralMessagesDialog.java index 338660eef..34ab45a29 100644 --- a/src/main/java/org/thoughtcrime/securesms/EphemeralMessagesDialog.java +++ b/src/main/java/org/thoughtcrime/securesms/EphemeralMessagesDialog.java @@ -6,99 +6,120 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.widget.TextViewCompat; - +import java.util.concurrent.TimeUnit; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.concurrent.TimeUnit; - public class EphemeralMessagesDialog { - private final static String TAG = EphemeralMessagesDialog.class.getSimpleName(); - - public static void show(final Context context, int currentSelectedTime, final @NonNull EphemeralMessagesInterface listener) { - CharSequence[] choices = context.getResources().getStringArray(R.array.ephemeral_message_durations); - int preselected = getPreselection(currentSelectedTime); - final int[] selectedChoice = new int[]{preselected}; - - View dialogView = View.inflate(context, R.layout.dialog_extended_options, null); - RadioGroup container = dialogView.findViewById(R.id.optionsContainer); - for (CharSequence choice : choices) { - - RadioButton radioButton = new RadioButton(context); - radioButton.setText(choice); - TextViewCompat.setTextAppearance(radioButton, android.R.style.TextAppearance_Medium); - - RadioGroup.LayoutParams params = new RadioGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMargins(0, 0, 0, ViewUtil.dpToPx(context, 8)); - radioButton.setLayoutParams(params); - container.addView(radioButton); - } + private static final String TAG = EphemeralMessagesDialog.class.getSimpleName(); + + public static void show( + final Context context, + int currentSelectedTime, + final @NonNull EphemeralMessagesInterface listener) { + CharSequence[] choices = + context.getResources().getStringArray(R.array.ephemeral_message_durations); + int preselected = getPreselection(currentSelectedTime); + final int[] selectedChoice = new int[] {preselected}; + + View dialogView = View.inflate(context, R.layout.dialog_extended_options, null); + RadioGroup container = dialogView.findViewById(R.id.optionsContainer); + for (CharSequence choice : choices) { + + RadioButton radioButton = new RadioButton(context); + radioButton.setText(choice); + TextViewCompat.setTextAppearance(radioButton, android.R.style.TextAppearance_Medium); + + RadioGroup.LayoutParams params = + new RadioGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, ViewUtil.dpToPx(context, 8)); + radioButton.setLayoutParams(params); + container.addView(radioButton); + } - container.setOnCheckedChangeListener((group, checkedId) -> { - int childCount = group.getChildCount(); - for (int x = 0; x < childCount; x++) { - RadioButton btn = (RadioButton) group.getChildAt(x); - if (btn.getId() == checkedId) { - selectedChoice[0] = x; - } + container.setOnCheckedChangeListener( + (group, checkedId) -> { + int childCount = group.getChildCount(); + for (int x = 0; x < childCount; x++) { + RadioButton btn = (RadioButton) group.getChildAt(x); + if (btn.getId() == checkedId) { + selectedChoice[0] = x; } + } }); - container.check(container.getChildAt(preselected).getId()); - - TextView messageView = dialogView.findViewById(R.id.description); - messageView.setText(context.getString(R.string.ephemeral_messages_hint)); - - AlertDialog.Builder builder = new AlertDialog.Builder(context) - .setTitle(R.string.ephemeral_messages) - .setView(dialogView) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.ok, (dialog, which) -> { - final long burnAfter; - switch (selectedChoice[0]) { - case 1: burnAfter = TimeUnit.MINUTES.toSeconds(5); break; - case 2: burnAfter = TimeUnit.HOURS.toSeconds(1); break; - case 3: burnAfter = TimeUnit.DAYS.toSeconds(1); break; - case 4: burnAfter = TimeUnit.DAYS.toSeconds(7); break; - case 5: burnAfter = TimeUnit.DAYS.toSeconds(35); break; - case 6: burnAfter = TimeUnit.DAYS.toSeconds(365); break; - default: burnAfter = 0; break; - } - listener.onTimeSelected(burnAfter); + container.check(container.getChildAt(preselected).getId()); + + TextView messageView = dialogView.findViewById(R.id.description); + messageView.setText(context.getString(R.string.ephemeral_messages_hint)); + + AlertDialog.Builder builder = + new AlertDialog.Builder(context) + .setTitle(R.string.ephemeral_messages) + .setView(dialogView) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton( + R.string.ok, + (dialog, which) -> { + final long burnAfter; + switch (selectedChoice[0]) { + case 1: + burnAfter = TimeUnit.MINUTES.toSeconds(5); + break; + case 2: + burnAfter = TimeUnit.HOURS.toSeconds(1); + break; + case 3: + burnAfter = TimeUnit.DAYS.toSeconds(1); + break; + case 4: + burnAfter = TimeUnit.DAYS.toSeconds(7); + break; + case 5: + burnAfter = TimeUnit.DAYS.toSeconds(35); + break; + case 6: + burnAfter = TimeUnit.DAYS.toSeconds(365); + break; + default: + burnAfter = 0; + break; + } + listener.onTimeSelected(burnAfter); }) - .setNeutralButton(R.string.learn_more, (d, w) -> DcHelper.openHelp(context, "#ephemeralmsgs")); - builder.show(); + .setNeutralButton( + R.string.learn_more, (d, w) -> DcHelper.openHelp(context, "#ephemeralmsgs")); + builder.show(); + } + + public interface EphemeralMessagesInterface { + void onTimeSelected(long duration); + } + + private static int getPreselection(int timespan) { + if (timespan == 0) { + return 0; // off } - - public interface EphemeralMessagesInterface { - void onTimeSelected(long duration); + // Choose timespan close to the current one out of available options. + if (timespan < TimeUnit.HOURS.toSeconds(1)) { + return 1; // 5 minutes } - - private static int getPreselection(int timespan) { - if (timespan == 0) { - return 0; // off - } - // Choose timespan close to the current one out of available options. - if (timespan < TimeUnit.HOURS.toSeconds(1)) { - return 1; // 5 minutes - } - if (timespan < TimeUnit.DAYS.toSeconds(1)) { - return 2; // 1 hour - } - if (timespan < TimeUnit.DAYS.toSeconds(7)) { - return 3; // 1 day - } - if (timespan < TimeUnit.DAYS.toSeconds(35)) { - return 4; // 1 week - } - if (timespan < TimeUnit.DAYS.toSeconds(365)) { - return 5; // 5 weeks - } - return 6; // 1 year + if (timespan < TimeUnit.DAYS.toSeconds(1)) { + return 2; // 1 hour } - + if (timespan < TimeUnit.DAYS.toSeconds(7)) { + return 3; // 1 day + } + if (timespan < TimeUnit.DAYS.toSeconds(35)) { + return 4; // 1 week + } + if (timespan < TimeUnit.DAYS.toSeconds(365)) { + return 5; // 5 weeks + } + return 6; // 1 year + } } diff --git a/src/main/java/org/thoughtcrime/securesms/FullMsgActivity.java b/src/main/java/org/thoughtcrime/securesms/FullMsgActivity.java index 017dc6e59..be195c9b5 100644 --- a/src/main/java/org/thoughtcrime/securesms/FullMsgActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/FullMsgActivity.java @@ -5,29 +5,23 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.Toast; import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; - +import android.widget.Toast; import androidx.appcompat.app.AlertDialog; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.types.HttpResponse; import com.b44t.messenger.DcContext; - +import java.io.ByteArrayInputStream; +import java.lang.ref.WeakReference; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.Util; -import java.io.ByteArrayInputStream; -import java.lang.ref.WeakReference; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.types.HttpResponse; - -public class FullMsgActivity extends WebViewActivity -{ +public class FullMsgActivity extends WebViewActivity { public static final String MSG_ID_EXTRA = "msg_id"; public static final String BLOCK_LOADING_REMOTE = "block_loading_remote"; private String imageUrl; @@ -91,7 +85,8 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMen WebView.HitTestResult result = ((WebView) v).getHitTestResult(); if (result != null) { int type = result.getType(); - if (type == WebView.HitTestResult.IMAGE_TYPE || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + if (type == WebView.HitTestResult.IMAGE_TYPE + || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { imageUrl = result.getExtra(); if (!imageUrl.startsWith("data:")) { super.onCreateContextMenu(menu, v, menuInfo); @@ -120,24 +115,30 @@ public boolean onContextItemSelected(MenuItem item) { } private static void loadHtmlAsync(final WeakReference activityReference) { - Util.runOnBackground(() -> { - try { - FullMsgActivity activity = activityReference.get(); - String html = activity.dcContext.getMsgHtml(activity.msgId); - - activity.runOnUiThread(() -> { + Util.runOnBackground( + () -> { try { - // a base URL is needed, otherwise clicking links that reference document sections will not jump to sections - activityReference.get().webView.loadDataWithBaseURL("file://index.html", html, "text/html", null, null); + FullMsgActivity activity = activityReference.get(); + String html = activity.dcContext.getMsgHtml(activity.msgId); + + activity.runOnUiThread( + () -> { + try { + // a base URL is needed, otherwise clicking links that reference document + // sections will not jump to sections + activityReference + .get() + .webView + .loadDataWithBaseURL("file://index.html", html, "text/html", null, null); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } catch (Exception e) { e.printStackTrace(); } }); - - } catch (Exception e) { - e.printStackTrace(); - } - }); } @Override @@ -159,17 +160,21 @@ public boolean onPrepareOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); if (item.getItemId() == R.id.load_remote_content) { - AlertDialog.Builder builder = new AlertDialog.Builder(this) - .setTitle(R.string.load_remote_content) - .setMessage(R.string.load_remote_content_ask); + AlertDialog.Builder builder = + new AlertDialog.Builder(this) + .setTitle(R.string.load_remote_content) + .setMessage(R.string.load_remote_content_ask); // we are using the buttons "[Always] [Never][Once]" in that order. // 1. Checkmarks before [Always] and [Never] show the current state. // 2. [Once] is also shown in always-mode and disables always-mode if selected - // (there was the idea to hide [Once] in always mode, but that looks more like a bug in the end) - // (maybe a usual Always-Checkbox and "[Cancel][OK]" buttons are an alternative, however, a [Once] + // (there was the idea to hide [Once] in always mode, but that looks more like a bug in the + // end) + // (maybe a usual Always-Checkbox and "[Cancel][OK]" buttons are an alternative, however, a + // [Once] // would be required as well - probably as the leftmost button which is not that usable in - // not-always-mode where the dialog is used more often. Or [Ok] would mean "Once" as well as "Change checkbox setting", + // not-always-mode where the dialog is used more often. Or [Ok] would mean "Once" as well as + // "Change checkbox setting", // which is also a bit weird. Anyway, let's give the three buttons a try :) final String checkmark = DynamicTheme.getCheckmarkEmoji(this) + " "; String alwaysCheckmark = ""; @@ -184,10 +189,16 @@ public boolean onOptionsItemSelected(MenuItem item) { } if (!blockLoadingRemote) { - builder.setNeutralButton(alwaysCheckmark + getString(R.string.always), (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ALWAYS)); + builder.setNeutralButton( + alwaysCheckmark + getString(R.string.always), + (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ALWAYS)); } - builder.setNegativeButton(neverCheckmark + getString(blockLoadingRemote ? R.string.no : R.string.never), (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.NEVER)); - builder.setPositiveButton(onceCheckmark + getString(R.string.once), (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ONCE)); + builder.setNegativeButton( + neverCheckmark + getString(blockLoadingRemote ? R.string.no : R.string.never), + (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.NEVER)); + builder.setPositiveButton( + onceCheckmark + getString(R.string.once), + (dialog, which) -> onChangeLoadRemoteContent(LoadRemoteContent.ONCE)); builder.show(); return true; @@ -234,10 +245,15 @@ protected WebResourceResponse interceptRequest(String url) { mimeType = "application/octet-stream"; } byte[] blob = JsonUtils.decodeBase64(httpResponse.blob); - res = new WebResourceResponse(mimeType, httpResponse.encoding, new ByteArrayInputStream(blob)); + res = + new WebResourceResponse(mimeType, httpResponse.encoding, new ByteArrayInputStream(blob)); } catch (Exception e) { e.printStackTrace(); - res = new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes())); + res = + new WebResourceResponse( + "text/plain", + "UTF-8", + new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes())); } return res; } diff --git a/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java index 8a97369a3..264e4a247 100644 --- a/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -16,18 +16,20 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.loader.app.LoaderManager; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; - +import java.io.File; +import java.util.ArrayList; +import java.util.Objects; import org.thoughtcrime.securesms.components.AvatarSelector; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; @@ -41,16 +43,8 @@ import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ViewUtil; -import java.io.File; -import java.util.ArrayList; -import java.util.Objects; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - public class GroupCreateActivity extends PassphraseRequiredActionBarActivity - implements ItemClickListener -{ + implements ItemClickListener { private static final String TAG = GroupCreateActivity.class.getSimpleName(); public static final String EDIT_GROUP_CHAT_ID = "edit_group_chat_id"; @@ -65,15 +59,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity private boolean unencrypted; private boolean broadcast; - private EditText groupName; - private EditText chatDescription; - private ListView lv; - private ImageView avatar; - private Bitmap avatarBmp; - private int groupChatId; - private boolean isEdit; - private boolean avatarChanged; - private boolean imageLoaded; + private EditText groupName; + private EditText chatDescription; + private ListView lv; + private ImageView avatar; + private Bitmap avatarBmp; + private int groupChatId; + private boolean isEdit; + private boolean avatarChanged; + private boolean imageLoaded; private AttachmentManager attachmentManager; @Override @@ -91,7 +85,7 @@ protected void onCreate(Bundle state, boolean ready) { // groupChatId may be set during creation, // so always check isEdit() - if(groupChatId !=0) { + if (groupChatId != 0) { isEdit = true; DcChat dcChat = dcContext.getChat(groupChatId); broadcast = dcChat.isOutBroadcast(); @@ -100,7 +94,7 @@ protected void onCreate(Bundle state, boolean ready) { int chatId = getIntent().getIntExtra(CLONE_CHAT_EXTRA, 0); if (chatId != 0) { - DcChat dcChat = dcContext.getChat(chatId); + DcChat dcChat = dcContext.getChat(chatId); broadcast = dcChat.isOutBroadcast(); unencrypted = !dcChat.isEncrypted(); } @@ -125,27 +119,24 @@ private void updateViewState() { groupName.setEnabled(true); String title; - if(isEdit()) { + if (isEdit()) { title = getString(R.string.global_menu_edit_desktop); - } - else if(broadcast) { + } else if (broadcast) { title = getString(R.string.new_channel); - } - else if(unencrypted) { + } else if (unencrypted) { title = getString(R.string.new_email); - } - else { + } else { title = getString(R.string.menu_new_group); } getSupportActionBar().setTitle(title); } private void initializeResources() { - lv = ViewUtil.findById(this, R.id.selected_contacts_list); - avatar = ViewUtil.findById(this, R.id.avatar); - groupName = ViewUtil.findById(this, R.id.group_name); - chatDescription = ViewUtil.findById(this, R.id.chat_description); - TextView chatHints = ViewUtil.findById(this, R.id.chat_hints); + lv = ViewUtil.findById(this, R.id.selected_contacts_list); + avatar = ViewUtil.findById(this, R.id.avatar); + groupName = ViewUtil.findById(this, R.id.group_name); + chatDescription = ViewUtil.findById(this, R.id.chat_description); + TextView chatHints = ViewUtil.findById(this, R.id.chat_hints); // add padding to avoid content hidden behind system bars ViewUtil.applyWindowInsets(lv, false, false, false, true); @@ -154,7 +145,8 @@ private void initializeResources() { initializeAvatarView(); - SelectedContactsAdapter adapter = new SelectedContactsAdapter(this, GlideApp.with(this), broadcast); + SelectedContactsAdapter adapter = + new SelectedContactsAdapter(this, GlideApp.with(this), broadcast); adapter.setItemClickListener(this); lv.setAdapter(adapter); @@ -189,7 +181,7 @@ private void initializeResources() { chatHints.setVisibility(View.GONE); } - if(isEdit()) { + if (isEdit()) { groupName.setText(dcContext.getChat(groupChatId).getName()); lv.setVisibility(View.GONE); @@ -210,19 +202,22 @@ private void initializeAvatarView() { File avatarFile = new File(avatarPath); if (avatarFile.exists()) { imageLoaded = true; - GlideApp.with(this) - .load(avatarFile) - .circleCrop() - .into(avatar); + GlideApp.with(this).load(avatarFile).circleCrop().into(avatar); } } if (!imageLoaded) { - avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_white_24dp).asDrawable(this, ThemeUtil.getDummyContactColor(this))); + avatar.setImageDrawable( + new ResourceContactPhoto(R.drawable.ic_group_white_24dp) + .asDrawable(this, ThemeUtil.getDummyContactColor(this))); } - avatar.setOnClickListener(view -> - new AvatarSelector(this, LoaderManager.getInstance(this), new AvatarSelectedListener(), imageLoaded) - .show(this, avatar) - ); + avatar.setOnClickListener( + view -> + new AvatarSelector( + this, + LoaderManager.getInstance(this), + new AvatarSelectedListener(), + imageLoaded) + .show(this, avatar)); } @Override @@ -299,7 +294,7 @@ private void createGroup(String groupName) { for (int contactId : getAdapter().getContacts()) { dcContext.addContactToChat(groupChatId, contactId); } - if (avatarBmp!=null) { + if (avatarBmp != null) { AvatarHelper.setGroupAvatar(this, groupChatId, avatarBmp); } @@ -311,8 +306,9 @@ private void createGroup(String groupName) { } private boolean showGroupNameEmptyToast(String groupName) { - if(groupName == null) { - Toast.makeText(this, getString(R.string.group_please_enter_group_name), Toast.LENGTH_LONG).show(); + if (groupName == null) { + Toast.makeText(this, getString(R.string.group_please_enter_group_name), Toast.LENGTH_LONG) + .show(); return true; } return false; @@ -342,14 +338,14 @@ private void updateGroup(String groupName) { } private SelectedContactsAdapter getAdapter() { - return (SelectedContactsAdapter)lv.getAdapter(); + return (SelectedContactsAdapter) lv.getAdapter(); } private @Nullable String getGroupName() { String ret = groupName.getText() != null ? groupName.getText().toString() : null; - if(ret!=null) { + if (ret != null) { ret = ret.trim(); - if(ret.isEmpty()) { + if (ret.isEmpty()) { ret = null; } } @@ -361,7 +357,8 @@ private String getChatDescription() { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } @@ -370,19 +367,20 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis public void onActivityResult(int reqCode, int resultCode, final Intent data) { super.onActivityResult(reqCode, resultCode, data); - if (resultCode != Activity.RESULT_OK) - return; + if (resultCode != Activity.RESULT_OK) return; switch (reqCode) { case REQUEST_CODE_AVATAR: - Uri inputFile = (data != null ? data.getData() : null); + Uri inputFile = (data != null ? data.getData() : null); onFileSelected(inputFile); break; case PICK_CONTACT: ArrayList contactIds = new ArrayList<>(); - for (Integer contactId : Objects.requireNonNull(data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA))) { - if(contactId != null) { + for (Integer contactId : + Objects.requireNonNull( + data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA))) { + if (contactId != null) { contactIds.add(contactId); } } @@ -397,15 +395,17 @@ public void onActivityResult(int reqCode, int resultCode, final Intent data) { private void setAvatarView(Uri output) { GlideApp.with(this) - .asBitmap() - .load(output) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE) - .into(new CustomTarget() { + .asBitmap() + .load(output) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE) + .into( + new CustomTarget() { @Override - public void onResourceReady(@NonNull Bitmap resource, Transition transition) { + public void onResourceReady( + @NonNull Bitmap resource, Transition transition) { setAvatar(output, resource); } @@ -419,18 +419,17 @@ private void setAvatar(T model, Bitmap bitmap) { avatarChanged = true; imageLoaded = true; GlideApp.with(this) - .load(model) - .circleCrop() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(avatar); + .load(model) + .circleCrop() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(avatar); } private boolean isEdit() { return isEdit; } - private class AvatarSelectedListener implements AvatarSelector.AttachmentClickedListener { @Override public void onClick(int type) { @@ -442,7 +441,11 @@ public void onClick(int type) { avatarBmp = null; imageLoaded = false; avatarChanged = true; - avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_white_24dp).asDrawable(GroupCreateActivity.this, ThemeUtil.getDummyContactColor(GroupCreateActivity.this))); + avatar.setImageDrawable( + new ResourceContactPhoto(R.drawable.ic_group_white_24dp) + .asDrawable( + GroupCreateActivity.this, + ThemeUtil.getDummyContactColor(GroupCreateActivity.this))); break; case AvatarSelector.TAKE_PHOTO: attachmentManager.capturePhoto(GroupCreateActivity.this, REQUEST_CODE_AVATAR); diff --git a/src/main/java/org/thoughtcrime/securesms/InstantOnboardingActivity.java b/src/main/java/org/thoughtcrime/securesms/InstantOnboardingActivity.java index df2497536..40be3609d 100644 --- a/src/main/java/org/thoughtcrime/securesms/InstantOnboardingActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/InstantOnboardingActivity.java @@ -3,7 +3,6 @@ import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_ENABLED; import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_PROXY_URL; -import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -12,7 +11,6 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; @@ -24,12 +22,12 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.loader.app.LoaderManager; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcLot; @@ -38,7 +36,12 @@ import com.bumptech.glide.request.transition.Transition; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.thoughtcrime.securesms.components.AvatarSelector; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -59,17 +62,8 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.ProgressDialog; -import java.io.File; -import java.io.IOException; -import java.security.SecureRandom; -import java.util.Objects; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - -public class InstantOnboardingActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate { +public class InstantOnboardingActivity extends BaseActionBarActivity + implements DcEventCenter.DcEventDelegate { private static final String TAG = InstantOnboardingActivity.class.getSimpleName(); private static final String DCACCOUNT = "dcaccount"; @@ -90,7 +84,7 @@ public class InstantOnboardingActivity extends BaseActionBarActivity implements private String providerHost; private String providerQrData; private String rawQrData; - private DcLot parsedQrData; + private DcLot parsedQrData; private boolean isDcLogin; private boolean isContactInvitation; private boolean isGroupInvitation; @@ -111,14 +105,16 @@ public void onCreate(Bundle bundle) { setContentView(R.layout.instant_onboarding_activity); - Objects.requireNonNull(getSupportActionBar()).setTitle(R.string.onboarding_create_instant_account); + Objects.requireNonNull(getSupportActionBar()) + .setTitle(R.string.onboarding_create_instant_account); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - boolean configured = DcHelper.getContext(this).isConfigured() == 1; + boolean configured = DcHelper.getContext(this).isConfigured() == 1; if (configured) { // if account is configured it means we didn't come from Welcome screen nor from QR scanner, - // instead, user clicked a dcaccount:// URI directly, so we need to just offer to add a new relay + // instead, user clicked a dcaccount:// URI directly, so we need to just offer to add a new + // relay Uri uri = getIntent().getData(); if (uri != null) { Intent intent = new Intent(this, RelayListActivity.class); @@ -157,7 +153,8 @@ public boolean onPrepareOptionsMenu(Menu menu) { proxyItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } else { boolean proxyEnabled = DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1; - proxyItem.setIcon(proxyEnabled? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24); + proxyItem.setIcon( + proxyEnabled ? R.drawable.ic_proxy_enabled_24 : R.drawable.ic_proxy_disabled_24); proxyItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); } return super.onPrepareOptionsMenu(menu); @@ -187,12 +184,12 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != RESULT_OK) { - return; + return; } switch (requestCode) { case REQUEST_CODE_AVATAR: - Uri inputFile = (data != null ? data.getData() : null); + Uri inputFile = (data != null ? data.getData() : null); onFileSelected(inputFile); break; @@ -217,7 +214,7 @@ private void setProviderFromQr(String rawQr) { DcLot qrParsed = dcContext.checkQr(rawQr); switch (qrParsed.getState()) { case DcContext.DC_QR_LOGIN: - isDcLogin = true; // Intentional fall-through + isDcLogin = true; // Intentional fall-through case DcContext.DC_QR_ACCOUNT: providerHost = qrParsed.getText1(); providerQrData = rawQr; @@ -237,14 +234,15 @@ private void setProviderFromQr(String rawQr) { break; default: new AlertDialog.Builder(this) - .setMessage(R.string.qraccount_qr_code_cannot_be_used) - .setPositiveButton(R.string.ok, null) - .show(); + .setMessage(R.string.qraccount_qr_code_cannot_be_used) + .setPositiveButton(R.string.ok, null) + .show(); } } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } @@ -254,12 +252,15 @@ protected void onPause() { super.onPause(); // Save display name and avatar in the unconfigured profile. - // If the currently selected profile is configured, then this means that rollbackAccountCreation() - // was called (see handleOnBackPressed() above), i.e. the newly created profile was removed already + // If the currently selected profile is configured, then this means that + // rollbackAccountCreation() + // was called (see handleOnBackPressed() above), i.e. the newly created profile was removed + // already // and we can't save the display name & avatar. if (DcHelper.getContext(this).isConfigured() == 0) { final String displayName = name.getText().toString(); - DcHelper.set(this, DcHelper.CONFIG_DISPLAY_NAME, TextUtils.isEmpty(displayName) ? null : displayName); + DcHelper.set( + this, DcHelper.CONFIG_DISPLAY_NAME, TextUtils.isEmpty(displayName) ? null : displayName); if (avatarChanged) { try { @@ -291,7 +292,8 @@ private void handleIntent() { Uri uri = getIntent().getData(); if (uri == null) return; - if (uri.getScheme().equalsIgnoreCase(DCACCOUNT) || uri.getScheme().equalsIgnoreCase(DCLOGIN)) { + if (uri.getScheme().equalsIgnoreCase(DCACCOUNT) + || uri.getScheme().equalsIgnoreCase(DCLOGIN)) { setProviderFromQr(uri.toString()); } } @@ -299,29 +301,31 @@ private void handleIntent() { private void setAvatarView(Uri output) { GlideApp.with(this) - .asBitmap() - .load(output) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE) - .into(new CustomTarget() { - @Override - public void onResourceReady(@NonNull Bitmap resource, Transition transition) { - avatarChanged = true; - imageLoaded = true; - avatarBmp = resource; - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) {} - }); + .asBitmap() + .load(output) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .override(AvatarHelper.AVATAR_SIZE, AvatarHelper.AVATAR_SIZE) + .into( + new CustomTarget() { + @Override + public void onResourceReady( + @NonNull Bitmap resource, Transition transition) { + avatarChanged = true; + imageLoaded = true; + avatarBmp = resource; + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) {} + }); GlideApp.with(this) - .load(output) - .circleCrop() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(avatar); + .load(output) + .circleCrop() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(avatar); } private void onFileSelected(Uri inputFile) { @@ -333,35 +337,43 @@ private void onFileSelected(Uri inputFile) { } private void initializeResources() { - this.avatar = findViewById(R.id.avatar); - this.name = findViewById(R.id.name_text); - this.invitationText = findViewById(R.id.invitation_label); + this.avatar = findViewById(R.id.avatar); + this.name = findViewById(R.id.name_text); + this.invitationText = findViewById(R.id.invitation_label); this.privacyPolicyBtn = findViewById(R.id.privacy_policy_button); - this.signUpBtn = findViewById(R.id.signup_button); + this.signUpBtn = findViewById(R.id.signup_button); // add padding to avoid content hidden behind system bars ViewUtil.applyWindowInsets(findViewById(R.id.container)); - privacyPolicyBtn.setOnClickListener(view -> { - if (!isDcLogin) { - IntentUtils.showInBrowser(this, "https://" + providerHost + "/privacy.html"); - } - }); + privacyPolicyBtn.setOnClickListener( + view -> { + if (!isDcLogin) { + IntentUtils.showInBrowser(this, "https://" + providerHost + "/privacy.html"); + } + }); signUpBtn.setOnClickListener(view -> createProfile()); Button otherServerButton = findViewById(R.id.use_other_server); otherServerButton.setText( - TextUtil.markAsExternal(getString(R.string.instant_onboarding_other_server))); - otherServerButton.setOnClickListener((v) -> { - IntentUtils.showInBrowser(this, INSTANCES_URL); - }); - findViewById(R.id.login_button).setOnClickListener((v) -> { - startActivity(new Intent(this, EditRelayActivity.class)); - }); - findViewById(R.id.scan_qr_button).setOnClickListener((v) -> { - new IntentIntegrator(this).setCaptureActivity(RegistrationQrActivity.class).initiateScan(); - }); + TextUtil.markAsExternal(getString(R.string.instant_onboarding_other_server))); + otherServerButton.setOnClickListener( + (v) -> { + IntentUtils.showInBrowser(this, INSTANCES_URL); + }); + findViewById(R.id.login_button) + .setOnClickListener( + (v) -> { + startActivity(new Intent(this, EditRelayActivity.class)); + }); + findViewById(R.id.scan_qr_button) + .setOnClickListener( + (v) -> { + new IntentIntegrator(this) + .setCaptureActivity(RegistrationQrActivity.class) + .initiateScan(); + }); } private void updateProvider() { @@ -372,16 +384,18 @@ private void updateProvider() { } else { signUpBtn.setText(R.string.instant_onboarding_create); - try (TypedArray typedArray = obtainStyledAttributes(new int[]{R.attr.colorAccent})) { + try (TypedArray typedArray = obtainStyledAttributes(new int[] {R.attr.colorAccent})) { privacyPolicyBtn.setTextColor(typedArray.getColor(0, Color.BLACK)); } if (DEFAULT_CHATMAIL_HOST.equals(providerHost)) { - privacyPolicyBtn.setText(TextUtil.markAsExternal( - getString(R.string.instant_onboarding_agree_default2, providerHost))); + privacyPolicyBtn.setText( + TextUtil.markAsExternal( + getString(R.string.instant_onboarding_agree_default2, providerHost))); } else { - privacyPolicyBtn.setText(TextUtil.markAsExternal( - getString(R.string.instant_onboarding_agree_instance, providerHost))); + privacyPolicyBtn.setText( + TextUtil.markAsExternal( + getString(R.string.instant_onboarding_agree_instance, providerHost))); } if (parsedQrData != null) { @@ -395,7 +409,6 @@ private void updateProvider() { invitationText.setVisibility(View.VISIBLE); } } - } } @@ -406,11 +419,18 @@ private void initializeProfile() { GlideApp.with(this).load(avatarFile).circleCrop().into(avatar); } else { imageLoaded = false; - avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(this, getResources().getColor(R.color.grey_400))); + avatar.setImageDrawable( + new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp) + .asDrawable(this, getResources().getColor(R.color.grey_400))); } - avatar.setOnClickListener(view -> - new AvatarSelector(this, LoaderManager.getInstance(this), new AvatarSelectedListener(), imageLoaded).show(this, avatar) - ); + avatar.setOnClickListener( + view -> + new AvatarSelector( + this, + LoaderManager.getInstance(this), + new AvatarSelectedListener(), + imageLoaded) + .show(this, avatar)); name.setText(DcHelper.get(this, DcHelper.CONFIG_DISPLAY_NAME)); } @@ -427,14 +447,15 @@ public void handleEvent(@NonNull DcEvent event) { if (eventId == DcContext.DC_EVENT_CONFIGURE_PROGRESS) { long progress = event.getData1Int(); - progressUpdate((int)progress); + progressUpdate((int) progress); } } private void progressUpdate(int progress) { int percent = progress / 10; if (progressDialog != null) { - progressDialog.setMessage(getResources().getString(R.string.one_moment)+String.format(" %d%%", percent)); + progressDialog.setMessage( + getResources().getString(R.string.one_moment) + String.format(" %d%%", percent)); } } @@ -472,35 +493,38 @@ private void createProfile() { } final String name = this.name.getText().toString(); - executor.execute(() -> { - Context context = InstantOnboardingActivity.this; - DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name); - - boolean result = true; - if (avatarChanged) { - try { - AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp); - Prefs.setProfileAvatarId(InstantOnboardingActivity.this, new SecureRandom().nextInt()); - } catch (IOException e) { - Log.w(TAG, e); - result = false; - } - } + executor.execute( + () -> { + Context context = InstantOnboardingActivity.this; + DcHelper.set(context, DcHelper.CONFIG_DISPLAY_NAME, name); + + boolean result = true; + if (avatarChanged) { + try { + AvatarHelper.setSelfAvatar(InstantOnboardingActivity.this, avatarBmp); + Prefs.setProfileAvatarId( + InstantOnboardingActivity.this, new SecureRandom().nextInt()); + } catch (IOException e) { + Log.w(TAG, e); + result = false; + } + } - boolean finalResult = result; - runOnUiThread(() -> { - if (finalResult) { - attachmentManager.cleanup(); - startQrAccountCreation(providerQrData); - } else { - Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG).show(); - } - }); - }); + boolean finalResult = result; + runOnUiThread( + () -> { + if (finalResult) { + attachmentManager.cleanup(); + startQrAccountCreation(providerQrData); + } else { + Toast.makeText(InstantOnboardingActivity.this, R.string.error, Toast.LENGTH_LONG) + .show(); + } + }); + }); } - private void startQrAccountCreation(String qrCode) - { + private void startQrAccountCreation(String qrCode) { if (progressDialog != null) { progressDialog.dismiss(); progressDialog = null; @@ -512,27 +536,32 @@ private void startQrAccountCreation(String qrCode) progressDialog.setMessage(getResources().getString(R.string.one_moment)); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setCancelable(false); - progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources().getString(android.R.string.cancel), (dialog, which) -> { - cancelled = true; - dcContext.stopOngoingProcess(); - }); + progressDialog.setButton( + DialogInterface.BUTTON_NEGATIVE, + getResources().getString(android.R.string.cancel), + (dialog, which) -> { + cancelled = true; + dcContext.stopOngoingProcess(); + }); progressDialog.show(); DcHelper.getEventCenter(this).captureNextError(); - new Thread(() -> { - Rpc rpc = DcHelper.getRpc(this); - try { - rpc.addTransportFromQr(dcContext.getAccountId(), qrCode); - DcHelper.getEventCenter(this).endCaptureNextError(); - progressSuccess(); - } catch (RpcException e) { - DcHelper.getEventCenter(this).endCaptureNextError(); - if (!cancelled) { - Util.runOnMain(() -> progressError(e.getMessage())); - } - } - }).start(); + new Thread( + () -> { + Rpc rpc = DcHelper.getRpc(this); + try { + rpc.addTransportFromQr(dcContext.getAccountId(), qrCode); + DcHelper.getEventCenter(this).endCaptureNextError(); + progressSuccess(); + } catch (RpcException e) { + DcHelper.getEventCenter(this).endCaptureNextError(); + if (!cancelled) { + Util.runOnMain(() -> progressError(e.getMessage())); + } + } + }) + .start(); } private class AvatarSelectedListener implements AvatarSelector.AttachmentClickedListener { @@ -546,7 +575,10 @@ public void onClick(int type) { avatarBmp = null; imageLoaded = false; avatarChanged = true; - avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(InstantOnboardingActivity.this, getResources().getColor(R.color.grey_400))); + avatar.setImageDrawable( + new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp) + .asDrawable( + InstantOnboardingActivity.this, getResources().getColor(R.color.grey_400))); break; case AvatarSelector.TAKE_PHOTO: attachmentManager.capturePhoto(InstantOnboardingActivity.this, REQUEST_CODE_AVATAR); @@ -559,5 +591,4 @@ public void onQuickAttachment(Uri inputFile) { onFileSelected(inputFile); } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/LocalHelpActivity.java b/src/main/java/org/thoughtcrime/securesms/LocalHelpActivity.java index ea881380a..526524dc0 100644 --- a/src/main/java/org/thoughtcrime/securesms/LocalHelpActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/LocalHelpActivity.java @@ -4,21 +4,19 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; - import androidx.activity.OnBackPressedCallback; - -import org.thoughtcrime.securesms.util.TextUtil; -import org.thoughtcrime.securesms.util.Util; - import java.io.InputStream; import java.util.Locale; +import org.thoughtcrime.securesms.util.TextUtil; +import org.thoughtcrime.securesms.util.Util; -public class LocalHelpActivity extends WebViewActivity -{ +public class LocalHelpActivity extends WebViewActivity { public static final String SECTION_EXTRA = "section_extra"; @Override - protected boolean allowInLockedMode() { return true; } + protected boolean allowInLockedMode() { + return true; + } @Override protected void onCreate(Bundle state, boolean ready) { @@ -35,31 +33,37 @@ protected void onCreate(Bundle state, boolean ready) { String appCountry = locale.getCountry(); if (assetExists(helpPath.replace("LANG", appLang))) { helpLang = appLang; - } else if (assetExists(helpPath.replace("LANG", appLang+"_"+appCountry))) { - helpLang = appLang+"_"+appCountry; + } else if (assetExists(helpPath.replace("LANG", appLang + "_" + appCountry))) { + helpLang = appLang + "_" + appCountry; } else { appLang = appLang.substring(0, 2); if (assetExists(helpPath.replace("LANG", appLang))) { helpLang = appLang; } } - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } - getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - if (webView.canGoBack()) { - webView.goBack(); - } else { - setEnabled(false); - getOnBackPressedDispatcher().onBackPressed(); - } - } - }); - - webView.loadUrl("file:///android_asset/" + helpPath.replace("LANG", helpLang) + (section!=null? section : "")); + getOnBackPressedDispatcher() + .addCallback( + this, + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (webView.canGoBack()) { + webView.goBack(); + } else { + setEnabled(false); + getOnBackPressedDispatcher().onBackPressed(); + } + } + }); + + webView.loadUrl( + "file:///android_asset/" + + helpPath.replace("LANG", helpLang) + + (section != null ? section : "")); } @Override @@ -124,7 +128,7 @@ private boolean assetExists(String fileName) { InputStream is = assetManager.open(fileName); exists = true; is.close(); - } catch(Exception e) { + } catch (Exception e) { ; // a non-existent asset is no error, the function's purpose is to check exactly that. } return exists; diff --git a/src/main/java/org/thoughtcrime/securesms/LogViewActivity.java b/src/main/java/org/thoughtcrime/securesms/LogViewActivity.java index 0f6ff0b6c..6bfe2e77b 100644 --- a/src/main/java/org/thoughtcrime/securesms/LogViewActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/LogViewActivity.java @@ -9,16 +9,13 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentTransaction; - +import java.io.File; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.FileProviderUtil; -import java.io.File; - public class LogViewActivity extends BaseActionBarActivity { private static final String TAG = LogViewActivity.class.getSimpleName(); @@ -60,18 +57,20 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } else if (itemId == R.id.save_log) { Permissions.with(this) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .alwaysGrantOnSdk30() - .ifNecessary() - .onAllGranted(() -> { - File outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - boolean success = logViewFragment.saveLogFile(outputDir) != null; - new AlertDialog.Builder(this) - .setMessage(success ? R.string.pref_saved_log : R.string.pref_save_log_failed) - .setPositiveButton(android.R.string.ok, null) - .show(); - }) - .execute(); + .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .alwaysGrantOnSdk30() + .ifNecessary() + .onAllGranted( + () -> { + File outputDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + boolean success = logViewFragment.saveLogFile(outputDir) != null; + new AlertDialog.Builder(this) + .setMessage(success ? R.string.pref_saved_log : R.string.pref_save_log_failed) + .setPositiveButton(android.R.string.ok, null) + .show(); + }) + .execute(); return true; } else if (itemId == R.id.share_log) { shareLog(); @@ -110,7 +109,8 @@ public void shareLog() { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } } diff --git a/src/main/java/org/thoughtcrime/securesms/LogViewFragment.java b/src/main/java/org/thoughtcrime/securesms/LogViewFragment.java index d8239a583..5c03a85ef 100644 --- a/src/main/java/org/thoughtcrime/securesms/LogViewFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/LogViewFragment.java @@ -32,17 +32,11 @@ import android.view.View; import android.view.ViewGroup; import android.widget.EditText; - import androidx.annotation.NonNull; import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; - -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.notifications.FcmReceiveService; -import org.thoughtcrime.securesms.util.Prefs; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.ViewUtil; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -53,15 +47,16 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.notifications.FcmReceiveService; +import org.thoughtcrime.securesms.util.Prefs; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.ViewUtil; public class LogViewFragment extends Fragment { private EditText logPreview; - public LogViewFragment() { - } + public LogViewFragment() {} @Override public void onCreate(Bundle savedInstanceState) { @@ -69,8 +64,8 @@ public void onCreate(Bundle savedInstanceState) { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_view_log, container, false); } @@ -78,19 +73,22 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - logPreview = (EditText) getView().findViewById(R.id.log_preview); + logPreview = (EditText) getView().findViewById(R.id.log_preview); // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(getView().findViewById(R.id.content_container), true, false, true, true); + ViewUtil.applyWindowInsets( + getView().findViewById(R.id.content_container), true, false, true, true); new PopulateLogcatAsyncTask(this).execute(); } public String getLogText() { - return logPreview==null? "null" : logPreview.getText().toString(); + return logPreview == null ? "null" : logPreview.getText().toString(); } - public Float getLogTextSize() { return logPreview.getTextSize(); } + public Float getLogTextSize() { + return logPreview.getTextSize(); + } public void setLogTextSize(Float textSize) { logPreview.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); @@ -107,16 +105,16 @@ public void scrollUpLog() { } public File saveLogFile(File outputDir) { - File logFile = null; - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss"); - Date now = new Date(); - String logFileName = "deltachat-log-" + dateFormat.format(now) + ".txt"; + File logFile = null; + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss"); + Date now = new Date(); + String logFileName = "deltachat-log-" + dateFormat.format(now) + ".txt"; try { - String logText = logPreview.getText().toString(); - if(!logText.trim().equals("")){ + String logText = logPreview.getText().toString(); + if (!logText.trim().equals("")) { logFile = new File(outputDir + "/" + logFileName); - if(!logFile.exists()) logFile.createNewFile(); + if (!logFile.exists()) logFile.createNewFile(); FileWriter logFileWriter = new FileWriter(logFile, false); BufferedWriter logFileBufferWriter = new BufferedWriter(logFileWriter); @@ -132,10 +130,11 @@ public File saveLogFile(File outputDir) { public static String grabLogcat() { String command = "logcat -v threadtime -d -t 10000 *:I"; try { - final Process process = Runtime.getRuntime().exec(command); - final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); - final StringBuilder log = new StringBuilder(); - final String separator = System.getProperty("line.separator"); + final Process process = Runtime.getRuntime().exec(command); + final BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(process.getInputStream())); + final StringBuilder log = new StringBuilder(); + final String separator = System.getProperty("line.separator"); String line; while ((line = bufferedReader.readLine()) != null) { @@ -152,7 +151,7 @@ public static String grabLogcat() { } } - private class PopulateLogcatAsyncTask extends AsyncTask { + private class PopulateLogcatAsyncTask extends AsyncTask { private final WeakReference weakFragment; public PopulateLogcatAsyncTask(LogViewFragment fragment) { @@ -164,8 +163,10 @@ protected String doInBackground(Void... voids) { LogViewFragment fragment = weakFragment.get(); if (fragment == null) return null; - return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n" + - buildDescription(fragment) + "\n" + grabLogcat(); + return "**This log may contain sensitive information. If you want to post it publicly you may examine and edit it beforehand.**\n\n" + + buildDescription(fragment) + + "\n" + + grabLogcat(); } @Override @@ -179,7 +180,8 @@ protected void onPostExecute(String logcat) { super.onPostExecute(logcat); if (TextUtils.isEmpty(logcat)) { // the log is in english, so it is fine if some of explaining strings are in english as well - logPreview.setText("Could not read the log on your device. You can still use ADB to get a debug log instead."); + logPreview.setText( + "Could not read the log on your device. You can still use ADB to get a debug log instead."); return; } logPreview.setText(logcat); @@ -192,15 +194,18 @@ private static long asMegs(long bytes) { public static String getMemoryUsage(Context context) { Runtime info = Runtime.getRuntime(); - return String.format(Locale.ENGLISH, "%dM (%.2f%% free, %dM max)", - asMegs(info.totalMemory()), - (float)info.freeMemory() / info.totalMemory() * 100f, - asMegs(info.maxMemory())); + return String.format( + Locale.ENGLISH, + "%dM (%.2f%% free, %dM max)", + asMegs(info.totalMemory()), + (float) info.freeMemory() / info.totalMemory() * 100f, + asMegs(info.maxMemory())); } public static String getMemoryClass(Context context) { - ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - String lowMem = ""; + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + String lowMem = ""; if (activityManager.isLowRamDevice()) { lowMem = ", low-mem device"; @@ -211,17 +216,26 @@ public static String getMemoryClass(Context context) { private static String buildDescription(LogViewFragment fragment) { Context context = fragment.getActivity(); - PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); - final PackageManager pm = context.getPackageManager(); - final StringBuilder builder = new StringBuilder(); - - builder.append("device=") - .append(Build.MANUFACTURER).append(" ") - .append(Build.MODEL).append(" (") - .append(Build.PRODUCT).append(")\n"); - builder.append("android=").append(VERSION.RELEASE).append(" (") - .append(VERSION.INCREMENTAL).append(", ") - .append(Build.DISPLAY).append(")\n"); + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + final PackageManager pm = context.getPackageManager(); + final StringBuilder builder = new StringBuilder(); + + builder + .append("device=") + .append(Build.MANUFACTURER) + .append(" ") + .append(Build.MODEL) + .append(" (") + .append(Build.PRODUCT) + .append(")\n"); + builder + .append("android=") + .append(VERSION.RELEASE) + .append(" (") + .append(VERSION.INCREMENTAL) + .append(", ") + .append(Build.DISPLAY) + .append(")\n"); builder.append("sdk=").append(Build.VERSION.SDK_INT).append("\n"); builder.append("memory=").append(getMemoryUsage(context)).append("\n"); builder.append("memoryClass=").append(getMemoryClass(context)).append("\n"); @@ -229,25 +243,29 @@ private static String buildDescription(LogViewFragment fragment) { builder.append("applicationId=").append(BuildConfig.APPLICATION_ID).append("\n"); builder.append("app="); try { - builder.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0))) - .append(" ") - .append(pm.getPackageInfo(context.getPackageName(), 0).versionName) - .append("-") - .append(BuildConfig.FLAVOR) - .append(BuildConfig.DEBUG? "-debug" : "") - .append("\n"); - builder.append("versionCode=") - .append(pm.getPackageInfo(context.getPackageName(), 0).versionCode) - .append("\n"); - builder.append("installer=") - .append(pm.getInstallerPackageName(context.getPackageName())) - .append("\n"); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - builder.append("ignoreBatteryOptimizations=").append( - powerManager.isIgnoringBatteryOptimizations(context.getPackageName())).append("\n"); + builder + .append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0))) + .append(" ") + .append(pm.getPackageInfo(context.getPackageName(), 0).versionName) + .append("-") + .append(BuildConfig.FLAVOR) + .append(BuildConfig.DEBUG ? "-debug" : "") + .append("\n"); + builder + .append("versionCode=") + .append(pm.getPackageInfo(context.getPackageName(), 0).versionCode) + .append("\n"); + builder + .append("installer=") + .append(pm.getInstallerPackageName(context.getPackageName())) + .append("\n"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder + .append("ignoreBatteryOptimizations=") + .append(powerManager.isIgnoringBatteryOptimizations(context.getPackageName())) + .append("\n"); } - builder.append("reliableService=").append( - Prefs.reliableService(context)).append("\n"); + builder.append("reliableService=").append(Prefs.reliableService(context)).append("\n"); Locale locale = Util.getLocale(); builder.append("lang=").append(locale.toString()).append("\n"); @@ -255,7 +273,9 @@ private static String buildDescription(LogViewFragment fragment) { builder.append("rtl=").append(isRtl).append("\n"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - boolean notifPermGranted = PermissionChecker.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PermissionChecker.PERMISSION_GRANTED; + boolean notifPermGranted = + PermissionChecker.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) + == PermissionChecker.PERMISSION_GRANTED; builder.append("post-notifications-granted=").append(notifPermGranted).append("\n"); } else { builder.append("post-notifications-granted=").append("\n"); diff --git a/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 8c451f7c4..7eed487fd 100644 --- a/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -34,7 +34,6 @@ import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -42,12 +41,12 @@ import androidx.loader.content.Loader; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMediaGalleryElement; import com.b44t.messenger.DcMsg; - +import java.io.IOException; +import java.util.WeakHashMap; import org.thoughtcrime.securesms.components.MediaView; import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener; import org.thoughtcrime.securesms.connect.DcHelper; @@ -66,24 +65,19 @@ import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.Util; -import java.io.IOException; -import java.util.WeakHashMap; - -/** - * Activity for displaying media attachments in-app - */ +/** Activity for displaying media attachments in-app */ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, LoaderManager.LoaderCallbacks { - private final static String TAG = MediaPreviewActivity.class.getSimpleName(); + private static final String TAG = MediaPreviewActivity.class.getSimpleName(); public static final String ACTIVITY_TITLE_EXTRA = "activity_title"; - public static final String EDIT_AVATAR_CHAT_ID = "avatar_for_chat_id"; - public static final String ADDRESS_EXTRA = "address"; - public static final String OUTGOING_EXTRA = "outgoing"; + public static final String EDIT_AVATAR_CHAT_ID = "avatar_for_chat_id"; + public static final String ADDRESS_EXTRA = "address"; + public static final String OUTGOING_EXTRA = "outgoing"; public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent"; - public static final String DC_MSG_ID = "dc_msg_id"; - public static final String OPENED_FROM_PROFILE = "opened_from_profile"; + public static final String DC_MSG_ID = "dc_msg_id"; + public static final String OPENED_FROM_PROFILE = "opened_from_profile"; /** USE ONLY IF YOU HAVE NO MESSAGE ID! */ public static final String DATE_EXTRA = "date"; @@ -91,13 +85,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity /** USE ONLY IF YOU HAVE NO MESSAGE ID! */ public static final String SIZE_EXTRA = "size"; - @Nullable - private DcMsg messageRecord; + @Nullable private DcMsg messageRecord; private DcContext dcContext; private MediaItem initialMedia; private ViewPager mediaPager; private Recipient conversationRecipient; - private boolean leftIsRecent; + private boolean leftIsRecent; private int restartItem = -1; @@ -105,12 +98,14 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity @Override protected void onPreCreate() { - dynamicTheme = new DynamicTheme() { - public void onCreate(Activity activity) { - activity.setTheme(R.style.TextSecure_DarkTheme); // force dark theme - } - public void onResume(Activity activity) {} - }; + dynamicTheme = + new DynamicTheme() { + public void onCreate(Activity activity) { + activity.setTheme(R.style.TextSecure_DarkTheme); // force dark theme + } + + public void onResume(Activity activity) {} + }; super.onPreCreate(); } @@ -118,15 +113,16 @@ public void onResume(Activity activity) {} @Override protected void onCreate(Bundle bundle, boolean ready) { setFullscreenIfPossible(); - getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, - WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow() + .setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); getSupportActionBar().setDisplayHomeAsUpEnabled(true); setContentView(R.layout.media_preview_activity); editAvatarChatId = getIntent().getIntExtra(EDIT_AVATAR_CHAT_ID, 0); @Nullable String title = getIntent().getStringExtra(ACTIVITY_TITLE_EXTRA); - if (title!=null) { + if (title != null) { getSupportActionBar().setTitle(title); } @@ -135,7 +131,8 @@ protected void onCreate(Bundle bundle, boolean ready) { } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } @@ -161,8 +158,8 @@ private void initializeActionBar() { relativeTimeSpan = getString(R.string.draft); } - if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.self)); - else { + if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.self)); + else { int fromId = dcContext.getMsg(mediaItem.msgId).getFromId(); getSupportActionBar().setTitle(dcContext.getContact(fromId).getDisplayName()); } @@ -203,12 +200,20 @@ private void initializeResources() { this.dcContext = DcHelper.getContext(context); final int msgId = getIntent().getIntExtra(DC_MSG_ID, DcMsg.DC_MSG_NO_ID); - if(msgId == DcMsg.DC_MSG_NO_ID) { + if (msgId == DcMsg.DC_MSG_NO_ID) { messageRecord = null; long date = getIntent().getLongExtra(DATE_EXTRA, 0); long size = getIntent().getLongExtra(SIZE_EXTRA, 0); - initialMedia = new MediaItem(null, getIntent().getData(), null, getIntent().getType(), - DcMsg.DC_MSG_NO_ID, date, size, false); + initialMedia = + new MediaItem( + null, + getIntent().getData(), + null, + getIntent().getType(), + DcMsg.DC_MSG_NO_ID, + date, + size, + false); if (address != null) { conversationRecipient = Recipient.from(context, address); @@ -217,14 +222,20 @@ private void initializeResources() { } } else { messageRecord = dcContext.getMsg(msgId); - initialMedia = new MediaItem(Recipient.fromChat(context, msgId), Uri.fromFile(messageRecord.getFileAsFile()), - messageRecord.getFilename(), messageRecord.getFilemime(), messageRecord.getId(), messageRecord.getDateReceived(), - messageRecord.getFilebytes(), messageRecord.isOutgoing()); + initialMedia = + new MediaItem( + Recipient.fromChat(context, msgId), + Uri.fromFile(messageRecord.getFileAsFile()), + messageRecord.getFilename(), + messageRecord.getFilemime(), + messageRecord.getId(), + messageRecord.getDateReceived(), + messageRecord.getFilebytes(), + messageRecord.isOutgoing()); conversationRecipient = Recipient.fromChat(context, msgId); } - leftIsRecent = getIntent().getBooleanExtra(LEFT_IS_RECENT_EXTRA, false); - restartItem = -1; - + leftIsRecent = getIntent().getBooleanExtra(LEFT_IS_RECENT_EXTRA, false); + restartItem = -1; } private void initializeMedia() { @@ -235,8 +246,15 @@ private void initializeMedia() { if (messageRecord != null) { getSupportLoaderManager().restartLoader(0, null, this); } else { - mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this), - getWindow(), initialMedia.uri, initialMedia.name, initialMedia.type, initialMedia.size)); + mediaPager.setAdapter( + new SingleItemPagerAdapter( + this, + GlideApp.with(this), + getWindow(), + initialMedia.uri, + initialMedia.name, + initialMedia.type, + initialMedia.size)); } } @@ -256,21 +274,20 @@ private void editAvatar() { finish(); // avoid the need to update the enlarged-avatar } - private void showOverview() { if (getIntent().getBooleanExtra(OPENED_FROM_PROFILE, false)) { finish(); - } - else if(conversationRecipient.getAddress().isDcChat()) { + } else if (conversationRecipient.getAddress().isDcChat()) { Intent intent = new Intent(this, AllMediaActivity.class); - intent.putExtra(AllMediaActivity.CHAT_ID_EXTRA, conversationRecipient.getAddress().getDcChatId()); + intent.putExtra( + AllMediaActivity.CHAT_ID_EXTRA, conversationRecipient.getAddress().getDcChatId()); intent.putExtra(AllMediaActivity.FORCE_GALLERY, true); startActivity(intent); finish(); - } - else if(conversationRecipient.getAddress().isDcContact()) { + } else if (conversationRecipient.getAddress().isDcContact()) { Intent intent = new Intent(this, AllMediaActivity.class); - intent.putExtra(AllMediaActivity.CONTACT_ID_EXTRA, conversationRecipient.getAddress().getDcContactId()); + intent.putExtra( + AllMediaActivity.CONTACT_ID_EXTRA, conversationRecipient.getAddress().getDcContactId()); intent.putExtra(AllMediaActivity.FORCE_GALLERY, true); startActivity(intent); finish(); @@ -290,29 +307,35 @@ private void saveToDisk() { MediaItem mediaItem = getCurrentMediaItem(); if (mediaItem != null) { - SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> { - if (StorageUtil.canWriteToMediaStore(this)) { - performSavetoDisk(mediaItem); - return; - } + SaveAttachmentTask.showWarningDialog( + this, + (dialogInterface, i) -> { + if (StorageUtil.canWriteToMediaStore(this)) { + performSavetoDisk(mediaItem); + return; + } - Permissions.with(this) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) - .alwaysGrantOnSdk30() - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) - .onAllGranted(() -> { - performSavetoDisk(mediaItem); - }) - .execute(); - }); + Permissions.with(this) + .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .alwaysGrantOnSdk30() + .ifNecessary() + .withPermanentDenialDialog( + getString(R.string.perm_explain_access_to_storage_denied)) + .onAllGranted( + () -> { + performSavetoDisk(mediaItem); + }) + .execute(); + }); } } private void performSavetoDisk(@NonNull MediaItem mediaItem) { SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this); - long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis(); - saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, mediaItem.name)); + long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis(); + saveTask.executeOnExecutor( + AsyncTask.THREAD_POOL_EXECUTOR, + new Attachment(mediaItem.uri, mediaItem.type, saveDate, mediaItem.name)); } private void showInChat() { @@ -330,7 +353,8 @@ private void showInChat() { Intent intent = new Intent(this, ConversationActivity.class); intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsg.getChatId()); - intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, dcContext)); + intent.putExtra( + ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, dcContext)); startActivity(intent); } @@ -344,24 +368,28 @@ private void deleteMedia() { DcMsg dcMsg = dcContext.getMsg(mediaItem.msgId); DcChat dcChat = dcContext.getChat(dcMsg.getChatId()); - String text = getResources().getQuantityString(R.plurals.ask_delete_messages,1, 1); + String text = getResources().getQuantityString(R.plurals.ask_delete_messages, 1, 1); int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me; - final int[] messageIds = new int[]{mediaItem.msgId}; + final int[] messageIds = new int[] {mediaItem.msgId}; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(text); builder.setCancelable(true); builder.setNeutralButton(android.R.string.cancel, null); - builder.setPositiveButton(positiveBtnLabel, (dialogInterface, which) -> { - Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds)); - finish(); - }); - - if(dcChat.isEncrypted() && dcChat.canSend() && !dcChat.isSelfTalk() && dcMsg.isOutgoing()) { - builder.setNegativeButton(R.string.delete_for_everyone, (d, which) -> { - Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds)); - finish(); - }); + builder.setPositiveButton( + positiveBtnLabel, + (dialogInterface, which) -> { + Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds)); + finish(); + }); + + if (dcChat.isEncrypted() && dcChat.canSend() && !dcChat.isSelfTalk() && dcMsg.isOutgoing()) { + builder.setNegativeButton( + R.string.delete_for_everyone, + (d, which) -> { + Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds)); + finish(); + }); AlertDialog dialog = builder.show(); Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE); Util.redPositiveButton(dialog); @@ -387,8 +415,8 @@ public boolean onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.show_in_chat).setVisible(false); } - if (editAvatarChatId==0) { - menu.findItem(R.id.media_preview__edit).setVisible(false); + if (editAvatarChatId == 0) { + menu.findItem(R.id.media_preview__edit).setVisible(false); } return true; @@ -430,7 +458,7 @@ private boolean isMediaInDb() { } private @Nullable MediaItem getCurrentMediaItem() { - MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); + MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter(); if (adapter != null) { return adapter.getMediaItemFor(mediaPager.getCurrentItem()); @@ -449,23 +477,22 @@ public Loader onCreateLoader(int id, Bundle args) { } @Override - public void onLoadFinished(Loader loader, @Nullable DcMediaGalleryElement data) { + public void onLoadFinished( + Loader loader, @Nullable DcMediaGalleryElement data) { if (data != null) { @SuppressWarnings("ConstantConditions") - DcMediaPagerAdapter adapter = new DcMediaPagerAdapter(this, GlideApp.with(this), - getWindow(), data, leftIsRecent); + DcMediaPagerAdapter adapter = + new DcMediaPagerAdapter(this, GlideApp.with(this), getWindow(), data, leftIsRecent); mediaPager.setAdapter(adapter); adapter.setActive(true); if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition()); - else mediaPager.setCurrentItem(restartItem); + else mediaPager.setCurrentItem(restartItem); } } @Override - public void onLoaderReset(Loader loader) { - - } + public void onLoaderReset(Loader loader) {} private class ViewPagerListener extends ExtendedOnPageChangedListener { @@ -473,7 +500,7 @@ private class ViewPagerListener extends ExtendedOnPageChangedListener { public void onPageSelected(int position) { super.onPageSelected(position); - MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); + MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter(); if (adapter != null) { MediaItem item = adapter.getMediaItemFor(position); @@ -483,10 +510,9 @@ public void onPageSelected(int position) { } } - @Override public void onPageUnselected(int position) { - MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter(); + MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter(); if (adapter != null) { try { @@ -503,25 +529,29 @@ public void onPageUnselected(int position) { private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter { private final GlideRequests glideRequests; - private final Window window; - private final Uri uri; - private final String name; - private final String mediaType; - private final long size; + private final Window window; + private final Uri uri; + private final String name; + private final String mediaType; + private final long size; private final LayoutInflater inflater; - SingleItemPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, - @NonNull Window window, @NonNull Uri uri, @Nullable String name, @NonNull String mediaType, - long size) - { + SingleItemPagerAdapter( + @NonNull Context context, + @NonNull GlideRequests glideRequests, + @NonNull Window window, + @NonNull Uri uri, + @Nullable String name, + @NonNull String mediaType, + long size) { this.glideRequests = glideRequests; - this.window = window; - this.uri = uri; - this.name = name; - this.mediaType = mediaType; - this.size = size; - this.inflater = LayoutInflater.from(context); + this.window = window; + this.uri = uri; + this.name = name; + this.mediaType = mediaType; + this.size = size; + this.inflater = LayoutInflater.from(context); } @Override @@ -536,7 +566,7 @@ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { @Override public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) { - View itemView = inflater.inflate(R.layout.media_view_page, container, false); + View itemView = inflater.inflate(R.layout.media_view_page, container, false); MediaView mediaView = itemView.findViewById(R.id.media_view); try { @@ -552,10 +582,10 @@ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { - MediaView mediaView = ((FrameLayout)object).findViewById(R.id.media_view); + MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view); mediaView.cleanup(); - container.removeView((FrameLayout)object); + container.removeView((FrameLayout) object); } @Override @@ -564,33 +594,33 @@ public MediaItem getMediaItemFor(int position) { } @Override - public void pause(int position) { - - } + public void pause(int position) {} } private static class DcMediaPagerAdapter extends PagerAdapter implements MediaItemAdapter { private final WeakHashMap mediaViews = new WeakHashMap<>(); - private final Context context; + private final Context context; private final GlideRequests glideRequests; - private final Window window; + private final Window window; private final DcMediaGalleryElement gallery; - private final boolean leftIsRecent; + private final boolean leftIsRecent; private boolean active; - private int autoPlayPosition; - - DcMediaPagerAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, - @NonNull Window window, @NonNull DcMediaGalleryElement gallery, - boolean leftIsRecent) - { - this.context = context.getApplicationContext(); - this.glideRequests = glideRequests; - this.window = window; - this.gallery = gallery; - this.leftIsRecent = leftIsRecent; + private int autoPlayPosition; + + DcMediaPagerAdapter( + @NonNull Context context, + @NonNull GlideRequests glideRequests, + @NonNull Window window, + @NonNull DcMediaGalleryElement gallery, + boolean leftIsRecent) { + this.context = context.getApplicationContext(); + this.glideRequests = glideRequests; + this.window = window; + this.gallery = gallery; + this.leftIsRecent = leftIsRecent; this.autoPlayPosition = gallery.getPosition(); } @@ -602,7 +632,7 @@ public void setActive(boolean active) { @Override public int getCount() { if (!active) return 0; - else return gallery.getCount(); + else return gallery.getCount(); } @Override @@ -612,10 +642,11 @@ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { @Override public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) { - View itemView = LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false); - MediaView mediaView = itemView.findViewById(R.id.media_view); - boolean autoplay = position == autoPlayPosition; - int cursorPosition = getCursorPosition(position); + View itemView = + LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false); + MediaView mediaView = itemView.findViewById(R.id.media_view); + boolean autoplay = position == autoPlayPosition; + int cursorPosition = getCursorPosition(position); autoPlayPosition = -1; @@ -625,8 +656,14 @@ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { try { //noinspection ConstantConditions - mediaView.set(glideRequests, window, Uri.fromFile(msg.getFileAsFile()), msg.getFilename(), - msg.getFilemime(), msg.getFilebytes(), autoplay); + mediaView.set( + glideRequests, + window, + Uri.fromFile(msg.getFileAsFile()), + msg.getFilename(), + msg.getFilemime(), + msg.getFilebytes(), + autoplay); } catch (IOException e) { Log.w(TAG, e); } @@ -639,27 +676,28 @@ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { - MediaView mediaView = ((FrameLayout)object).findViewById(R.id.media_view); + MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view); mediaView.cleanup(); mediaViews.remove(position); - container.removeView((FrameLayout)object); + container.removeView((FrameLayout) object); } public MediaItem getMediaItemFor(int position) { gallery.moveToPosition(getCursorPosition(position)); - DcMsg msg = gallery.getMessage(); + DcMsg msg = gallery.getMessage(); if (msg.getFile() == null) throw new AssertionError(); - return new MediaItem(Recipient.fromChat(context, msg.getId()), - Uri.fromFile(msg.getFileAsFile()), - msg.getFilename(), - msg.getFilemime(), - msg.getId(), - msg.getDateReceived(), - msg.getFilebytes(), - msg.isOutgoing()); + return new MediaItem( + Recipient.fromChat(context, msg.getId()), + Uri.fromFile(msg.getFileAsFile()), + msg.getFilename(), + msg.getFilemime(), + msg.getId(), + msg.getDateReceived(), + msg.getFilebytes(), + msg.isOutgoing()); } @Override @@ -670,42 +708,43 @@ public void pause(int position) { private int getCursorPosition(int position) { if (leftIsRecent) return position; - else return gallery.getCount() - 1 - position; + else return gallery.getCount() - 1 - position; } } private static class MediaItem { - private final @Nullable Recipient recipient; - private final @NonNull Uri uri; - private final @Nullable String name; - private final @NonNull String type; - private final int msgId; - private final long date; - private final long size; - private final boolean outgoing; - - private MediaItem(@Nullable Recipient recipient, - @NonNull Uri uri, - @Nullable String name, - @NonNull String type, - int msgId, - long date, - long size, - boolean outgoing) - { - this.recipient = recipient; - this.uri = uri; - this.name = name; - this.type = type; - this.msgId = msgId; - this.date = date; - this.size = size; - this.outgoing = outgoing; + private final @Nullable Recipient recipient; + private final @NonNull Uri uri; + private final @Nullable String name; + private final @NonNull String type; + private final int msgId; + private final long date; + private final long size; + private final boolean outgoing; + + private MediaItem( + @Nullable Recipient recipient, + @NonNull Uri uri, + @Nullable String name, + @NonNull String type, + int msgId, + long date, + long size, + boolean outgoing) { + this.recipient = recipient; + this.uri = uri; + this.name = name; + this.type = type; + this.msgId = msgId; + this.date = date; + this.size = size; + this.outgoing = outgoing; } } interface MediaItemAdapter { MediaItem getMediaItemFor(int position); + void pause(int position); } } diff --git a/src/main/java/org/thoughtcrime/securesms/MessageSelectorFragment.java b/src/main/java/org/thoughtcrime/securesms/MessageSelectorFragment.java index 501fca6b8..883fef068 100644 --- a/src/main/java/org/thoughtcrime/securesms/MessageSelectorFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/MessageSelectorFragment.java @@ -10,16 +10,14 @@ import android.view.View; import android.widget.TextView; import android.widget.Toast; - import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.core.util.Consumer; import androidx.fragment.app.Fragment; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - +import java.util.Set; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.permissions.Permissions; @@ -27,12 +25,8 @@ import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.Util; -import java.util.Set; - -public abstract class MessageSelectorFragment - extends Fragment - implements DcEventCenter.DcEventDelegate -{ +public abstract class MessageSelectorFragment extends Fragment + implements DcEventCenter.DcEventDelegate { protected ActionMode actionMode; protected abstract void setCorrectMenuVisibility(Menu menu); @@ -43,7 +37,7 @@ protected ActionMode getActionMode() { protected DcMsg getSelectedMessageRecord(Set messageRecords) { if (messageRecords.size() == 1) return messageRecords.iterator().next(); - else throw new AssertionError(); + else throw new AssertionError(); } protected void handleDisplayDetails(DcMsg dcMsg) { @@ -51,7 +45,8 @@ protected void handleDisplayDetails(DcMsg dcMsg) { TextView detailsText = view.findViewById(R.id.details_text); detailsText.setText(DcHelper.getContext(getContext()).getMsgInfo(dcMsg.getId())); - AlertDialog d = new AlertDialog.Builder(getActivity()) + AlertDialog d = + new AlertDialog.Builder(getActivity()) .setView(view) .setPositiveButton(android.R.string.ok, null) .create(); @@ -62,16 +57,28 @@ protected void handleDeleteMessages(int chatId, final Set messageRecords) handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), null, null); } - protected void handleDeleteMessages(int chatId, final Set messageRecords, Consumer deleteForMeListenerExtra, Consumer deleteForAllListenerExtra) { - handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), deleteForMeListenerExtra, deleteForAllListenerExtra); + protected void handleDeleteMessages( + int chatId, + final Set messageRecords, + Consumer deleteForMeListenerExtra, + Consumer deleteForAllListenerExtra) { + handleDeleteMessages( + chatId, + DcMsg.msgSetToIds(messageRecords), + deleteForMeListenerExtra, + deleteForAllListenerExtra); } - protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer deleteForMeListenerExtra, Consumer deleteForAllListenerExtra) { + protected void handleDeleteMessages( + int chatId, + final int[] messageIds, + Consumer deleteForMeListenerExtra, + Consumer deleteForAllListenerExtra) { DcContext dcContext = DcHelper.getContext(getContext()); DcChat dcChat = dcContext.getChat(chatId); boolean canDeleteForAll = true; if (dcChat.isEncrypted() && dcChat.canSend() && !dcChat.isSelfTalk()) { - for(int msgId : messageIds) { + for (int msgId : messageIds) { DcMsg msg = dcContext.getMsg(msgId); if (!msg.isOutgoing() || msg.isInfo()) { canDeleteForAll = false; @@ -82,26 +89,32 @@ protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer canDeleteForAll = false; } - String text = getActivity().getResources().getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length); + String text = + getActivity() + .getResources() + .getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length); int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me; - DialogInterface.OnClickListener deleteForMeListener = (d, which) -> { - Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds)); - if (actionMode != null) actionMode.finish(); - if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds); - }; - AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()) - .setMessage(text) - .setCancelable(true) - .setNeutralButton(android.R.string.cancel, null) - .setPositiveButton(positiveBtnLabel, deleteForMeListener); - - if(canDeleteForAll) { - DialogInterface.OnClickListener deleteForAllListener = (d, which) -> { - Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds)); - if (actionMode != null) actionMode.finish(); - if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds); - }; + DialogInterface.OnClickListener deleteForMeListener = + (d, which) -> { + Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds)); + if (actionMode != null) actionMode.finish(); + if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds); + }; + AlertDialog.Builder builder = + new AlertDialog.Builder(requireActivity()) + .setMessage(text) + .setCancelable(true) + .setNeutralButton(android.R.string.cancel, null) + .setPositiveButton(positiveBtnLabel, deleteForMeListener); + + if (canDeleteForAll) { + DialogInterface.OnClickListener deleteForAllListener = + (d, which) -> { + Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds)); + if (actionMode != null) actionMode.finish(); + if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds); + }; builder.setNegativeButton(R.string.delete_for_everyone, deleteForAllListener); AlertDialog dialog = builder.show(); Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE); @@ -113,29 +126,36 @@ protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer } protected void handleSaveAttachment(final Set messageRecords) { - SaveAttachmentTask.showWarningDialog(getContext(), (dialogInterface, i) -> { - if (StorageUtil.canWriteToMediaStore(getContext())) { - performSave(messageRecords); - return; - } - - Permissions.with(getActivity()) + SaveAttachmentTask.showWarningDialog( + getContext(), + (dialogInterface, i) -> { + if (StorageUtil.canWriteToMediaStore(getContext())) { + performSave(messageRecords); + return; + } + + Permissions.with(getActivity()) .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .alwaysGrantOnSdk30() .ifNecessary() .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) .onAllGranted(() -> performSave(messageRecords)) .execute(); - }); + }); } private void performSave(Set messageRecords) { - SaveAttachmentTask.Attachment[] attachments = new SaveAttachmentTask.Attachment[messageRecords.size()]; + SaveAttachmentTask.Attachment[] attachments = + new SaveAttachmentTask.Attachment[messageRecords.size()]; int index = 0; for (DcMsg message : messageRecords) { - attachments[index] = new SaveAttachmentTask.Attachment( - Uri.fromFile(message.getFileAsFile()), message.getFilemime(), message.getDateReceived(), message.getFilename()); - index++; + attachments[index] = + new SaveAttachmentTask.Attachment( + Uri.fromFile(message.getFileAsFile()), + message.getFilemime(), + message.getDateReceived(), + message.getFilename()); + index++; } SaveAttachmentTask saveTask = new SaveAttachmentTask(getContext()); saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments); @@ -144,7 +164,9 @@ private void performSave(Set messageRecords) { protected void handleShowInChat(final DcMsg dcMsg) { Intent intent = new Intent(getContext(), ConversationActivity.class); intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsg.getChatId()); - intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcMsg, DcHelper.getContext(getContext()))); + intent.putExtra( + ConversationActivity.STARTING_POSITION_EXTRA, + DcMsg.getMessagePosition(dcMsg, DcHelper.getContext(getContext()))); startActivity(intent); } @@ -155,22 +177,24 @@ protected void handleShare(final DcMsg dcMsg) { protected void handleResendMessage(final Set dcMsgsSet) { int[] ids = DcMsg.msgSetToIds(dcMsgsSet); DcContext dcContext = DcHelper.getContext(getContext()); - Util.runOnAnyBackgroundThread(() -> { - boolean success = dcContext.resendMsgs(ids); - Util.runOnMain(() -> { - Activity activity = getActivity(); - if (activity == null || activity.isFinishing()) return; - if (success) { - actionMode.finish(); - Toast.makeText(getContext(), R.string.sending, Toast.LENGTH_SHORT).show(); - } else { - new AlertDialog.Builder(activity) - .setMessage(dcContext.getLastError()) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - }); - }); + Util.runOnAnyBackgroundThread( + () -> { + boolean success = dcContext.resendMsgs(ids); + Util.runOnMain( + () -> { + Activity activity = getActivity(); + if (activity == null || activity.isFinishing()) return; + if (success) { + actionMode.finish(); + Toast.makeText(getContext(), R.string.sending, Toast.LENGTH_SHORT).show(); + } else { + new AlertDialog.Builder(activity) + .setMessage(dcContext.getLastError()) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + }); + }); } } diff --git a/src/main/java/org/thoughtcrime/securesms/MuteDialog.java b/src/main/java/org/thoughtcrime/securesms/MuteDialog.java index 175ee2066..dc222f961 100644 --- a/src/main/java/org/thoughtcrime/securesms/MuteDialog.java +++ b/src/main/java/org/thoughtcrime/securesms/MuteDialog.java @@ -1,10 +1,8 @@ package org.thoughtcrime.securesms; import android.content.Context; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; - import java.util.concurrent.TimeUnit; public class MuteDialog { @@ -13,21 +11,35 @@ public static void show(final Context context, final @NonNull MuteSelectionListe AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.menu_mute); builder.setNegativeButton(R.string.cancel, null); - builder.setItems(R.array.mute_durations, (dialog, which) -> { - final long muteUntil; - - // See https://c.delta.chat/classdc__context__t.html#a6460395925d49d2053bc95224bf5ce37. - switch (which) { - case 0: muteUntil = TimeUnit.HOURS.toSeconds(1); break; - case 1: muteUntil = TimeUnit.HOURS.toSeconds(8); break; - case 2: muteUntil = TimeUnit.DAYS.toSeconds(1); break; - case 3: muteUntil = TimeUnit.DAYS.toSeconds(7); break; - case 4: muteUntil = -1; break; // mute forever - default: muteUntil = 0; break; - } - - listener.onMuted(muteUntil); - }); + builder.setItems( + R.array.mute_durations, + (dialog, which) -> { + final long muteUntil; + + // See https://c.delta.chat/classdc__context__t.html#a6460395925d49d2053bc95224bf5ce37. + switch (which) { + case 0: + muteUntil = TimeUnit.HOURS.toSeconds(1); + break; + case 1: + muteUntil = TimeUnit.HOURS.toSeconds(8); + break; + case 2: + muteUntil = TimeUnit.DAYS.toSeconds(1); + break; + case 3: + muteUntil = TimeUnit.DAYS.toSeconds(7); + break; + case 4: + muteUntil = -1; + break; // mute forever + default: + muteUntil = 0; + break; + } + + listener.onMuted(muteUntil); + }); builder.show(); } @@ -35,5 +47,4 @@ public static void show(final Context context, final @NonNull MuteSelectionListe public interface MuteSelectionListener { void onMuted(long duration); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java index 6743a12df..ea488c9b4 100644 --- a/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java @@ -23,26 +23,21 @@ import android.content.Intent; import android.os.Bundle; - import androidx.appcompat.app.AlertDialog; - +import chat.delta.rpc.types.SecurejoinSource; +import chat.delta.rpc.types.SecurejoinUiPath; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.qr.QrActivity; import org.thoughtcrime.securesms.qr.QrCodeHandler; -import chat.delta.rpc.types.SecurejoinSource; -import chat.delta.rpc.types.SecurejoinUiPath; - /** * Activity container for starting a new conversation. * * @author Moxie Marlinspike - * */ public class NewConversationActivity extends ContactSelectionActivity { @@ -57,32 +52,34 @@ public void onCreate(Bundle bundle, boolean ready) { @Override public void onContactSelected(int contactId) { - if(contactId == DcContact.DC_CONTACT_ID_NEW_GROUP) { + if (contactId == DcContact.DC_CONTACT_ID_NEW_GROUP) { startActivity(new Intent(this, GroupCreateActivity.class)); - } else if(contactId == DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP) { + } else if (contactId == DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP) { Intent intent = new Intent(this, GroupCreateActivity.class); intent.putExtra(GroupCreateActivity.UNENCRYPTED, true); startActivity(intent); - } else if(contactId == DcContact.DC_CONTACT_ID_NEW_BROADCAST) { + } else if (contactId == DcContact.DC_CONTACT_ID_NEW_BROADCAST) { Intent intent = new Intent(this, GroupCreateActivity.class); intent.putExtra(GroupCreateActivity.CREATE_BROADCAST, true); startActivity(intent); } else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) { new IntentIntegrator(this).setCaptureActivity(QrActivity.class).initiateScan(); - } - else { + } else { final DcContext dcContext = DcHelper.getContext(this); - if (dcContext.getChatIdByContactId(contactId)!=0) { + if (dcContext.getChatIdByContactId(contactId) != 0) { openConversation(dcContext.getChatIdByContactId(contactId)); } else { String name = dcContext.getContact(contactId).getDisplayName(); new AlertDialog.Builder(this) - .setMessage(getString(R.string.ask_start_chat_with, name)) - .setCancelable(true) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { + .setMessage(getString(R.string.ask_start_chat_with, name)) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton( + android.R.string.ok, + (dialog, which) -> { openConversation(dcContext.createChatByContactId(contactId)); - }).show(); + }) + .show(); } } } @@ -96,7 +93,8 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { case IntentIntegrator.REQUEST_CODE: IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data); QrCodeHandler qrCodeHandler = new QrCodeHandler(this); - qrCodeHandler.handleOnlySecureJoinQr(scanResult.getContents(), SecurejoinSource.Scan, SecurejoinUiPath.NewContact); + qrCodeHandler.handleOnlySecureJoinQr( + scanResult.getContents(), SecurejoinSource.Scan, SecurejoinUiPath.NewContact); break; default: break; diff --git a/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java index c043a046b..6c902d301 100644 --- a/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java @@ -3,7 +3,6 @@ import android.content.Intent; import android.os.Bundle; import android.util.Log; - import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.service.GenericForegroundService; @@ -50,7 +49,9 @@ protected void onCreate(Bundle savedInstanceState, boolean ready) {} // The app is "locked" to an activity if you will. // In "Locked Mode" the user should not leave that activity otherwise the state would be lost - // so eg. tapping app icon or notifications MUST NOT replace activity stack. - // However, sometimes it is fine to allow to pushing activities in these situations, + // However, sometimes it is fine to allow to pushing activities in these situations, // like to see the logs or offline help. - protected boolean allowInLockedMode() { return false; } + protected boolean allowInLockedMode() { + return false; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/ProfileActivity.java b/src/main/java/org/thoughtcrime/securesms/ProfileActivity.java index 48e390ca3..5f9acbe6e 100644 --- a/src/main/java/org/thoughtcrime/securesms/ProfileActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ProfileActivity.java @@ -12,18 +12,19 @@ import android.view.MenuItem; import android.view.View; import android.widget.EditText; +import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - +import java.io.File; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; @@ -32,31 +33,25 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.io.File; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - public class ProfileActivity extends PassphraseRequiredActionBarActivity - implements DcEventCenter.DcEventDelegate -{ + implements DcEventCenter.DcEventDelegate { - public static final String CHAT_ID_EXTRA = "chat_id"; + public static final String CHAT_ID_EXTRA = "chat_id"; public static final String CONTACT_ID_EXTRA = "contact_id"; private static final int REQUEST_CODE_PICK_RINGTONE = 1; - private DcContext dcContext; + private DcContext dcContext; private Rpc rpc; - private int chatId; - private boolean chatIsMultiUser; - private boolean chatIsDeviceTalk; - private boolean chatIsMailingList; - private boolean chatIsOutBroadcast; - private boolean chatIsInBroadcast; - private int contactId; - private boolean contactIsBot; - private Toolbar toolbar; + private int chatId; + private boolean chatIsMultiUser; + private boolean chatIsDeviceTalk; + private boolean chatIsMailingList; + private boolean chatIsOutBroadcast; + private boolean chatIsInBroadcast; + private int contactId; + private boolean contactIsBot; + private Toolbar toolbar; @Override protected void onPreCreate() { @@ -110,7 +105,9 @@ public boolean onCreateOptionsMenu(Menu menu) { if (chatId != 0) { DcChat dcChat = dcContext.getChat(chatId); - menu.findItem(R.id.menu_clone).setVisible(chatIsMultiUser && !chatIsInBroadcast && !chatIsOutBroadcast && !chatIsMailingList); + menu.findItem(R.id.menu_clone) + .setVisible( + chatIsMultiUser && !chatIsInBroadcast && !chatIsOutBroadcast && !chatIsMailingList); if (chatIsDeviceTalk) { menu.findItem(R.id.edit_name).setVisible(false); menu.findItem(R.id.show_encr_info).setVisible(false); @@ -120,9 +117,7 @@ public boolean onCreateOptionsMenu(Menu menu) { if (chatIsOutBroadcast) { canReceive = false; } else { - if (!dcChat.isEncrypted() - || !dcChat.canSend() - || chatIsMailingList) { + if (!dcChat.isEncrypted() || !dcChat.canSend() || chatIsMailingList) { menu.findItem(R.id.edit_name).setVisible(false); } } @@ -155,14 +150,18 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuItem item = menu.findItem(R.id.block_contact); - if(item!=null) { - item.setTitle(dcContext.getContact(contactId).isBlocked()? R.string.menu_unblock_contact : R.string.menu_block_contact); + if (item != null) { + item.setTitle( + dcContext.getContact(contactId).isBlocked() + ? R.string.menu_unblock_contact + : R.string.menu_block_contact); Util.redMenuItem(menu, R.id.block_contact); } item = menu.findItem(R.id.menu_mute_notifications); - if(item!=null) { - item.setTitle(dcContext.getChat(chatId).isMuted()? R.string.menu_unmute : R.string.menu_mute); + if (item != null) { + item.setTitle( + dcContext.getChat(chatId).isMuted() ? R.string.menu_unmute : R.string.menu_mute); } super.onPrepareOptionsMenu(menu); @@ -182,48 +181,47 @@ public void onDestroy() { } @Override - public void handleEvent(@NonNull DcEvent event) { - } + public void handleEvent(@NonNull DcEvent event) {} private void initializeResources() { - chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, 0); - contactId = getIntent().getIntExtra(CONTACT_ID_EXTRA, 0); - contactIsBot = false; - chatIsMultiUser = false; + chatId = getIntent().getIntExtra(CHAT_ID_EXTRA, 0); + contactId = getIntent().getIntExtra(CONTACT_ID_EXTRA, 0); + contactIsBot = false; + chatIsMultiUser = false; chatIsDeviceTalk = false; - chatIsMailingList= false; + chatIsMailingList = false; chatIsInBroadcast = false; chatIsOutBroadcast = false; - if (contactId!=0) { + if (contactId != 0) { DcContact dcContact = dcContext.getContact(contactId); chatId = dcContext.getChatIdByContactId(contactId); contactIsBot = dcContact.isBot(); } - if(chatId!=0) { + if (chatId != 0) { DcChat dcChat = dcContext.getChat(chatId); chatIsMultiUser = dcChat.isMultiUser(); chatIsDeviceTalk = dcChat.isDeviceTalk(); chatIsMailingList = dcChat.isMailingList(); chatIsInBroadcast = dcChat.isInBroadcast(); chatIsOutBroadcast = dcChat.isOutBroadcast(); - if(!chatIsMultiUser) { + if (!chatIsMultiUser) { final int[] members = dcContext.getChatContacts(chatId); - contactId = members.length>=1? members[0] : 0; + contactId = members.length >= 1 ? members[0] : 0; } } - this.toolbar = ViewUtil.findById(this, R.id.toolbar); + this.toolbar = ViewUtil.findById(this, R.id.toolbar); } private boolean isContactProfile() { // contact-profiles are profiles without a chat or with a one-to-one chat - return contactId!=0 && (chatId==0 || !chatIsMultiUser); + return contactId != 0 && (chatId == 0 || !chatIsMultiUser); } private boolean isSelfProfile() { - return isContactProfile() && contactId==DcContact.DC_CONTACT_ID_SELF; + return isContactProfile() && contactId == DcContact.DC_CONTACT_ID_SELF; } // handle events @@ -270,8 +268,7 @@ public boolean onContextItemSelected(@NonNull MenuItem item) { private void onNotifyOnOff() { if (dcContext.getChat(chatId).isMuted()) { setMuted(0); - } - else { + } else { MuteDialog.show(this, this::setMuted); } } @@ -286,7 +283,7 @@ private void onSoundSettings() { Uri current = Prefs.getChatRingtone(this, dcContext.getAccountId(), chatId); Uri defaultUri = Prefs.getNotificationRingtone(this); - if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI; + if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI; else if (current.toString().isEmpty()) current = null; Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); @@ -301,15 +298,23 @@ private void onSoundSettings() { private void onVibrateSettings() { int checkedItem = Prefs.getChatVibrate(this, dcContext.getAccountId(), chatId).getId(); - int[] selectedChoice = new int[]{checkedItem}; + int[] selectedChoice = new int[] {checkedItem}; new AlertDialog.Builder(this) - .setTitle(R.string.pref_vibrate) - .setSingleChoiceItems(R.array.recipient_vibrate_entries, checkedItem, - (dialog, which) -> selectedChoice[0] = which) - .setPositiveButton(R.string.ok, - (dialog, which) -> Prefs.setChatVibrate(this, dcContext.getAccountId(), chatId, Prefs.VibrateState.fromId(selectedChoice[0]))) - .setNegativeButton(R.string.cancel, null) - .show(); + .setTitle(R.string.pref_vibrate) + .setSingleChoiceItems( + R.array.recipient_vibrate_entries, + checkedItem, + (dialog, which) -> selectedChoice[0] = which) + .setPositiveButton( + R.string.ok, + (dialog, which) -> + Prefs.setChatVibrate( + this, + dcContext.getAccountId(), + chatId, + Prefs.VibrateState.fromId(selectedChoice[0]))) + .setNegativeButton(R.string.cancel, null) + .show(); } public void onEnlargeAvatar() { @@ -317,7 +322,7 @@ public void onEnlargeAvatar() { String title; Uri profileImageUri; boolean enlargeAvatar = true; - if(chatId!=0) { + if (chatId != 0) { DcChat dcChat = dcContext.getChat(chatId); profileImagePath = dcChat.getProfileImage(); title = dcChat.getName(); @@ -338,11 +343,10 @@ public void onEnlargeAvatar() { intent.setDataAndType(profileImageUri, type); intent.putExtra(MediaPreviewActivity.ACTIVITY_TITLE_EXTRA, title); intent.putExtra( // show edit-button, if the user is allowed to edit the name/avatar - MediaPreviewActivity.EDIT_AVATAR_CHAT_ID, - (chatIsMultiUser && !chatIsInBroadcast && !chatIsMailingList) ? chatId : 0 - ); + MediaPreviewActivity.EDIT_AVATAR_CHAT_ID, + (chatIsMultiUser && !chatIsInBroadcast && !chatIsMailingList) ? chatId : 0); startActivity(intent); - } else if (chatIsMultiUser){ + } else if (chatIsMultiUser) { onEditName(); } } @@ -355,8 +359,7 @@ private void onEditName() { intent.putExtra(GroupCreateActivity.EDIT_GROUP_CHAT_ID, chatId); startActivity(intent); } - } - else { + } else { int accountId = dcContext.getAccountId(); DcContact dcContact = dcContext.getContact(contactId); @@ -375,14 +378,16 @@ private void onEditName() { .setTitle(R.string.menu_edit_name) .setMessage(getString(R.string.edit_name_explain, authName)) .setView(gl) - .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { - String newName = inputField.getText().toString(); - try { - rpc.changeContactName(accountId, contactId, newName); - } catch (RpcException e) { - e.printStackTrace(); - } - }) + .setPositiveButton( + android.R.string.ok, + (dialog, whichButton) -> { + String newName = inputField.getText().toString(); + try { + rpc.changeContactName(accountId, contactId, newName); + } catch (RpcException e) { + e.printStackTrace(); + } + }) .setNegativeButton(android.R.string.cancel, null) .setCancelable(false) .show(); @@ -407,33 +412,46 @@ private void onCopyAddrToClipboard() { } private void onEncrInfo() { - String infoStr = isContactProfile() ? - dcContext.getContactEncrInfo(contactId) : dcContext.getChatEncrInfo(chatId); - new AlertDialog.Builder(this) - .setMessage(infoStr) - .setPositiveButton(android.R.string.ok, null) - .show(); + String infoStr = + isContactProfile() + ? dcContext.getContactEncrInfo(contactId) + : dcContext.getChatEncrInfo(chatId); + AlertDialog dialog = + new AlertDialog.Builder(this) + .setMessage(infoStr) + .setPositiveButton(android.R.string.ok, null) + .show(); + TextView messageView = dialog.findViewById(android.R.id.message); + if (messageView != null) { + messageView.setTextIsSelectable(true); + } } private void onBlockContact() { DcContact dcContact = dcContext.getContact(contactId); - if(dcContact.isBlocked()) { + if (dcContact.isBlocked()) { new AlertDialog.Builder(this) .setMessage(R.string.ask_unblock_contact) .setCancelable(true) .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.menu_unblock_contact, (dialog, which) -> { - dcContext.blockContact(contactId, 0); - }).show(); - } - else { - AlertDialog dialog = new AlertDialog.Builder(this) - .setMessage(R.string.ask_block_contact) - .setCancelable(true) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.menu_block_contact, (d, which) -> { - dcContext.blockContact(contactId, 1); - }).show(); + .setPositiveButton( + R.string.menu_unblock_contact, + (dialog, which) -> { + dcContext.blockContact(contactId, 0); + }) + .show(); + } else { + AlertDialog dialog = + new AlertDialog.Builder(this) + .setMessage(R.string.ask_block_contact) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton( + R.string.menu_block_contact, + (d, which) -> { + dcContext.blockContact(contactId, 1); + }) + .show(); Util.redPositiveButton(dialog); } } @@ -447,15 +465,16 @@ private void onClone() { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode==REQUEST_CODE_PICK_RINGTONE && resultCode== Activity.RESULT_OK && data!=null) { + if (requestCode == REQUEST_CODE_PICK_RINGTONE + && resultCode == Activity.RESULT_OK + && data != null) { Uri value = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); Uri defaultValue = Prefs.getNotificationRingtone(this); if (defaultValue.equals(value)) value = null; - else if (value == null) value = Uri.EMPTY; + else if (value == null) value = Uri.EMPTY; Prefs.setChatRingtone(this, dcContext.getAccountId(), chatId, value); } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/ProfileAdapter.java b/src/main/java/org/thoughtcrime/securesms/ProfileAdapter.java index d37516f91..94ee9fb97 100644 --- a/src/main/java/org/thoughtcrime/securesms/ProfileAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/ProfileAdapter.java @@ -6,36 +6,30 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcLot; import com.b44t.messenger.DcMsg; - -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.Util; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.Util; -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - -public class ProfileAdapter extends RecyclerView.Adapter -{ +public class ProfileAdapter extends RecyclerView.Adapter { private static final String TAG = ProfileAdapter.class.getSimpleName(); public static final int ITEM_AVATAR = 10; @@ -50,22 +44,22 @@ public class ProfileAdapter extends RecyclerView.Adapter public static final int ITEM_MEMBERS = 55; public static final int ITEM_SHARED_CHATS = 60; - private final @NonNull Context context; - private final @NonNull Fragment fragment; - private final @NonNull DcContext dcContext; - private @Nullable DcChat dcChat; - private @Nullable DcContact dcContact; + private final @NonNull Context context; + private final @NonNull Fragment fragment; + private final @NonNull DcContext dcContext; + private @Nullable DcChat dcChat; + private @Nullable DcContact dcContact; - private final @NonNull ArrayList itemData = new ArrayList<>(); - private DcChatlist itemDataSharedChats; - private String itemDataStatusText; - private boolean isOutBroadcast; - private int[] memberList; - private final Set selectedMembers; + private final @NonNull ArrayList itemData = new ArrayList<>(); + private DcChatlist itemDataSharedChats; + private String itemDataStatusText; + private boolean isOutBroadcast; + private int[] memberList; + private final Set selectedMembers; - private final LayoutInflater layoutInflater; - private final ItemClickListener clickListener; - private final GlideRequests glideRequests; + private final LayoutInflater layoutInflater; + private final ItemClickListener clickListener; + private final GlideRequests glideRequests; static class ItemData { final int viewType; @@ -82,27 +76,29 @@ static class ItemData { this(viewType, contactId, chatlistIndex, null, 0); } - private ItemData(int viewType, int contactId, int chatlistIndex, @Nullable String label, int icon) { - this.viewType = viewType; - this.contactId = contactId; + private ItemData( + int viewType, int contactId, int chatlistIndex, @Nullable String label, int icon) { + this.viewType = viewType; + this.contactId = contactId; this.chatlistIndex = chatlistIndex; - this.label = label; - this.icon = icon; + this.label = label; + this.icon = icon; } - }; + } + ; - public ProfileAdapter(@NonNull Fragment fragment, - @NonNull GlideRequests glideRequests, - @Nullable ItemClickListener clickListener) - { + public ProfileAdapter( + @NonNull Fragment fragment, + @NonNull GlideRequests glideRequests, + @Nullable ItemClickListener clickListener) { super(); - this.fragment = fragment; - this.context = fragment.requireContext(); - this.glideRequests = glideRequests; - this.clickListener = clickListener; - this.dcContext = DcHelper.getContext(context); + this.fragment = fragment; + this.context = fragment.requireContext(); + this.glideRequests = glideRequests; + this.clickListener = clickListener; + this.dcContext = DcHelper.getContext(context); this.layoutInflater = LayoutInflater.from(context); - this.selectedMembers= new HashSet<>(); + this.selectedMembers = new HashSet<>(); } @Override @@ -125,32 +121,47 @@ public ViewHolder(View itemView) { @Override public ProfileAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == ITEM_HEADER) { - final View item = LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false); + final View item = + LayoutInflater.from(context) + .inflate(R.layout.contact_selection_list_divider, parent, false); return new ViewHolder(item); } else if (viewType == ITEM_DIVIDER) { - final View item = LayoutInflater.from(context).inflate(R.layout.profile_divider, parent, false); + final View item = + LayoutInflater.from(context).inflate(R.layout.profile_divider, parent, false); return new ViewHolder(item); } else if (viewType == ITEM_MEMBERS) { - final ContactSelectionListItem item = (ContactSelectionListItem)layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false); + final ContactSelectionListItem item = + (ContactSelectionListItem) + layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false); return new ViewHolder(item); } else if (viewType == ITEM_SHARED_CHATS) { - final ConversationListItem item = (ConversationListItem)layoutInflater.inflate(R.layout.conversation_list_item_view, parent, false); + final ConversationListItem item = + (ConversationListItem) + layoutInflater.inflate(R.layout.conversation_list_item_view, parent, false); item.hideItemDivider(); return new ViewHolder(item); } else if (viewType == ITEM_SIGNATURE) { - final ProfileStatusItem item = (ProfileStatusItem)layoutInflater.inflate(R.layout.profile_status_item, parent, false); + final ProfileStatusItem item = + (ProfileStatusItem) layoutInflater.inflate(R.layout.profile_status_item, parent, false); return new ViewHolder(item); } else if (viewType == ITEM_AVATAR) { - final ProfileAvatarItem item = (ProfileAvatarItem)layoutInflater.inflate(R.layout.profile_avatar_item, parent, false); + final ProfileAvatarItem item = + (ProfileAvatarItem) layoutInflater.inflate(R.layout.profile_avatar_item, parent, false); return new ViewHolder(item); } else if (viewType == ITEM_ALL_MEDIA_BUTTON || viewType == ITEM_SEND_MESSAGE_BUTTON) { - final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item_button, parent, false); + final ProfileTextItem item = + (ProfileTextItem) + layoutInflater.inflate(R.layout.profile_text_item_button, parent, false); return new ViewHolder(item); - } else if (viewType == ITEM_LAST_SEEN || viewType == ITEM_INTRODUCED_BY || viewType == ITEM_BLOCKED) { - final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item_small, parent, false); + } else if (viewType == ITEM_LAST_SEEN + || viewType == ITEM_INTRODUCED_BY + || viewType == ITEM_BLOCKED) { + final ProfileTextItem item = + (ProfileTextItem) layoutInflater.inflate(R.layout.profile_text_item_small, parent, false); return new ViewHolder(item); } else { - final ProfileTextItem item = (ProfileTextItem)layoutInflater.inflate(R.layout.profile_text_item, parent, false); + final ProfileTextItem item = + (ProfileTextItem) layoutInflater.inflate(R.layout.profile_text_item, parent, false); return new ViewHolder(item); } } @@ -170,11 +181,9 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) if (contactId == DcContact.DC_CONTACT_ID_ADD_MEMBER) { name = context.getString(R.string.group_add_members); - } - else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) { + } else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) { name = context.getString(R.string.qrshow_title); - } - else { + } else { dcContact = dcContext.getContact(contactId); name = dcContact.getDisplayName(); addr = dcContact.getAddr(); @@ -184,9 +193,12 @@ else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) { contactItem.set(glideRequests, contactId, dcContact, name, addr, label, false, true); contactItem.setSelected(selectedMembers.contains(contactId)); contactItem.setOnClickListener(view -> clickListener.onMemberClicked(contactId)); - contactItem.setOnLongClickListener(view -> {clickListener.onMemberLongClicked(contactId); return true;}); - } - else if (holder.itemView instanceof ConversationListItem) { + contactItem.setOnLongClickListener( + view -> { + clickListener.onMemberLongClicked(contactId); + return true; + }); + } else if (holder.itemView instanceof ConversationListItem) { ConversationListItem conversationListItem = (ConversationListItem) holder.itemView; int chatlistIndex = data.chatlistIndex; @@ -194,39 +206,55 @@ else if (holder.itemView instanceof ConversationListItem) { DcChat chat = dcContext.getChat(chatId); DcLot summary = itemDataSharedChats.getSummary(chatlistIndex, chat); - conversationListItem.bind(DcHelper.getThreadRecord(context, summary, chat), - itemDataSharedChats.getMsgId(chatlistIndex), summary, glideRequests, - Collections.emptySet(), false); + conversationListItem.bind( + DcHelper.getThreadRecord(context, summary, chat), + itemDataSharedChats.getMsgId(chatlistIndex), + summary, + glideRequests, + Collections.emptySet(), + false); conversationListItem.setOnClickListener(view -> clickListener.onSharedChatClicked(chatId)); - } - else if(holder.itemView instanceof ProfileStatusItem) { + } else if (holder.itemView instanceof ProfileStatusItem) { ProfileStatusItem item = (ProfileStatusItem) holder.itemView; - item.setOnLongClickListener(view -> {clickListener.onStatusLongClicked(dcContact == null); return true;}); + item.setOnLongClickListener( + view -> { + clickListener.onStatusLongClicked(dcContact == null); + return true; + }); item.set(data.label); - } - else if(holder.itemView instanceof ProfileAvatarItem) { + } else if (holder.itemView instanceof ProfileAvatarItem) { ProfileAvatarItem item = (ProfileAvatarItem) holder.itemView; item.setAvatarClickListener(view -> clickListener.onAvatarClicked()); item.set(glideRequests, dcChat, dcContact, memberList); - } - else if(holder.itemView instanceof ProfileTextItem) { + } else if (holder.itemView instanceof ProfileTextItem) { ProfileTextItem item = (ProfileTextItem) holder.itemView; item.setOnClickListener(view -> clickListener.onSettingsClicked(data.viewType)); boolean tintIcon = data.viewType != ITEM_INTRODUCED_BY && data.viewType != ITEM_BLOCKED; item.set(data.label, data.icon, tintIcon); if (data.viewType == ITEM_BLOCKED) { - int padding = (int)((float)context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding) * 1.2); - item.setPadding(item.getPaddingLeft(), item.getPaddingTop(), item.getPaddingRight(), padding); + int padding = + (int) + ((float) + context + .getResources() + .getDimensionPixelSize(R.dimen.contact_list_normal_padding) + * 1.2); + item.setPadding( + item.getPaddingLeft(), item.getPaddingTop(), item.getPaddingRight(), padding); } else if (data.viewType == ITEM_INTRODUCED_BY) { - int padding = context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding); - item.setPadding(item.getPaddingLeft(), padding, item.getPaddingRight(), item.getPaddingBottom()); + int padding = + context.getResources().getDimensionPixelSize(R.dimen.contact_list_normal_padding); + item.setPadding( + item.getPaddingLeft(), padding, item.getPaddingRight(), item.getPaddingBottom()); } else if (data.viewType == ITEM_ALL_MEDIA_BUTTON && dcChat != null) { - Util.runOnAnyBackgroundThread(() -> { - String c = getAllMediaCountString(dcChat.getId()); - Util.runOnMain(() -> { - item.setValue(c); - }); - }); + Util.runOnAnyBackgroundThread( + () -> { + String c = getAllMediaCountString(dcChat.getId()); + Util.runOnMain( + () -> { + item.setValue(c); + }); + }); } } else if (data.viewType == ITEM_HEADER) { TextView textView = holder.itemView.findViewById(R.id.label); @@ -236,10 +264,15 @@ else if(holder.itemView instanceof ProfileTextItem) { public interface ItemClickListener { void onSettingsClicked(int settingsId); + void onStatusLongClicked(boolean isMultiUser); + void onSharedChatClicked(int chatId); + void onMemberClicked(int contactId); + void onMemberLongClicked(int contactId); + void onAvatarClicked(); } @@ -269,7 +302,11 @@ public void clearSelection() { notifyDataSetChanged(); } - public void changeData(@Nullable int[] memberList, @Nullable DcContact dcContact, @Nullable DcChatlist sharedChats, @Nullable DcChat dcChat) { + public void changeData( + @Nullable int[] memberList, + @Nullable DcContact dcContact, + @Nullable DcChatlist sharedChats, + @Nullable DcChat dcChat) { this.dcChat = dcChat; this.dcContact = dcContact; itemData.clear(); @@ -304,10 +341,18 @@ public void changeData(@Nullable int[] memberList, @Nullable DcContact dcContact itemData.add(new ItemData(ITEM_DIVIDER, null, 0)); } - itemData.add(new ItemData(ITEM_ALL_MEDIA_BUTTON, context.getString(R.string.apps_and_media), R.drawable.ic_apps_24)); + itemData.add( + new ItemData( + ITEM_ALL_MEDIA_BUTTON, + context.getString(R.string.apps_and_media), + R.drawable.ic_apps_24)); if (dcContact != null && !isDeviceTalk && !isSelfTalk) { - itemData.add(new ItemData(ITEM_SEND_MESSAGE_BUTTON, context.getString(R.string.send_message), R.drawable.ic_send_sms_white_24dp)); + itemData.add( + new ItemData( + ITEM_SEND_MESSAGE_BUTTON, + context.getString(R.string.send_message), + R.drawable.ic_send_sms_white_24dp)); } /* @@ -316,19 +361,25 @@ public void changeData(@Nullable int[] memberList, @Nullable DcContact dcContact String lastSeenTxt; if (lastSeenTimestamp == 0) { lastSeenTxt = context.getString(R.string.last_seen_unknown); - } - else { - lastSeenTxt = context.getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(context, lastSeenTimestamp)); + } else { + lastSeenTxt = + context.getString( + R.string.last_seen_at, + DateUtils.getExtendedTimeSpanString(context, lastSeenTimestamp)); } itemData.add(new ItemData(ITEM_LAST_SEEN, lastSeenTxt, 0)); } */ if (dcContact != null && !isDeviceTalk && !isSelfTalk && dcContact.isBlocked()) { - itemData.add(new ItemData(ITEM_BLOCKED, context.getString(R.string.contact_blocked), R.drawable.contact_blocked_24)); + itemData.add( + new ItemData( + ITEM_BLOCKED, + context.getString(R.string.contact_blocked), + R.drawable.contact_blocked_24)); } - if (memberList!=null && !isInBroadcast && !isMailingList) { + if (memberList != null && !isInBroadcast && !isMailingList) { itemData.add(new ItemData(ITEM_DIVIDER, null, 0)); if (dcChat != null) { if (dcChat.canSend() && dcChat.isEncrypted()) { @@ -358,9 +409,15 @@ public void changeData(@Nullable int[] memberList, @Nullable DcContact dcContact if (verifierId == DcContact.DC_CONTACT_ID_SELF) { introducedBy = context.getString(R.string.verified_by_you); } else { - introducedBy = context.getString(R.string.verified_by, dcContext.getContact(verifierId).getDisplayName()); + introducedBy = + context.getString( + R.string.verified_by, dcContext.getContact(verifierId).getDisplayName()); } - itemData.add(new ItemData(ITEM_INTRODUCED_BY, introducedBy, dcContact.isVerified()? R.drawable.ic_verified : 0)); + itemData.add( + new ItemData( + ITEM_INTRODUCED_BY, + introducedBy, + dcContact.isVerified() ? R.drawable.ic_verified : 0)); } else if (dcContact.isVerified()) { String introducedBy = context.getString(R.string.verified_by_unknown); itemData.add(new ItemData(ITEM_INTRODUCED_BY, introducedBy, R.drawable.ic_verified)); @@ -371,8 +428,11 @@ public void changeData(@Nullable int[] memberList, @Nullable DcContact dcContact } public int ALL_MEDIA_COUNT_MAX = 500; + public int getAllMediaCount(int chatId) { - int c = dcContext.getChatMedia(chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO).length; + int c = + dcContext.getChatMedia(chatId, DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO) + .length; if (c < ALL_MEDIA_COUNT_MAX) { c += dcContext.getChatMedia(chatId, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_VOICE, 0).length; } diff --git a/src/main/java/org/thoughtcrime/securesms/ProfileAvatarItem.java b/src/main/java/org/thoughtcrime/securesms/ProfileAvatarItem.java index 3e319ba3b..1c3bbb29c 100644 --- a/src/main/java/org/thoughtcrime/securesms/ProfileAvatarItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ProfileAvatarItem.java @@ -5,13 +5,10 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; - import org.thoughtcrime.securesms.components.AvatarView; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; @@ -22,12 +19,12 @@ public class ProfileAvatarItem extends LinearLayout implements RecipientModifiedListener { - private AvatarView avatarView; - private TextView nameView; - private TextView subtitleView; + private AvatarView avatarView; + private TextView nameView; + private TextView subtitleView; - private Recipient recipient; - private GlideRequests glideRequests; + private Recipient recipient; + private GlideRequests glideRequests; public ProfileAvatarItem(Context context) { super(context); @@ -40,14 +37,18 @@ public ProfileAvatarItem(Context context, AttributeSet attrs) { @Override protected void onFinishInflate() { super.onFinishInflate(); - avatarView = findViewById(R.id.avatar); - nameView = findViewById(R.id.name); - subtitleView = findViewById(R.id.subtitle); + avatarView = findViewById(R.id.avatar); + nameView = findViewById(R.id.name); + subtitleView = findViewById(R.id.subtitle); ViewUtil.setTextViewGravityStart(nameView, getContext()); } - public void set(@NonNull GlideRequests glideRequests, @Nullable DcChat dcChat, @Nullable DcContact dcContact, @Nullable int[] members) { + public void set( + @NonNull GlideRequests glideRequests, + @Nullable DcChat dcChat, + @Nullable DcContact dcContact, + @Nullable int[] members) { this.glideRequests = glideRequests; int memberCount = members != null ? members.length : 0; @@ -60,17 +61,27 @@ public void set(@NonNull GlideRequests glideRequests, @Nullable DcChat dcChat, @ if (dcChat.isMailingList()) { subtitle = dcChat.getMailinglistAddr(); } else if (dcChat.isOutBroadcast()) { - subtitle = getContext().getResources().getQuantityString(R.plurals.n_recipients, memberCount, memberCount); + subtitle = + getContext() + .getResources() + .getQuantityString(R.plurals.n_recipients, memberCount, memberCount); } else if (dcChat.getType() == DcChat.DC_CHAT_TYPE_GROUP) { if (memberCount > 1 || Util.contains(members, DcContact.DC_CONTACT_ID_SELF)) { - subtitle = getContext().getResources().getQuantityString(R.plurals.n_members, memberCount, memberCount); + subtitle = + getContext() + .getResources() + .getQuantityString(R.plurals.n_members, memberCount, memberCount); } } else if (dcContact != null && !dcChat.isSelfTalk() && !dcChat.isDeviceTalk()) { long timestamp = dcContact.getLastSeen(); if (timestamp == 0) { subtitle = getContext().getString(R.string.last_seen_unknown); } else { - subtitle = getContext().getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(getContext(), timestamp)); + subtitle = + getContext() + .getString( + R.string.last_seen_at, + DateUtils.getExtendedTimeSpanString(getContext(), timestamp)); } } } else if (dcContact != null) { @@ -81,7 +92,11 @@ public void set(@NonNull GlideRequests glideRequests, @Nullable DcChat dcChat, @ if (timestamp == 0) { subtitle = getContext().getString(R.string.last_seen_unknown); } else { - subtitle = getContext().getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(getContext(), timestamp)); + subtitle = + getContext() + .getString( + R.string.last_seen_at, + DateUtils.getExtendedTimeSpanString(getContext(), timestamp)); } } @@ -115,12 +130,13 @@ public void unbind(GlideRequests glideRequests) { @Override public void onModified(final Recipient recipient) { if (this.recipient == recipient) { - Util.runOnMain(() -> { - avatarView.setAvatar(glideRequests, recipient, false); - DcContact contact = recipient.getDcContact(); - avatarView.setSeenRecently(contact != null && contact.wasSeenRecently()); - nameView.setText(recipient.toShortString()); - }); + Util.runOnMain( + () -> { + avatarView.setAvatar(glideRequests, recipient, false); + DcContact contact = recipient.getDcContact(); + avatarView.setSeenRecently(contact != null && contact.wasSeenRecently()); + nameView.setText(recipient.toShortString()); + }); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/ProfileFragment.java b/src/main/java/org/thoughtcrime/securesms/ProfileFragment.java index e1c9a072c..46d5b8c92 100644 --- a/src/main/java/org/thoughtcrime/securesms/ProfileFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ProfileFragment.java @@ -11,7 +11,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -21,13 +20,14 @@ import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.mms.GlideApp; @@ -35,12 +35,8 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - public class ProfileFragment extends Fragment - implements ProfileAdapter.ItemClickListener, DcEventCenter.DcEventDelegate { + implements ProfileAdapter.ItemClickListener, DcEventCenter.DcEventDelegate { private static final String TAG = ProfileFragment.class.getSimpleName(); public static final String CHAT_ID_EXTRA = "chat_id"; @@ -48,13 +44,12 @@ public class ProfileFragment extends Fragment private ActivityResultLauncher pickContactLauncher; private ProfileAdapter adapter; - private ActionMode actionMode; + private ActionMode actionMode; private final ActionModeCallback actionModeCallback = new ActionModeCallback(); - - private DcContext dcContext; - protected int chatId; - private int contactId; + private DcContext dcContext; + protected int chatId; + private int contactId; @Override public void onCreate(Bundle bundle) { @@ -63,44 +58,54 @@ public void onCreate(Bundle bundle) { chatId = getArguments() != null ? getArguments().getInt(CHAT_ID_EXTRA, -1) : -1; contactId = getArguments().getInt(CONTACT_ID_EXTRA, -1); dcContext = DcHelper.getContext(requireContext()); - pickContactLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - Intent data = result.getData(); - Log.i(TAG, "Received result from activity, resultCode=" + result.getResultCode() + ", data=" + data); - if (result.getResultCode() == Activity.RESULT_OK && data != null) { - List selected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA); - List deselected = data.getIntegerArrayListExtra(ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA); - Util.runOnAnyBackgroundThread(() -> { - if (deselected != null) { // Remove members that were deselected - Log.i(TAG, deselected.size() + " members removed"); - int[] members = dcContext.getChatContacts(chatId); - for (int contactId : deselected) { - for (int memberId : members) { - if (memberId == contactId) { - dcContext.removeContactFromChat(chatId, memberId); - break; - } - } + pickContactLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + Intent data = result.getData(); + Log.i( + TAG, + "Received result from activity, resultCode=" + + result.getResultCode() + + ", data=" + + data); + if (result.getResultCode() == Activity.RESULT_OK && data != null) { + List selected = + data.getIntegerArrayListExtra(ContactMultiSelectionActivity.CONTACTS_EXTRA); + List deselected = + data.getIntegerArrayListExtra( + ContactMultiSelectionActivity.DESELECTED_CONTACTS_EXTRA); + Util.runOnAnyBackgroundThread( + () -> { + if (deselected != null) { // Remove members that were deselected + Log.i(TAG, deselected.size() + " members removed"); + int[] members = dcContext.getChatContacts(chatId); + for (int contactId : deselected) { + for (int memberId : members) { + if (memberId == contactId) { + dcContext.removeContactFromChat(chatId, memberId); + break; + } + } + } + } + + if (selected != null) { // Add new members + Log.i(TAG, selected.size() + " members added"); + for (Integer contactId : selected) { + if (contactId != null) { + dcContext.addContactToChat(chatId, contactId); + } + } + } + }); } - } - - if (selected != null) { // Add new members - Log.i(TAG, selected.size() + " members added"); - for (Integer contactId : selected) { - if (contactId != null) { - dcContext.addContactToChat(chatId, contactId); - } - } - } - }); - } - } - ); + }); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.profile_fragment, container, false); adapter = new ProfileAdapter(this, GlideApp.with(this), this); @@ -110,7 +115,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, ViewUtil.applyWindowInsets(list); list.setAdapter(adapter); - list.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); + list.setLayoutManager( + new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); update(); @@ -133,33 +139,34 @@ public void handleEvent(@NonNull DcEvent event) { update(); } - private void update() - { - int[] memberList = null; + private void update() { + int[] memberList = null; DcChatlist sharedChats = null; DcChat dcChat = null; DcContact dcContact = null; - if (contactId>0) { dcContact = dcContext.getContact(contactId); } - if (chatId>0) { dcChat = dcContext.getChat(chatId); } + if (contactId > 0) { + dcContact = dcContext.getContact(contactId); + } + if (chatId > 0) { + dcChat = dcContext.getChat(chatId); + } - if(dcChat!=null && dcChat.isMultiUser()) { + if (dcChat != null && dcChat.isMultiUser()) { memberList = dcContext.getChatContacts(chatId); - } - else if(contactId>0 && contactId!=DcContact.DC_CONTACT_ID_SELF) { + } else if (contactId > 0 && contactId != DcContact.DC_CONTACT_ID_SELF) { sharedChats = dcContext.getChatlist(0, null, contactId); } adapter.changeData(memberList, dcContact, sharedChats, dcChat); } - // handle events // ========================================================================= @Override public void onSettingsClicked(int settingsId) { - switch(settingsId) { + switch (settingsId) { case ProfileAdapter.ITEM_ALL_MEDIA_BUTTON: if (chatId > 0) { Intent intent = new Intent(getActivity(), AllMediaActivity.class); @@ -178,28 +185,31 @@ public void onSettingsClicked(int settingsId) { @Override public void onStatusLongClicked(boolean isMultiUser) { - Context context = requireContext(); - new AlertDialog.Builder(context) - .setTitle(isMultiUser? R.string.chat_description : R.string.pref_default_status_label) - .setItems(new CharSequence[]{ - context.getString(R.string.menu_copy_to_clipboard) - }, - (dialogInterface, i) -> { - Util.writeTextToClipboard(context, adapter.getStatusText()); - Toast.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show(); - }) + Context context = requireContext(); + new AlertDialog.Builder(context) + .setTitle(isMultiUser ? R.string.chat_description : R.string.pref_default_status_label) + .setItems( + new CharSequence[] {context.getString(R.string.menu_copy_to_clipboard)}, + (dialogInterface, i) -> { + Util.writeTextToClipboard(context, adapter.getStatusText()); + Toast.makeText( + context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) + .show(); + }) .setNegativeButton(R.string.cancel, null) .show(); } @Override public void onMemberLongClicked(int contactId) { - if (contactId>DcContact.DC_CONTACT_ID_LAST_SPECIAL || contactId==DcContact.DC_CONTACT_ID_SELF) { - if (actionMode==null) { + if (contactId > DcContact.DC_CONTACT_ID_LAST_SPECIAL + || contactId == DcContact.DC_CONTACT_ID_SELF) { + if (actionMode == null) { DcChat dcChat = dcContext.getChat(chatId); if (dcChat.canSend() && dcChat.isEncrypted()) { adapter.toggleMemberSelection(contactId); - actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback); + actionMode = + ((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback); } } else { onMemberClicked(contactId); @@ -209,8 +219,9 @@ public void onMemberLongClicked(int contactId) { @Override public void onMemberClicked(int contactId) { - if (actionMode!=null) { - if (contactId>DcContact.DC_CONTACT_ID_LAST_SPECIAL || contactId==DcContact.DC_CONTACT_ID_SELF) { + if (actionMode != null) { + if (contactId > DcContact.DC_CONTACT_ID_LAST_SPECIAL + || contactId == DcContact.DC_CONTACT_ID_SELF) { adapter.toggleMemberSelection(contactId); if (adapter.getSelectedMembersCount() == 0) { actionMode.finish(); @@ -219,14 +230,11 @@ public void onMemberClicked(int contactId) { actionMode.setTitle(String.valueOf(adapter.getSelectedMembersCount())); } } - } - else if(contactId==DcContact.DC_CONTACT_ID_ADD_MEMBER) { + } else if (contactId == DcContact.DC_CONTACT_ID_ADD_MEMBER) { onAddMember(); - } - else if(contactId==DcContact.DC_CONTACT_ID_QR_INVITE) { + } else if (contactId == DcContact.DC_CONTACT_ID_QR_INVITE) { onQrInvite(); - } - else if(contactId>DcContact.DC_CONTACT_ID_LAST_SPECIAL) { + } else if (contactId > DcContact.DC_CONTACT_ID_LAST_SPECIAL) { Intent intent = new Intent(getContext(), ProfileActivity.class); intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, contactId); startActivity(intent); @@ -235,7 +243,7 @@ else if(contactId>DcContact.DC_CONTACT_ID_LAST_SPECIAL) { @Override public void onAvatarClicked() { - ProfileActivity activity = (ProfileActivity)getActivity(); + ProfileActivity activity = (ProfileActivity) getActivity(); activity.onEnlargeAvatar(); } @@ -319,16 +327,24 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) { readableToDelList.append(dcContext.getContact(toDelId).getDisplayName()); } DcChat dcChat = dcContext.getChat(chatId); - AlertDialog dialog = new AlertDialog.Builder(requireContext()) - .setPositiveButton(R.string.remove_desktop, (d, which) -> { - for (Integer toDelId : toDelIds) { - dcContext.removeContactFromChat(chatId, toDelId); - } - mode.finish(); - }) - .setNegativeButton(android.R.string.cancel, null) - .setMessage(getString(dcChat.isOutBroadcast() ? R.string.ask_remove_from_channel : R.string.ask_remove_members, readableToDelList)) - .show(); + AlertDialog dialog = + new AlertDialog.Builder(requireContext()) + .setPositiveButton( + R.string.remove_desktop, + (d, which) -> { + for (Integer toDelId : toDelIds) { + dcContext.removeContactFromChat(chatId, toDelId); + } + mode.finish(); + }) + .setNegativeButton(android.R.string.cancel, null) + .setMessage( + getString( + dcChat.isOutBroadcast() + ? R.string.ask_remove_from_channel + : R.string.ask_remove_members, + readableToDelList)) + .show(); Util.redPositiveButton(dialog); return true; } diff --git a/src/main/java/org/thoughtcrime/securesms/ProfileStatusItem.java b/src/main/java/org/thoughtcrime/securesms/ProfileStatusItem.java index 4e55e046c..1f1a9d2de 100644 --- a/src/main/java/org/thoughtcrime/securesms/ProfileStatusItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ProfileStatusItem.java @@ -5,16 +5,14 @@ import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; - import androidx.appcompat.widget.AppCompatTextView; - import org.thoughtcrime.securesms.util.Linkifier; import org.thoughtcrime.securesms.util.LongClickMovementMethod; public class ProfileStatusItem extends LinearLayout { private AppCompatTextView statusTextView; - private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); + private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); public ProfileStatusItem(Context context) { super(context); diff --git a/src/main/java/org/thoughtcrime/securesms/ProfileTextItem.java b/src/main/java/org/thoughtcrime/securesms/ProfileTextItem.java index 73b440510..8e7288fd5 100644 --- a/src/main/java/org/thoughtcrime/securesms/ProfileTextItem.java +++ b/src/main/java/org/thoughtcrime/securesms/ProfileTextItem.java @@ -1,17 +1,14 @@ package org.thoughtcrime.securesms; import android.content.Context; -import android.graphics.Color; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; - import org.thoughtcrime.securesms.util.ResUtil; public class ProfileTextItem extends LinearLayout { @@ -40,7 +37,10 @@ public void set(String label, int icon, boolean tint) { if (icon != 0) { Drawable orgDrawable = ContextCompat.getDrawable(getContext(), icon); if (orgDrawable != null) { - Drawable drawable = orgDrawable.mutate(); // avoid global state modification and showing eg. app-icon tinted also elsewhere + Drawable drawable = + orgDrawable + .mutate(); // avoid global state modification and showing eg. app-icon tinted also + // elsewhere drawable = DrawableCompat.wrap(drawable); if (tint) { int color = ResUtil.getColor(getContext(), R.attr.colorAccent); diff --git a/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java b/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java index cdb64f2a3..5764ff732 100644 --- a/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java +++ b/src/main/java/org/thoughtcrime/securesms/ResolveMediaTask.java @@ -8,112 +8,117 @@ import android.os.AsyncTask; import android.provider.OpenableColumns; import android.util.Log; - -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; - +import de.cketti.safecontentresolver.SafeContentResolver; import java.io.FileNotFoundException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.HashSet; - -import de.cketti.safecontentresolver.SafeContentResolver; +import org.thoughtcrime.securesms.providers.PersistentBlobProvider; public class ResolveMediaTask extends AsyncTask { - private static final String TAG = ResolveMediaTask.class.getSimpleName(); - - interface OnMediaResolvedListener { - void onMediaResolved(Uri uri); - } - - private final WeakReference contextRef; - private final WeakReference listenerWeakReference; - - private static final HashSet instances = new HashSet<>(); - - ResolveMediaTask(Activity activityContext, ResolveMediaTask.OnMediaResolvedListener listener) { - this.contextRef = new WeakReference<>(activityContext); - this.listenerWeakReference = new WeakReference<>(listener); - instances.add(this); - } - - @Override - protected Uri doInBackground(Uri... uris) { - try { - Uri uri = uris[0]; - if (uris.length != 1 || uri == null) { - return null; - } - - InputStream inputStream; - String fileName = null; - Long fileSize = null; - - SafeContentResolver safeContentResolver = SafeContentResolver.newInstance(contextRef.get()); - inputStream = safeContentResolver.openInputStream(uri); - - if (inputStream == null) { - return null; - } - - Cursor cursor = contextRef.get().getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null); - - try { - if (cursor != null && cursor.moveToFirst()) { - try { - fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); - fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); - } catch (IllegalArgumentException e) { - Log.w(TAG, e); - } - } - } finally { - if (cursor != null) cursor.close(); - } - - if (fileName == null) { - fileName = uri.getLastPathSegment(); - } - - String mimeType = getMimeType(contextRef.get(), uri); - return PersistentBlobProvider.getInstance().create(contextRef.get(), inputStream, mimeType, fileName, fileSize); - } catch (NullPointerException | FileNotFoundException ioe) { - Log.w(TAG, ioe); - return null; - } - } - - @Override - protected void onPostExecute(Uri uri) { - instances.remove(this); - if (!this.isCancelled()) { - listenerWeakReference.get().onMediaResolved(uri); - } - } - - @Override - protected void onCancelled() { - instances.remove(this); - super.onCancelled(); - listenerWeakReference.get().onMediaResolved(null); - - } - - public static boolean isExecuting() { - return !instances.isEmpty(); - } - - public static void cancelTasks() { - for (ResolveMediaTask task : instances) { - task.cancel(true); - } + private static final String TAG = ResolveMediaTask.class.getSimpleName(); + + interface OnMediaResolvedListener { + void onMediaResolved(Uri uri); + } + + private final WeakReference contextRef; + private final WeakReference listenerWeakReference; + + private static final HashSet instances = new HashSet<>(); + + ResolveMediaTask(Activity activityContext, ResolveMediaTask.OnMediaResolvedListener listener) { + this.contextRef = new WeakReference<>(activityContext); + this.listenerWeakReference = new WeakReference<>(listener); + instances.add(this); + } + + @Override + protected Uri doInBackground(Uri... uris) { + try { + Uri uri = uris[0]; + if (uris.length != 1 || uri == null) { + return null; + } + + InputStream inputStream; + String fileName = null; + Long fileSize = null; + + SafeContentResolver safeContentResolver = SafeContentResolver.newInstance(contextRef.get()); + inputStream = safeContentResolver.openInputStream(uri); + + if (inputStream == null) { + return null; + } + + Cursor cursor = + contextRef + .get() + .getContentResolver() + .query( + uri, + new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, + null, + null, + null); + + try { + if (cursor != null && cursor.moveToFirst()) { + try { + fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); + fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); + } catch (IllegalArgumentException e) { + Log.w(TAG, e); + } } + } finally { + if (cursor != null) cursor.close(); + } + + if (fileName == null) { + fileName = uri.getLastPathSegment(); + } + + String mimeType = getMimeType(contextRef.get(), uri); + return PersistentBlobProvider.getInstance() + .create(contextRef.get(), inputStream, mimeType, fileName, fileSize); + } catch (NullPointerException | FileNotFoundException ioe) { + Log.w(TAG, ioe); + return null; + } + } - private boolean hasFileScheme(Uri uri) { - if (uri == null) { - return false; - } - return "file".equals(uri.getScheme()); + @Override + protected void onPostExecute(Uri uri) { + instances.remove(this); + if (!this.isCancelled()) { + listenerWeakReference.get().onMediaResolved(uri); + } + } + + @Override + protected void onCancelled() { + instances.remove(this); + super.onCancelled(); + listenerWeakReference.get().onMediaResolved(null); + } + + public static boolean isExecuting() { + return !instances.isEmpty(); + } + + public static void cancelTasks() { + for (ResolveMediaTask task : instances) { + task.cancel(true); } + } + private boolean hasFileScheme(Uri uri) { + if (uri == null) { + return false; + } + return "file".equals(uri.getScheme()); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/SetStartingPositionLinearLayoutManager.java b/src/main/java/org/thoughtcrime/securesms/SetStartingPositionLinearLayoutManager.java index 2bc698593..22f90480e 100644 --- a/src/main/java/org/thoughtcrime/securesms/SetStartingPositionLinearLayoutManager.java +++ b/src/main/java/org/thoughtcrime/securesms/SetStartingPositionLinearLayoutManager.java @@ -2,15 +2,11 @@ import android.content.Context; import android.os.Parcelable; - import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import org.thoughtcrime.securesms.util.ViewUtil; -/** - * Like LinearLayoutManager but you can set a starting position - */ +/** Like LinearLayoutManager but you can set a starting position */ class SetStartingPositionLinearLayoutManager extends LinearLayoutManager { private int pendingStartingPos = -1; @@ -46,4 +42,4 @@ public void onRestoreInstanceState(Parcelable state) { public void setStartingPosition(int position) { pendingStartingPos = position; } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/ShareActivity.java b/src/main/java/org/thoughtcrime/securesms/ShareActivity.java index 46db468b8..a9fb0ccc3 100644 --- a/src/main/java/org/thoughtcrime/securesms/ShareActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ShareActivity.java @@ -24,15 +24,14 @@ import android.util.Log; import android.view.MenuItem; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.core.content.pm.ShortcutManagerCompat; - import com.b44t.messenger.DcContext; - +import java.util.ArrayList; +import java.util.List; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.permissions.Permissions; @@ -41,24 +40,21 @@ import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ShareUtil; -import java.util.ArrayList; -import java.util.List; - /** * An activity to quickly share content with chats * * @author Jake McGinty */ -public class ShareActivity extends PassphraseRequiredActionBarActivity implements ResolveMediaTask.OnMediaResolvedListener -{ +public class ShareActivity extends PassphraseRequiredActionBarActivity + implements ResolveMediaTask.OnMediaResolvedListener { private static final String TAG = ShareActivity.class.getSimpleName(); public static final String EXTRA_ACC_ID = "acc_id"; public static final String EXTRA_CHAT_ID = "chat_id"; - private ArrayList resolvedExtras; - private DcContext dcContext; - private boolean isResolvingUrisOnMainThread; + private ArrayList resolvedExtras; + private DcContext dcContext; + private boolean isResolvingUrisOnMainThread; @Override protected void onPreCreate() { @@ -115,10 +111,10 @@ private void initializeMedia() { } } - if (Intent.ACTION_SEND.equals(getIntent().getAction()) && - getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) { - Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); - streamExtras.add(uri); + if (Intent.ACTION_SEND.equals(getIntent().getAction()) + && getIntent().getParcelableExtra(Intent.EXTRA_STREAM) != null) { + Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + streamExtras.add(uri); } else if (getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM) != null) { streamExtras = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM); } else if (getIntent().getData() != null) { @@ -137,7 +133,7 @@ private void initializeMedia() { } private boolean needsFilePermission(List uris) { - for(Uri uri : uris) { + for (Uri uri : uris) { // uri may be null, however, hasFileScheme() just returns false in this case if (hasFileScheme(uri)) { return true; @@ -148,17 +144,19 @@ private boolean needsFilePermission(List uris) { private void requestPermissionForFiles(List streamExtras) { Permissions.with(this) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) - .alwaysGrantOnSdk33() - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) - .onAllGranted(() -> resolveUris(streamExtras)) - .onAnyDenied(this::abortShare) - .execute(); + .request( + Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) + .alwaysGrantOnSdk33() + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) + .onAllGranted(() -> resolveUris(streamExtras)) + .onAnyDenied(this::abortShare) + .execute(); } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } @@ -187,7 +185,9 @@ private void resolveUris(List streamExtras) { public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); switch (item.getItemId()) { - case android.R.id.home: finish(); return true; + case android.R.id.home: + finish(); + return true; } return false; } @@ -199,15 +199,16 @@ public void onMediaResolved(Uri uri) { } if (!ResolveMediaTask.isExecuting() && !isResolvingUrisOnMainThread) { - handleResolvedMedia(getIntent()); + handleResolvedMedia(getIntent()); } } private void handleResolvedMedia(Intent intent) { - int accId = intent.getIntExtra(EXTRA_ACC_ID, -1); - int chatId = intent.getIntExtra(EXTRA_CHAT_ID, -1); + int accId = intent.getIntExtra(EXTRA_ACC_ID, -1); + int chatId = intent.getIntExtra(EXTRA_CHAT_ID, -1); - // the intent coming from shortcuts in the share selector might not include the custom extras but the shortcut ID + // the intent coming from shortcuts in the share selector might not include the custom extras + // but the shortcut ID String shortcutId = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID); if ((chatId == -1 || accId == -1) && shortcutId != null && shortcutId.startsWith("chat-")) { String[] args = shortcutId.split("-"); @@ -219,47 +220,47 @@ private void handleResolvedMedia(Intent intent) { String[] extraEmail = getIntent().getStringArrayExtra(Intent.EXTRA_EMAIL); /* - usually, external app will try to start "e-mail sharing" intent, providing it: - 1. address(s), packed in array, marked as Intent.EXTRA_EMAIL - mandatory - 2. shared content (files, pics, video), packed in Intent.EXTRA_STREAM - optional - - here is a sample code to trigger this routine from within external app: - - try { - Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts( - "mailto", "someone@example.com", null)); - File f = new File(getFilesDir() + "/somebinaryfile.bin"); - f.createNewFile(); - f.setReadable(true, false); - byte[] b = new byte[1024]; - new Random().nextBytes(b); - FileOutputStream fOut = new FileOutputStream(f); - DataOutputStream dataStream = new DataOutputStream(fOut); - dataStream.write(b); - dataStream.close(); - - Uri sharedURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", f); - emailIntent.setAction(Intent.ACTION_SEND); - emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"someone@example.com"}); - emailIntent.setType("text/plain"); - emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - emailIntent.putExtra(Intent.EXTRA_STREAM, sharedURI); - - // to EXPLICITLY fire DC's sharing activity: - // emailIntent.setComponent(new ComponentName("com.b44t.messenger.beta", "org.thoughtcrime.securesms.ShareActivity")); - - startActivity(emailIntent); - } catch (IOException e) { - e.printStackTrace(); - }catch (ActivityNotFoundException e) { - } - */ + usually, external app will try to start "e-mail sharing" intent, providing it: + 1. address(s), packed in array, marked as Intent.EXTRA_EMAIL - mandatory + 2. shared content (files, pics, video), packed in Intent.EXTRA_STREAM - optional + + here is a sample code to trigger this routine from within external app: + + try { + Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts( + "mailto", "someone@example.com", null)); + File f = new File(getFilesDir() + "/somebinaryfile.bin"); + f.createNewFile(); + f.setReadable(true, false); + byte[] b = new byte[1024]; + new Random().nextBytes(b); + FileOutputStream fOut = new FileOutputStream(f); + DataOutputStream dataStream = new DataOutputStream(fOut); + dataStream.write(b); + dataStream.close(); + + Uri sharedURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", f); + emailIntent.setAction(Intent.ACTION_SEND); + emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"someone@example.com"}); + emailIntent.setType("text/plain"); + emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + emailIntent.putExtra(Intent.EXTRA_STREAM, sharedURI); + + // to EXPLICITLY fire DC's sharing activity: + // emailIntent.setComponent(new ComponentName("com.b44t.messenger.beta", "org.thoughtcrime.securesms.ShareActivity")); + + startActivity(emailIntent); + } catch (IOException e) { + e.printStackTrace(); + }catch (ActivityNotFoundException e) { + } + */ - if(chatId == -1 && extraEmail != null && extraEmail.length > 0) { + if (chatId == -1 && extraEmail != null && extraEmail.length > 0) { final String addr = extraEmail[0]; int contactId = dcContext.lookupContactIdByAddr(addr); - if(contactId == 0) { + if (contactId == 0) { contactId = dcContext.createContact(null, addr); } @@ -290,9 +291,9 @@ private Intent getBaseShareIntent(final @NonNull Class target) { ShareUtil.setSharedUris(intent, resolvedExtras); String text = getIntent().getStringExtra(Intent.EXTRA_TEXT); - if (text==null) { + if (text == null) { CharSequence cs = getIntent().getCharSequenceExtra(Intent.EXTRA_TEXT); - if (cs!=null) { + if (cs != null) { text = cs.toString(); } } @@ -320,10 +321,9 @@ private String getMimeType(@Nullable Uri uri) { } private boolean hasFileScheme(Uri uri) { - if (uri==null) { + if (uri == null) { return false; } return "file".equals(uri.getScheme()); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/ShareLocationDialog.java b/src/main/java/org/thoughtcrime/securesms/ShareLocationDialog.java index e251e4de2..61be592f4 100644 --- a/src/main/java/org/thoughtcrime/securesms/ShareLocationDialog.java +++ b/src/main/java/org/thoughtcrime/securesms/ShareLocationDialog.java @@ -1,30 +1,44 @@ package org.thoughtcrime.securesms; import android.content.Context; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; public class ShareLocationDialog { - public static void show(final Context context, final @NonNull ShareLocationDurationSelectionListener listener) { + public static void show( + final Context context, final @NonNull ShareLocationDurationSelectionListener listener) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.title_share_location); - builder.setItems(R.array.share_location_durations, (dialog, which) -> { - final int shareLocationUnit; - - switch (which) { - default: - case 0: shareLocationUnit = 5 * 60; break; - case 1: shareLocationUnit = 30 * 60; break; - case 2: shareLocationUnit = 60 * 60; break; - case 3: shareLocationUnit = 2 * 60 * 60; break; - case 4: shareLocationUnit = 6 * 60 * 60; break; - case 5: shareLocationUnit = 12 * 60 * 60; break; - } - - listener.onSelected(shareLocationUnit); - }); + builder.setItems( + R.array.share_location_durations, + (dialog, which) -> { + final int shareLocationUnit; + + switch (which) { + default: + case 0: + shareLocationUnit = 5 * 60; + break; + case 1: + shareLocationUnit = 30 * 60; + break; + case 2: + shareLocationUnit = 60 * 60; + break; + case 3: + shareLocationUnit = 2 * 60 * 60; + break; + case 4: + shareLocationUnit = 6 * 60 * 60; + break; + case 5: + shareLocationUnit = 12 * 60 * 60; + break; + } + + listener.onSelected(shareLocationUnit); + }); builder.setNegativeButton(R.string.cancel, null); builder.show(); diff --git a/src/main/java/org/thoughtcrime/securesms/StatsSending.java b/src/main/java/org/thoughtcrime/securesms/StatsSending.java index 4b98dfbc0..7dad0cae9 100644 --- a/src/main/java/org/thoughtcrime/securesms/StatsSending.java +++ b/src/main/java/org/thoughtcrime/securesms/StatsSending.java @@ -7,21 +7,19 @@ import android.content.Context; import android.text.util.Linkify; import android.widget.TextView; - import androidx.appcompat.app.AlertDialog; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - +import java.util.Locale; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.IntentUtils; import org.thoughtcrime.securesms.util.Prefs; -import java.util.Locale; - public class StatsSending { - /** @noinspection unused: We will start adding a device message once stats-sending is tested a bit */ + /** + * @noinspection unused: We will start adding a device message once stats-sending is tested a bit + */ public static void maybeAddStatsSendingDeviceMsg(Context context) { if (Prefs.getStatsDeviceMsgId(context) != 0) { return; @@ -33,15 +31,15 @@ public static void maybeAddStatsSendingDeviceMsg(Context context) { DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); msg.setText(context.getString(R.string.stats_device_message)); int msgId = dcContext.addDeviceMsg("stats_device_message", msg); - if (msgId!=0) { + if (msgId != 0) { Prefs.setStatsDeviceMsgId(context, msgId); } } public static boolean isStatsSendingDeviceMsg(Context context, DcMsg msg) { return msg != null - && msg.getFromId() == DcContact.DC_CONTACT_ID_DEVICE - && msg.getId() == Prefs.getStatsDeviceMsgId(context); + && msg.getFromId() == DcContact.DC_CONTACT_ID_DEVICE + && msg.getId() == Prefs.getStatsDeviceMsgId(context); } public static void statsDeviceMsgTapped(Activity activity) { @@ -52,17 +50,22 @@ public static void statsDeviceMsgTapped(Activity activity) { } } - public static void showStatsConfirmationDialog(Activity activity, Runnable onConfigChangedListener) { - AlertDialog d = new AlertDialog.Builder(activity) - .setMessage(R.string.stats_confirmation_dialog) - .setNegativeButton(R.string.cancel, (_d, i) -> {}) - .setPositiveButton(R.string.yes, (_d, i) -> { - DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "1"); - onConfigChangedListener.run(); - showStatsThanksDialog(activity); - }) - .setNeutralButton(R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending")) - .create(); + public static void showStatsConfirmationDialog( + Activity activity, Runnable onConfigChangedListener) { + AlertDialog d = + new AlertDialog.Builder(activity) + .setMessage(R.string.stats_confirmation_dialog) + .setNegativeButton(R.string.cancel, (_d, i) -> {}) + .setPositiveButton( + R.string.yes, + (_d, i) -> { + DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "1"); + onConfigChangedListener.run(); + showStatsThanksDialog(activity); + }) + .setNeutralButton( + R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending")) + .create(); d.show(); try { //noinspection DataFlowIssue @@ -76,25 +79,35 @@ public static void showStatsConfirmationDialog(Activity activity, Runnable onCon private static void showStatsThanksDialog(Activity activity) { String stats_id = DcHelper.get(activity, DcHelper.CONFIG_STATS_ID); new AlertDialog.Builder(activity) - .setMessage(R.string.stats_thanks) - .setNeutralButton(R.string.no, (d, i) -> {}) - .setPositiveButton(R.string.yes, (d, i) -> { - String ln = Locale.getDefault().getLanguage(); - IntentUtils.showInBrowser(activity, "https://cispa.qualtrics.com/jfe/form/SV_9YmhkpGa48KxfLg?id=" + stats_id + "&ln=" + ln); - }) - .show(); + .setMessage(R.string.stats_thanks) + .setNeutralButton(R.string.no, (d, i) -> {}) + .setPositiveButton( + R.string.yes, + (d, i) -> { + String ln = Locale.getDefault().getLanguage(); + IntentUtils.showInBrowser( + activity, + "https://cispa.qualtrics.com/jfe/form/SV_9YmhkpGa48KxfLg?id=" + + stats_id + + "&ln=" + + ln); + }) + .show(); } public static void showStatsDisableDialog(Activity activity) { - AlertDialog d = new AlertDialog.Builder(activity) - .setMessage(R.string.stats_disable_dialog) - .setNegativeButton(R.string.disable, (_d, i) -> { - DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "0"); - }) - .setPositiveButton(R.string.stats_keep_sending, (_d, i) -> { - }) - .setNeutralButton(R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending")) - .create(); + AlertDialog d = + new AlertDialog.Builder(activity) + .setMessage(R.string.stats_disable_dialog) + .setNegativeButton( + R.string.disable, + (_d, i) -> { + DcHelper.set(activity, DcHelper.CONFIG_STATS_SENDING, "0"); + }) + .setPositiveButton(R.string.stats_keep_sending, (_d, i) -> {}) + .setNeutralButton( + R.string.more_info_desktop, (_d, i) -> openHelp(activity, "#statssending")) + .create(); d.show(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/TransportOption.java b/src/main/java/org/thoughtcrime/securesms/TransportOption.java index 50b190afd..c6a725e74 100644 --- a/src/main/java/org/thoughtcrime/securesms/TransportOption.java +++ b/src/main/java/org/thoughtcrime/securesms/TransportOption.java @@ -9,18 +9,18 @@ public enum Type { NORMAL_MAIL } - private final int drawable; - private final @NonNull String text; - private final @NonNull String composeHint; - - public TransportOption(@NonNull Type type, - @DrawableRes int drawable, - @NonNull String text, - @NonNull String composeHint) - { - this.drawable = drawable; - this.text = text; - this.composeHint = composeHint; + private final int drawable; + private final @NonNull String text; + private final @NonNull String composeHint; + + public TransportOption( + @NonNull Type type, + @DrawableRes int drawable, + @NonNull String text, + @NonNull String composeHint) { + this.drawable = drawable; + this.text = text; + this.composeHint = composeHint; } public @NonNull Type getType() { diff --git a/src/main/java/org/thoughtcrime/securesms/TransportOptions.java b/src/main/java/org/thoughtcrime/securesms/TransportOptions.java index 7761e1dcb..3bf38ab4d 100644 --- a/src/main/java/org/thoughtcrime/securesms/TransportOptions.java +++ b/src/main/java/org/thoughtcrime/securesms/TransportOptions.java @@ -3,26 +3,23 @@ import static org.thoughtcrime.securesms.TransportOption.Type; import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.util.guava.Optional; - import java.util.LinkedList; import java.util.List; +import org.thoughtcrime.securesms.util.guava.Optional; public class TransportOptions { private final List listeners = new LinkedList<>(); - private final Context context; - private final List enabledTransports; + private final Context context; + private final List enabledTransports; - private Type defaultTransportType = Type.NORMAL_MAIL; - private Optional selectedOption = Optional.absent(); + private Type defaultTransportType = Type.NORMAL_MAIL; + private Optional selectedOption = Optional.absent(); public TransportOptions(Context context) { - this.context = context; - this.enabledTransports = initializeAvailableTransports(); + this.context = context; + this.enabledTransports = initializeAvailableTransports(); } public void reset() { @@ -48,7 +45,7 @@ public void setDefaultTransport(Type type) { } } - public void setSelectedTransport(@Nullable TransportOption transportOption) { + public void setSelectedTransport(@Nullable TransportOption transportOption) { this.selectedOption = Optional.fromNullable(transportOption); notifyTransportChangeListeners(); } @@ -76,9 +73,12 @@ public void addOnTransportChangedListener(OnTransportChangedListener listener) { private List initializeAvailableTransports() { List results = new LinkedList<>(); - results.add(new TransportOption(Type.NORMAL_MAIL, R.drawable.ic_send_sms_white_24dp, - context.getString(R.string.menu_send), - context.getString(R.string.chat_input_placeholder))); + results.add( + new TransportOption( + Type.NORMAL_MAIL, + R.drawable.ic_send_sms_white_24dp, + context.getString(R.string.menu_send), + context.getString(R.string.chat_input_placeholder))); return results; } diff --git a/src/main/java/org/thoughtcrime/securesms/TransportOptionsAdapter.java b/src/main/java/org/thoughtcrime/securesms/TransportOptionsAdapter.java index 4ed9c7f6b..15bf07b37 100644 --- a/src/main/java/org/thoughtcrime/securesms/TransportOptionsAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/TransportOptionsAdapter.java @@ -7,12 +7,9 @@ import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.util.ViewUtil; - import java.util.List; +import org.thoughtcrime.securesms.util.ViewUtil; public class TransportOptionsAdapter extends BaseAdapter { @@ -20,11 +17,10 @@ public class TransportOptionsAdapter extends BaseAdapter { private List enabledTransports; - public TransportOptionsAdapter(@NonNull Context context, - @NonNull List enabledTransports) - { + public TransportOptionsAdapter( + @NonNull Context context, @NonNull List enabledTransports) { super(); - this.inflater = LayoutInflater.from(context); + this.inflater = LayoutInflater.from(context); this.enabledTransports = enabledTransports; } @@ -53,10 +49,10 @@ public View getView(int position, View convertView, ViewGroup parent) { convertView = inflater.inflate(R.layout.transport_selection_list_item, parent, false); } - TransportOption transport = (TransportOption) getItem(position); - ImageView imageView = ViewUtil.findById(convertView, R.id.icon); - TextView textView = ViewUtil.findById(convertView, R.id.text); - TextView subtextView = ViewUtil.findById(convertView, R.id.subtext); + TransportOption transport = (TransportOption) getItem(position); + ImageView imageView = ViewUtil.findById(convertView, R.id.icon); + TextView textView = ViewUtil.findById(convertView, R.id.text); + TextView subtextView = ViewUtil.findById(convertView, R.id.subtext); imageView.setImageResource(transport.getDrawable()); textView.setText(transport.getDescription()); diff --git a/src/main/java/org/thoughtcrime/securesms/TransportOptionsPopup.java b/src/main/java/org/thoughtcrime/securesms/TransportOptionsPopup.java index 63e8096f1..1173e00cc 100644 --- a/src/main/java/org/thoughtcrime/securesms/TransportOptionsPopup.java +++ b/src/main/java/org/thoughtcrime/securesms/TransportOptionsPopup.java @@ -4,30 +4,32 @@ import android.view.View; import android.widget.AdapterView; import android.widget.ListView; - import androidx.annotation.NonNull; import androidx.appcompat.widget.ListPopupWindow; - import java.util.LinkedList; import java.util.List; public class TransportOptionsPopup extends ListPopupWindow implements ListView.OnItemClickListener { private final TransportOptionsAdapter adapter; - private final SelectedListener listener; + private final SelectedListener listener; - public TransportOptionsPopup(@NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) { + public TransportOptionsPopup( + @NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) { super(context); this.listener = listener; - this.adapter = new TransportOptionsAdapter(context, new LinkedList()); + this.adapter = new TransportOptionsAdapter(context, new LinkedList()); - setVerticalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff)); - setHorizontalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff)); + setVerticalOffset( + context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff)); + setHorizontalOffset( + context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff)); setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); setModal(true); setAnchorView(anchor); setAdapter(adapter); - setContentWidth(context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width)); + setContentWidth( + context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width)); setOnItemClickListener(this); } @@ -40,11 +42,10 @@ public void display(List enabledTransports) { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - listener.onSelected((TransportOption)adapter.getItem(position)); + listener.onSelected((TransportOption) adapter.getItem(position)); } public interface SelectedListener { void onSelected(TransportOption option); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/WebViewActivity.java b/src/main/java/org/thoughtcrime/securesms/WebViewActivity.java index f6d1ec8fe..d30fe206e 100644 --- a/src/main/java/org/thoughtcrime/securesms/WebViewActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/WebViewActivity.java @@ -12,7 +12,6 @@ import android.webkit.WebViewClient; import android.widget.ImageView; import android.widget.Toast; - import androidx.annotation.RequiresApi; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; @@ -21,44 +20,46 @@ import androidx.webkit.ProxyController; import androidx.webkit.WebSettingsCompat; import androidx.webkit.WebViewFeature; - +import chat.delta.rpc.types.SecurejoinSource; +import java.net.IDN; import org.thoughtcrime.securesms.qr.QrCodeHandler; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.IntentUtils; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import chat.delta.rpc.types.SecurejoinSource; - -import java.net.IDN; - public class WebViewActivity extends PassphraseRequiredActionBarActivity - implements SearchView.OnQueryTextListener, - WebView.FindListener -{ + implements SearchView.OnQueryTextListener, WebView.FindListener { private static final String TAG = WebViewActivity.class.getSimpleName(); protected WebView webView; - /** Return true the window content should display fullscreen/edge-to-edge ex. in the integrated maps app */ - protected boolean immersiveMode() { return false; } + /** + * Return true the window content should display fullscreen/edge-to-edge ex. in the integrated + * maps app + */ + protected boolean immersiveMode() { + return false; + } - protected boolean shouldAskToOpenLink() { return false; } + protected boolean shouldAskToOpenLink() { + return false; + } protected void toggleFakeProxy(boolean enable) { if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { if (enable) { // Set proxy to non-routable address. - ProxyConfig proxyConfig = new ProxyConfig.Builder() - .removeImplicitRules() - .addProxyRule("0.0.0.0") - .build(); - ProxyController.getInstance().setProxyOverride(proxyConfig, Runnable::run, () -> Log.i(TAG, "Set WebView proxy.")); + ProxyConfig proxyConfig = + new ProxyConfig.Builder().removeImplicitRules().addProxyRule("0.0.0.0").build(); + ProxyController.getInstance() + .setProxyOverride(proxyConfig, Runnable::run, () -> Log.i(TAG, "Set WebView proxy.")); } else { - ProxyController.getInstance().clearProxyOverride(Runnable::run, () -> Log.i(TAG, "Cleared WebView proxy.")); + ProxyController.getInstance() + .clearProxyOverride(Runnable::run, () -> Log.i(TAG, "Cleared WebView proxy.")); } } else { - Log.w(TAG, "Cannot " + (enable? "set": "clear") + " WebView proxy."); + Log.w(TAG, "Cannot " + (enable ? "set" : "clear") + " WebView proxy."); } } @@ -72,65 +73,70 @@ protected void onCreate(Bundle state, boolean ready) { webView = findViewById(R.id.webview); - if(immersiveMode()) { + if (immersiveMode()) { // set a shadow in the status bar to make it more readable - findViewById(R.id.status_bar_background).setBackgroundResource(R.drawable.search_toolbar_shadow); + findViewById(R.id.status_bar_background) + .setBackgroundResource(R.drawable.search_toolbar_shadow); } else { // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true, false); + ViewUtil.applyWindowInsets( + findViewById(R.id.content_container), true, true, true, true, true, false); } - webView.setWebViewClient(new WebViewClient() { - // IMPORTANT: this is will likely not be called inside iframes unless target=_blank is used in the anchor/link tag. - // `shouldOverrideUrlLoading()` is called when the user clicks a URL, - // returning `true` causes the WebView to abort loading the URL, - // returning `false` causes the WebView to continue loading the URL as usual. - // the method is not called for POST request nor for on-page-links. - // - // nb: from API 24, `shouldOverrideUrlLoading(String)` is deprecated and - // `shouldOverrideUrlLoading(WebResourceRequest)` shall be used. - // the new one has the same functionality, and the old one still exist, - // so, to support all systems, for now, using the old one seems to be the simplest way. - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url != null) { - String schema = url.split(":")[0].toLowerCase(); - switch (schema) { - case "http": - case "https": - case "gemini": - case "tel": - case "sms": - case "mailto": - case "openpgp4fpr": - case "geo": - case "dcaccount": - case "dclogin": - return openOnlineUrl(url); + webView.setWebViewClient( + new WebViewClient() { + // IMPORTANT: this is will likely not be called inside iframes unless target=_blank is + // used in the anchor/link tag. + // `shouldOverrideUrlLoading()` is called when the user clicks a URL, + // returning `true` causes the WebView to abort loading the URL, + // returning `false` causes the WebView to continue loading the URL as usual. + // the method is not called for POST request nor for on-page-links. + // + // nb: from API 24, `shouldOverrideUrlLoading(String)` is deprecated and + // `shouldOverrideUrlLoading(WebResourceRequest)` shall be used. + // the new one has the same functionality, and the old one still exist, + // so, to support all systems, for now, using the old one seems to be the simplest way. + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (url != null) { + String schema = url.split(":")[0].toLowerCase(); + switch (schema) { + case "http": + case "https": + case "gemini": + case "tel": + case "sms": + case "mailto": + case "openpgp4fpr": + case "geo": + case "dcaccount": + case "dclogin": + return openOnlineUrl(url); + } + } + return true; // returning `true` aborts loading } - } - return true; // returning `true` aborts loading - } - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - WebResourceResponse res = interceptRequest(url); - if (res!=null) { - return res; - } - return super.shouldInterceptRequest(view, url); - } + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + WebResourceResponse res = interceptRequest(url); + if (res != null) { + return res; + } + return super.shouldInterceptRequest(view, url); + } - @Override - @RequiresApi(21) - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - WebResourceResponse res = interceptRequest(request.getUrl().toString()); - if (res!=null) { - return res; - } - return super.shouldInterceptRequest(view, request); - } - }); + @Override + @RequiresApi(21) + public WebResourceResponse shouldInterceptRequest( + WebView view, WebResourceRequest request) { + WebResourceResponse res = interceptRequest(request.getUrl().toString()); + if (res != null) { + return res; + } + return super.shouldInterceptRequest(view, request); + } + }); webView.setFindListener(this); // disable "safe browsing" as this has privacy issues, @@ -143,10 +149,15 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque } protected void setForceDark() { - if (Build.VERSION.SDK_INT <= 32 && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { - // needed for older API (tested on android7) that do not set `color-scheme` without the following hint - WebSettingsCompat.setForceDark(webView.getSettings(), - DynamicTheme.isDarkTheme(this) ? WebSettingsCompat.FORCE_DARK_ON : WebSettingsCompat.FORCE_DARK_OFF); + if (Build.VERSION.SDK_INT <= 32 + && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + // needed for older API (tested on android7) that do not set `color-scheme` without the + // following hint + WebSettingsCompat.setForceDark( + webView.getSettings(), + DynamicTheme.isDarkTheme(this) + ? WebSettingsCompat.FORCE_DARK_ON + : WebSettingsCompat.FORCE_DARK_OFF); } } @@ -177,29 +188,31 @@ public boolean onPrepareOptionsMenu(Menu menu) { try { MenuItem searchItem = menu.findItem(R.id.menu_search_web_view); - searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(final MenuItem item) { - searchMenu = menu; - WebViewActivity.this.lastQuery = ""; - WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, true); - return true; - } - - @Override - public boolean onMenuItemActionCollapse(final MenuItem item) { - WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, false); - return true; - } - }); + searchItem.setOnActionExpandListener( + new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(final MenuItem item) { + searchMenu = menu; + WebViewActivity.this.lastQuery = ""; + WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, true); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(final MenuItem item) { + WebViewActivity.this.makeSearchMenuVisible(menu, searchItem, false); + return true; + } + }); SearchView searchView = (SearchView) searchItem.getActionView(); searchView.setOnQueryTextListener(this); searchView.setQueryHint(getString(R.string.search)); searchView.setIconifiedByDefault(true); - // hide the [X] beside the search field - this is too much noise, search can be aborted eg. by "back" + // hide the [X] beside the search field - this is too much noise, search can be aborted eg. by + // "back" ImageView closeBtn = searchView.findViewById(R.id.search_close_btn); - if (closeBtn!=null) { + if (closeBtn != null) { closeBtn.setEnabled(false); closeBtn.setImageDrawable(null); } @@ -211,18 +224,17 @@ public boolean onMenuItemActionCollapse(final MenuItem item) { return true; } - // search - private Menu searchMenu = null; + private Menu searchMenu = null; private String lastQuery = ""; - private Toast lastToast = null; + private Toast lastToast = null; private void updateResultCounter(int curr, int total) { - if (searchMenu!=null) { + if (searchMenu != null) { MenuItem item = searchMenu.findItem(R.id.menu_search_counter); - if (curr!=-1) { - item.setTitle(String.format("%d/%d", total==0? 0 : curr+1, total)); + if (curr != -1) { + item.setTitle(String.format("%d/%d", total == 0 ? 0 : curr + 1, total)); item.setVisible(true); } else { item.setVisible(false); @@ -239,7 +251,7 @@ public boolean onQueryTextSubmit(String query) { public boolean onQueryTextChange(String query) { String normQuery = query.trim(); lastQuery = normQuery; - if (lastToast!=null) { + if (lastToast != null) { lastToast.cancel(); lastToast = null; } @@ -248,10 +260,10 @@ public boolean onQueryTextChange(String query) { } @Override - public void onFindResultReceived (int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) - { + public void onFindResultReceived( + int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { if (isDoneCounting) { - if (numberOfMatches>0) { + if (numberOfMatches > 0) { updateResultCounter(activeMatchOrdinal, numberOfMatches); } else { if (lastQuery.isEmpty()) { @@ -269,7 +281,6 @@ public void onFindResultReceived (int activeMatchOrdinal, int numberOfMatches, b } } - // other actions @Override @@ -310,15 +321,17 @@ protected boolean openOnlineUrl(String url) { if (shouldAskToOpenLink()) { new AlertDialog.Builder(this) - .setTitle(R.string.open_url_confirmation) - .setMessage(IDN.toASCII(url)) - .setNeutralButton(R.string.cancel, null) - .setPositiveButton(R.string.open, (d, w) -> IntentUtils.showInBrowser(this, url)) - .setNegativeButton(R.string.global_menu_edit_copy_desktop, (d, w) -> { - Util.writeTextToClipboard(this, url); - Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - }) - .show(); + .setTitle(R.string.open_url_confirmation) + .setMessage(IDN.toASCII(url)) + .setNeutralButton(R.string.cancel, null) + .setPositiveButton(R.string.open, (d, w) -> IntentUtils.showInBrowser(this, url)) + .setNegativeButton( + R.string.global_menu_edit_copy_desktop, + (d, w) -> { + Util.writeTextToClipboard(this, url); + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); + }) + .show(); } else { IntentUtils.showInBrowser(this, url); } diff --git a/src/main/java/org/thoughtcrime/securesms/WebxdcActivity.java b/src/main/java/org/thoughtcrime/securesms/WebxdcActivity.java index ed4a286c0..2dcb57ac9 100644 --- a/src/main/java/org/thoughtcrime/securesms/WebxdcActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/WebxdcActivity.java @@ -25,7 +25,6 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.ActionBar; @@ -33,23 +32,13 @@ import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; import com.google.common.base.Charsets; - -import org.json.JSONObject; -import org.thoughtcrime.securesms.connect.AccountManager; -import org.thoughtcrime.securesms.connect.DcEventCenter; -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.util.IntentUtils; -import org.thoughtcrime.securesms.util.JsonUtils; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.Prefs; -import org.thoughtcrime.securesms.util.Util; - import java.io.ByteArrayInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -60,11 +49,16 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import org.json.JSONObject; +import org.thoughtcrime.securesms.connect.AccountManager; +import org.thoughtcrime.securesms.connect.DcEventCenter; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.util.IntentUtils; +import org.thoughtcrime.securesms.util.JsonUtils; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.Util; -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - -public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate { +public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate { private static final String TAG = WebxdcActivity.class.getSimpleName(); private static final String EXTRA_ACCOUNT_ID = "accountId"; private static final String EXTRA_APP_MSG_ID = "appMessageId"; @@ -109,8 +103,9 @@ public static void openMaps(Context context, int chatId, String href) { e.printStackTrace(); } if (msgId == 0) { - Toast.makeText(context, "Cannot get maps.xdc, see log for details.", Toast.LENGTH_LONG).show(); - return; + Toast.makeText(context, "Cannot get maps.xdc, see log for details.", Toast.LENGTH_LONG) + .show(); + return; } dcContext.setConfigInt("ui.maps_version", mapsVersion); } @@ -125,13 +120,15 @@ public static void openWebxdcActivity(Context context, @NonNull DcMsg instance, openWebxdcActivity(context, instance.getId(), false, href); } - public static void openWebxdcActivity(Context context, int msgId, boolean hideActionBar, String href) { + public static void openWebxdcActivity( + Context context, int msgId, boolean hideActionBar, String href) { if (!Util.isClickedRecently()) { context.startActivity(getWebxdcIntent(context, msgId, hideActionBar, href)); } } - private static Intent getWebxdcIntent(Context context, int msgId, boolean hideActionBar, String href) { + private static Intent getWebxdcIntent( + Context context, int msgId, boolean hideActionBar, String href) { DcContext dcContext = DcHelper.getContext(context); Intent intent = new Intent(context, WebxdcActivity.class); intent.setAction(Intent.ACTION_VIEW); @@ -145,20 +142,23 @@ private static Intent getWebxdcIntent(Context context, int msgId, boolean hideAc private static Intent[] getWebxdcIntentWithParentStack(Context context, int msgId) { DcContext dcContext = DcHelper.getContext(context); - final Intent chatIntent = new Intent(context, ConversationActivity.class) - .putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId()) - .setAction(Intent.ACTION_VIEW); + final Intent chatIntent = + new Intent(context, ConversationActivity.class) + .putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId()) + .setAction(Intent.ACTION_VIEW); final Intent webxdcIntent = getWebxdcIntent(context, msgId, false, ""); return TaskStackBuilder.create(context) - .addNextIntentWithParentStack(chatIntent) - .addNextIntent(webxdcIntent) - .getIntents(); + .addNextIntentWithParentStack(chatIntent) + .addNextIntent(webxdcIntent) + .getIntents(); } @Override - protected boolean immersiveMode() { return hideActionBar; } + protected boolean immersiveMode() { + return hideActionBar; + } @Override protected void onCreate(Bundle state, boolean ready) { @@ -169,29 +169,36 @@ protected void onCreate(Bundle state, boolean ready) { rpc = DcHelper.getRpc(this); initTTS(); + webView.setWebChromeClient( + new WebChromeClient() { + @Override + @RequiresApi(21) + public boolean onShowFileChooser( + WebView webView, + ValueCallback filePathCallback, + WebChromeClient.FileChooserParams fileChooserParams) { + if (WebxdcActivity.this.filePathCallback != null) { + WebxdcActivity.this.filePathCallback.onReceiveValue(null); + } + WebxdcActivity.this.filePathCallback = filePathCallback; + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + intent.putExtra( + Intent.EXTRA_ALLOW_MULTIPLE, + fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE); + WebxdcActivity.this.startActivityForResult( + Intent.createChooser(intent, getString(R.string.select)), REQUEST_CODE_FILE_PICKER); + return true; + } + }); - webView.setWebChromeClient(new WebChromeClient() { - @Override - @RequiresApi(21) - public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { - if (WebxdcActivity.this.filePathCallback != null) { - WebxdcActivity.this.filePathCallback.onReceiveValue(null); - } - WebxdcActivity.this.filePathCallback = filePathCallback; - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE); - WebxdcActivity.this.startActivityForResult(Intent.createChooser(intent, getString(R.string.select)), REQUEST_CODE_FILE_PICKER); - return true; - } - }); - - DcEventCenter eventCenter = DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext()); + DcEventCenter eventCenter = + DcHelper.getEventCenter(WebxdcActivity.this.getApplicationContext()); eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this); eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this); eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_REALTIME_DATA, this); - + int appMessageId = b.getInt(EXTRA_APP_MSG_ID); int accountId = b.getInt(EXTRA_ACCOUNT_ID); this.dcContext = DcHelper.getContext(getApplicationContext()); @@ -207,20 +214,23 @@ public boolean onShowFileChooser(WebView webView, ValueCallback filePathC return; } - // `msg_id` in the subdomain makes sure, different apps using same files do not share the same cache entry + // `msg_id` in the subdomain makes sure, different apps using same files do not share the same + // cache entry // (WebView may use a global cache shared across objects). - // (a random-id would also work, but would need maintenance and does not add benefits as we regard the file-part interceptRequest() only, + // (a random-id would also work, but would need maintenance and does not add benefits as we + // regard the file-part interceptRequest() only, // also a random-id is not that useful for debugging) this.baseURL = "https://acc" + dcContext.getAccountId() + "-msg" + appMessageId + ".localhost"; final JSONObject info = this.dcAppMsg.getWebxdcInfo(); internetAccess = JsonUtils.optBoolean(info, "internet_access"); if ("landscape".equals(JsonUtils.optString(info, "orientation"))) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } else { - // enter fullscreen mode if necessary, - // this is needed here because if the app is opened while already in landscape mode, onConfigurationChanged() is not triggered - setScreenMode(getResources().getConfiguration()); + // enter fullscreen mode if necessary, + // this is needed here because if the app is opened while already in landscape mode, + // onConfigurationChanged() is not triggered + setScreenMode(getResources().getConfiguration()); } selfAddr = info.optString("self_addr"); sendUpdateMaxSize = info.optInt("send_update_max_size"); @@ -238,7 +248,9 @@ public boolean onShowFileChooser(WebView webView, ValueCallback filePathC webSettings.setAllowUniversalAccessFromFileURLs(false); webSettings.setDatabaseEnabled(true); webSettings.setDomStorageEnabled(true); - webView.setNetworkAvailable(internetAccess); // this does not block network but sets `window.navigator.isOnline` in js land + webView.setNetworkAvailable( + internetAccess); // this does not block network but sets `window.navigator.isOnline` in js + // land webView.addJavascriptInterface(new InternalJSApi(), "InternalJSApi"); String extraHref = b.getString(EXTRA_HREF, ""); @@ -256,18 +268,21 @@ public boolean onShowFileChooser(WebView webView, ValueCallback filePathC webView.loadUrl(this.baseURL + "/webxdc_bootstrap324567869.html?i=1&href=" + encodedHref); - Util.runOnAnyBackgroundThread(() -> { - final DcChat chat = dcContext.getChat(dcAppMsg.getChatId()); - Util.runOnMain(() -> { - updateTitleAndMenu(info, chat); - }); - }); + Util.runOnAnyBackgroundThread( + () -> { + final DcChat chat = dcContext.getChat(dcAppMsg.getChatId()); + Util.runOnMain( + () -> { + updateTitleAndMenu(info, chat); + }); + }); } @Override public void onResume() { super.onResume(); - DcHelper.getNotificationCenter(this).updateVisibleWebxdc(dcContext.getAccountId(), dcAppMsg.getId()); + DcHelper.getNotificationCenter(this) + .updateVisibleWebxdc(dcContext.getAccountId(), dcAppMsg.getId()); } @Override @@ -322,18 +337,24 @@ public void onConfigurationChanged(Configuration newConfig) { } private void initTTS() { - tts = new TextToSpeech(this, new TextToSpeech.OnInitListener() { - @Override - public void onInit(int status) { - Log.i(TAG, "TTS Init Status: " + status); - } - }); + tts = + new TextToSpeech( + this, + new TextToSpeech.OnInitListener() { + @Override + public void onInit(int status) { + Log.i(TAG, "TTS Init Status: " + status); + } + }); } + private void setScreenMode(Configuration config) { // enter/exit fullscreen mode depending on orientation (landscape/portrait), // on tablets there is enough height so fullscreen mode is never enabled there - boolean enable = config.orientation == Configuration.ORIENTATION_LANDSCAPE && !getResources().getBoolean(R.bool.isBigScreen); - getWindow().getDecorView().setSystemUiVisibility(enable? View.SYSTEM_UI_FLAG_FULLSCREEN : 0); + boolean enable = + config.orientation == Configuration.ORIENTATION_LANDSCAPE + && !getResources().getBoolean(R.bool.isBigScreen); + getWindow().getDecorView().setSystemUiVisibility(enable ? View.SYSTEM_UI_FLAG_FULLSCREEN : 0); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { if (hideActionBar || enable) { @@ -345,11 +366,14 @@ private void setScreenMode(Configuration config) { } @Override - protected boolean shouldAskToOpenLink() { return true; } + protected boolean shouldAskToOpenLink() { + return true; + } // This is usually only called when internetAccess == true or for mailto/openpgp4fpr scheme, // because when internetAccess == false, the page is loaded inside an iframe, - // and WebViewClient.shouldOverrideUrlLoading is not called for HTTP(S) links inside the iframe unless target=_blank is used + // and WebViewClient.shouldOverrideUrlLoading is not called for HTTP(S) links inside the iframe + // unless target=_blank is used @Override protected boolean openOnlineUrl(String url) { Log.i(TAG, "openOnlineUrl: " + url); @@ -378,8 +402,10 @@ protected WebResourceResponse interceptRequest(String rawUrl) { } else if (path.equalsIgnoreCase("/webxdc_bootstrap324567869.html")) { InputStream targetStream = getResources().openRawResource(R.raw.webxdc_wrapper); res = new WebResourceResponse("text/html", "UTF-8", targetStream); - } else if (path.equalsIgnoreCase("/sandboxed_iframe_rtcpeerconnection_check_5965668501706.html")) { - InputStream targetStream = getResources().openRawResource(R.raw.sandboxed_iframe_rtcpeerconnection_check); + } else if (path.equalsIgnoreCase( + "/sandboxed_iframe_rtcpeerconnection_check_5965668501706.html")) { + InputStream targetStream = + getResources().openRawResource(R.raw.sandboxed_iframe_rtcpeerconnection_check); res = new WebResourceResponse("text/html", "UTF-8", targetStream); } else { byte[] blob = this.dcAppMsg.getWebxdcBlob(path); @@ -393,33 +419,41 @@ protected WebResourceResponse interceptRequest(String rawUrl) { String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); if (mimeType == null) { switch (ext) { - case "js": mimeType = "text/javascript"; break; - case "wasm": mimeType = "application/wasm"; break; - default: mimeType = "application/octet-stream"; Log.i(TAG, "unknown mime type for " + rawUrl); break; + case "js": + mimeType = "text/javascript"; + break; + case "wasm": + mimeType = "application/wasm"; + break; + default: + mimeType = "application/octet-stream"; + Log.i(TAG, "unknown mime type for " + rawUrl); + break; } } - String encoding = mimeType.startsWith("text/")? "UTF-8" : null; + String encoding = mimeType.startsWith("text/") ? "UTF-8" : null; InputStream targetStream = new ByteArrayInputStream(blob); res = new WebResourceResponse(mimeType, encoding, targetStream); } } catch (Exception e) { e.printStackTrace(); - InputStream targetStream = new ByteArrayInputStream(("Webxdc Request Error: " + e.getMessage()).getBytes()); + InputStream targetStream = + new ByteArrayInputStream(("Webxdc Request Error: " + e.getMessage()).getBytes()); res = new WebResourceResponse("text/plain", "UTF-8", targetStream); } if (!internetAccess) { Map headers = new HashMap<>(); - headers.put("Content-Security-Policy", + headers.put( + "Content-Security-Policy", "default-src 'self'; " - + "style-src 'self' 'unsafe-inline' blob: ; " - + "font-src 'self' data: blob: ; " - + "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: ; " - + "connect-src 'self' data: blob: ; " - + "img-src 'self' data: blob: ; " - + "media-src 'self' data: blob: ;" - + "webrtc 'block' ; " - ); + + "style-src 'self' 'unsafe-inline' blob: ; " + + "font-src 'self' data: blob: ; " + + "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: ; " + + "connect-src 'self' data: blob: ; " + + "img-src 'self' data: blob: ; " + + "media-src 'self' data: blob: ;" + + "webrtc 'block' ; "); headers.put("X-DNS-Prefetch-Control", "off"); res.setResponseHeaders(headers); } @@ -427,49 +461,59 @@ protected WebResourceResponse interceptRequest(String rawUrl) { } private void callJavaScriptFunction(String func) { - webView.evaluateJavascript("document.getElementById('frame').contentWindow." + func + ";", null); + webView.evaluateJavascript( + "document.getElementById('frame').contentWindow." + func + ";", null); } @Override public void handleEvent(@NonNull DcEvent event) { int eventId = event.getId(); - if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE && event.getData1Int() == dcAppMsg.getId())) { + if ((eventId == DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE + && event.getData1Int() == dcAppMsg.getId())) { Log.i(TAG, "handling status update event"); callJavaScriptFunction("__webxdcUpdate()"); - } else if ((eventId == DcContext.DC_EVENT_WEBXDC_REALTIME_DATA && event.getData1Int() == dcAppMsg.getId())) { + } else if ((eventId == DcContext.DC_EVENT_WEBXDC_REALTIME_DATA + && event.getData1Int() == dcAppMsg.getId())) { Log.i(TAG, "handling realtime data event"); StringBuilder data = new StringBuilder(); for (byte b : event.getData2Blob()) { - data.append(((int) b) + ","); + data.append(((int) b) + ","); } callJavaScriptFunction("__webxdcRealtimeData([" + data + "])"); - } else if ((eventId == DcContext.DC_EVENT_MSGS_CHANGED && event.getData2Int() == dcAppMsg.getId())) { - this.dcAppMsg = this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db - Util.runOnAnyBackgroundThread(() -> { - final JSONObject info = dcAppMsg.getWebxdcInfo(); - final DcChat chat = dcContext.getChat(dcAppMsg.getChatId()); - Util.runOnMain(() -> { - updateTitleAndMenu(info, chat); - }); - }); + } else if ((eventId == DcContext.DC_EVENT_MSGS_CHANGED + && event.getData2Int() == dcAppMsg.getId())) { + this.dcAppMsg = + this.dcContext.getMsg(event.getData2Int()); // msg changed, reload data from db + Util.runOnAnyBackgroundThread( + () -> { + final JSONObject info = dcAppMsg.getWebxdcInfo(); + final DcChat chat = dcContext.getChat(dcAppMsg.getChatId()); + Util.runOnMain( + () -> { + updateTitleAndMenu(info, chat); + }); + }); } } private void updateTitleAndMenu(JSONObject info, DcChat chat) { - final String docName = JsonUtils.optString(info, "document"); - final String xdcName = JsonUtils.optString(info, "name"); - final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url"); - getSupportActionBar().setTitle((docName.isEmpty() ? xdcName : docName) + " – " + chat.getName()); - if (!sourceCodeUrl.equals(currSourceCodeUrl)) { - sourceCodeUrl = currSourceCodeUrl; - invalidateOptionsMenu(); - } + final String docName = JsonUtils.optString(info, "document"); + final String xdcName = JsonUtils.optString(info, "name"); + final String currSourceCodeUrl = JsonUtils.optString(info, "source_code_url"); + getSupportActionBar() + .setTitle((docName.isEmpty() ? xdcName : docName) + " – " + chat.getName()); + if (!sourceCodeUrl.equals(currSourceCodeUrl)) { + sourceCodeUrl = currSourceCodeUrl; + invalidateOptionsMenu(); + } } private void showInChat() { Intent intent = new Intent(this, ConversationActivity.class); intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcAppMsg.getChatId()); - intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, DcMsg.getMessagePosition(dcAppMsg, dcContext)); + intent.putExtra( + ConversationActivity.STARTING_POSITION_EXTRA, + DcMsg.getMessagePosition(dcAppMsg, dcContext)); startActivity(intent); } @@ -487,17 +531,23 @@ public static void addToHomeScreen(Activity activity, int msgId) { BitmapDrawable drawable = (BitmapDrawable) Drawable.createFromStream(is, "icon"); Bitmap bitmap = drawable.getBitmap(); - ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId) - .setShortLabel(docName.isEmpty() ? xdcName : docName) - .setIcon(IconCompat.createWithBitmap(bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too small circle and defamiliarize the icon too much - .setIntents(getWebxdcIntentWithParentStack(context, msgId)) - .build(); + ShortcutInfoCompat shortcutInfoCompat = + new ShortcutInfoCompat.Builder(context, "xdc-" + dcContext.getAccountId() + "-" + msgId) + .setShortLabel(docName.isEmpty() ? xdcName : docName) + .setIcon( + IconCompat.createWithBitmap( + bitmap)) // createWithAdaptiveBitmap() removes decorations but cuts out a too + // small circle and defamiliarize the icon too much + .setIntents(getWebxdcIntentWithParentStack(context, msgId)) + .build(); Toast.makeText(context, R.string.one_moment, Toast.LENGTH_SHORT).show(); if (!ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) { - Toast.makeText(context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG).show(); + Toast.makeText( + context, "ErrAddToHomescreen: requestPinShortcut() failed", Toast.LENGTH_LONG) + .show(); } - } catch(Exception e) { + } catch (Exception e) { Toast.makeText(context, "ErrAddToHomescreen: " + e, Toast.LENGTH_LONG).show(); } } @@ -509,7 +559,7 @@ public void onActivityResult(int reqCode, int resultCode, final Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { try { if (data.getDataString() != null) { - dataUris = new Uri[]{Uri.parse(data.getDataString())}; + dataUris = new Uri[] {Uri.parse(data.getDataString())}; } else if (data.getClipData() != null) { final int numSelectedFiles = data.getClipData().getItemCount(); dataUris = new Uri[numSelectedFiles]; @@ -558,36 +608,49 @@ public String selfAddr() { return WebxdcActivity.this.selfAddr; } - /** @noinspection unused*/ + /** + * @noinspection unused + */ @JavascriptInterface public String selfName() { return WebxdcActivity.this.dcContext.getName(); } - /** @noinspection unused*/ + /** + * @noinspection unused + */ @JavascriptInterface public boolean sendStatusUpdate(String payload) { Log.i(TAG, "sendStatusUpdate"); - if (!WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate(WebxdcActivity.this.dcAppMsg.getId(), payload)) { - DcChat dcChat = WebxdcActivity.this.dcContext.getChat(WebxdcActivity.this.dcAppMsg.getChatId()); - Toast.makeText(WebxdcActivity.this, - dcChat.isContactRequest() ? - WebxdcActivity.this.getString(R.string.accept_request_first) : - WebxdcActivity.this.dcContext.getLastError(), - Toast.LENGTH_LONG).show(); + if (!WebxdcActivity.this.dcContext.sendWebxdcStatusUpdate( + WebxdcActivity.this.dcAppMsg.getId(), payload)) { + DcChat dcChat = + WebxdcActivity.this.dcContext.getChat(WebxdcActivity.this.dcAppMsg.getChatId()); + Toast.makeText( + WebxdcActivity.this, + dcChat.isContactRequest() + ? WebxdcActivity.this.getString(R.string.accept_request_first) + : WebxdcActivity.this.dcContext.getLastError(), + Toast.LENGTH_LONG) + .show(); return false; } return true; } - /** @noinspection unused*/ + /** + * @noinspection unused + */ @JavascriptInterface public String getStatusUpdates(int lastKnownSerial) { Log.i(TAG, "getStatusUpdates"); - return WebxdcActivity.this.dcContext.getWebxdcStatusUpdates(WebxdcActivity.this.dcAppMsg.getId(), lastKnownSerial ); + return WebxdcActivity.this.dcContext.getWebxdcStatusUpdates( + WebxdcActivity.this.dcAppMsg.getId(), lastKnownSerial); } - /** @noinspection unused*/ + /** + * @noinspection unused + */ @JavascriptInterface public String sendToChat(String message) { Log.i(TAG, "sendToChat"); @@ -601,24 +664,27 @@ public String sendToChat(String message) { String name = null; String type = null; if (jsonObject.has("base64")) { - data = Base64.decode(jsonObject.getString("base64"), Base64.NO_WRAP | Base64.NO_PADDING); - name = jsonObject.getString("name"); + data = Base64.decode(jsonObject.getString("base64"), Base64.NO_WRAP | Base64.NO_PADDING); + name = jsonObject.getString("name"); } if (jsonObject.has("type")) { - type = jsonObject.getString("type"); + type = jsonObject.getString("type"); } if (jsonObject.has("text")) { - text = jsonObject.getString("text"); + text = jsonObject.getString("text"); } if (jsonObject.has("subject")) { - subject = jsonObject.getString("subject"); + subject = jsonObject.getString("subject"); } if (jsonObject.has("html")) { - html = jsonObject.getString("html"); + html = jsonObject.getString("html"); } - if (TextUtils.isEmpty(text) && TextUtils.isEmpty(subject) && TextUtils.isEmpty(html) && TextUtils.isEmpty(name)) { - return "provided file is invalid, you need to set both name and base64 content"; + if (TextUtils.isEmpty(text) + && TextUtils.isEmpty(subject) + && TextUtils.isEmpty(html) + && TextUtils.isEmpty(name)) { + return "provided file is invalid, you need to set both name and base64 content"; } DcHelper.sendToChat(WebxdcActivity.this, data, type, name, html, subject, text); @@ -629,7 +695,9 @@ public String sendToChat(String message) { } } - /** @noinspection unused*/ + /** + * @noinspection unused + */ @JavascriptInterface public void sendRealtimeAdvertisement() { int accountId = WebxdcActivity.this.dcContext.getAccountId(); @@ -641,13 +709,17 @@ public void sendRealtimeAdvertisement() { } } - /** @noinspection unused*/ + /** + * @noinspection unused + */ @JavascriptInterface public void leaveRealtimeChannel() { WebxdcActivity.this.leaveRealtimeChannel(); } - /** @noinspection unused*/ + /** + * @noinspection unused + */ @JavascriptInterface public void sendRealtimeData(String jsonData) { int accountId = WebxdcActivity.this.dcContext.getAccountId(); @@ -665,6 +737,5 @@ public void ttsSpeak(String text, String lang) { if (lang != null && !lang.isEmpty()) tts.setLanguage(Locale.forLanguageTag(lang)); tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, null); } - } } diff --git a/src/main/java/org/thoughtcrime/securesms/WebxdcStoreActivity.java b/src/main/java/org/thoughtcrime/securesms/WebxdcStoreActivity.java index 8d1f72734..bfadde2a2 100644 --- a/src/main/java/org/thoughtcrime/securesms/WebxdcStoreActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/WebxdcStoreActivity.java @@ -13,11 +13,13 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; - import androidx.appcompat.app.ActionBar; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.HttpResponse; import com.b44t.messenger.DcContext; - +import java.io.ByteArrayInputStream; +import java.util.HashMap; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.util.IntentUtils; @@ -27,13 +29,6 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.io.ByteArrayInputStream; -import java.util.HashMap; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; -import chat.delta.rpc.types.HttpResponse; - public class WebxdcStoreActivity extends PassphraseRequiredActionBarActivity { private static final String TAG = WebxdcStoreActivity.class.getSimpleName(); @@ -56,42 +51,58 @@ protected void onCreate(Bundle state, boolean ready) { actionBar.setTitle(R.string.webxdc_apps); } - webView.setWebViewClient(new WebViewClient() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - String ext = MediaUtil.getFileExtensionFromUrl(Uri.parse(url).getPath()); - if ("xdc".equals(ext)) { - Util.runOnAnyBackgroundThread(() -> { - try { - HttpResponse httpResponse = rpc.getHttpResponse(dcContext.getAccountId(), url); - byte[] blob = JsonUtils.decodeBase64(httpResponse.blob); - Uri uri = PersistentBlobProvider.getInstance().create(WebxdcStoreActivity.this, blob, "application/octet-stream", "app.xdc"); - Intent intent = new Intent(); - intent.setData(uri); - setResult(Activity.RESULT_OK, intent); - finish(); - } catch (RpcException e) { - e.printStackTrace(); - Util.runOnMain(() -> Toast.makeText(WebxdcStoreActivity.this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show()); + webView.setWebViewClient( + new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + String ext = MediaUtil.getFileExtensionFromUrl(Uri.parse(url).getPath()); + if ("xdc".equals(ext)) { + Util.runOnAnyBackgroundThread( + () -> { + try { + HttpResponse httpResponse = + rpc.getHttpResponse(dcContext.getAccountId(), url); + byte[] blob = JsonUtils.decodeBase64(httpResponse.blob); + Uri uri = + PersistentBlobProvider.getInstance() + .create( + WebxdcStoreActivity.this, + blob, + "application/octet-stream", + "app.xdc"); + Intent intent = new Intent(); + intent.setData(uri); + setResult(Activity.RESULT_OK, intent); + finish(); + } catch (RpcException e) { + e.printStackTrace(); + Util.runOnMain( + () -> + Toast.makeText( + WebxdcStoreActivity.this, + "Error: " + e.getMessage(), + Toast.LENGTH_LONG) + .show()); + } + }); + } else { + IntentUtils.showInBrowser(WebxdcStoreActivity.this, url); } - }); - } else { - IntentUtils.showInBrowser(WebxdcStoreActivity.this, url); - } - return true; - } - - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return shouldOverrideUrlLoading(view, request.getUrl().toString()); - } - - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - return interceptRequest(request.getUrl().toString()); - } - }); + return true; + } + + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return shouldOverrideUrlLoading(view, request.getUrl().toString()); + } + + @Override + public WebResourceResponse shouldInterceptRequest( + WebView view, WebResourceRequest request) { + return interceptRequest(request.getUrl().toString()); + } + }); WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); @@ -102,7 +113,8 @@ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceReque webSettings.setAllowUniversalAccessFromFileURLs(false); webSettings.setDatabaseEnabled(true); webSettings.setDomStorageEnabled(true); - webView.setNetworkAvailable(true); // this does not block network but sets `window.navigator.isOnline` in js land + webView.setNetworkAvailable( + true); // this does not block network but sets `window.navigator.isOnline` in js land webView.loadUrl(Prefs.getWebxdcStoreUrl(this)); } @@ -124,7 +136,9 @@ private WebResourceResponse interceptRequest(String url) { res = new WebResourceResponse(mimeType, httpResponse.encoding, data); } catch (Exception e) { e.printStackTrace(); - ByteArrayInputStream data = new ByteArrayInputStream(("Could not load apps. Are you online?\n\n" + e.getMessage()).getBytes()); + ByteArrayInputStream data = + new ByteArrayInputStream( + ("Could not load apps. Are you online?\n\n" + e.getMessage()).getBytes()); res = new WebResourceResponse("text/plain", "UTF-8", data); } @@ -144,5 +158,4 @@ public boolean onOptionsItemSelected(MenuItem item) { } return false; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/WelcomeActivity.java b/src/main/java/org/thoughtcrime/securesms/WelcomeActivity.java index 09dfc6260..57008932a 100644 --- a/src/main/java/org/thoughtcrime/securesms/WelcomeActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/WelcomeActivity.java @@ -11,21 +11,21 @@ import android.text.util.Linkify; import android.util.Log; import android.view.MenuItem; -import android.view.View; -import android.widget.Button; import android.widget.TextView; - import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import org.thoughtcrime.securesms.connect.AccountManager; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -44,340 +44,362 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.ProgressDialog; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class WelcomeActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate { - public static final String BACKUP_QR_EXTRA = "backup_qr_extra"; - public static final int PICK_BACKUP = 20574; - private final static String TAG = WelcomeActivity.class.getSimpleName(); - public static final String TMP_BACKUP_FILE = "tmp-backup-file"; - - private ProgressDialog progressDialog = null; - private boolean imexUserAborted; - DcContext dcContext; - private NotificationController notificationController; - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - setContentView(R.layout.welcome_activity); - - // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(findViewById(R.id.content_container)); - - findViewById(R.id.signup_button).setOnClickListener((v) -> startActivity(new Intent(this, InstantOnboardingActivity.class))); - findViewById(R.id.add_as_second_device_button).setOnClickListener((v) -> showSignInDialogWithPermission()); - findViewById(R.id.backup_button).setOnClickListener((v) -> startImportBackup()); - - registerForEvents(); - initializeActionBar(); - - getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { +public class WelcomeActivity extends BaseActionBarActivity + implements DcEventCenter.DcEventDelegate { + public static final String BACKUP_QR_EXTRA = "backup_qr_extra"; + public static final int PICK_BACKUP = 20574; + private static final String TAG = WelcomeActivity.class.getSimpleName(); + public static final String TMP_BACKUP_FILE = "tmp-backup-file"; + + private ProgressDialog progressDialog = null; + private boolean imexUserAborted; + DcContext dcContext; + private NotificationController notificationController; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + setContentView(R.layout.welcome_activity); + + // add padding to avoid content hidden behind system bars + ViewUtil.applyWindowInsets(findViewById(R.id.content_container)); + + findViewById(R.id.signup_button) + .setOnClickListener( + (v) -> startActivity(new Intent(this, InstantOnboardingActivity.class))); + findViewById(R.id.add_as_second_device_button) + .setOnClickListener((v) -> showSignInDialogWithPermission()); + findViewById(R.id.backup_button).setOnClickListener((v) -> startImportBackup()); + + registerForEvents(); + initializeActionBar(); + + getOnBackPressedDispatcher() + .addCallback( + this, + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { AccountManager accountManager = AccountManager.getInstance(); if (accountManager.canRollbackAccountCreation(WelcomeActivity.this)) { - accountManager.rollbackAccountCreation(WelcomeActivity.this); + accountManager.rollbackAccountCreation(WelcomeActivity.this); } else { - setEnabled(false); - getOnBackPressedDispatcher().onBackPressed(); + setEnabled(false); + getOnBackPressedDispatcher().onBackPressed(); } - } - }); - - DcHelper.maybeShowMigrationError(this); - } - - private void showSignInDialogWithPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - && !Prefs.getBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) { - Prefs.setBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, true); - Permissions.with(this) - .request(Manifest.permission.POST_NOTIFICATIONS) - .ifNecessary() - .onAllGranted(() -> { - startAddAsSecondDeviceActivity(); - }) - .onAnyDenied(() -> { - startAddAsSecondDeviceActivity(); - }) - .execute(); - } else { - startAddAsSecondDeviceActivity(); - } + } + }); + + DcHelper.maybeShowMigrationError(this); + } + + private void showSignInDialogWithPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && !Prefs.getBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, false)) { + Prefs.setBooleanPreference(this, Prefs.ASKED_FOR_NOTIFICATION_PERMISSION, true); + Permissions.with(this) + .request(Manifest.permission.POST_NOTIFICATIONS) + .ifNecessary() + .onAllGranted( + () -> { + startAddAsSecondDeviceActivity(); + }) + .onAnyDenied( + () -> { + startAddAsSecondDeviceActivity(); + }) + .execute(); + } else { + startAddAsSecondDeviceActivity(); } - - protected void initializeActionBar() { - ActionBar supportActionBar = getSupportActionBar(); - if (supportActionBar == null) throw new AssertionError(); - - boolean canGoBack = AccountManager.getInstance().canRollbackAccountCreation(this); - supportActionBar.setDisplayHomeAsUpEnabled(canGoBack); - getSupportActionBar().setTitle(canGoBack? R.string.add_account : R.string.app_name); + } + + protected void initializeActionBar() { + ActionBar supportActionBar = getSupportActionBar(); + if (supportActionBar == null) throw new AssertionError(); + + boolean canGoBack = AccountManager.getInstance().canRollbackAccountCreation(this); + supportActionBar.setDisplayHomeAsUpEnabled(canGoBack); + getSupportActionBar().setTitle(canGoBack ? R.string.add_account : R.string.app_name); + } + + private void registerForEvents() { + dcContext = DcHelper.getContext(this); + DcHelper.getEventCenter(this).addObserver(DcContext.DC_EVENT_IMEX_PROGRESS, this); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + switch (item.getItemId()) { + case android.R.id.home: + getOnBackPressedDispatcher().onBackPressed(); + return true; } - private void registerForEvents() { - dcContext = DcHelper.getContext(this); - DcHelper.getEventCenter(this).addObserver(DcContext.DC_EVENT_IMEX_PROGRESS, this); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - switch (item.getItemId()) { - case android.R.id.home: - getOnBackPressedDispatcher().onBackPressed(); - return true; - } - - return false; + return false; + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + @Override + public void onStart() { + super.onStart(); + String backupQr = getIntent().getStringExtra(BACKUP_QR_EXTRA); + if (backupQr != null) { + getIntent().removeExtra(BACKUP_QR_EXTRA); + startBackupTransfer(backupQr); } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } - - @Override - public void onStart() { - super.onStart(); - String backupQr = getIntent().getStringExtra(BACKUP_QR_EXTRA); - if (backupQr != null) { - getIntent().removeExtra(BACKUP_QR_EXTRA); - startBackupTransfer(backupQr); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - DcHelper.getEventCenter(this).removeObservers(this); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - } - - private void startAddAsSecondDeviceActivity() { - new IntentIntegrator(this).setCaptureActivity(RegistrationQrActivity.class) - .addExtra(RegistrationQrActivity.ADD_AS_SECOND_DEVICE_EXTRA, true) - .initiateScan(); - } - - @SuppressLint("InlinedApi") - private void startImportBackup() { - Permissions.with(this) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) - .alwaysGrantOnSdk30() - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) - .onAllGranted(() -> { - File imexDir = DcHelper.getImexDir(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - AttachmentManager.selectMediaType(this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri()); - } else { - final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath()); - if (backupFile != null) { - new AlertDialog.Builder(this) - .setTitle(R.string.import_backup_title) - .setMessage(String.format(getResources().getString(R.string.import_backup_ask), backupFile)) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (dialog, which) -> startImport(backupFile, null)) - .show(); - } - else { - new AlertDialog.Builder(this) - .setTitle(R.string.import_backup_title) - .setMessage(String.format(getResources().getString(R.string.import_backup_no_backup_found), imexDir.getAbsolutePath())) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - } - }) - .execute(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + DcHelper.getEventCenter(this).removeObservers(this); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + } + + private void startAddAsSecondDeviceActivity() { + new IntentIntegrator(this) + .setCaptureActivity(RegistrationQrActivity.class) + .addExtra(RegistrationQrActivity.ADD_AS_SECOND_DEVICE_EXTRA, true) + .initiateScan(); + } + + @SuppressLint("InlinedApi") + private void startImportBackup() { + Permissions.with(this) + .request( + Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) + .alwaysGrantOnSdk30() + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) + .onAllGranted( + () -> { + File imexDir = DcHelper.getImexDir(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + AttachmentManager.selectMediaType( + this, "application/x-tar", null, PICK_BACKUP, StorageUtil.getDownloadUri()); + } else { + final String backupFile = dcContext.imexHasBackup(imexDir.getAbsolutePath()); + if (backupFile != null) { + new AlertDialog.Builder(this) + .setTitle(R.string.import_backup_title) + .setMessage( + String.format( + getResources().getString(R.string.import_backup_ask), backupFile)) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton( + android.R.string.ok, (dialog, which) -> startImport(backupFile, null)) + .show(); + } else { + new AlertDialog.Builder(this) + .setTitle(R.string.import_backup_title) + .setMessage( + String.format( + getResources().getString(R.string.import_backup_no_backup_found), + imexDir.getAbsolutePath())) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + } + }) + .execute(); + } + + private void startImport(@Nullable final String backupFile, final @Nullable Uri backupFileUri) { + notificationController = + GenericForegroundService.startForegroundTask(this, getString(R.string.import_backup_title)); + + if (progressDialog != null) { + progressDialog.dismiss(); + progressDialog = null; } - private void startImport(@Nullable final String backupFile, final @Nullable Uri backupFileUri) - { - notificationController = GenericForegroundService.startForegroundTask(this, getString(R.string.import_backup_title)); - - if( progressDialog!=null ) { - progressDialog.dismiss(); - progressDialog = null; - } - - imexUserAborted = false; - progressDialog = new ProgressDialog(this); - progressDialog.setMessage(getResources().getString(R.string.one_moment)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); - progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources().getString(android.R.string.cancel), (dialog, which) -> { - imexUserAborted = true; - dcContext.stopOngoingProcess(); - notificationController.close(); - cleanupTempBackupFile(); + imexUserAborted = false; + progressDialog = new ProgressDialog(this); + progressDialog.setMessage(getResources().getString(R.string.one_moment)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setCancelable(false); + progressDialog.setButton( + DialogInterface.BUTTON_NEGATIVE, + getResources().getString(android.R.string.cancel), + (dialog, which) -> { + imexUserAborted = true; + dcContext.stopOngoingProcess(); + notificationController.close(); + cleanupTempBackupFile(); }); - progressDialog.show(); - - Util.runOnBackground(() -> { - String file = backupFile; - if (backupFile == null) { - try { - file = copyToCacheDir(backupFileUri).getAbsolutePath(); - } catch (IOException e) { - e.printStackTrace(); - notificationController.close(); - cleanupTempBackupFile(); - return; - } - } + progressDialog.show(); - DcHelper.getEventCenter(this).captureNextError(); - dcContext.imex(DcContext.DC_IMEX_IMPORT_BACKUP, file); - }); - } - - private File copyToCacheDir(Uri uri) throws IOException { - try (InputStream inputStream = PartAuthority.getAttachmentStream(this, uri)) { - File file = File.createTempFile(TMP_BACKUP_FILE, ".tmp", getCacheDir()); - try (OutputStream outputStream = new FileOutputStream(file)) { - StreamUtil.copy(inputStream, outputStream); + Util.runOnBackground( + () -> { + String file = backupFile; + if (backupFile == null) { + try { + file = copyToCacheDir(backupFileUri).getAbsolutePath(); + } catch (IOException e) { + e.printStackTrace(); + notificationController.close(); + cleanupTempBackupFile(); + return; } - return file; - } - } - - private void startBackupTransfer(String qrCode) - { - if (progressDialog!=null) { - progressDialog.dismiss(); - progressDialog = null; - } - - Intent intent = new Intent(this, BackupTransferActivity.class); - intent.putExtra(BackupTransferActivity.TRANSFER_MODE, BackupTransferActivity.TransferMode.RECEIVER_SCAN_QR.getInt()); - intent.putExtra(BackupTransferActivity.QR_CODE, qrCode); - startActivity(intent); - } + } - private void progressError(String data2) { - progressDialog.dismiss(); - maybeShowConfigurationError(this, data2); + DcHelper.getEventCenter(this).captureNextError(); + dcContext.imex(DcContext.DC_IMEX_IMPORT_BACKUP, file); + }); + } + + private File copyToCacheDir(Uri uri) throws IOException { + try (InputStream inputStream = PartAuthority.getAttachmentStream(this, uri)) { + File file = File.createTempFile(TMP_BACKUP_FILE, ".tmp", getCacheDir()); + try (OutputStream outputStream = new FileOutputStream(file)) { + StreamUtil.copy(inputStream, outputStream); + } + return file; } + } - private void progressUpdate(int progress) { - int percent = progress / 10; - progressDialog.setMessage(getResources().getString(R.string.one_moment)+String.format(" %d%%", percent)); + private void startBackupTransfer(String qrCode) { + if (progressDialog != null) { + progressDialog.dismiss(); + progressDialog = null; } - private void progressSuccess() { - DcHelper.getEventCenter(this).endCaptureNextError(); - progressDialog.dismiss(); - Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class); - intent.putExtra(ConversationListActivity.FROM_WELCOME, true); - startActivity(intent); - finish(); + Intent intent = new Intent(this, BackupTransferActivity.class); + intent.putExtra( + BackupTransferActivity.TRANSFER_MODE, + BackupTransferActivity.TransferMode.RECEIVER_SCAN_QR.getInt()); + intent.putExtra(BackupTransferActivity.QR_CODE, qrCode); + startActivity(intent); + } + + private void progressError(String data2) { + progressDialog.dismiss(); + maybeShowConfigurationError(this, data2); + } + + private void progressUpdate(int progress) { + int percent = progress / 10; + progressDialog.setMessage( + getResources().getString(R.string.one_moment) + String.format(" %d%%", percent)); + } + + private void progressSuccess() { + DcHelper.getEventCenter(this).endCaptureNextError(); + progressDialog.dismiss(); + Intent intent = new Intent(getApplicationContext(), ConversationListActivity.class); + intent.putExtra(ConversationListActivity.FROM_WELCOME, true); + startActivity(intent); + finish(); + } + + public static void maybeShowConfigurationError(Activity activity, String data2) { + if (activity.isFinishing()) return; // avoid android.view.WindowManager$BadTokenException + + if (data2 != null && !data2.isEmpty()) { + AlertDialog d = + new AlertDialog.Builder(activity) + .setMessage(data2) + .setPositiveButton(android.R.string.ok, null) + .create(); + d.show(); + try { + //noinspection ConstantConditions + Linkify.addLinks( + (TextView) d.findViewById(android.R.id.message), + Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); + } catch (NullPointerException e) { + e.printStackTrace(); + } } - - public static void maybeShowConfigurationError(Activity activity, String data2) { - if (activity.isFinishing()) return; // avoid android.view.WindowManager$BadTokenException - - if (data2 != null && !data2.isEmpty()) { - AlertDialog d = new AlertDialog.Builder(activity) - .setMessage(data2) - .setPositiveButton(android.R.string.ok, null) - .create(); - d.show(); - try { - //noinspection ConstantConditions - Linkify.addLinks((TextView) d.findViewById(android.R.id.message), Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); - } catch(NullPointerException e) { - e.printStackTrace(); - } + } + + @Override + public void handleEvent(@NonNull DcEvent event) { + int eventId = event.getId(); + + if (eventId == DcContext.DC_EVENT_IMEX_PROGRESS) { + long progress = event.getData1Int(); + if (progressDialog == null || notificationController == null) { + // IMEX runs in BackupTransferActivity + if (progress == 1000) { + finish(); // transfer done - remove ourself from the activity stack (finishAffinity is + // available in API 16, we're targeting API 14) } - } - - @Override - public void handleEvent(@NonNull DcEvent event) { - int eventId = event.getId(); - - if (eventId== DcContext.DC_EVENT_IMEX_PROGRESS ) { - long progress = event.getData1Int(); - if (progressDialog == null || notificationController == null) { - // IMEX runs in BackupTransferActivity - if (progress == 1000) { - finish(); // transfer done - remove ourself from the activity stack (finishAffinity is available in API 16, we're targeting API 14) - } - return; - } - if (progress==0/*error/aborted*/) { - if (!imexUserAborted) { - progressError(dcContext.getLastError()); - } - notificationController.close(); - cleanupTempBackupFile(); - } - else if (progress<1000/*progress in permille*/) { - progressUpdate((int)progress); - notificationController.setProgress(1000, progress, String.format(" %d%%", (int) progress / 10)); - } - else if (progress==1000/*done*/) { - DcHelper.getAccounts(this).startIo(); - progressSuccess(); - notificationController.close(); - cleanupTempBackupFile(); - } + return; + } + if (progress == 0 /*error/aborted*/) { + if (!imexUserAborted) { + progressError(dcContext.getLastError()); } + notificationController.close(); + cleanupTempBackupFile(); + } else if (progress < 1000 /*progress in permille*/) { + progressUpdate((int) progress); + notificationController.setProgress( + 1000, progress, String.format(" %d%%", (int) progress / 10)); + } else if (progress == 1000 /*done*/) { + DcHelper.getAccounts(this).startIo(); + progressSuccess(); + notificationController.close(); + cleanupTempBackupFile(); + } } + } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (resultCode != RESULT_OK) { - return; - } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); - if (requestCode==IntentIntegrator.REQUEST_CODE) { - String qrRaw = data.getStringExtra(RegistrationQrActivity.QRDATA_EXTRA); - if (qrRaw == null) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data); - qrRaw = scanResult.getContents(); - } - if (!new QrCodeHandler(this).handleBackupQr(qrRaw)) { - new AlertDialog.Builder(this) - .setMessage(R.string.qraccount_qr_code_cannot_be_used) - .setPositiveButton(R.string.ok, null) - .show(); - } - } else if (requestCode == PICK_BACKUP) { - Uri uri = (data != null ? data.getData() : null); - if (uri == null) { - Log.e(TAG, " Can't import null URI"); - return; - } - startImport(null, uri); - } + if (resultCode != RESULT_OK) { + return; } - private void cleanupTempBackupFile() { - try { - File[] files = getCacheDir().listFiles((dir, name) -> name.startsWith(TMP_BACKUP_FILE)); - for (File file : files) { - if (file.getName().endsWith("tmp")) { - Log.i(TAG, "Deleting temp backup file " + file); - file.delete(); - } - } - } catch (Exception e) { - e.printStackTrace(); + if (requestCode == IntentIntegrator.REQUEST_CODE) { + String qrRaw = data.getStringExtra(RegistrationQrActivity.QRDATA_EXTRA); + if (qrRaw == null) { + IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data); + qrRaw = scanResult.getContents(); + } + if (!new QrCodeHandler(this).handleBackupQr(qrRaw)) { + new AlertDialog.Builder(this) + .setMessage(R.string.qraccount_qr_code_cannot_be_used) + .setPositiveButton(R.string.ok, null) + .show(); + } + } else if (requestCode == PICK_BACKUP) { + Uri uri = (data != null ? data.getData() : null); + if (uri == null) { + Log.e(TAG, " Can't import null URI"); + return; + } + startImport(null, uri); + } + } + + private void cleanupTempBackupFile() { + try { + File[] files = getCacheDir().listFiles((dir, name) -> name.startsWith(TMP_BACKUP_FILE)); + for (File file : files) { + if (file.getName().endsWith("tmp")) { + Log.i(TAG, "Deleting temp backup file " + file); + file.delete(); } + } + } catch (Exception e) { + e.printStackTrace(); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListAdapter.java b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListAdapter.java index 8a990f408..175847917 100644 --- a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListAdapter.java @@ -4,27 +4,24 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcAccounts; import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.mms.GlideRequests; -public class AccountSelectionListAdapter extends RecyclerView.Adapter -{ +public class AccountSelectionListAdapter + extends RecyclerView.Adapter { private final @NonNull AccountSelectionListFragment fragment; - private final @NonNull DcAccounts accounts; - private @NonNull int[] accountList = new int[0]; - private int selectedAccountId; - private final LayoutInflater li; - private final ItemClickListener clickListener; - private final GlideRequests glideRequests; + private final @NonNull DcAccounts accounts; + private @NonNull int[] accountList = new int[0]; + private int selectedAccountId; + private final LayoutInflater li; + private final ItemClickListener clickListener; + private final GlideRequests glideRequests; @Override public int getItemCount() { @@ -32,21 +29,27 @@ public int getItemCount() { } public static class AccountViewHolder extends RecyclerView.ViewHolder { - AccountViewHolder(@NonNull final View itemView, - @Nullable final ItemClickListener clickListener) { + AccountViewHolder( + @NonNull final View itemView, @Nullable final ItemClickListener clickListener) { super(itemView); - itemView.setOnClickListener(view -> { - if (clickListener != null) { - clickListener.onItemClick(getView()); - } - }); + itemView.setOnClickListener( + view -> { + if (clickListener != null) { + clickListener.onItemClick(getView()); + } + }); } public AccountSelectionListItem getView() { return (AccountSelectionListItem) itemView; } - public void bind(@NonNull GlideRequests glideRequests, int accountId, DcContext dcContext, boolean selected, AccountSelectionListFragment fragment) { + public void bind( + @NonNull GlideRequests glideRequests, + int accountId, + DcContext dcContext, + boolean selected, + AccountSelectionListFragment fragment) { getView().bind(glideRequests, accountId, dcContext, selected, fragment); } @@ -55,15 +58,15 @@ public void unbind(@NonNull GlideRequests glideRequests) { } } - public AccountSelectionListAdapter(@NonNull AccountSelectionListFragment fragment, - @NonNull GlideRequests glideRequests, - @Nullable ItemClickListener clickListener) - { + public AccountSelectionListAdapter( + @NonNull AccountSelectionListFragment fragment, + @NonNull GlideRequests glideRequests, + @Nullable ItemClickListener clickListener) { super(); - Context context = fragment.requireActivity(); - this.fragment = fragment; - this.accounts = DcHelper.getAccounts(context); - this.li = LayoutInflater.from(context); + Context context = fragment.requireActivity(); + this.fragment = fragment; + this.accounts = DcHelper.getAccounts(context); + this.li = LayoutInflater.from(context); this.glideRequests = glideRequests; this.clickListener = clickListener; } @@ -71,7 +74,8 @@ public AccountSelectionListAdapter(@NonNull AccountSelectionListFragment fragme @NonNull @Override public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new AccountViewHolder(li.inflate(R.layout.account_selection_list_item, parent, false), clickListener); + return new AccountViewHolder( + li.inflate(R.layout.account_selection_list_item, parent, false), clickListener); } @Override @@ -88,7 +92,7 @@ public interface ItemClickListener { } public void changeData(int[] ids, int selectedAccountId) { - this.accountList = ids==null? new int[0] : ids; + this.accountList = ids == null ? new int[0] : ids; this.selectedAccountId = selectedAccountId; notifyDataSetChanged(); } diff --git a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java index 6cfed6fa1..a543d39d2 100644 --- a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListFragment.java @@ -15,18 +15,18 @@ import android.view.View; import android.widget.EditText; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcAccounts; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - +import java.util.Arrays; import org.thoughtcrime.securesms.ConnectivityActivity; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; @@ -39,13 +39,8 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.Arrays; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - -public class AccountSelectionListFragment extends DialogFragment implements DcEventCenter.DcEventDelegate -{ +public class AccountSelectionListFragment extends DialogFragment + implements DcEventCenter.DcEventDelegate { private static final String TAG = AccountSelectionListFragment.class.getSimpleName(); private static final String ARG_SELECT_ONLY = "select_only"; private RecyclerView recyclerView; @@ -64,13 +59,16 @@ public static AccountSelectionListFragment newInstance(boolean selectOnly) { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { selectOnly = getArguments() != null && getArguments().getBoolean(ARG_SELECT_ONLY, false); - AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()) + AlertDialog.Builder builder = + new AlertDialog.Builder(requireActivity()) .setTitle(R.string.switch_account) .setNegativeButton(R.string.cancel, null); if (!selectOnly) { - builder.setNeutralButton(R.string.connectivity, ((dialog, which) -> { - startActivity(new Intent(getActivity(), ConnectivityActivity.class)); - })); + builder.setNeutralButton( + R.string.connectivity, + ((dialog, which) -> { + startActivity(new Intent(getActivity(), ConnectivityActivity.class)); + })); } LayoutInflater inflater = getActivity().getLayoutInflater(); @@ -78,7 +76,9 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { recyclerView = ViewUtil.findById(view, R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - adapter = new AccountSelectionListAdapter(this, GlideApp.with(getActivity()), new ListClickListener()); + adapter = + new AccountSelectionListAdapter( + this, GlideApp.with(getActivity()), new ListClickListener()); recyclerView.setAdapter(adapter); refreshData(); DcEventCenter eventCenter = DcHelper.getEventCenter(requireActivity()); @@ -106,7 +106,7 @@ private void refreshData() { DcAccounts accounts = DcHelper.getAccounts(getActivity()); int[] accountIds = accounts.getAll(); - int[] ids = new int[(selectOnly? 0 : 1) + accountIds.length]; + int[] ids = new int[(selectOnly ? 0 : 1) + accountIds.length]; int j = 0; for (int accountId : accountIds) { ids[j++] = accountId; @@ -116,7 +116,8 @@ private void refreshData() { } @Override - public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { + public void onCreateContextMenu( + @NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); if (selectOnly) return; @@ -133,11 +134,13 @@ public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, Cont } // hack to make onContextItemSelected() work with DialogFragment, - // see https://stackoverflow.com/questions/15929026/oncontextitemselected-does-not-get-called-in-a-dialogfragment - MenuItem.OnMenuItemClickListener listener = item -> { - onContextItemSelected(item, accountId); - return true; - }; + // see + // https://stackoverflow.com/questions/15929026/oncontextitemselected-does-not-get-called-in-a-dialogfragment + MenuItem.OnMenuItemClickListener listener = + item -> { + onContextItemSelected(item, accountId); + return true; + }; for (int i = 0, n = menu.size(); i < n; i++) { menu.getItem(i).setOnMenuItemClickListener(listener); } @@ -182,7 +185,7 @@ private void onMoveToTop(int accountId) { } private void onSetTag(int accountId) { - ConversationListActivity activity = (ConversationListActivity)requireActivity(); + ConversationListActivity activity = (ConversationListActivity) requireActivity(); AccountSelectionListFragment.this.dismiss(); DcContext dcContext = DcHelper.getAccounts(activity).getAccount(accountId); @@ -192,21 +195,25 @@ private void onSetTag(int accountId) { inputField.setText(dcContext.getConfig(CONFIG_PRIVATE_TAG)); new AlertDialog.Builder(activity) - .setTitle(R.string.profile_tag) - .setMessage(R.string.profile_tag_explain) - .setView(view) - .setPositiveButton(android.R.string.ok, (d, b) -> { - String newTag = inputField.getText().toString().trim(); - dcContext.setConfig(CONFIG_PRIVATE_TAG, newTag); - AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly); - }) - .setNegativeButton(R.string.cancel, (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) - .show(); + .setTitle(R.string.profile_tag) + .setMessage(R.string.profile_tag_explain) + .setView(view) + .setPositiveButton( + android.R.string.ok, + (d, b) -> { + String newTag = inputField.getText().toString().trim(); + dcContext.setConfig(CONFIG_PRIVATE_TAG, newTag); + AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly); + }) + .setNegativeButton( + R.string.cancel, + (d, b) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) + .show(); } private void onDeleteProfile(int accountId) { AccountSelectionListFragment.this.dismiss(); - ConversationListActivity activity = (ConversationListActivity)requireActivity(); + ConversationListActivity activity = (ConversationListActivity) requireActivity(); DcAccounts accounts = DcHelper.getAccounts(activity); Rpc rpc = DcHelper.getRpc(activity); @@ -226,24 +233,30 @@ private void onDeleteProfile(int accountId) { avatar.setAvatar(GlideApp.with(activity), recipient, false); nameView.setText(name); addrView.setText(contact.getAddr()); - Util.runOnAnyBackgroundThread(() -> { - try { - final int sizeBytes = rpc.getAccountFileSize(accountId); - Util.runOnMain(() -> { - sizeView.setText(Util.getPrettyFileSize(sizeBytes)); + Util.runOnAnyBackgroundThread( + () -> { + try { + final int sizeBytes = rpc.getAccountFileSize(accountId); + Util.runOnMain( + () -> { + sizeView.setText(Util.getPrettyFileSize(sizeBytes)); + }); + } catch (RpcException e) { + Log.e(TAG, "Error calling rpc.getAccountFileSize()", e); + } }); - } catch (RpcException e) { - Log.e(TAG, "Error calling rpc.getAccountFileSize()", e); - } - }); description.setText(activity.getString(R.string.delete_account_explain_with_name, name)); - AlertDialog dialog = new AlertDialog.Builder(activity) - .setTitle(R.string.delete_account) - .setView(dialogView) - .setNegativeButton(R.string.cancel, (d, which) -> AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) - .setPositiveButton(R.string.delete, (d2, w2) -> activity.onDeleteProfile(accountId)) - .show(); + AlertDialog dialog = + new AlertDialog.Builder(activity) + .setTitle(R.string.delete_account) + .setView(dialogView) + .setNegativeButton( + R.string.cancel, + (d, which) -> + AccountManager.getInstance().showSwitchAccountMenu(activity, selectOnly)) + .setPositiveButton(R.string.delete, (d2, w2) -> activity.onDeleteProfile(accountId)) + .show(); Util.redPositiveButton(dialog); } @@ -259,7 +272,7 @@ private class ListClickListener implements AccountSelectionListAdapter.ItemClick @Override public void onItemClick(AccountSelectionListItem contact) { AccountSelectionListFragment.this.dismiss(); - ConversationListActivity activity = (ConversationListActivity)requireActivity(); + ConversationListActivity activity = (ConversationListActivity) requireActivity(); int accountId = contact.getAccountId(); if (accountId == DC_CONTACT_ID_ADD_ACCOUNT) { AccountManager.getInstance().switchAccountAndStartActivity(activity, 0); @@ -269,5 +282,4 @@ public void onItemClick(AccountSelectionListItem contact) { } } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListItem.java b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListItem.java index 3ab22f19b..84a9a682c 100644 --- a/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListItem.java +++ b/src/main/java/org/thoughtcrime/securesms/accounts/AccountSelectionListItem.java @@ -13,14 +13,11 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.widget.SwitchCompat; - import com.amulyakhare.textdrawable.TextDrawable; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AvatarView; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -30,15 +27,15 @@ public class AccountSelectionListItem extends LinearLayout { - private AvatarView contactPhotoImage; - private View addrContainer; - private TextView addrOrTagView; - private TextView nameView; - private ImageView unreadIndicator; + private AvatarView contactPhotoImage; + private View addrContainer; + private TextView addrOrTagView; + private TextView nameView; + private ImageView unreadIndicator; private SwitchCompat enableSwitch; - private int accountId; - private DcContext dcContext; + private int accountId; + private DcContext dcContext; public AccountSelectionListItem(Context context) { super(context); @@ -52,19 +49,25 @@ public AccountSelectionListItem(Context context, AttributeSet attrs) { protected void onFinishInflate() { super.onFinishInflate(); this.contactPhotoImage = findViewById(R.id.contact_photo_image); - this.addrContainer = findViewById(R.id.addr_container); - this.addrOrTagView = findViewById(R.id.addr_or_tag); - this.nameView = findViewById(R.id.name); - this.unreadIndicator = findViewById(R.id.unread_indicator); - this.enableSwitch = findViewById(R.id.enable_switch); - - enableSwitch.setOnCheckedChangeListener((view, isChecked) -> { - if (isChecked != this.dcContext.isEnabled()) this.dcContext.setEnabled(isChecked); - }); + this.addrContainer = findViewById(R.id.addr_container); + this.addrOrTagView = findViewById(R.id.addr_or_tag); + this.nameView = findViewById(R.id.name); + this.unreadIndicator = findViewById(R.id.unread_indicator); + this.enableSwitch = findViewById(R.id.enable_switch); + + enableSwitch.setOnCheckedChangeListener( + (view, isChecked) -> { + if (isChecked != this.dcContext.isEnabled()) this.dcContext.setEnabled(isChecked); + }); ViewUtil.setTextViewGravityStart(this.nameView, getContext()); } - public void bind(@NonNull GlideRequests glideRequests, int accountId, DcContext dcContext, boolean selected, AccountSelectionListFragment fragment) { + public void bind( + @NonNull GlideRequests glideRequests, + int accountId, + DcContext dcContext, + boolean selected, + AccountSelectionListFragment fragment) { this.accountId = accountId; this.dcContext = dcContext; DcContact self = null; @@ -96,7 +99,8 @@ public void bind(@NonNull GlideRequests glideRequests, int accountId, DcContext } this.contactPhotoImage.setAvatar(glideRequests, recipient, false); - nameView.setCompoundDrawablesWithIntrinsicBounds(isMuted? R.drawable.ic_volume_off_grey600_18dp : 0, 0, 0, 0); + nameView.setCompoundDrawablesWithIntrinsicBounds( + isMuted ? R.drawable.ic_volume_off_grey600_18dp : 0, 0, 0, 0); setSelected(selected); if (selected) { @@ -122,19 +126,28 @@ public void unbind(GlideRequests glideRequests) { } private void updateUnreadIndicator(int unreadCount, boolean isMuted) { - if(unreadCount == 0) { + if (unreadCount == 0) { unreadIndicator.setVisibility(View.GONE); } else { final int color; if (isMuted) { - color = getResources().getColor(ThemeUtil.isDarkTheme(getContext()) ? R.color.unread_count_muted_dark : R.color.unread_count_muted); + color = + getResources() + .getColor( + ThemeUtil.isDarkTheme(getContext()) + ? R.color.unread_count_muted_dark + : R.color.unread_count_muted); } else { - final TypedArray attrs = getContext().obtainStyledAttributes(new int[] { - R.attr.conversation_list_item_unreadcount_color, - }); + final TypedArray attrs = + getContext() + .obtainStyledAttributes( + new int[] { + R.attr.conversation_list_item_unreadcount_color, + }); color = attrs.getColor(0, Color.BLACK); } - unreadIndicator.setImageDrawable(TextDrawable.builder() + unreadIndicator.setImageDrawable( + TextDrawable.builder() .beginConfig() .width(ViewUtil.dpToPx(getContext(), 24)) .height(ViewUtil.dpToPx(getContext(), 24)) @@ -147,9 +160,9 @@ private void updateUnreadIndicator(int unreadCount, boolean isMuted) { } private void setText(String name, String addrOrTag) { - this.nameView.setText(name==null? "#" : name); + this.nameView.setText(name == null ? "#" : name); - if(!TextUtils.isEmpty(addrOrTag)) { + if (!TextUtils.isEmpty(addrOrTag)) { this.addrOrTagView.setText(addrOrTag); this.addrContainer.setVisibility(View.VISIBLE); } else { diff --git a/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java b/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java index ceb452fbc..fe915f11a 100644 --- a/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java +++ b/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.animation; - import android.animation.Animator; - import androidx.annotation.NonNull; public abstract class AnimationCompleteListener implements Animator.AnimatorListener { @@ -14,6 +12,7 @@ public final void onAnimationStart(@NonNull Animator animation) {} @Override public final void onAnimationCancel(@NonNull Animator animation) {} + @Override public final void onAnimationRepeat(@NonNull Animator animation) {} } diff --git a/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java b/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java index daec88d9c..05f6b1ab6 100644 --- a/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java +++ b/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java @@ -4,51 +4,51 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.Util; - import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.Util; public abstract class Attachment { - @NonNull - private final String contentType; - private int transferState; - private final long size; + @NonNull private final String contentType; + private int transferState; + private final long size; - @Nullable - private final String fileName; + @Nullable private final String fileName; - @Nullable - private final String location; + @Nullable private final String location; - @Nullable - private final String fastPreflightId; + @Nullable private final String fastPreflightId; private final boolean voiceNote; private final int width; private final int height; - public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName, - @Nullable String location, @Nullable String fastPreflightId, boolean voiceNote, - int width, int height) - { - this.contentType = contentType; - this.transferState = transferState; - this.size = size; - this.fileName = fileName; - this.location = location; + public Attachment( + @NonNull String contentType, + int transferState, + long size, + @Nullable String fileName, + @Nullable String location, + @Nullable String fastPreflightId, + boolean voiceNote, + int width, + int height) { + this.contentType = contentType; + this.transferState = transferState; + this.size = size; + this.fileName = fileName; + this.location = location; this.fastPreflightId = fastPreflightId; - this.voiceNote = voiceNote; - this.width = width; - this.height = height; + this.voiceNote = voiceNote; + this.width = width; + this.height = height; } @Nullable @@ -106,13 +106,12 @@ public String getRealPath(Context context) { // get file in the blobdir as `/[-].` String filename = getFileName(); String ext = ""; - if(filename==null) { + if (filename == null) { filename = new SimpleDateFormat("yyyy-MM-dd-HH-mm").format(new Date()); ext = "." + MediaUtil.getExtensionFromMimeType(getContentType()); - } - else { + } else { int i = filename.lastIndexOf("."); - if(i>=0) { + if (i >= 0) { ext = filename.substring(i); filename = filename.substring(0, i); } @@ -120,13 +119,12 @@ public String getRealPath(Context context) { String path = DcHelper.getBlobdirFile(DcHelper.getContext(context), filename, ext); // copy content to this file - InputStream inputStream = PartAuthority.getAttachmentStream(context, getDataUri()); - OutputStream outputStream = new FileOutputStream(path); - Util.copy(inputStream, outputStream); + InputStream inputStream = PartAuthority.getAttachmentStream(context, getDataUri()); + OutputStream outputStream = new FileOutputStream(path); + Util.copy(inputStream, outputStream); - return path; - } - catch(Exception e) { + return path; + } catch (Exception e) { e.printStackTrace(); return null; } diff --git a/src/main/java/org/thoughtcrime/securesms/attachments/DcAttachment.java b/src/main/java/org/thoughtcrime/securesms/attachments/DcAttachment.java index ce5164ad4..bfe8fd177 100644 --- a/src/main/java/org/thoughtcrime/securesms/attachments/DcAttachment.java +++ b/src/main/java/org/thoughtcrime/securesms/attachments/DcAttachment.java @@ -1,13 +1,10 @@ package org.thoughtcrime.securesms.attachments; -import java.io.File; - import android.content.Context; import android.net.Uri; import androidx.annotation.Nullable; - import com.b44t.messenger.DcMsg; - +import java.io.File; import org.thoughtcrime.securesms.database.AttachmentDatabase; public class DcAttachment extends Attachment { @@ -15,11 +12,16 @@ public class DcAttachment extends Attachment { private final DcMsg dcMsg; public DcAttachment(DcMsg dcMsg) { - super(dcMsg.getFilemime(), AttachmentDatabase.TRANSFER_PROGRESS_DONE, dcMsg.getFilebytes(), + super( + dcMsg.getFilemime(), + AttachmentDatabase.TRANSFER_PROGRESS_DONE, + dcMsg.getFilebytes(), dcMsg.getFilename(), Uri.fromFile(new File(dcMsg.getFile())).toString(), - null, dcMsg.getType() == DcMsg.DC_MSG_VOICE, - 0, 0); + null, + dcMsg.getType() == DcMsg.DC_MSG_VOICE, + 0, + 0); this.dcMsg = dcMsg; } @@ -32,8 +34,8 @@ public Uri getDataUri() { @Nullable @Override public Uri getThumbnailUri() { - if(dcMsg.getType()==DcMsg.DC_MSG_VIDEO) { - return Uri.fromFile(new File(dcMsg.getFile()+"-preview.jpg")); + if (dcMsg.getType() == DcMsg.DC_MSG_VIDEO) { + return Uri.fromFile(new File(dcMsg.getFile() + "-preview.jpg")); } return getDataUri(); } diff --git a/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java b/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java index 3c941af3c..bafea71cd 100644 --- a/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -6,16 +6,31 @@ public class UriAttachment extends Attachment { - private final @NonNull Uri dataUri; + private final @NonNull Uri dataUri; private final @Nullable Uri thumbnailUri; - public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri, - @NonNull String contentType, int transferState, long size, int width, int height, - @Nullable String fileName, @Nullable String fastPreflightId, - boolean voiceNote) - { - super(contentType, transferState, size, fileName, null, fastPreflightId, voiceNote, width, height); - this.dataUri = dataUri; + public UriAttachment( + @NonNull Uri dataUri, + @Nullable Uri thumbnailUri, + @NonNull String contentType, + int transferState, + long size, + int width, + int height, + @Nullable String fileName, + @Nullable String fastPreflightId, + boolean voiceNote) { + super( + contentType, + transferState, + size, + fileName, + null, + fastPreflightId, + voiceNote, + width, + height); + this.dataUri = dataUri; this.thumbnailUri = thumbnailUri; } diff --git a/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java b/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java index 30a5c7fb3..8397e9336 100644 --- a/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java +++ b/src/main/java/org/thoughtcrime/securesms/audio/AudioCodec.java @@ -8,41 +8,41 @@ import android.media.MediaFormat; import android.media.MediaRecorder; import android.util.Log; - -import org.thoughtcrime.securesms.util.Prefs; -import org.thoughtcrime.securesms.util.Util; - import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; +import org.thoughtcrime.securesms.util.Prefs; +import org.thoughtcrime.securesms.util.Util; public class AudioCodec { private static final String TAG = AudioCodec.class.getSimpleName(); - private static final int SAMPLE_RATE_BALANCED = 44100; - private static final int SAMPLE_RATE_INDEX_BALANCED = 4; - private static final int BIT_RATE_BALANCED = 32000; + private static final int SAMPLE_RATE_BALANCED = 44100; + private static final int SAMPLE_RATE_INDEX_BALANCED = 4; + private static final int BIT_RATE_BALANCED = 32000; - private static final int SAMPLE_RATE_WORSE = 24000; - private static final int SAMPLE_RATE_INDEX_WORSE = 6; - private static final int BIT_RATE_WORSE = 16000; + private static final int SAMPLE_RATE_WORSE = 24000; + private static final int SAMPLE_RATE_INDEX_WORSE = 6; + private static final int BIT_RATE_WORSE = 16000; - private static final int CHANNELS = 1; + private static final int CHANNELS = 1; - private final int bufferSize; - private final MediaCodec mediaCodec; + private final int bufferSize; + private final MediaCodec mediaCodec; private final AudioRecord audioRecord; private final Context context; - private boolean running = true; + private boolean running = true; private boolean finished = false; public AudioCodec(Context context) throws IOException { - this.context = context; - this.bufferSize = AudioRecord.getMinBufferSize(getSampleRate(), AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); + this.context = context; + this.bufferSize = + AudioRecord.getMinBufferSize( + getSampleRate(), AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); this.audioRecord = createAudioRecord(this.bufferSize); - this.mediaCodec = createMediaCodec(this.bufferSize); + this.mediaCodec = createMediaCodec(this.bufferSize); this.mediaCodec.start(); @@ -61,37 +61,41 @@ public synchronized void stop() { } public void start(final OutputStream outputStream) { - new Thread(new Runnable() { - @Override - public void run() { - MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - byte[] audioRecordData = new byte[bufferSize]; - ByteBuffer[] codecInputBuffers = mediaCodec.getInputBuffers(); - ByteBuffer[] codecOutputBuffers = mediaCodec.getOutputBuffers(); - - try { - while (true) { - boolean running = isRunning(); - - handleCodecInput(audioRecord, audioRecordData, mediaCodec, codecInputBuffers, running); - handleCodecOutput(mediaCodec, codecOutputBuffers, bufferInfo, outputStream); - - if (!running) break; - } - } catch (IOException e) { - Log.w(TAG, e); - } finally { - mediaCodec.stop(); - audioRecord.stop(); - - mediaCodec.release(); - audioRecord.release(); - - Util.close(outputStream); - setFinished(); - } - } - }, AudioCodec.class.getSimpleName()).start(); + new Thread( + new Runnable() { + @Override + public void run() { + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + byte[] audioRecordData = new byte[bufferSize]; + ByteBuffer[] codecInputBuffers = mediaCodec.getInputBuffers(); + ByteBuffer[] codecOutputBuffers = mediaCodec.getOutputBuffers(); + + try { + while (true) { + boolean running = isRunning(); + + handleCodecInput( + audioRecord, audioRecordData, mediaCodec, codecInputBuffers, running); + handleCodecOutput(mediaCodec, codecOutputBuffers, bufferInfo, outputStream); + + if (!running) break; + } + } catch (IOException e) { + Log.w(TAG, e); + } finally { + mediaCodec.stop(); + audioRecord.stop(); + + mediaCodec.release(); + audioRecord.release(); + + Util.close(outputStream); + setFinished(); + } + } + }, + AudioCodec.class.getSimpleName()) + .start(); } private synchronized boolean isRunning() { @@ -103,27 +107,30 @@ private synchronized void setFinished() { notifyAll(); } - private void handleCodecInput(AudioRecord audioRecord, byte[] audioRecordData, - MediaCodec mediaCodec, ByteBuffer[] codecInputBuffers, - boolean running) - { - int length = audioRecord.read(audioRecordData, 0, audioRecordData.length); + private void handleCodecInput( + AudioRecord audioRecord, + byte[] audioRecordData, + MediaCodec mediaCodec, + ByteBuffer[] codecInputBuffers, + boolean running) { + int length = audioRecord.read(audioRecordData, 0, audioRecordData.length); int codecInputBufferIndex = mediaCodec.dequeueInputBuffer(10 * 1000); if (codecInputBufferIndex >= 0) { ByteBuffer codecBuffer = codecInputBuffers[codecInputBufferIndex]; codecBuffer.clear(); codecBuffer.put(audioRecordData); - mediaCodec.queueInputBuffer(codecInputBufferIndex, 0, length, 0, running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM); + mediaCodec.queueInputBuffer( + codecInputBufferIndex, 0, length, 0, running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM); } } - private void handleCodecOutput(MediaCodec mediaCodec, - ByteBuffer[] codecOutputBuffers, - MediaCodec.BufferInfo bufferInfo, - OutputStream outputStream) - throws IOException - { + private void handleCodecOutput( + MediaCodec mediaCodec, + ByteBuffer[] codecOutputBuffers, + MediaCodec.BufferInfo bufferInfo, + OutputStream outputStream) + throws IOException { int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { @@ -133,10 +140,10 @@ private void handleCodecOutput(MediaCodec mediaCodec, encoderOutputBuffer.position(bufferInfo.offset); encoderOutputBuffer.limit(bufferInfo.offset + bufferInfo.size); - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) + != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { byte[] header = createAdtsHeader(bufferInfo.size - bufferInfo.offset); - outputStream.write(header); byte[] data = new byte[encoderOutputBuffer.remaining()]; @@ -147,40 +154,42 @@ private void handleCodecOutput(MediaCodec mediaCodec, encoderOutputBuffer.clear(); mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false); - } else if (codecOutputBufferIndex== MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + } else if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { codecOutputBuffers = mediaCodec.getOutputBuffers(); } codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0); } - } private byte[] createAdtsHeader(int length) { - int frameLength = length + 7; - byte[] adtsHeader = new byte[7]; + int frameLength = length + 7; + byte[] adtsHeader = new byte[7]; - adtsHeader[0] = (byte) 0xFF; // Sync Word - adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer (0), No CRC - adtsHeader[2] = (byte) ((MediaCodecInfo.CodecProfileLevel.AACObjectLC - 1) << 6); + adtsHeader[0] = (byte) 0xFF; // Sync Word + adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer (0), No CRC + adtsHeader[2] = (byte) ((MediaCodecInfo.CodecProfileLevel.AACObjectLC - 1) << 6); adtsHeader[2] |= (((byte) getSampleRateIndex()) << 2); adtsHeader[2] |= (((byte) CHANNELS) >> 2); - adtsHeader[3] = (byte) (((CHANNELS & 3) << 6) | ((frameLength >> 11) & 0x03)); - adtsHeader[4] = (byte) ((frameLength >> 3) & 0xFF); - adtsHeader[5] = (byte) (((frameLength & 0x07) << 5) | 0x1f); - adtsHeader[6] = (byte) 0xFC; + adtsHeader[3] = (byte) (((CHANNELS & 3) << 6) | ((frameLength >> 11) & 0x03)); + adtsHeader[4] = (byte) ((frameLength >> 3) & 0xFF); + adtsHeader[5] = (byte) (((frameLength & 0x07) << 5) | 0x1f); + adtsHeader[6] = (byte) 0xFC; return adtsHeader; } private AudioRecord createAudioRecord(int bufferSize) { - return new AudioRecord(MediaRecorder.AudioSource.MIC, getSampleRate(), - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10); + return new AudioRecord( + MediaRecorder.AudioSource.MIC, + getSampleRate(), + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize * 10); } private MediaCodec createMediaCodec(int bufferSize) throws IOException { - MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); + MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm"); MediaFormat mediaFormat = new MediaFormat(); mediaFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); @@ -188,7 +197,8 @@ private MediaCodec createMediaCodec(int bufferSize) throws IOException { mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize); mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, getBitRate()); - mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + mediaFormat.setInteger( + MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); try { mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); @@ -202,15 +212,16 @@ private MediaCodec createMediaCodec(int bufferSize) throws IOException { } private int getSampleRate() { - return Prefs.isHardCompressionEnabled(context)? SAMPLE_RATE_WORSE : SAMPLE_RATE_BALANCED; + return Prefs.isHardCompressionEnabled(context) ? SAMPLE_RATE_WORSE : SAMPLE_RATE_BALANCED; } private int getSampleRateIndex() { - return Prefs.isHardCompressionEnabled(context)? SAMPLE_RATE_INDEX_WORSE : SAMPLE_RATE_INDEX_BALANCED; + return Prefs.isHardCompressionEnabled(context) + ? SAMPLE_RATE_INDEX_WORSE + : SAMPLE_RATE_INDEX_BALANCED; } private int getBitRate() { - return Prefs.isHardCompressionEnabled(context)? BIT_RATE_WORSE : BIT_RATE_BALANCED; + return Prefs.isHardCompressionEnabled(context) ? BIT_RATE_WORSE : BIT_RATE_BALANCED; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java b/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java index 0bfd9decf..01386543a 100644 --- a/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ b/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java @@ -5,58 +5,60 @@ import android.os.ParcelFileDescriptor; import android.util.Log; import android.util.Pair; - import androidx.annotation.NonNull; - +import chat.delta.util.ListenableFuture; +import chat.delta.util.SettableFuture; +import java.io.IOException; +import java.util.concurrent.ExecutorService; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ThreadUtil; import org.thoughtcrime.securesms.util.Util; -import java.io.IOException; -import java.util.concurrent.ExecutorService; - -import chat.delta.util.ListenableFuture; -import chat.delta.util.SettableFuture; - public class AudioRecorder { private static final String TAG = AudioRecorder.class.getSimpleName(); private static final ExecutorService executor = ThreadUtil.newDynamicSingleThreadedExecutor(); - private final Context context; + private final Context context; private final PersistentBlobProvider blobProvider; private AudioCodec audioCodec; - private Uri captureUri; + private Uri captureUri; public AudioRecorder(@NonNull Context context) { - this.context = context; + this.context = context; this.blobProvider = PersistentBlobProvider.getInstance(); } public void startRecording() { Log.w(TAG, "startRecording()"); - executor.execute(() -> { - Log.w(TAG, "Running startRecording() + " + Thread.currentThread().getId()); - try { - if (audioCodec != null) { - throw new AssertionError("We can only record once at a time."); - } - - ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); - - captureUri = blobProvider.create(context, new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), - MediaUtil.AUDIO_AAC, "voice.aac", null); - audioCodec = new AudioCodec(context); - - audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); - } catch (IOException e) { - Log.w(TAG, e); - } - }); + executor.execute( + () -> { + Log.w(TAG, "Running startRecording() + " + Thread.currentThread().getId()); + try { + if (audioCodec != null) { + throw new AssertionError("We can only record once at a time."); + } + + ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); + + captureUri = + blobProvider.create( + context, + new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), + MediaUtil.AUDIO_AAC, + "voice.aac", + null); + audioCodec = new AudioCodec(context); + + audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); + } catch (IOException e) { + Log.w(TAG, e); + } + }); } public @NonNull ListenableFuture> stopRecording() { @@ -64,25 +66,27 @@ public void startRecording() { final SettableFuture> future = new SettableFuture<>(); - executor.execute(() -> { - if (audioCodec == null) { - sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!")); - return; - } - - audioCodec.stop(); - - try { - long size = MediaUtil.getMediaSize(context, captureUri); - sendToFuture(future, new Pair<>(captureUri, size)); - } catch (IOException ioe) { - Log.w(TAG, ioe); - sendToFuture(future, ioe); - } - - audioCodec = null; - captureUri = null; - }); + executor.execute( + () -> { + if (audioCodec == null) { + sendToFuture( + future, new IOException("MediaRecorder was never initialized successfully!")); + return; + } + + audioCodec.stop(); + + try { + long size = MediaUtil.getMediaSize(context, captureUri); + sendToFuture(future, new Pair<>(captureUri, size)); + } catch (IOException ioe) { + Log.w(TAG, ioe); + sendToFuture(future, ioe); + } + + audioCodec = null; + captureUri = null; + }); return future; } diff --git a/src/main/java/org/thoughtcrime/securesms/components/AnimatingToggle.java b/src/main/java/org/thoughtcrime/securesms/components/AnimatingToggle.java index f2b6a3f76..a4258b895 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/AnimatingToggle.java +++ b/src/main/java/org/thoughtcrime/securesms/components/AnimatingToggle.java @@ -1,16 +1,15 @@ package org.thoughtcrime.securesms.components; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ViewUtil; @@ -32,7 +31,7 @@ public AnimatingToggle(Context context, AttributeSet attrs) { public AnimatingToggle(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.outAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.animation_toggle_out); - this.inAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.animation_toggle_in); + this.inAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.animation_toggle_in); this.outAnimation.setInterpolator(new FastOutSlowInInterpolator()); this.inAnimation.setInterpolator(new FastOutSlowInInterpolator()); } @@ -53,7 +52,7 @@ public void addView(@NonNull View child, int index, ViewGroup.LayoutParams param public void display(@Nullable View view) { if (view == current) return; if (current != null) ViewUtil.animateOut(current, outAnimation, View.GONE); - if (view != null) ViewUtil.animateIn(view, inAnimation); + if (view != null) ViewUtil.animateIn(view, inAnimation); current = view; } @@ -61,7 +60,7 @@ public void display(@Nullable View view) { public void displayQuick(@Nullable View view) { if (view == current) return; if (current != null) current.setVisibility(View.GONE); - if (view != null) view.setVisibility(View.VISIBLE); + if (view != null) view.setVisibility(View.VISIBLE); current = view; } diff --git a/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java b/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java index a31d60f31..dfeb38074 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java +++ b/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java @@ -5,11 +5,6 @@ import android.content.Context; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.loader.app.LoaderManager; -import androidx.core.content.ContextCompat; import android.util.Pair; import android.view.Gravity; import android.view.LayoutInflater; @@ -24,71 +19,79 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.PopupWindow; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.loader.app.LoaderManager; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.ViewUtil; public class AttachmentTypeSelector extends PopupWindow { - public static final int ADD_GALLERY = 1; - public static final int ADD_DOCUMENT = 2; - public static final int ADD_CONTACT_INFO = 3; - public static final int TAKE_PHOTO = 4; - public static final int ADD_LOCATION = 5; - public static final int RECORD_VIDEO = 6; - public static final int ADD_WEBXDC = 7; + public static final int ADD_GALLERY = 1; + public static final int ADD_DOCUMENT = 2; + public static final int ADD_CONTACT_INFO = 3; + public static final int TAKE_PHOTO = 4; + public static final int ADD_LOCATION = 5; + public static final int RECORD_VIDEO = 6; + public static final int ADD_WEBXDC = 7; private static final int ANIMATION_DURATION = 300; - private final @NonNull LoaderManager loaderManager; + private final @NonNull LoaderManager loaderManager; private final @NonNull RecentPhotoViewRail recentRail; - private final @NonNull ImageView imageButton; - private final @NonNull ImageView documentButton; - private final @NonNull ImageView contactButton; - //private final @NonNull ImageView cameraButton; - private final @NonNull ImageView videoButton; - private final @NonNull ImageView locationButton; - private final @NonNull ImageView webxdcButton; - - private @Nullable View currentAnchor; + private final @NonNull ImageView imageButton; + private final @NonNull ImageView documentButton; + private final @NonNull ImageView contactButton; + // private final @NonNull ImageView cameraButton; + private final @NonNull ImageView videoButton; + private final @NonNull ImageView locationButton; + private final @NonNull ImageView webxdcButton; + + private @Nullable View currentAnchor; private @Nullable AttachmentClickedListener listener; private final int chatId; - public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener, int chatId) { + public AttachmentTypeSelector( + @NonNull Context context, + @NonNull LoaderManager loaderManager, + @Nullable AttachmentClickedListener listener, + int chatId) { super(context); - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LinearLayout layout = + (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true); - this.listener = listener; - this.loaderManager = loaderManager; - this.chatId = chatId; - this.recentRail = ViewUtil.findById(layout, R.id.recent_photos); - this.imageButton = ViewUtil.findById(layout, R.id.gallery_button); + this.listener = listener; + this.loaderManager = loaderManager; + this.chatId = chatId; + this.recentRail = ViewUtil.findById(layout, R.id.recent_photos); + this.imageButton = ViewUtil.findById(layout, R.id.gallery_button); this.documentButton = ViewUtil.findById(layout, R.id.document_button); - this.contactButton = ViewUtil.findById(layout, R.id.contact_button); - //this.cameraButton = ViewUtil.findById(layout, R.id.camera_button); - this.videoButton = ViewUtil.findById(layout, R.id.record_video_button); + this.contactButton = ViewUtil.findById(layout, R.id.contact_button); + // this.cameraButton = ViewUtil.findById(layout, R.id.camera_button); + this.videoButton = ViewUtil.findById(layout, R.id.record_video_button); this.locationButton = ViewUtil.findById(layout, R.id.location_button); - this.webxdcButton = ViewUtil.findById(layout, R.id.webxdc_button); + this.webxdcButton = ViewUtil.findById(layout, R.id.webxdc_button); this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_GALLERY)); this.documentButton.setOnClickListener(new PropagatingClickListener(ADD_DOCUMENT)); this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO)); - //this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO)); + // this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO)); this.videoButton.setOnClickListener(new PropagatingClickListener(RECORD_VIDEO)); this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION)); this.webxdcButton.setOnClickListener(new PropagatingClickListener(ADD_WEBXDC)); this.recentRail.setListener(new RecentPhotoSelectedListener()); // disable location streaming button for now - //if (!Prefs.isLocationStreamingEnabled(context)) { - this.locationButton.setVisibility(View.GONE); - ViewUtil.findById(layout, R.id.location_button_label).setVisibility(View.GONE); - //} + // if (!Prefs.isLocationStreamingEnabled(context)) { + this.locationButton.setVisibility(View.GONE); + ViewUtil.findById(layout, R.id.location_button_label).setVisibility(View.GONE); + // } setLocationButtonImage(context); @@ -117,16 +120,19 @@ public void show(@NonNull Activity activity, final @NonNull View anchor) { showAtLocation(anchor, Gravity.BOTTOM, 0, 0); - getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this); + getContentView() + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this); - animateWindowInCircular(anchor, getContentView()); - } - }); + animateWindowInCircular(anchor, getContentView()); + } + }); - //animateButtonIn(cameraButton, ANIMATION_DURATION / 2); + // animateButtonIn(cameraButton, ANIMATION_DURATION / 2); animateButtonIn(videoButton, ANIMATION_DURATION / 2); animateButtonIn(imageButton, ANIMATION_DURATION / 3); animateButtonIn(contactButton, ANIMATION_DURATION / 3); @@ -157,8 +163,16 @@ private void setLocationButtonImage(Context context) { private void animateButtonIn(View button, int delay) { AnimationSet animation = new AnimationSet(true); - Animation scale = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f); + Animation scale = + new ScaleAnimation( + 0.0f, + 1.0f, + 0.0f, + 1.0f, + Animation.RELATIVE_TO_SELF, + 0.5f, + Animation.RELATIVE_TO_SELF, + 0.0f); animation.addAnimation(scale); animation.setInterpolator(new OvershootInterpolator(1)); @@ -169,63 +183,65 @@ private void animateButtonIn(View button, int delay) { private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) { Pair coordinates = getClickOrigin(anchor, contentView); - Animator animator = ViewAnimationUtils.createCircularReveal(contentView, - coordinates.first, - coordinates.second, - 0, - Math.max(contentView.getWidth(), contentView.getHeight())); + Animator animator = + ViewAnimationUtils.createCircularReveal( + contentView, + coordinates.first, + coordinates.second, + 0, + Math.max(contentView.getWidth(), contentView.getHeight())); animator.setDuration(ANIMATION_DURATION); animator.start(); } private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) { Pair coordinates = getClickOrigin(anchor, contentView); - Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(), - coordinates.first, - coordinates.second, - Math.max(getContentView().getWidth(), getContentView().getHeight()), - 0); + Animator animator = + ViewAnimationUtils.createCircularReveal( + getContentView(), + coordinates.first, + coordinates.second, + Math.max(getContentView().getWidth(), getContentView().getHeight()), + 0); animator.setDuration(ANIMATION_DURATION); - animator.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(@NonNull Animator animation) { - } + animator.addListener( + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(@NonNull Animator animation) {} - @Override - public void onAnimationEnd(@NonNull Animator animation) { - AttachmentTypeSelector.super.dismiss(); - } + @Override + public void onAnimationEnd(@NonNull Animator animation) { + AttachmentTypeSelector.super.dismiss(); + } - @Override - public void onAnimationCancel(@NonNull Animator animation) { - } + @Override + public void onAnimationCancel(@NonNull Animator animation) {} - @Override - public void onAnimationRepeat(@NonNull Animator animation) { - } - }); + @Override + public void onAnimationRepeat(@NonNull Animator animation) {} + }); animator.start(); } private void animateWindowOutTranslate(@NonNull View contentView) { - Animation animation = new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight()); + Animation animation = + new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight()); animation.setDuration(ANIMATION_DURATION); - animation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - @Override - public void onAnimationEnd(Animation animation) { - AttachmentTypeSelector.super.dismiss(); - } + @Override + public void onAnimationEnd(Animation animation) { + AttachmentTypeSelector.super.dismiss(); + } - @Override - public void onAnimationRepeat(Animation animation) { - } - }); + @Override + public void onAnimationRepeat(Animation animation) {} + }); getContentView().startAnimation(animation); } @@ -270,12 +286,11 @@ public void onClick(View v) { if (listener != null) listener.onClick(type); } - } public interface AttachmentClickedListener { public void onClick(int type); + public void onQuickAttachment(Uri uri); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java index f21f09b77..dbc5a11db 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -2,14 +2,11 @@ import android.content.Context; import android.content.Intent; - +import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; -import android.util.AttributeSet; - import com.bumptech.glide.load.engine.DiskCacheStrategy; - import org.thoughtcrime.securesms.ProfileActivity; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; @@ -37,19 +34,25 @@ public void setOnClickListener(OnClickListener listener) { super.setOnClickListener(listener); } - public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { + public void setAvatar( + @NonNull GlideRequests requestManager, + @Nullable Recipient recipient, + boolean quickContactEnabled) { if (recipient != null) { ContactPhoto contactPhoto = recipient.getContactPhoto(getContext()); - requestManager.load(contactPhoto) - .error(recipient.getFallbackAvatarDrawable(getContext())) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .circleCrop() - .into(this); - if(quickContactEnabled) { + requestManager + .load(contactPhoto) + .error(recipient.getFallbackAvatarDrawable(getContext())) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .circleCrop() + .into(this); + if (quickContactEnabled) { setAvatarClickHandler(recipient); } } else { - setImageDrawable(new GeneratedContactPhoto("+").asDrawable(getContext(), ThemeUtil.getDummyContactColor(getContext()))); + setImageDrawable( + new GeneratedContactPhoto("+") + .asDrawable(getContext(), ThemeUtil.getDummyContactColor(getContext()))); if (listener != null) super.setOnClickListener(listener); } } @@ -60,16 +63,17 @@ public void clear(@NonNull GlideRequests glideRequests) { private void setAvatarClickHandler(final Recipient recipient) { if (!recipient.isMultiUserRecipient()) { - super.setOnClickListener(v -> { - if(recipient.getAddress().isDcContact()) { - Intent intent = new Intent(getContext(), ProfileActivity.class); - intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, recipient.getAddress().getDcContactId()); - getContext().startActivity(intent); - } - }); + super.setOnClickListener( + v -> { + if (recipient.getAddress().isDcContact()) { + Intent intent = new Intent(getContext(), ProfileActivity.class); + intent.putExtra( + ProfileActivity.CONTACT_ID_EXTRA, recipient.getAddress().getDcContactId()); + getContext().startActivity(intent); + } + }); } else { super.setOnClickListener(listener); } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/AvatarSelector.java b/src/main/java/org/thoughtcrime/securesms/components/AvatarSelector.java index 25430e22f..dff75346d 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/AvatarSelector.java +++ b/src/main/java/org/thoughtcrime/securesms/components/AvatarSelector.java @@ -14,37 +14,40 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.PopupWindow; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.loader.app.LoaderManager; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.ViewUtil; public class AvatarSelector extends PopupWindow { - public static final int ADD_GALLERY = 1; - public static final int TAKE_PHOTO = 5; - public static final int REMOVE_PHOTO = 8; + public static final int ADD_GALLERY = 1; + public static final int TAKE_PHOTO = 5; + public static final int REMOVE_PHOTO = 8; private static final int ANIMATION_DURATION = 300; - private final @NonNull LoaderManager loaderManager; + private final @NonNull LoaderManager loaderManager; private final @NonNull RecentPhotoViewRail recentRail; private @Nullable AttachmentClickedListener listener; - public AvatarSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener, boolean includeClear) { + public AvatarSelector( + @NonNull Context context, + @NonNull LoaderManager loaderManager, + @Nullable AttachmentClickedListener listener, + boolean includeClear) { super(context); - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.avatar_selector, null, true); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.avatar_selector, null, true); - this.listener = listener; - this.loaderManager = loaderManager; - this.recentRail = ViewUtil.findById(layout, R.id.recent_photos); + this.listener = listener; + this.loaderManager = loaderManager; + this.recentRail = ViewUtil.findById(layout, R.id.recent_photos); ImageView imageButton = ViewUtil.findById(layout, R.id.gallery_button); ImageView cameraButton = ViewUtil.findById(layout, R.id.camera_button); ImageView closeButton = ViewUtil.findById(layout, R.id.close_button); @@ -82,26 +85,28 @@ public void show(@NonNull Activity activity, final @NonNull View anchor) { showAtLocation(anchor, Gravity.BOTTOM, 0, 0); - getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getContentView().getViewTreeObserver().removeOnGlobalLayoutListener(this); - - animateWindowInTranslate(getContentView()); - } - }); + getContentView() + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getContentView().getViewTreeObserver().removeOnGlobalLayoutListener(this); + + animateWindowInTranslate(getContentView()); + } + }); } @Override public void dismiss() { - animateWindowOutTranslate(getContentView()); + animateWindowOutTranslate(getContentView()); } public void setListener(@Nullable AttachmentClickedListener listener) { this.listener = listener; } - private void animateWindowInTranslate(@NonNull View contentView) { Animation animation = new TranslateAnimation(0, 0, contentView.getHeight(), 0); animation.setDuration(ANIMATION_DURATION); @@ -110,22 +115,22 @@ private void animateWindowInTranslate(@NonNull View contentView) { } private void animateWindowOutTranslate(@NonNull View contentView) { - Animation animation = new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight()); + Animation animation = + new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight()); animation.setDuration(ANIMATION_DURATION); - animation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - @Override - public void onAnimationEnd(Animation animation) { - AvatarSelector.super.dismiss(); - } + @Override + public void onAnimationEnd(Animation animation) { + AvatarSelector.super.dismiss(); + } - @Override - public void onAnimationRepeat(Animation animation) { - } - }); + @Override + public void onAnimationRepeat(Animation animation) {} + }); getContentView().startAnimation(animation); } @@ -153,7 +158,6 @@ public void onClick(View v) { if (listener != null) listener.onClick(type); } - } private class CloseClickListener implements View.OnClickListener { @@ -165,7 +169,7 @@ public void onClick(View v) { public interface AttachmentClickedListener { void onClick(int type); + void onQuickAttachment(Uri uri); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/AvatarView.java b/src/main/java/org/thoughtcrime/securesms/components/AvatarView.java index 4ee5c516e..d553b0c4c 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/AvatarView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/AvatarView.java @@ -6,14 +6,11 @@ import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; - import com.amulyakhare.textdrawable.TextDrawable; import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; @@ -21,8 +18,8 @@ public class AvatarView extends ConstraintLayout { - private AvatarImageView avatarImage; - private ImageView seenRecentlyIndicator; + private AvatarImageView avatarImage; + private ImageView seenRecentlyIndicator; public AvatarView(Context context) { super(context); @@ -42,11 +39,14 @@ public AvatarView(Context context, @Nullable AttributeSet attrs, int defStyleAtt private void init() { inflate(getContext(), R.layout.avatar_view, this); - avatarImage = findViewById(R.id.avatar_image); - seenRecentlyIndicator = findViewById(R.id.status_indicator); + avatarImage = findViewById(R.id.avatar_image); + seenRecentlyIndicator = findViewById(R.id.status_indicator); } - public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { + public void setAvatar( + @NonNull GlideRequests requestManager, + @Nullable Recipient recipient, + boolean quickContactEnabled) { avatarImage.setAvatar(requestManager, recipient, quickContactEnabled); } @@ -59,37 +59,37 @@ public void setAvatarClickListener(OnClickListener listener) { } public void setSeenRecently(boolean enabled) { - seenRecentlyIndicator.setVisibility(enabled? View.VISIBLE : View.GONE); + seenRecentlyIndicator.setVisibility(enabled ? View.VISIBLE : View.GONE); } public void setConnectivity(int connectivity) { - final int id; - String text = ""; - if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) { - id = R.color.status_dot_online; - } else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) { - text = "⇅"; - id = R.color.status_dot_online; - } else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) { - id = R.color.status_dot_connecting; - } else { - id = R.color.status_dot_offline; - } - int size = ViewUtil.dpToPx(getContext(), 24); - seenRecentlyIndicator.setImageDrawable(TextDrawable.builder() - .beginConfig() - .width(size) - .height(size) - .textColor(Color.WHITE) - .fontSize(ViewUtil.dpToPx(getContext(), 23)) - .bold() - .endConfig() - .buildRound(text, getResources().getColor(id))); - seenRecentlyIndicator.setVisibility(View.VISIBLE); + final int id; + String text = ""; + if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) { + id = R.color.status_dot_online; + } else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) { + text = "⇅"; + id = R.color.status_dot_online; + } else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) { + id = R.color.status_dot_connecting; + } else { + id = R.color.status_dot_offline; + } + int size = ViewUtil.dpToPx(getContext(), 24); + seenRecentlyIndicator.setImageDrawable( + TextDrawable.builder() + .beginConfig() + .width(size) + .height(size) + .textColor(Color.WHITE) + .fontSize(ViewUtil.dpToPx(getContext(), 23)) + .bold() + .endConfig() + .buildRound(text, getResources().getColor(id))); + seenRecentlyIndicator.setVisibility(View.VISIBLE); } public void clear(@NonNull GlideRequests glideRequests) { avatarImage.clear(glideRequests); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java b/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java index 4d134588d..90ada870c 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java @@ -5,10 +5,8 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; @@ -17,7 +15,7 @@ public class BorderlessImageView extends FrameLayout { private ThumbnailView image; - private View missingShade; + private View missingShade; private ConversationItemFooter footer; public BorderlessImageView(@NonNull Context context) { @@ -33,9 +31,9 @@ public BorderlessImageView(@NonNull Context context, @Nullable AttributeSet attr private void init() { inflate(getContext(), R.layout.sticker_view, this); - this.image = findViewById(R.id.sticker_thumbnail); + this.image = findViewById(R.id.sticker_thumbnail); this.missingShade = findViewById(R.id.sticker_missing_shade); - this.footer = findViewById(R.id.sticker_footer); + this.footer = findViewById(R.id.sticker_footer); } @Override @@ -61,7 +59,8 @@ public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) image.setImageResource(glideRequests, slide); } else { image.setScaleType(ImageView.ScaleType.CENTER_CROP); - image.setImageResource(glideRequests, slide, slide.asAttachment().getWidth(), slide.asAttachment().getHeight()); + image.setImageResource( + glideRequests, slide, slide.asAttachment().getWidth(), slide.asAttachment().getHeight()); } missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE); @@ -78,5 +77,4 @@ public ConversationItemFooter getFooter() { public void setThumbnailClickListener(@NonNull SlideClickListener listener) { image.setThumbnailClickListener(listener); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java b/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java index aa9dbf9e9..4b41c166b 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/CallItemView.java @@ -8,14 +8,11 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.DateUtils; - import chat.delta.rpc.types.CallInfo; import chat.delta.rpc.types.CallState; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.DateUtils; public class CallItemView extends FrameLayout { private static final String TAG = CallItemView.class.getSimpleName(); @@ -43,11 +40,12 @@ public CallItemView(final Context context, AttributeSet attrs, int defStyle) { this.title = findViewById(R.id.title); this.duration = findViewById(R.id.duration); - setOnClickListener(v -> { - if (viewListener != null && callInfo != null) { - viewListener.onClick(v, callInfo); - } - }); + setOnClickListener( + v -> { + if (viewListener != null && callInfo != null) { + viewListener.onClick(v, callInfo); + } + }); } public void setCallClickListener(CallClickListener listener) { @@ -58,7 +56,9 @@ public void setCallItem(boolean isOutgoing, CallInfo callInfo) { this.callInfo = callInfo; if (callInfo.state instanceof CallState.Completed) { - duration.setText(DateUtils.getFormattedCallDuration(getContext(), ((CallState.Completed) callInfo.state).duration)); + duration.setText( + DateUtils.getFormattedCallDuration( + getContext(), ((CallState.Completed) callInfo.state).duration)); duration.setVisibility(VISIBLE); } else { duration.setVisibility(GONE); @@ -71,22 +71,25 @@ public void setCallItem(boolean isOutgoing, CallInfo callInfo) { } else if (callInfo.state instanceof CallState.Declined) { title.setText(R.string.declined_call); } else if (callInfo.hasVideo) { - title.setText(isOutgoing? R.string.outgoing_video_call : R.string.incoming_video_call); + title.setText(R.string.video_call); } else { - title.setText(isOutgoing? R.string.outgoing_audio_call : R.string.incoming_audio_call); + title.setText(R.string.audio_call); } - icon.setImageResource(callInfo.hasVideo? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24); + icon.setImageResource( + callInfo.hasVideo ? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24); int[] attrs; if (isOutgoing) { - attrs = new int[]{ - R.attr.conversation_item_outgoing_text_primary_color, - }; + attrs = + new int[] { + R.attr.conversation_item_outgoing_text_primary_color, + }; } else { - attrs = new int[]{ - R.attr.conversation_item_incoming_text_primary_color, - }; + attrs = + new int[] { + R.attr.conversation_item_incoming_text_primary_color, + }; } try (TypedArray ta = getContext().obtainStyledAttributes(attrs)) { icon.setColorFilter(ta.getColor(0, Color.BLACK)); @@ -94,7 +97,8 @@ public void setCallItem(boolean isOutgoing, CallInfo callInfo) { } public String getDescription() { - return title.getText() + (duration.getVisibility()==VISIBLE ? ("\n" + duration.getText()) : ""); + return title.getText() + + (duration.getVisibility() == VISIBLE ? ("\n" + duration.getText()) : ""); } public interface CallClickListener { diff --git a/src/main/java/org/thoughtcrime/securesms/components/CircleColorImageView.java b/src/main/java/org/thoughtcrime/securesms/components/CircleColorImageView.java index 880e2cd92..83837be68 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/CircleColorImageView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/CircleColorImageView.java @@ -5,14 +5,13 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import androidx.appcompat.widget.AppCompatImageView; import android.util.AttributeSet; - +import androidx.appcompat.widget.AppCompatImageView; import org.thoughtcrime.securesms.R; public class CircleColorImageView extends AppCompatImageView { - public CircleColorImageView(Context context) { + public CircleColorImageView(Context context) { this(context, null); } @@ -26,8 +25,12 @@ public CircleColorImageView(Context context, AttributeSet attrs, int defStyleAtt int circleColor = Color.WHITE; if (attrs != null) { - try (TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleColorImageView, 0, 0)) { - circleColor = typedArray.getColor(R.styleable.CircleColorImageView_circleColor, Color.WHITE); + try (TypedArray typedArray = + context + .getTheme() + .obtainStyledAttributes(attrs, R.styleable.CircleColorImageView, 0, 0)) { + circleColor = + typedArray.getColor(R.styleable.CircleColorImageView_circleColor, Color.WHITE); } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java b/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java index ad6db3ff2..ac830ea3f 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java @@ -6,16 +6,6 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatEditText; -import androidx.core.view.ContentInfoCompat; -import androidx.core.view.ViewCompat; -import androidx.core.view.inputmethod.EditorInfoCompat; -import androidx.core.view.inputmethod.InputConnectionCompat; -import androidx.core.view.inputmethod.InputContentInfoCompat; -import androidx.core.os.BuildCompat; - import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -26,14 +16,22 @@ import android.util.Log; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.core.os.BuildCompat; +import androidx.core.view.ContentInfoCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.inputmethod.EditorInfoCompat; +import androidx.core.view.inputmethod.InputConnectionCompat; +import androidx.core.view.inputmethod.InputContentInfoCompat; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.util.Prefs; public class ComposeText extends AppCompatEditText { - private CharSequence hint; + private CharSequence hint; private SpannableString subHint; @Nullable private InputPanel.MediaListener mediaListener; @@ -60,13 +58,14 @@ public boolean onTextContextMenuItem(int id) { id = android.R.id.pasteAsPlainText; } else if (ViewCompat.getOnReceiveContentMimeTypes(this) != null) { // older device, manually paste as plain text - ClipboardManager cm = (ClipboardManager) getContext().getSystemService( - Context.CLIPBOARD_SERVICE); + ClipboardManager cm = + (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = (cm == null) ? null : cm.getPrimaryClip(); if (clip != null && clip.getItemCount() > 0) { - ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, ContentInfoCompat.SOURCE_CLIPBOARD) - .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT) - .build(); + ContentInfoCompat payload = + new ContentInfoCompat.Builder(clip, ContentInfoCompat.SOURCE_CLIPBOARD) + .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT) + .build(); ViewCompat.performReceiveContent(this, payload); } return true; @@ -75,7 +74,7 @@ public boolean onTextContextMenuItem(int id) { return super.onTextContextMenuItem(id); } - public String getTextTrimmed(){ + public String getTextTrimmed() { return getText().toString().trim(); } @@ -85,9 +84,11 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto if (!TextUtils.isEmpty(hint)) { if (!TextUtils.isEmpty(subHint)) { - setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint)) - .append("\n") - .append(ellipsizeToWidth(subHint))); + setHint( + new SpannableStringBuilder() + .append(ellipsizeToWidth(hint)) + .append("\n") + .append(ellipsizeToWidth(subHint))); } else { setHint(ellipsizeToWidth(hint)); } @@ -95,10 +96,8 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto } private CharSequence ellipsizeToWidth(CharSequence text) { - return TextUtils.ellipsize(text, - getPaint(), - getWidth() - getPaddingLeft() - getPaddingRight(), - TruncateAt.END); + return TextUtils.ellipsize( + text, getPaint(), getWidth() - getPaddingLeft() - getPaddingRight(), TruncateAt.END); } public void setHint(@NonNull String hint, @Nullable CharSequence subHint) { @@ -106,15 +105,18 @@ public void setHint(@NonNull String hint, @Nullable CharSequence subHint) { if (subHint != null) { this.subHint = new SpannableString(subHint); - this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + this.subHint.setSpan( + new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else { this.subHint = null; } if (this.subHint != null) { - super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint)) - .append("\n") - .append(ellipsizeToWidth(this.subHint))); + super.setHint( + new SpannableStringBuilder() + .append(ellipsizeToWidth(this.hint)) + .append("\n") + .append(ellipsizeToWidth(this.subHint))); } else { super.setHint(ellipsizeToWidth(this.hint)); } @@ -129,33 +131,36 @@ public void setTransport(TransportOption transport) { // as this removes the ability to compose multi-line messages. int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND; - int inputType = getInputType(); + int inputType = getInputType(); - if (isLandscape()) setImeActionLabel(getContext().getString(R.string.menu_send), EditorInfo.IME_ACTION_SEND); - else setImeActionLabel(null, 0); + if (isLandscape()) + setImeActionLabel(getContext().getString(R.string.menu_send), EditorInfo.IME_ACTION_SEND); + else setImeActionLabel(null, 0); setInputType(inputType); setImeOptions(imeOptions); - setHint(transport.getComposeHint(),null); + setHint(transport.getComposeHint(), null); } @Override public InputConnection onCreateInputConnection(@NonNull EditorInfo editorInfo) { InputConnection inputConnection = super.onCreateInputConnection(editorInfo); - if(Prefs.isEnterSendsEnabled(getContext())) { + if (Prefs.isEnterSendsEnabled(getContext())) { editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; } - if (mediaListener == null) return inputConnection; - if (inputConnection == null) return null; + if (mediaListener == null) return inputConnection; + if (inputConnection == null) return null; // media with mime-types defined by setContentMimeTypes() may be selected in the system keyboard // and are passed to onCommitContent() then; // from there we use them as stickers. - EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] {"image/jpeg", "image/png", "image/gif", "image/webp"}); + EditorInfoCompat.setContentMimeTypes( + editorInfo, new String[] {"image/jpeg", "image/png", "image/gif", "image/webp"}); - return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener)); + return InputConnectionCompat.createWrapper( + inputConnection, editorInfo, new CommitContentListener(mediaListener)); } public void setMediaListener(@Nullable InputPanel.MediaListener mediaListener) { @@ -168,7 +173,8 @@ private void initialize() { } } - private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener { + private static class CommitContentListener + implements InputConnectionCompat.OnCommitContentListener { private static final String TAG = CommitContentListener.class.getName(); @@ -179,8 +185,10 @@ private CommitContentListener(@NonNull InputPanel.MediaListener mediaListener) { } @Override - public boolean onCommitContent(@NonNull InputContentInfoCompat inputContentInfo, int flags, Bundle opts) { - if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { + public boolean onCommitContent( + @NonNull InputContentInfoCompat inputContentInfo, int flags, Bundle opts) { + if (BuildCompat.isAtLeastNMR1() + && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { try { inputContentInfo.requestPermission(); } catch (Exception e) { @@ -190,8 +198,8 @@ public boolean onCommitContent(@NonNull InputContentInfoCompat inputContentInfo, } if (inputContentInfo.getDescription().getMimeTypeCount() > 0) { - mediaListener.onMediaSelected(inputContentInfo.getContentUri(), - inputContentInfo.getDescription().getMimeType(0)); + mediaListener.onMediaSelected( + inputContentInfo.getContentUri(), inputContentInfo.getDescription().getMimeType(0)); return true; } @@ -199,5 +207,4 @@ public boolean onCommitContent(@NonNull InputContentInfoCompat inputContentInfo, return false; } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java b/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java index 651f600e5..c77df0119 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java @@ -2,7 +2,6 @@ import android.content.Context; import android.graphics.Rect; -import androidx.appcompat.widget.Toolbar; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; @@ -12,18 +11,18 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; - +import androidx.appcompat.widget.Toolbar; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ViewUtil; public class ContactFilterToolbar extends Toolbar { - private OnFilterChangedListener listener; + private OnFilterChangedListener listener; - private final EditText searchText; + private final EditText searchText; private final AnimatingToggle toggle; - private final ImageView clearToggle; - private final LinearLayout toggleContainer; - private boolean useClearButton; + private final ImageView clearToggle; + private final LinearLayout toggleContainer; + private boolean useClearButton; public ContactFilterToolbar(Context context) { this(context, null); @@ -38,42 +37,40 @@ public ContactFilterToolbar(Context context, AttributeSet attrs, int defStyleAtt super(context, attrs, defStyleAttr); inflate(context, R.layout.contact_filter_toolbar, this); - this.searchText = ViewUtil.findById(this, R.id.search_view); - this.toggle = ViewUtil.findById(this, R.id.button_toggle); - this.clearToggle = ViewUtil.findById(this, R.id.search_clear); + this.searchText = ViewUtil.findById(this, R.id.search_view); + this.toggle = ViewUtil.findById(this, R.id.button_toggle); + this.clearToggle = ViewUtil.findById(this, R.id.search_clear); this.toggleContainer = ViewUtil.findById(this, R.id.toggle_container); - searchText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); - - this.clearToggle.setOnClickListener(v -> { - searchText.setText(""); - displayTogglingView(null); - }); - - this.searchText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { + searchText.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - if (!SearchUtil.isEmpty(searchText)) { - if(useClearButton) { - displayTogglingView(clearToggle); - } - } - else { + this.clearToggle.setOnClickListener( + v -> { + searchText.setText(""); displayTogglingView(null); - } - notifyListener(); - } - }); + }); + + this.searchText.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (!SearchUtil.isEmpty(searchText)) { + if (useClearButton) { + displayTogglingView(clearToggle); + } + } else { + displayTogglingView(null); + } + notifyListener(); + } + }); setLogo(null); setContentInsetStartWithNavigation(0); @@ -104,25 +101,27 @@ private void notifyListener() { private void displayTogglingView(View view) { toggle.display(view); - if (view!=null) { + if (view != null) { expandTapArea(toggleContainer, view); } } private void expandTapArea(final View container, final View child) { - final int padding = getResources().getDimensionPixelSize(R.dimen.contact_selection_actions_tap_area); + final int padding = + getResources().getDimensionPixelSize(R.dimen.contact_selection_actions_tap_area); - container.post(() -> { - Rect rect = new Rect(); - child.getHitRect(rect); + container.post( + () -> { + Rect rect = new Rect(); + child.getHitRect(rect); - rect.top -= padding; - rect.left -= padding; - rect.right += padding; - rect.bottom += padding; + rect.top -= padding; + rect.left -= padding; + rect.right += padding; + rect.bottom += padding; - container.setTouchDelegate(new TouchDelegate(rect, child)); - }); + container.setTouchDelegate(new TouchDelegate(rect, child)); + }); } private static class SearchUtil { diff --git a/src/main/java/org/thoughtcrime/securesms/components/ControllableViewPager.java b/src/main/java/org/thoughtcrime/securesms/components/ControllableViewPager.java index 9b917fa13..be7cb48ac 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ControllableViewPager.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ControllableViewPager.java @@ -1,17 +1,14 @@ package org.thoughtcrime.securesms.components; import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager.widget.ViewPager; -import android.util.AttributeSet; -import android.view.MotionEvent; - import org.thoughtcrime.securesms.components.viewpager.HackyViewPager; -/** - * An implementation of {@link ViewPager} that disables swiping when the view is disabled. - */ +/** An implementation of {@link ViewPager} that disables swiping when the view is disabled. */ public class ControllableViewPager extends HackyViewPager { public ControllableViewPager(@NonNull Context context) { diff --git a/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index 33f01f8df..ff2a96a92 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -7,30 +7,26 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.DateUtils; -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - public class ConversationItemFooter extends LinearLayout { - private TextView dateView; - private TextView editedView; - private TextView viewsLabel; - private ImageView viewsIcon; - private ImageView bookmarkIndicatorView; - private ImageView emailIndicatorView; - private ImageView locationIndicatorView; - private DeliveryStatusView deliveryStatusView; - private Integer textColor = null; + private TextView dateView; + private TextView editedView; + private TextView viewsLabel; + private ImageView viewsIcon; + private ImageView bookmarkIndicatorView; + private ImageView emailIndicatorView; + private ImageView locationIndicatorView; + private DeliveryStatusView deliveryStatusView; + private Integer textColor = null; private Context context; private Rpc rpc; @@ -54,18 +50,24 @@ private void init(Context context, @Nullable AttributeSet attrs) { this.rpc = DcHelper.getRpc(context); inflate(getContext(), R.layout.conversation_item_footer, this); - dateView = findViewById(R.id.footer_date); - editedView = findViewById(R.id.footer_edited); - viewsLabel = findViewById(R.id.footer_views); - viewsIcon = findViewById(R.id.footer_views_icon); + dateView = findViewById(R.id.footer_date); + editedView = findViewById(R.id.footer_edited); + viewsLabel = findViewById(R.id.footer_views); + viewsIcon = findViewById(R.id.footer_views_icon); bookmarkIndicatorView = findViewById(R.id.footer_bookmark_indicator); - emailIndicatorView = findViewById(R.id.footer_email_indicator); + emailIndicatorView = findViewById(R.id.footer_email_indicator); locationIndicatorView = findViewById(R.id.footer_location_indicator); - deliveryStatusView = new DeliveryStatusView(findViewById(R.id.delivery_indicator)); + deliveryStatusView = new DeliveryStatusView(findViewById(R.id.delivery_indicator)); if (attrs != null) { - try (TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0)) { - setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white))); + try (TypedArray typedArray = + getContext() + .getTheme() + .obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0)) { + setTextColor( + typedArray.getInt( + R.styleable.ConversationItemFooter_footer_text_color, + getResources().getColor(R.color.core_white))); } } } @@ -77,7 +79,10 @@ public void setMessageRecord(@NonNull DcMsg messageRecord) { editedView.setVisibility(messageRecord.isEdited() ? View.VISIBLE : View.GONE); int downloadState = messageRecord.getDownloadState(); - if (messageRecord.isSecure() || downloadState == DcMsg.DC_DOWNLOAD_AVAILABLE || downloadState == DcMsg.DC_DOWNLOAD_FAILURE || downloadState == DcMsg.DC_DOWNLOAD_IN_PROGRESS) { + if (messageRecord.isSecure() + || downloadState == DcMsg.DC_DOWNLOAD_AVAILABLE + || downloadState == DcMsg.DC_DOWNLOAD_FAILURE + || downloadState == DcMsg.DC_DOWNLOAD_IN_PROGRESS) { emailIndicatorView.setVisibility(View.GONE); } else { emailIndicatorView.setVisibility(View.VISIBLE); @@ -85,7 +90,8 @@ public void setMessageRecord(@NonNull DcMsg messageRecord) { locationIndicatorView.setVisibility(messageRecord.hasLocation() ? View.VISIBLE : View.GONE); - boolean isOutChannel = DcHelper.getContext(context).getChat(messageRecord.getChatId()).isOutBroadcast(); + boolean isOutChannel = + DcHelper.getContext(context).getChat(messageRecord.getChatId()).isOutBroadcast(); if (isOutChannel && messageRecord.isOutgoing()) { try { @@ -119,29 +125,30 @@ private void setTextColor(int color) { private void presentDate(@NonNull DcMsg dcMsg) { dateView.forceLayout(); - dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), dcMsg.getTimestamp())); + dateView.setText( + DateUtils.getExtendedRelativeTimeSpanString(getContext(), dcMsg.getTimestamp())); } private void presentDeliveryStatus(@NonNull DcMsg messageRecord, boolean isOutChannel) { // isDownloading is temporary and should be checked first. boolean isDownloading = messageRecord.getDownloadState() == DcMsg.DC_DOWNLOAD_IN_PROGRESS; - if (isDownloading) deliveryStatusView.setDownloading(); - else if (messageRecord.isPending()) deliveryStatusView.setPending(); - else if (messageRecord.isFailed()) deliveryStatusView.setFailed(); - else if (!messageRecord.isOutgoing() || isOutChannel) deliveryStatusView.setNone(); + if (isDownloading) deliveryStatusView.setDownloading(); + else if (messageRecord.isPending()) deliveryStatusView.setPending(); + else if (messageRecord.isFailed()) deliveryStatusView.setFailed(); + else if (!messageRecord.isOutgoing() || isOutChannel) deliveryStatusView.setNone(); else if (messageRecord.isRemoteRead()) deliveryStatusView.setRead(); - else if (messageRecord.isDelivered()) deliveryStatusView.setSent(); - else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing(); - else deliveryStatusView.setPending(); + else if (messageRecord.isDelivered()) deliveryStatusView.setSent(); + else if (messageRecord.isPreparing()) deliveryStatusView.setPreparing(); + else deliveryStatusView.setPending(); } public String getDescription() { - String desc = dateView.getText().toString(); - String deliveryDesc = deliveryStatusView.getDescription(); - if (!"".equals(deliveryDesc)) { - desc += "\n" + deliveryDesc; - } - return desc; + String desc = dateView.getText().toString(); + String deliveryDesc = deliveryStatusView.getDescription(); + if (!"".equals(deliveryDesc)) { + desc += "\n" + deliveryDesc; + } + return desc; } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java b/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java index ed8bf0255..1575d76bd 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java @@ -10,12 +10,12 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; - import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; - +import chat.delta.util.ListenableFuture; +import java.util.concurrent.ExecutionException; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -24,10 +24,6 @@ import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; -import java.util.concurrent.ExecutionException; - -import chat.delta.util.ListenableFuture; - public class ConversationItemThumbnail extends FrameLayout { private static final Paint LIGHT_THEME_OUTLINE_PAINT = new Paint(); @@ -46,15 +42,15 @@ public class ConversationItemThumbnail extends FrameLayout { DARK_THEME_OUTLINE_PAINT.setAntiAlias(true); } - private final float[] radii = new float[8]; - private final RectF bounds = new RectF(); - private final Path corners = new Path(); + private final float[] radii = new float[8]; + private final RectF bounds = new RectF(); + private final Path corners = new Path(); - private ThumbnailView thumbnail; - private ImageView shade; + private ThumbnailView thumbnail; + private ImageView shade; private ConversationItemFooter footer; - private Paint outlinePaint; - private CornerMask cornerMask; + private Paint outlinePaint; + private CornerMask cornerMask; private int naturalWidth; private int naturalHeight; @@ -76,11 +72,12 @@ public ConversationItemThumbnail(final Context context, AttributeSet attrs, int private void init() { inflate(getContext(), R.layout.conversation_item_thumbnail, this); - this.thumbnail = findViewById(R.id.conversation_thumbnail_image); - this.shade = findViewById(R.id.conversation_thumbnail_shade); - this.footer = findViewById(R.id.conversation_thumbnail_footer); - this.outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT; - this.cornerMask = new CornerMask(this); + this.thumbnail = findViewById(R.id.conversation_thumbnail_image); + this.shade = findViewById(R.id.conversation_thumbnail_shade); + this.footer = findViewById(R.id.conversation_thumbnail_footer); + this.outlinePaint = + ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT; + this.cornerMask = new CornerMask(this); setTouchDelegate(thumbnail.getTouchDelegate()); } @@ -125,9 +122,9 @@ protected void dispatchDraw(@NonNull Canvas canvas) { final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2; - bounds.left = halfStrokeWidth; - bounds.top = halfStrokeWidth; - bounds.right = canvas.getWidth() - halfStrokeWidth; + bounds.left = halfStrokeWidth; + bounds.top = halfStrokeWidth; + bounds.right = canvas.getWidth() - halfStrokeWidth; bounds.bottom = canvas.getHeight() - halfStrokeWidth; corners.reset(); @@ -170,17 +167,18 @@ public ConversationItemFooter getFooter() { } private void refreshSlideAttachmentState(ListenableFuture signal, Slide slide) { - signal.addListener(new ListenableFuture.Listener() { - @Override - public void onSuccess(Boolean result) { - slide.asAttachment().setTransferState(AttachmentDatabase.TRANSFER_PROGRESS_DONE); - } + signal.addListener( + new ListenableFuture.Listener() { + @Override + public void onSuccess(Boolean result) { + slide.asAttachment().setTransferState(AttachmentDatabase.TRANSFER_PROGRESS_DONE); + } - @Override - public void onFailure(ExecutionException e) { - slide.asAttachment().setTransferState(AttachmentDatabase.TRANSFER_PROGRESS_FAILED); - } - }); + @Override + public void onFailure(ExecutionException e) { + slide.asAttachment().setTransferState(AttachmentDatabase.TRANSFER_PROGRESS_FAILED); + } + }); } public void setThumbnailClickListener(SlideClickListener listener) { @@ -193,9 +191,11 @@ public boolean performClick() { } @UiThread - public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - int naturalWidth, int naturalHeight) - { + public void setImageResource( + @NonNull GlideRequests glideRequests, + @NonNull Slide slide, + int naturalWidth, + int naturalHeight) { this.naturalWidth = naturalWidth; this.naturalHeight = naturalHeight; refreshSlideAttachmentState(thumbnail.setImageResource(glideRequests, slide), slide); diff --git a/src/main/java/org/thoughtcrime/securesms/components/CornerMask.java b/src/main/java/org/thoughtcrime/securesms/components/CornerMask.java index ce5eeed81..d19ee108e 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/CornerMask.java +++ b/src/main/java/org/thoughtcrime/securesms/components/CornerMask.java @@ -7,17 +7,16 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; - -import androidx.annotation.NonNull; import android.view.View; +import androidx.annotation.NonNull; public class CornerMask { - private final float[] radii = new float[8]; - private final Paint clearPaint = new Paint(); - private final Path outline = new Path(); - private final Path corners = new Path(); - private final RectF bounds = new RectF(); + private final float[] radii = new float[8]; + private final Paint clearPaint = new Paint(); + private final Path outline = new Path(); + private final Path corners = new Path(); + private final RectF bounds = new RectF(); public CornerMask(@NonNull View view) { view.setLayerType(View.LAYER_TYPE_HARDWARE, null); @@ -29,9 +28,9 @@ public CornerMask(@NonNull View view) { } public void mask(Canvas canvas) { - bounds.left = 0; - bounds.top = 0; - bounds.right = canvas.getWidth(); + bounds.left = 0; + bounds.top = 0; + bounds.right = canvas.getWidth(); bounds.bottom = canvas.getHeight(); corners.reset(); diff --git a/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java b/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java index b4db7fb68..03a441163 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/DeliveryStatusView.java @@ -6,14 +6,13 @@ import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; import android.widget.ImageView; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.AccessibilityUtil; public class DeliveryStatusView { private final ImageView deliveryIndicator; - private final Context context; + private final Context context; private static RotateAnimation prepareAnimation; private static RotateAnimation sendingAnimation; private boolean animated; @@ -23,14 +22,13 @@ public DeliveryStatusView(ImageView deliveryIndicator) { this.context = deliveryIndicator.getContext(); } - private void animatePrepare() - { + private void animatePrepare() { if (AccessibilityUtil.areAnimationsDisabled(context)) return; - - if(prepareAnimation ==null) { - prepareAnimation = new RotateAnimation(360f, 0f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f); + + if (prepareAnimation == null) { + prepareAnimation = + new RotateAnimation( + 360f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); prepareAnimation.setInterpolator(new LinearInterpolator()); prepareAnimation.setDuration(2500); prepareAnimation.setRepeatCount(Animation.INFINITE); @@ -40,14 +38,13 @@ private void animatePrepare() animated = true; } - private void animateSending() - { + private void animateSending() { if (AccessibilityUtil.areAnimationsDisabled(context)) return; - if(sendingAnimation ==null) { - sendingAnimation = new RotateAnimation(0, 360f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f); + if (sendingAnimation == null) { + sendingAnimation = + new RotateAnimation( + 0, 360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); sendingAnimation.setInterpolator(new LinearInterpolator()); sendingAnimation.setDuration(1500); sendingAnimation.setRepeatCount(Animation.INFINITE); @@ -57,9 +54,8 @@ private void animateSending() animated = true; } - private void clearAnimation() - { - if(animated) { + private void clearAnimation() { + if (animated) { deliveryIndicator.clearAnimation(); animated = false; } @@ -80,21 +76,24 @@ public void setDownloading() { public void setPreparing() { deliveryIndicator.setVisibility(View.VISIBLE); deliveryIndicator.setImageResource(R.drawable.ic_delivery_status_sending); - deliveryIndicator.setContentDescription(context.getString(R.string.a11y_delivery_status_sending)); + deliveryIndicator.setContentDescription( + context.getString(R.string.a11y_delivery_status_sending)); animatePrepare(); } public void setPending() { deliveryIndicator.setVisibility(View.VISIBLE); deliveryIndicator.setImageResource(R.drawable.ic_delivery_status_sending); - deliveryIndicator.setContentDescription(context.getString(R.string.a11y_delivery_status_sending)); + deliveryIndicator.setContentDescription( + context.getString(R.string.a11y_delivery_status_sending)); animateSending(); } public void setSent() { deliveryIndicator.setVisibility(View.VISIBLE); deliveryIndicator.setImageResource(R.drawable.ic_delivery_status_sent); - deliveryIndicator.setContentDescription(context.getString(R.string.a11y_delivery_status_delivered)); + deliveryIndicator.setContentDescription( + context.getString(R.string.a11y_delivery_status_delivered)); clearAnimation(); } @@ -108,7 +107,8 @@ public void setRead() { public void setFailed() { deliveryIndicator.setVisibility(View.VISIBLE); deliveryIndicator.setImageResource(R.drawable.ic_delivery_status_failed); - deliveryIndicator.setContentDescription(context.getString(R.string.a11y_delivery_status_invalid)); + deliveryIndicator.setContentDescription( + context.getString(R.string.a11y_delivery_status_invalid)); clearAnimation(); } diff --git a/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java b/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java index a9b86c1a0..060e606d7 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java @@ -1,15 +1,13 @@ package org.thoughtcrime.securesms.components; import android.content.Context; - -import androidx.annotation.AttrRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; - +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.DocumentSlide; import org.thoughtcrime.securesms.mms.SlideClickListener; @@ -18,8 +16,8 @@ public class DocumentView extends FrameLayout { - private final @NonNull TextView fileName; - private final @NonNull TextView fileSize; + private final @NonNull TextView fileName; + private final @NonNull TextView fileSize; private @Nullable SlideClickListener viewListener; @@ -31,24 +29,26 @@ public DocumentView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public DocumentView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + public DocumentView( + @NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); inflate(context, R.layout.document_view, this); - this.fileName = findViewById(R.id.file_name); - this.fileSize = findViewById(R.id.file_size); + this.fileName = findViewById(R.id.file_name); + this.fileSize = findViewById(R.id.file_size); } public void setDocumentClickListener(@Nullable SlideClickListener listener) { this.viewListener = listener; } - public void setDocument(final @NonNull DocumentSlide documentSlide) - { + public void setDocument(final @NonNull DocumentSlide documentSlide) { this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.unknown))); - String fileSize = Util.getPrettyFileSize(documentSlide.getFileSize()) - + " " + getFileType(documentSlide.getFileName()).toUpperCase(); + String fileSize = + Util.getPrettyFileSize(documentSlide.getFileSize()) + + " " + + getFileType(documentSlide.getFileName()).toUpperCase(); this.fileSize.setText(fileSize); this.setOnClickListener(new OpenClickedListener(documentSlide)); @@ -108,5 +108,4 @@ public void onClick(View v) { } } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java b/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java index bdfa2782e..6a733628d 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java @@ -2,9 +2,6 @@ import android.content.Context; import android.graphics.Typeface; - -import androidx.appcompat.widget.AppCompatTextView; - import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -15,7 +12,7 @@ import android.text.style.TypefaceSpan; import android.util.AttributeSet; import android.view.View; - +import androidx.appcompat.widget.AppCompatTextView; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.ResUtil; @@ -49,14 +46,27 @@ public void setText(Recipient recipient, boolean read) { SpannableStringBuilder builder = new SpannableStringBuilder(); SpannableString fromSpan = new SpannableString(fromString); - fromSpan.setSpan(new StyleSpan(typeface), 0, builder.length(), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + fromSpan.setSpan( + new StyleSpan(typeface), 0, builder.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) { SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") "); - profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - profileName.setSpan(new ForegroundColorSpan(ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + profileName.setSpan( + new CenterAlignedRelativeSizeSpan(0.75f), + 0, + profileName.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + profileName.setSpan( + new TypefaceSpan("sans-serif-light"), + 0, + profileName.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + profileName.setSpan( + new ForegroundColorSpan( + ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), + 0, + profileName.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); if (this.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { builder.append(profileName); @@ -71,6 +81,4 @@ public void setText(Recipient recipient, boolean read) { setText(builder); } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java b/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java index 9d47e18fe..d22a39724 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java +++ b/src/main/java/org/thoughtcrime/securesms/components/GlideDrawableListeningTarget.java @@ -1,19 +1,18 @@ package org.thoughtcrime.securesms.components; import android.graphics.drawable.Drawable; +import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.widget.ImageView; - -import com.bumptech.glide.request.target.DrawableImageViewTarget; - import chat.delta.util.SettableFuture; +import com.bumptech.glide.request.target.DrawableImageViewTarget; public class GlideDrawableListeningTarget extends DrawableImageViewTarget { private final SettableFuture loaded; - public GlideDrawableListeningTarget(@NonNull ImageView view, @NonNull SettableFuture loaded) { + public GlideDrawableListeningTarget( + @NonNull ImageView view, @NonNull SettableFuture loaded) { super(view); this.loaded = loaded; } diff --git a/src/main/java/org/thoughtcrime/securesms/components/HidingLinearLayout.java b/src/main/java/org/thoughtcrime/securesms/components/HidingLinearLayout.java index 01c78690f..2632f624d 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/HidingLinearLayout.java +++ b/src/main/java/org/thoughtcrime/securesms/components/HidingLinearLayout.java @@ -1,14 +1,13 @@ package org.thoughtcrime.securesms.components; import android.content.Context; - -import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import android.util.AttributeSet; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationSet; import android.view.animation.ScaleAnimation; import android.widget.LinearLayout; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; public class HidingLinearLayout extends LinearLayout { @@ -28,24 +27,25 @@ public void hide() { if (!isEnabled() || getVisibility() == GONE) return; AnimationSet animation = new AnimationSet(true); - animation.addAnimation(new ScaleAnimation(1, 0, 1, 1, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0.5f)); + animation.addAnimation( + new ScaleAnimation( + 1, 0, 1, 1, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0.5f)); animation.addAnimation(new AlphaAnimation(1, 0)); animation.setDuration(100); - animation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - @Override - public void onAnimationRepeat(Animation animation) { - } + @Override + public void onAnimationRepeat(Animation animation) {} - @Override - public void onAnimationEnd(Animation animation) { - setVisibility(GONE); - } - }); + @Override + public void onAnimationEnd(Animation animation) { + setVisibility(GONE); + } + }); animateWith(animation); } @@ -56,7 +56,9 @@ public void show() { setVisibility(VISIBLE); AnimationSet animation = new AnimationSet(true); - animation.addAnimation(new ScaleAnimation(0, 1, 1, 1, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0.5f)); + animation.addAnimation( + new ScaleAnimation( + 0, 1, 1, 1, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0.5f)); animation.addAnimation(new AlphaAnimation(0, 1)); animation.setDuration(100); diff --git a/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java b/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java index 9ccd07933..a8d99e9d7 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java +++ b/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java @@ -1,11 +1,10 @@ package org.thoughtcrime.securesms.components; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.widget.EditText; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -25,17 +24,20 @@ public InputAwareLayout(Context context, AttributeSet attrs, int defStyle) { addOnKeyboardShownListener(this); } - @Override public void onKeyboardShown() { + @Override + public void onKeyboardShown() { hideAttachedInput(true); } public void show(@NonNull final EditText imeTarget, @NonNull final InputView input) { if (isKeyboardOpen()) { - hideSoftkey(imeTarget, () -> { - hideAttachedInput(true); - input.show(getKeyboardHeight(), true); - current = input; - }); + hideSoftkey( + imeTarget, + () -> { + hideAttachedInput(true); + input.show(getKeyboardHeight(), true); + current = input; + }); } else { if (current != null) current.hide(true); input.show(getKeyboardHeight(), current != null); @@ -49,7 +51,7 @@ public InputView getCurrentInput() { public void hideCurrentInput(EditText imeTarget) { if (isKeyboardOpen()) hideSoftkey(imeTarget, null); - else hideAttachedInput(false); + else hideAttachedInput(false); } public void hideAttachedInput(boolean instant) { @@ -63,23 +65,25 @@ public boolean isInputOpen() { public void showSoftkey(final EditText inputTarget) { postOnKeyboardOpen(() -> hideAttachedInput(true)); - inputTarget.post(() -> { - inputTarget.requestFocus(); - ServiceUtil.getInputMethodManager(inputTarget.getContext()).showSoftInput(inputTarget, 0); - }); + inputTarget.post( + () -> { + inputTarget.requestFocus(); + ServiceUtil.getInputMethodManager(inputTarget.getContext()).showSoftInput(inputTarget, 0); + }); } private void hideSoftkey(final EditText inputTarget, @Nullable Runnable runAfterClose) { if (runAfterClose != null) postOnKeyboardClose(runAfterClose); ServiceUtil.getInputMethodManager(inputTarget.getContext()) - .hideSoftInputFromWindow(inputTarget.getWindowToken(), 0); + .hideSoftInputFromWindow(inputTarget.getWindowToken(), 0); } public interface InputView { void show(int height, boolean immediate); + void hide(boolean immediate); + boolean isShowing(); } } - diff --git a/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 4c0895b1d..a77c781d7 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java +++ b/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java @@ -17,13 +17,14 @@ import android.view.animation.TranslateAnimation; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; - +import chat.delta.util.ListenableFuture; +import chat.delta.util.SettableFuture; import com.b44t.messenger.DcMsg; - +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; import org.thoughtcrime.securesms.components.emoji.EmojiToggle; @@ -37,35 +38,28 @@ import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.guava.Optional; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -import chat.delta.util.ListenableFuture; -import chat.delta.util.SettableFuture; - public class InputPanel extends ConstraintLayout - implements MicrophoneRecorderView.Listener, - KeyboardAwareLinearLayout.OnKeyboardShownListener, - MediaKeyboard.MediaKeyboardListener -{ + implements MicrophoneRecorderView.Listener, + KeyboardAwareLinearLayout.OnKeyboardShownListener, + MediaKeyboard.MediaKeyboardListener { private static final String TAG = InputPanel.class.getSimpleName(); private static final int FADE_TIME = 150; - private QuoteView quoteView; - private EmojiToggle emojiToggle; - private ComposeText composeText; + private QuoteView quoteView; + private EmojiToggle emojiToggle; + private ComposeText composeText; private android.widget.EditText subjectText; - private View quickCameraToggle; - private View quickAudioToggle; - private View buttonToggle; - private View recordingContainer; - private View recordLockCancel; + private View quickCameraToggle; + private View quickAudioToggle; + private View buttonToggle; + private View recordingContainer; + private View recordLockCancel; private MicrophoneRecorderView microphoneRecorderView; - private SlideToCancel slideToCancel; - private RecordTime recordTime; + private SlideToCancel slideToCancel; + private RecordTime recordTime; private ValueAnimator quoteAnimator; private @Nullable Listener listener; @@ -87,19 +81,19 @@ public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) { public void onFinishInflate() { super.onFinishInflate(); - View quoteDismiss = findViewById(R.id.quote_dismiss); - - this.quoteView = findViewById(R.id.quote_view); - this.emojiToggle = findViewById(R.id.emoji_toggle); - this.composeText = findViewById(R.id.embedded_text_editor); - this.subjectText = findViewById(R.id.subject_text); - this.quickCameraToggle = findViewById(R.id.quick_camera_toggle); - this.quickAudioToggle = findViewById(R.id.quick_audio_toggle); - this.buttonToggle = findViewById(R.id.button_toggle); - this.recordingContainer = findViewById(R.id.recording_container); - this.recordLockCancel = findViewById(R.id.record_cancel); - this.recordTime = new RecordTime(findViewById(R.id.record_time)); - this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel)); + View quoteDismiss = findViewById(R.id.quote_dismiss); + + this.quoteView = findViewById(R.id.quote_view); + this.emojiToggle = findViewById(R.id.emoji_toggle); + this.composeText = findViewById(R.id.embedded_text_editor); + this.subjectText = findViewById(R.id.subject_text); + this.quickCameraToggle = findViewById(R.id.quick_camera_toggle); + this.quickAudioToggle = findViewById(R.id.quick_audio_toggle); + this.buttonToggle = findViewById(R.id.button_toggle); + this.recordingContainer = findViewById(R.id.recording_container); + this.recordLockCancel = findViewById(R.id.record_cancel); + this.recordTime = new RecordTime(findViewById(R.id.record_time)); + this.slideToCancel = new SlideToCancel(findViewById(R.id.slide_to_cancel)); this.microphoneRecorderView = findViewById(R.id.recorder_view); this.microphoneRecorderView.setListener(this); @@ -118,18 +112,18 @@ public void setMediaListener(@NonNull MediaListener listener) { composeText.setMediaListener(listener); } - public void setQuote(@NonNull GlideRequests glideRequests, - @NonNull DcMsg msg, - long id, - @NonNull Recipient author, - @NonNull CharSequence body, - @NonNull SlideDeck attachments, - @NonNull boolean isEdit) - { - this.quoteView.setQuote(glideRequests, msg, author, body, attachments, false, isEdit); + public void setQuote( + @NonNull GlideRequests glideRequests, + @NonNull DcMsg msg, + long id, + @NonNull Recipient author, + @NonNull CharSequence body, + @NonNull SlideDeck attachments, + @NonNull boolean isEdit) { + this.quoteView.setQuote(glideRequests, msg, author, body, attachments, false, isEdit); - int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight() - : 0; + int originalHeight = + this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight() : 0; this.quoteView.setVisibility(VISIBLE); this.quoteView.measure(0, 0); @@ -138,7 +132,8 @@ public void setQuote(@NonNull GlideRequests glideRequests, quoteAnimator.cancel(); } - quoteAnimator = createHeightAnimator(quoteView, originalHeight, this.quoteView.getMeasuredHeight(), null); + quoteAnimator = + createHeightAnimator(quoteView, originalHeight, this.quoteView.getMeasuredHeight(), null); quoteAnimator.start(); } @@ -153,30 +148,35 @@ public void clearQuote() { quoteAnimator.cancel(); } - quoteAnimator = createHeightAnimator(quoteView, quoteView.getMeasuredHeight(), 0, new AnimationCompleteListener() { - @Override - public void onAnimationEnd(@NonNull Animator animation) { - quoteView.dismiss(); - if (listener != null) listener.onQuoteDismissed(); - } - }); + quoteAnimator = + createHeightAnimator( + quoteView, + quoteView.getMeasuredHeight(), + 0, + new AnimationCompleteListener() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + quoteView.dismiss(); + if (listener != null) listener.onQuoteDismissed(); + } + }); quoteAnimator.start(); } - private static ValueAnimator createHeightAnimator(@NonNull View view, - int originalHeight, - int finalHeight, - @Nullable AnimationCompleteListener onAnimationComplete) - { - ValueAnimator animator = ValueAnimator.ofInt(originalHeight, finalHeight) - .setDuration(200); + private static ValueAnimator createHeightAnimator( + @NonNull View view, + int originalHeight, + int finalHeight, + @Nullable AnimationCompleteListener onAnimationComplete) { + ValueAnimator animator = ValueAnimator.ofInt(originalHeight, finalHeight).setDuration(200); - animator.addUpdateListener(animation -> { - ViewGroup.LayoutParams params = view.getLayoutParams(); - params.height = Math.max(1, (int) animation.getAnimatedValue()); - view.setLayoutParams(params); - }); + animator.addUpdateListener( + animation -> { + ViewGroup.LayoutParams params = view.getLayoutParams(); + params.height = Math.max(1, (int) animation.getAnimatedValue()); + view.setLayoutParams(params); + }); if (onAnimationComplete != null) { animator.addListener(onAnimationComplete); @@ -187,10 +187,10 @@ private static ValueAnimator createHeightAnimator(@NonNull View view, public Optional getQuote() { if (quoteView.getVisibility() == View.VISIBLE && quoteView.getBody() != null) { - return Optional.of(new QuoteModel( + return Optional.of( + new QuoteModel( quoteView.getDcContact(), quoteView.getBody().toString(), - quoteView.getAttachments(), quoteView.getOriginalMsg() - )); + quoteView.getAttachments(), quoteView.getOriginalMsg())); } else { return Optional.absent(); } @@ -202,7 +202,8 @@ public void clickOnComposeInput() { public void setSubjectVisible(boolean visible) { subjectText.setVisibility(visible ? View.VISIBLE : View.GONE); - // don't make it visible if visible is false to avoid showing it while recording audio and an event triggers setSubjectVisible(false) + // don't make it visible if visible is false to avoid showing it while recording audio and an + // event triggers setSubjectVisible(false) if (visible) emojiToggle.setVisibility(View.GONE); } @@ -266,11 +267,9 @@ public void onRecordReleased() { public void onRecordMoved(float offsetX, float absoluteX) { slideToCancel.moveTo(offsetX); - float position = absoluteX / recordingContainer.getWidth(); + float position = absoluteX / recordingContainer.getWidth(); - if (ViewUtil.isLtr(this) && position <= 0.5 || - ViewUtil.isRtl(this) && position >= 0.6) - { + if (ViewUtil.isLtr(this) && position <= 0.5 || ViewUtil.isRtl(this) && position >= 0.6) { this.microphoneRecorderView.cancelAction(); } } @@ -303,20 +302,21 @@ public void setEnabled(boolean enabled) { private long onRecordHideEvent() { recordLockCancel.setVisibility(View.GONE); - ListenableFuture future = slideToCancel.hide(); - long elapsedTime = recordTime.hide(); - - future.addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Void result) { - ViewUtil.fadeIn(emojiToggle, FADE_TIME); - ViewUtil.fadeIn(composeText, FADE_TIME); - ViewUtil.fadeIn(quickCameraToggle, FADE_TIME); - ViewUtil.fadeIn(quickAudioToggle, FADE_TIME); - buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start(); - composeText.requestFocus(); - } - }); + ListenableFuture future = slideToCancel.hide(); + long elapsedTime = recordTime.hide(); + + future.addListener( + new AssertedSuccessListener() { + @Override + public void onSuccess(Void result) { + ViewUtil.fadeIn(emojiToggle, FADE_TIME); + ViewUtil.fadeIn(composeText, FADE_TIME); + ViewUtil.fadeIn(quickCameraToggle, FADE_TIME); + ViewUtil.fadeIn(quickAudioToggle, FADE_TIME); + buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start(); + composeText.requestFocus(); + } + }); return elapsedTime; } @@ -347,7 +347,7 @@ public void onHidden() { @Override public void onEmojiPicked(String emoji) { final int start = composeText.getSelectionStart(); - final int end = composeText.getSelectionEnd(); + final int end = composeText.getSelectionEnd(); composeText.getText().replace(Math.min(start, end), Math.max(start, end), emoji); composeText.setSelection(start + emoji.length()); @@ -362,12 +362,19 @@ public void onStickerPicked(Uri stickerUri) { public interface Listener { void onRecorderStarted(); + void onRecorderLocked(); + void onRecorderFinished(); + void onRecorderCanceled(); + void onRecorderPermissionRequired(); + void onEmojiToggle(); + void onQuoteDismissed(); + void onStickerPicked(Uri stickerUri); } @@ -387,17 +394,20 @@ public ListenableFuture hide() { final SettableFuture future = new SettableFuture<>(); AnimationSet animation = new AnimationSet(true); - animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, slideToCancelView.getTranslationX(), - Animation.ABSOLUTE, 0, - Animation.RELATIVE_TO_SELF, 0, - Animation.RELATIVE_TO_SELF, 0)); + animation.addAnimation( + new TranslateAnimation( + Animation.ABSOLUTE, slideToCancelView.getTranslationX(), + Animation.ABSOLUTE, 0, + Animation.RELATIVE_TO_SELF, 0, + Animation.RELATIVE_TO_SELF, 0)); animation.addAnimation(new AlphaAnimation(1, 0)); animation.setDuration(MicrophoneRecorderView.ANIMATION_DURATION); animation.setFillBefore(true); animation.setFillAfter(false); - slideToCancelView.postDelayed(() -> future.set(null), MicrophoneRecorderView.ANIMATION_DURATION); + slideToCancelView.postDelayed( + () -> future.set(null), MicrophoneRecorderView.ANIMATION_DURATION); slideToCancelView.setVisibility(View.GONE); slideToCancelView.startAnimation(animation); @@ -405,10 +415,16 @@ public ListenableFuture hide() { } void moveTo(float offset) { - Animation animation = new TranslateAnimation(Animation.ABSOLUTE, offset, - Animation.ABSOLUTE, offset, - Animation.RELATIVE_TO_SELF, 0, - Animation.RELATIVE_TO_SELF, 0); + Animation animation = + new TranslateAnimation( + Animation.ABSOLUTE, + offset, + Animation.ABSOLUTE, + offset, + Animation.RELATIVE_TO_SELF, + 0, + Animation.RELATIVE_TO_SELF, + 0); animation.setDuration(0); animation.setFillAfter(true); @@ -452,11 +468,9 @@ public void run() { } } - private String formatElapsedTime(long ms) - { + private String formatElapsedTime(long ms) { return DateUtils.formatElapsedTime(TimeUnit.MILLISECONDS.toSeconds(ms)) - + String.format(".%01d", ((ms/100)%10)); - + + String.format(".%01d", ((ms / 100) % 10)); } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java b/src/main/java/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java index 7c3d499cc..e44b3cd43 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java +++ b/src/main/java/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java @@ -1,18 +1,16 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.components; @@ -20,37 +18,34 @@ import android.graphics.Rect; import android.os.Build; import android.util.AttributeSet; -import android.util.Log; import android.util.DisplayMetrics; +import android.util.Log; import android.view.Surface; import android.view.View; import android.view.WindowInsets; - import androidx.appcompat.widget.LinearLayoutCompat; import androidx.preference.PreferenceManager; - +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.lang.reflect.Field; -import java.util.HashSet; -import java.util.Set; - /** - * LinearLayout that, when a view container, will report back when it thinks a soft keyboard - * has been opened and what its height would be. + * LinearLayout that, when a view container, will report back when it thinks a soft keyboard has + * been opened and what its height would be. */ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { private static final String TAG = KeyboardAwareLinearLayout.class.getSimpleName(); private static final long KEYBOARD_DEBOUNCE = 150; - private final Rect rect = new Rect(); + private final Rect rect = new Rect(); private final Set hiddenListeners = new HashSet<>(); - private final Set shownListeners = new HashSet<>(); - private final DisplayMetrics displayMetrics = new DisplayMetrics(); + private final Set shownListeners = new HashSet<>(); + private final DisplayMetrics displayMetrics = new DisplayMetrics(); private final int minKeyboardSize; private final int minCustomKeyboardSize; @@ -62,8 +57,8 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat { private int viewInset; private boolean keyboardOpen = false; - private int rotation = 0; - private long openedAt = 0; + private int rotation = 0; + private long openedAt = 0; public KeyboardAwareLinearLayout(Context context) { this(context, null); @@ -75,13 +70,16 @@ public KeyboardAwareLinearLayout(Context context, AttributeSet attrs) { public KeyboardAwareLinearLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size); - minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size); - defaultCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size); - minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait); - minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait); - statusBarHeight = ViewUtil.getStatusBarHeight(this); - viewInset = getViewInset(); + minKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_keyboard_size); + minCustomKeyboardSize = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_size); + defaultCustomKeyboardSize = + getResources().getDimensionPixelSize(R.dimen.default_custom_keyboard_size); + minCustomKeyboardTopMarginPortrait = + getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait); + minCustomKeyboardTopMarginLandscape = + getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait); + statusBarHeight = ViewUtil.getStatusBarHeight(this); + viewInset = getViewInset(); } @Override @@ -106,7 +104,7 @@ private void updateKeyboardState() { getWindowVisibleDisplayFrame(rect); final int availableHeight = getAvailableHeight(); - final int keyboardHeight = availableHeight - rect.bottom; + final int keyboardHeight = availableHeight - rect.bottom; if (keyboardHeight > minKeyboardSize) { if (getKeyboardHeight() != keyboardHeight) { @@ -129,7 +127,7 @@ protected void onAttachedToWindow() { super.onAttachedToWindow(); rotation = getDeviceRotation(); if (Build.VERSION.SDK_INT >= 23 && getRootWindowInsets() != null) { - int bottomInset; + int bottomInset; WindowInsets windowInsets = getRootWindowInsets(); if (Build.VERSION.SDK_INT >= 30) { @@ -139,7 +137,12 @@ protected void onAttachedToWindow() { } if (bottomInset != 0 && (viewInset == 0 || viewInset == statusBarHeight)) { - Log.i(TAG, "Updating view inset based on WindowInsets. viewInset: " + viewInset + " windowInset: " + bottomInset); + Log.i( + TAG, + "Updating view inset based on WindowInsets. viewInset: " + + viewInset + + " windowInset: " + + bottomInset); viewInset = bottomInset; } } @@ -166,7 +169,7 @@ private int getViewInset() { private int getAvailableHeight() { final int availableHeight = this.getRootView().getHeight() - viewInset; - final int availableWidth = this.getRootView().getWidth(); + final int availableWidth = this.getRootView().getWidth(); if (isLandscape() && availableHeight > availableWidth) { //noinspection SuspiciousNameCombination @@ -218,41 +221,57 @@ private int getDeviceRotation() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { getContext().getDisplay().getRealMetrics(displayMetrics); } else { - ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRealMetrics(displayMetrics); + ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRealMetrics(displayMetrics); } - return displayMetrics.widthPixels > displayMetrics.heightPixels ? Surface.ROTATION_90 : Surface.ROTATION_0; + return displayMetrics.widthPixels > displayMetrics.heightPixels + ? Surface.ROTATION_90 + : Surface.ROTATION_0; } private int getKeyboardLandscapeHeight() { - int keyboardHeight = PreferenceManager.getDefaultSharedPreferences(getContext()) - .getInt("keyboard_height_landscape", defaultCustomKeyboardSize); - return Util.clamp(keyboardHeight, minCustomKeyboardSize, getRootView().getHeight() - minCustomKeyboardTopMarginLandscape); + int keyboardHeight = + PreferenceManager.getDefaultSharedPreferences(getContext()) + .getInt("keyboard_height_landscape", defaultCustomKeyboardSize); + return Util.clamp( + keyboardHeight, + minCustomKeyboardSize, + getRootView().getHeight() - minCustomKeyboardTopMarginLandscape); } private int getKeyboardPortraitHeight() { - int keyboardHeight = PreferenceManager.getDefaultSharedPreferences(getContext()) - .getInt("keyboard_height_portrait", defaultCustomKeyboardSize); - return Util.clamp(keyboardHeight, minCustomKeyboardSize, getRootView().getHeight() - minCustomKeyboardTopMarginPortrait); + int keyboardHeight = + PreferenceManager.getDefaultSharedPreferences(getContext()) + .getInt("keyboard_height_portrait", defaultCustomKeyboardSize); + return Util.clamp( + keyboardHeight, + minCustomKeyboardSize, + getRootView().getHeight() - minCustomKeyboardTopMarginPortrait); } private void setKeyboardPortraitHeight(int height) { PreferenceManager.getDefaultSharedPreferences(getContext()) - .edit().putInt("keyboard_height_portrait", height).apply(); + .edit() + .putInt("keyboard_height_portrait", height) + .apply(); } private void setKeyboardLandscapeHeight(int height) { PreferenceManager.getDefaultSharedPreferences(getContext()) - .edit().putInt("keyboard_height_landscape", height).apply(); + .edit() + .putInt("keyboard_height_landscape", height) + .apply(); } public void postOnKeyboardClose(final Runnable runnable) { if (keyboardOpen) { - addOnKeyboardHiddenListener(new OnKeyboardHiddenListener() { - @Override public void onKeyboardHidden() { - removeOnKeyboardHiddenListener(this); - runnable.run(); - } - }); + addOnKeyboardHiddenListener( + new OnKeyboardHiddenListener() { + @Override + public void onKeyboardHidden() { + removeOnKeyboardHiddenListener(this); + runnable.run(); + } + }); } else { runnable.run(); } @@ -260,12 +279,14 @@ public void postOnKeyboardClose(final Runnable runnable) { public void postOnKeyboardOpen(final Runnable runnable) { if (!keyboardOpen) { - addOnKeyboardShownListener(new OnKeyboardShownListener() { - @Override public void onKeyboardShown() { - removeOnKeyboardShownListener(this); - runnable.run(); - } - }); + addOnKeyboardShownListener( + new OnKeyboardShownListener() { + @Override + public void onKeyboardShown() { + removeOnKeyboardShownListener(this); + runnable.run(); + } + }); } else { runnable.run(); } diff --git a/src/main/java/org/thoughtcrime/securesms/components/MediaView.java b/src/main/java/org/thoughtcrime/securesms/components/MediaView.java index bd335218f..8aed3cc18 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/MediaView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/MediaView.java @@ -1,26 +1,23 @@ package org.thoughtcrime.securesms.components; - import android.content.Context; import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.view.Window; import android.widget.FrameLayout; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.IOException; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.video.VideoPlayer; -import java.io.IOException; - public class MediaView extends FrameLayout { - private ZoomingImageView imageView; + private ZoomingImageView imageView; private Stub videoView; public MediaView(@NonNull Context context) { @@ -38,7 +35,8 @@ public MediaView(@NonNull Context context, @Nullable AttributeSet attrs, int def initialize(); } - public MediaView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public MediaView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initialize(); } @@ -50,15 +48,15 @@ private void initialize() { this.videoView = new Stub<>(findViewById(R.id.video_player_stub)); } - public void set(@NonNull GlideRequests glideRequests, - @NonNull Window window, - @NonNull Uri source, - @Nullable String fileName, - @NonNull String mediaType, - long size, - boolean autoplay) - throws IOException - { + public void set( + @NonNull GlideRequests glideRequests, + @NonNull Window window, + @NonNull Uri source, + @Nullable String fileName, + @NonNull String mediaType, + long size, + boolean autoplay) + throws IOException { if (mediaType.startsWith("image/")) { imageView.setVisibility(View.VISIBLE); if (videoView.resolved()) videoView.get().setVisibility(View.GONE); @@ -67,14 +65,16 @@ public void set(@NonNull GlideRequests glideRequests, imageView.setVisibility(View.GONE); videoView.get().setVisibility(View.VISIBLE); videoView.get().setWindow(window); - videoView.get().setVideoSource(new VideoSlide(getContext(), source, fileName, size), autoplay); + videoView + .get() + .setVideoSource(new VideoSlide(getContext(), source, fileName, size), autoplay); } else { throw new IOException("Unsupported media type: " + mediaType); } } public void pause() { - if (this.videoView.resolved()){ + if (this.videoView.resolved()) { this.videoView.get().pause(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java b/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java index 4c9dfe638..724cb82db 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/MicrophoneRecorderView.java @@ -16,10 +16,8 @@ import android.view.animation.TranslateAnimation; import android.widget.FrameLayout; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.ViewUtil; @@ -34,10 +32,10 @@ enum State { public static final int ANIMATION_DURATION = 200; - private FloatingRecordButton floatingRecordButton; - private LockDropTarget lockDropTarget; - private @Nullable Listener listener; - private @NonNull State state = State.NOT_RUNNING; + private FloatingRecordButton floatingRecordButton; + private LockDropTarget lockDropTarget; + private @Nullable Listener listener; + private @NonNull State state = State.NOT_RUNNING; public MicrophoneRecorderView(Context context) { super(context); @@ -51,8 +49,9 @@ public MicrophoneRecorderView(Context context, AttributeSet attrs) { public void onFinishInflate() { super.onFinishInflate(); - floatingRecordButton = new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab)); - lockDropTarget = new LockDropTarget (getContext(), findViewById(R.id.lock_drop_target)); + floatingRecordButton = + new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab)); + lockDropTarget = new LockDropTarget(getContext(), findViewById(R.id.lock_drop_target)); View recordButton = findViewById(R.id.quick_audio_toggle); recordButton.setOnTouchListener(this); @@ -118,9 +117,11 @@ public boolean onTouch(View v, final MotionEvent event) { case MotionEvent.ACTION_MOVE: if (this.state == State.RUNNING_HELD) { this.floatingRecordButton.moveTo(event.getX(), event.getY()); - if (listener != null) listener.onRecordMoved(floatingRecordButton.lastOffsetX, event.getRawX()); + if (listener != null) + listener.onRecordMoved(floatingRecordButton.lastOffsetX, event.getRawX()); - int dimensionPixelSize = getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target); + int dimensionPixelSize = + getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target); if (floatingRecordButton.lastOffsetY <= dimensionPixelSize) { lockAction(); } @@ -137,10 +138,15 @@ public void setListener(@Nullable Listener listener) { public interface Listener { void onRecordPressed(); + void onRecordReleased(); + void onRecordCanceled(); + void onRecordLocked(); + void onRecordMoved(float offsetX, float absoluteX); + void onRecordPermissionRequired(); } @@ -155,9 +161,10 @@ private static class FloatingRecordButton { FloatingRecordButton(Context context, ImageView recordButtonFab) { this.recordButtonFab = recordButtonFab; - this.recordButtonFab.getBackground().setColorFilter(context.getResources() - .getColor(R.color.audio_icon), - PorterDuff.Mode.SRC_IN); + this.recordButtonFab + .getBackground() + .setColorFilter( + context.getResources().getColor(R.color.audio_icon), PorterDuff.Mode.SRC_IN); } void display(float x, float y) { @@ -167,14 +174,16 @@ void display(float x, float y) { recordButtonFab.setVisibility(View.VISIBLE); AnimationSet animation = new AnimationSet(true); - animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, 0)); + animation.addAnimation( + new TranslateAnimation( + Animation.ABSOLUTE, 0, + Animation.ABSOLUTE, 0, + Animation.ABSOLUTE, 0, + Animation.ABSOLUTE, 0)); - animation.addAnimation(new ScaleAnimation(.5f, 1f, .5f, 1f, - Animation.RELATIVE_TO_SELF, .5f, - Animation.RELATIVE_TO_SELF, .5f)); + animation.addAnimation( + new ScaleAnimation( + .5f, 1f, .5f, 1f, Animation.RELATIVE_TO_SELF, .5f, Animation.RELATIVE_TO_SELF, .5f)); animation.setDuration(ANIMATION_DURATION); animation.setInterpolator(new OvershootInterpolator()); @@ -183,8 +192,8 @@ void display(float x, float y) { } void moveTo(float x, float y) { - lastOffsetX = getXOffset(x); - lastOffsetY = getYOffset(y); + lastOffsetX = getXOffset(x); + lastOffsetY = getYOffset(y); if (Math.abs(lastOffsetX) > Math.abs(lastOffsetY)) { lastOffsetY = 0; @@ -202,14 +211,20 @@ void hide() { if (recordButtonFab.getVisibility() != VISIBLE) return; AnimationSet animation = new AnimationSet(false); - Animation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f, - Animation.RELATIVE_TO_SELF, 0.5f); - - Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, lastOffsetX, - Animation.ABSOLUTE, 0, - Animation.ABSOLUTE, lastOffsetY, - Animation.ABSOLUTE, 0); + Animation scaleAnimation = + new ScaleAnimation( + 1, 0.5f, 1, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + + Animation translateAnimation = + new TranslateAnimation( + Animation.ABSOLUTE, + lastOffsetX, + Animation.ABSOLUTE, + 0, + Animation.ABSOLUTE, + lastOffsetY, + Animation.ABSOLUTE, + 0); scaleAnimation.setInterpolator(new AnticipateOvershootInterpolator(1.5f)); translateAnimation.setInterpolator(new DecelerateInterpolator()); @@ -224,8 +239,9 @@ void hide() { } private float getXOffset(float x) { - return ViewUtil.isLtr(recordButtonFab) ? -Math.max(0, this.startPositionX - x) - : Math.max(0, x - this.startPositionX); + return ViewUtil.isLtr(recordButtonFab) + ? -Math.max(0, this.startPositionX - x) + : Math.max(0, x - this.startPositionX); } private float getYOffset(float y) { @@ -236,11 +252,12 @@ private float getYOffset(float y) { private static class LockDropTarget { private final View lockDropTarget; - private final int dropTargetPosition; + private final int dropTargetPosition; LockDropTarget(Context context, View lockDropTarget) { - this.lockDropTarget = lockDropTarget; - this.dropTargetPosition = context.getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target); + this.lockDropTarget = lockDropTarget; + this.dropTargetPosition = + context.getResources().getDimensionPixelSize(R.dimen.recording_voice_lock_target); } void display() { @@ -249,22 +266,25 @@ void display() { lockDropTarget.setAlpha(0); lockDropTarget.setTranslationY(0); lockDropTarget.setVisibility(VISIBLE); - lockDropTarget.animate() - .setStartDelay(ANIMATION_DURATION * 2) - .setDuration(ANIMATION_DURATION) - .setInterpolator(new DecelerateInterpolator()) - .translationY(dropTargetPosition) - .alpha(1) - .start(); + lockDropTarget + .animate() + .setStartDelay(ANIMATION_DURATION * 2) + .setDuration(ANIMATION_DURATION) + .setInterpolator(new DecelerateInterpolator()) + .translationY(dropTargetPosition) + .alpha(1) + .start(); } void hide() { - lockDropTarget.animate() - .setStartDelay(0) - .setDuration(ANIMATION_DURATION) - .setInterpolator(new LinearInterpolator()) - .scaleX(0).scaleY(0) - .start(); + lockDropTarget + .animate() + .setStartDelay(0) + .setDuration(ANIMATION_DURATION) + .setInterpolator(new LinearInterpolator()) + .scaleX(0) + .scaleY(0) + .start(); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java b/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java index c40cd8a22..79022aaeb 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.components; - import android.content.Context; import android.content.res.TypedArray; import android.net.Uri; @@ -13,14 +12,14 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.VcardContact; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcMsg; import com.bumptech.glide.load.engine.DiskCacheStrategy; - +import java.util.List; import org.json.JSONObject; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; @@ -37,33 +36,28 @@ import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; -import java.util.List; - -import chat.delta.rpc.RpcException; -import chat.delta.rpc.types.VcardContact; - public class QuoteView extends FrameLayout implements RecipientForeverObserver { private static final String TAG = QuoteView.class.getSimpleName(); - private static final int MESSAGE_TYPE_PREVIEW = 0; + private static final int MESSAGE_TYPE_PREVIEW = 0; private ViewGroup mainView; - private TextView authorView; - private TextView bodyView; + private TextView authorView; + private TextView bodyView; private ImageView quoteBarView; private ImageView thumbnailView; - private View attachmentVideoOverlayView; + private View attachmentVideoOverlayView; private ViewGroup attachmentContainerView; private ImageView dismissView; private DcMsg quotedMsg; - private DcContact author; - private CharSequence body; - private SlideDeck attachments; - private int messageType; - private boolean hasSticker; - private boolean isEdit; + private DcContact author; + private CharSequence body; + private SlideDeck attachments; + private int messageType; + private boolean hasSticker; + private boolean isEdit; public QuoteView(Context context) { super(context); @@ -88,17 +82,18 @@ public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defS private void initialize(@Nullable AttributeSet attrs) { inflate(getContext(), R.layout.quote_view, this); - this.mainView = findViewById(R.id.quote_main); - this.authorView = findViewById(R.id.quote_author); - this.bodyView = findViewById(R.id.quote_text); - this.quoteBarView = findViewById(R.id.quote_bar); - this.thumbnailView = findViewById(R.id.quote_thumbnail); - this.attachmentVideoOverlayView = findViewById(R.id.quote_video_overlay); - this.attachmentContainerView = findViewById(R.id.quote_attachment_container); - this.dismissView = findViewById(R.id.quote_dismiss); + this.mainView = findViewById(R.id.quote_main); + this.authorView = findViewById(R.id.quote_author); + this.bodyView = findViewById(R.id.quote_text); + this.quoteBarView = findViewById(R.id.quote_bar); + this.thumbnailView = findViewById(R.id.quote_thumbnail); + this.attachmentVideoOverlayView = findViewById(R.id.quote_video_overlay); + this.attachmentContainerView = findViewById(R.id.quote_attachment_container); + this.dismissView = findViewById(R.id.quote_dismiss); if (attrs != null) { - TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0); + TypedArray typedArray = + getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0); messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0); typedArray.recycle(); @@ -113,19 +108,19 @@ private void initialize(@Nullable AttributeSet attrs) { dismissView.setOnClickListener(view -> setVisibility(GONE)); } - public void setQuote(GlideRequests glideRequests, - DcMsg msg, - @Nullable Recipient author, - @Nullable CharSequence body, - @NonNull SlideDeck attachments, - boolean hasSticker, - boolean isEdit) - { - quotedMsg = msg; - this.author = author != null ? author.getDcContact() : null; - this.body = body; + public void setQuote( + GlideRequests glideRequests, + DcMsg msg, + @Nullable Recipient author, + @Nullable CharSequence body, + @NonNull SlideDeck attachments, + boolean hasSticker, + boolean isEdit) { + quotedMsg = msg; + this.author = author != null ? author.getDcContact() : null; + this.body = body; this.attachments = attachments; - this.hasSticker = hasSticker; + this.hasSticker = hasSticker; this.isEdit = isEdit; if (hasSticker) { @@ -139,7 +134,7 @@ public void setQuote(GlideRequests glideRequests, public void dismiss() { this.author = null; - this.body = null; + this.body = null; setVisibility(GONE); } @@ -164,7 +159,8 @@ private void setQuoteAuthor(@Nullable Recipient author) { if (contact == null) { authorView.setText(getContext().getString(R.string.forwarded_message)); } else { - authorView.setText(getContext().getString(R.string.forwarded_by, quotedMsg.getSenderName(contact))); + authorView.setText( + getContext().getString(R.string.forwarded_by, quotedMsg.getSenderName(contact))); } authorView.setTextColor(getForwardedColor()); quoteBarView.setBackgroundColor(getForwardedColor()); @@ -190,15 +186,17 @@ private void setQuoteAuthor(@Nullable Recipient author) { private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) { if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) { bodyView.setVisibility(VISIBLE); - bodyView.setText((Spannable) MarkdownUtil.toMarkdown(getContext(), body == null ? "" : body.toString())); + bodyView.setText( + (Spannable) MarkdownUtil.toMarkdown(getContext(), body == null ? "" : body.toString())); } else { bodyView.setVisibility(GONE); } } - private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) { + private void setQuoteAttachment( + @NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) { List slides = slideDeck.getSlides(); - Slide slide = slides.isEmpty()? null : slides.get(0); + Slide slide = slides.isEmpty() ? null : slides.get(0); attachmentVideoOverlayView.setVisibility(GONE); @@ -211,25 +209,28 @@ private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull S try { JSONObject info = quotedMsg.getWebxdcInfo(); byte[] blob = quotedMsg.getWebxdcBlob(info.getString("icon")); - glideRequests.load(blob) - .centerCrop() - .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .into(thumbnailView); + glideRequests + .load(blob) + .centerCrop() + .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(thumbnailView); } catch (Exception e) { Log.e(TAG, "failed to get webxdc icon", e); thumbnailView.setVisibility(GONE); } } else if (slide.isVcard()) { try { - VcardContact vcardContact = DcHelper.getRpc(getContext()).parseVcard(quotedMsg.getFile()).get(0); + VcardContact vcardContact = + DcHelper.getRpc(getContext()).parseVcard(quotedMsg.getFile()).get(0); Recipient recipient = new Recipient(getContext(), vcardContact); - glideRequests.load(recipient.getContactPhoto(getContext())) - .error(recipient.getFallbackAvatarDrawable(getContext(), false)) - .centerCrop() - .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(thumbnailView); + glideRequests + .load(recipient.getContactPhoto(getContext())) + .error(recipient.getFallbackAvatarDrawable(getContext(), false)) + .centerCrop() + .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(thumbnailView); } catch (RpcException e) { Log.e(TAG, "failed to parse vCard", e); thumbnailView.setVisibility(GONE); @@ -238,18 +239,20 @@ private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull S Uri thumbnailUri = slide.getUri(); if (slide.hasVideo()) { attachmentVideoOverlayView.setVisibility(VISIBLE); - MediaUtil.createVideoThumbnailIfNeeded(getContext(), slide.getUri(), slide.getThumbnailUri(), null); + MediaUtil.createVideoThumbnailIfNeeded( + getContext(), slide.getUri(), slide.getThumbnailUri(), null); thumbnailUri = slide.getThumbnailUri(); } if (thumbnailUri != null) { - glideRequests.load(new DecryptableUri(thumbnailUri)) - .centerCrop() - .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .into(thumbnailView); + glideRequests + .load(new DecryptableUri(thumbnailUri)) + .centerCrop() + .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(thumbnailView); } } - } else if(slide != null && slide.hasAudio()) { + } else if (slide != null && slide.hasAudio()) { thumbnailView.setVisibility(GONE); attachmentContainerView.setVisibility(GONE); } else if (slide != null && slide.hasDocument()) { @@ -282,7 +285,7 @@ public DcMsg getOriginalMsg() { } private int getForwardedColor() { - return getResources().getColor(hasSticker? R.color.core_dark_05 : R.color.unknown_sender); + return getResources().getColor(hasSticker ? R.color.core_dark_05 : R.color.unknown_sender); } private int getEditColor() { diff --git a/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java b/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java index 3468d89c6..f4afc970f 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java +++ b/src/main/java/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.components; - import android.content.ContentUris; import android.content.Context; import android.database.Cursor; @@ -13,7 +12,6 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.loader.app.LoaderManager; @@ -21,21 +19,20 @@ import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.signature.MediaStoreSignature; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.util.ViewUtil; -public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks { +public class RecentPhotoViewRail extends FrameLayout + implements LoaderManager.LoaderCallbacks { - @NonNull private final RecyclerView recyclerView; - @Nullable private OnItemClickedListener listener; + @NonNull private final RecyclerView recyclerView; + @Nullable private OnItemClickedListener listener; public RecentPhotoViewRail(Context context) { this(context, null); @@ -51,7 +48,8 @@ public RecentPhotoViewRail(Context context, AttributeSet attrs, int defStyleAttr inflate(context, R.layout.recent_photo_view, this); this.recyclerView = ViewUtil.findById(this, R.id.photo_list); - this.recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)); + this.recyclerView.setLayoutManager( + new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)); this.recyclerView.setItemAnimator(new DefaultItemAnimator()); } @@ -59,7 +57,7 @@ public void setListener(@Nullable OnItemClickedListener listener) { this.listener = listener; if (this.recyclerView.getAdapter() != null) { - ((RecentPhotoAdapter)this.recyclerView.getAdapter()).setListener(listener); + ((RecentPhotoAdapter) this.recyclerView.getAdapter()).setListener(listener); } } @@ -71,29 +69,36 @@ public Loader onCreateLoader(int id, Bundle args) { @Override public void onLoadFinished(@NonNull Loader loader, Cursor data) { - this.recyclerView.setAdapter(new RecentPhotoAdapter(getContext(), data, RecentPhotosLoader.BASE_URL, listener)); + this.recyclerView.setAdapter( + new RecentPhotoAdapter(getContext(), data, RecentPhotosLoader.BASE_URL, listener)); } @Override public void onLoaderReset(@NonNull Loader loader) { - ((CursorRecyclerViewAdapter)this.recyclerView.getAdapter()).changeCursor(null); + ((CursorRecyclerViewAdapter) this.recyclerView.getAdapter()).changeCursor(null); } - private static class RecentPhotoAdapter extends CursorRecyclerViewAdapter { + private static class RecentPhotoAdapter + extends CursorRecyclerViewAdapter { - @NonNull private final Uri baseUri; + @NonNull private final Uri baseUri; @Nullable private OnItemClickedListener clickedListener; - private RecentPhotoAdapter(@NonNull Context context, @NonNull Cursor cursor, @NonNull Uri baseUri, @Nullable OnItemClickedListener listener) { + private RecentPhotoAdapter( + @NonNull Context context, + @NonNull Cursor cursor, + @NonNull Uri baseUri, + @Nullable OnItemClickedListener listener) { super(context, cursor); - this.baseUri = baseUri; + this.baseUri = baseUri; this.clickedListener = listener; } @Override public RecentPhotoViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { - View itemView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.recent_photo_view_item, parent, false); + View itemView = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.recent_photo_view_item, parent, false); return new RecentPhotoViewHolder(itemView); } @@ -102,26 +107,31 @@ public RecentPhotoViewHolder onCreateItemViewHolder(ViewGroup parent, int viewTy public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) { viewHolder.imageView.setImageDrawable(null); - long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)); - long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN)); - long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED)); - String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE)); - int orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION)); + long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)); + long dateTaken = + cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN)); + long dateModified = + cursor.getLong( + cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED)); + String mimeType = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE)); + int orientation = + cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION)); final Uri uri = ContentUris.withAppendedId(RecentPhotosLoader.BASE_URL, rowId); Key signature = new MediaStoreSignature(mimeType, dateModified, orientation); GlideApp.with(getContext().getApplicationContext()) - .load(uri) - .signature(signature) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(viewHolder.imageView); - - viewHolder.imageView.setOnClickListener(v -> { - if (clickedListener != null) clickedListener.onItemClicked(uri); - }); - + .load(uri) + .signature(signature) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(viewHolder.imageView); + + viewHolder.imageView.setOnClickListener( + v -> { + if (clickedListener != null) clickedListener.onItemClicked(uri); + }); } public void setListener(@Nullable OnItemClickedListener listener) { diff --git a/src/main/java/org/thoughtcrime/securesms/components/RemovableEditableMediaView.java b/src/main/java/org/thoughtcrime/securesms/components/RemovableEditableMediaView.java index 870ea4216..2bb0addbc 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/RemovableEditableMediaView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/RemovableEditableMediaView.java @@ -1,18 +1,16 @@ package org.thoughtcrime.securesms.components; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; - -import org.thoughtcrime.securesms.R; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import org.thoughtcrime.securesms.R; public class RemovableEditableMediaView extends FrameLayout { @@ -35,8 +33,12 @@ public RemovableEditableMediaView(Context context, AttributeSet attrs) { public RemovableEditableMediaView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - this.remove = (ImageView)LayoutInflater.from(context).inflate(R.layout.media_view_remove_button, this, false); - this.edit = (ImageView)LayoutInflater.from(context).inflate(R.layout.media_view_edit_button, this, false); + this.remove = + (ImageView) + LayoutInflater.from(context).inflate(R.layout.media_view_remove_button, this, false); + this.edit = + (ImageView) + LayoutInflater.from(context).inflate(R.layout.media_view_edit_button, this, false); this.removeSize = getResources().getDimensionPixelSize(R.dimen.media_bubble_remove_button_size); @@ -53,12 +55,13 @@ public void onFinishInflate() { public void display(@Nullable View view, boolean editable) { edit.setVisibility(editable ? View.VISIBLE : View.GONE); - + if (view == current) return; if (current != null) current.setVisibility(View.GONE); if (view != null) { - view.setPadding(view.getPaddingLeft(), removeSize / 2, removeSize / 2, view.getPaddingRight()); + view.setPadding( + view.getPaddingLeft(), removeSize / 2, removeSize / 2, view.getPaddingRight()); edit.setPadding(0, 0, removeSize / 2, 0); view.setVisibility(View.VISIBLE); @@ -87,11 +90,12 @@ public void removeRemoveClickListener(View.OnClickListener listener) { } private void updateRemoveClickListener() { - this.remove.setOnClickListener(v -> { - for (OnClickListener listener : removeClickListeners) { - listener.onClick(v); - } - }); + this.remove.setOnClickListener( + v -> { + for (OnClickListener listener : removeClickListeners) { + listener.onClick(v); + } + }); } public void setEditClickListener(View.OnClickListener listener) { diff --git a/src/main/java/org/thoughtcrime/securesms/components/ScaleStableImageView.java b/src/main/java/org/thoughtcrime/securesms/components/ScaleStableImageView.java index 1fc3f006e..8e5e36c62 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ScaleStableImageView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ScaleStableImageView.java @@ -1,183 +1,180 @@ package org.thoughtcrime.securesms.components; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; - +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; import com.bumptech.glide.load.engine.DiskCacheStrategy; - -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.util.Util; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; - -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageView; - -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.util.Util; /** - * An image view that maintains the size of the drawable provided. - * That means: when you set an image drawable it will be scaled to fit the screen once. - * If you rotate the screen it will be rescaled to fit. - * If you crop the screen (e.g. because the soft keyboard is displayed) the image is cropped instead. + * An image view that maintains the size of the drawable provided. That means: when you set an image + * drawable it will be scaled to fit the screen once. If you rotate the screen it will be rescaled + * to fit. If you crop the screen (e.g. because the soft keyboard is displayed) the image is cropped + * instead. * * @author Angelo Fuchs */ -public class ScaleStableImageView - extends AppCompatImageView - implements KeyboardAwareLinearLayout.OnKeyboardShownListener, KeyboardAwareLinearLayout.OnKeyboardHiddenListener { - - private static final String TAG = ScaleStableImageView.class.getSimpleName(); - - private Drawable defaultDrawable; - private Drawable currentDrawable; - private final Map storedSizes = new HashMap<>(); - - public ScaleStableImageView(Context context) { - this(context, null); - } - - public ScaleStableImageView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); +public class ScaleStableImageView extends AppCompatImageView + implements KeyboardAwareLinearLayout.OnKeyboardShownListener, + KeyboardAwareLinearLayout.OnKeyboardHiddenListener { + + private static final String TAG = ScaleStableImageView.class.getSimpleName(); + + private Drawable defaultDrawable; + private Drawable currentDrawable; + private final Map storedSizes = new HashMap<>(); + + public ScaleStableImageView(Context context) { + this(context, null); + } + + public ScaleStableImageView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ScaleStableImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public void setImageDrawable(@Nullable Drawable drawable) { + defaultDrawable = drawable; + storedSizes.clear(); + overrideDrawable(defaultDrawable); + } + + private void overrideDrawable(Drawable newDrawable) { + if (currentDrawable == newDrawable) return; + currentDrawable = newDrawable; + super.setImageDrawable(newDrawable); + } + + private int landscapeWidth = 0; + private int landscapeHeight = 0; + private int portraitWidth = 0; + private int portraitHeight = 0; + private boolean keyboardShown = false; + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + if (width == 0 || height == 0) return; + final String newKey = width + "x" + height; + int orientation = getResources().getConfiguration().orientation; + boolean portrait; + if (orientation == ORIENTATION_PORTRAIT) { + portrait = true; + } else if (orientation == ORIENTATION_LANDSCAPE) { + portrait = false; + } else { + Log.i(TAG, "orientation was: " + orientation); + return; // something fishy happened. } - public ScaleStableImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + if (!(defaultDrawable instanceof BitmapDrawable)) { + return; // need Bitmap for scaling and cropping. } - @Override - public void setImageDrawable(@Nullable Drawable drawable) { - defaultDrawable = drawable; - storedSizes.clear(); - overrideDrawable(defaultDrawable); + measureViewSize(width, height, oldWidth, oldHeight, portrait); + // if the image is already fit for the screen, just show it. + if (defaultDrawable.getIntrinsicWidth() == width + && defaultDrawable.getIntrinsicHeight() == height) { + overrideDrawable(defaultDrawable); } - private void overrideDrawable(Drawable newDrawable) { - if (currentDrawable == newDrawable) return; - currentDrawable = newDrawable; - super.setImageDrawable(newDrawable); + // check if we have the new one already + if (storedSizes.containsKey(newKey)) { + super.setImageDrawable(storedSizes.get(newKey)); + return; } - private int landscapeWidth = 0; - private int landscapeHeight = 0; - private int portraitWidth = 0; - private int portraitHeight = 0; - private boolean keyboardShown = false; - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - if (width == 0 || height == 0) return; - final String newKey = width + "x" + height; - int orientation = getResources().getConfiguration().orientation; - boolean portrait; - if (orientation == ORIENTATION_PORTRAIT) { - portrait = true; - } else if (orientation == ORIENTATION_LANDSCAPE) { - portrait = false; - } else { - Log.i(TAG, "orientation was: " + orientation); - return; // something fishy happened. - } - - if (!(defaultDrawable instanceof BitmapDrawable)) { - return; // need Bitmap for scaling and cropping. - } - - measureViewSize(width, height, oldWidth, oldHeight, portrait); - // if the image is already fit for the screen, just show it. - if (defaultDrawable.getIntrinsicWidth() == width && - defaultDrawable.getIntrinsicHeight() == height) { - overrideDrawable(defaultDrawable); - } - - // check if we have the new one already - if (storedSizes.containsKey(newKey)) { - super.setImageDrawable(storedSizes.get(newKey)); - return; - } - - if (keyboardShown) { - // don't scale; Crop. - Drawable large; - if (portrait) - large = storedSizes.get(portraitWidth + "x" + portraitHeight); - else - large = storedSizes.get(landscapeWidth + "x" + landscapeHeight); - if (large == null) return; // no baseline. can't work. - Bitmap original = ((BitmapDrawable) large).getBitmap(); - if (height <= original.getHeight() && width <= original.getWidth()) { - Bitmap cropped = Bitmap.createBitmap(original, 0, 0, width, height); - Drawable croppedDrawable = new BitmapDrawable(getResources(), cropped); - overrideDrawable(croppedDrawable); + if (keyboardShown) { + // don't scale; Crop. + Drawable large; + if (portrait) large = storedSizes.get(portraitWidth + "x" + portraitHeight); + else large = storedSizes.get(landscapeWidth + "x" + landscapeHeight); + if (large == null) return; // no baseline. can't work. + Bitmap original = ((BitmapDrawable) large).getBitmap(); + if (height <= original.getHeight() && width <= original.getWidth()) { + Bitmap cropped = Bitmap.createBitmap(original, 0, 0, width, height); + Drawable croppedDrawable = new BitmapDrawable(getResources(), cropped); + overrideDrawable(croppedDrawable); + } + } else { + Util.runOnBackground( + () -> { + Bitmap bitmap = ((BitmapDrawable) defaultDrawable).getBitmap(); + Context context = getContext(); + try { + Bitmap scaledBitmap = + GlideApp.with(context) + .asBitmap() + .load(bitmap) + .centerCrop() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .submit(width, height) + .get(); + Drawable rescaled = new BitmapDrawable(getResources(), scaledBitmap); + storedSizes.put(newKey, rescaled); + Util.runOnMain(() -> overrideDrawable(rescaled)); + } catch (ExecutionException | InterruptedException ex) { + Log.e(TAG, "could not rescale background", ex); + // No background set. } - } else { - Util.runOnBackground(() -> { - Bitmap bitmap = ((BitmapDrawable) defaultDrawable).getBitmap(); - Context context = getContext(); - try { - Bitmap scaledBitmap = GlideApp.with(context) - .asBitmap() - .load(bitmap) - .centerCrop() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .submit(width, height) - .get(); - Drawable rescaled = new BitmapDrawable(getResources(), scaledBitmap); - storedSizes.put(newKey, rescaled); - Util.runOnMain(() -> overrideDrawable(rescaled)); - } catch (ExecutionException | InterruptedException ex) { - Log.e(TAG, "could not rescale background", ex); - // No background set. - } - }); - } - super.onSizeChanged(width, height, oldWidth, oldHeight); + }); } - - private void measureViewSize(int width, int height, int oldWidth, int oldHeight, boolean portrait) { - if (portraitWidth != 0 && portraitHeight != 0 && landscapeWidth != 0 && landscapeHeight != 0) - return; - - if (oldWidth == 0 && oldHeight == 0) { // screen just opened from inside the app - if (portrait) { // portrait - portraitHeight = height; - portraitWidth = width; - } else { // landscape - landscapeHeight = height; - landscapeWidth = width; - } - } else { - if (oldWidth == portraitWidth) { // was in portrait - if (!portrait) { // rotate to landscape - landscapeHeight = height; - landscapeWidth = width; - } - } else if (oldHeight == landscapeHeight) { - if (portrait) { - portraitHeight = height; - portraitWidth = width; - } - } + super.onSizeChanged(width, height, oldWidth, oldHeight); + } + + private void measureViewSize( + int width, int height, int oldWidth, int oldHeight, boolean portrait) { + if (portraitWidth != 0 && portraitHeight != 0 && landscapeWidth != 0 && landscapeHeight != 0) + return; + + if (oldWidth == 0 && oldHeight == 0) { // screen just opened from inside the app + if (portrait) { // portrait + portraitHeight = height; + portraitWidth = width; + } else { // landscape + landscapeHeight = height; + landscapeWidth = width; + } + } else { + if (oldWidth == portraitWidth) { // was in portrait + if (!portrait) { // rotate to landscape + landscapeHeight = height; + landscapeWidth = width; } + } else if (oldHeight == landscapeHeight) { + if (portrait) { + portraitHeight = height; + portraitWidth = width; + } + } } - - @Override - public void onKeyboardHidden() { - keyboardShown = false; - Log.i(TAG, "Keyboard hidden"); - } - - @Override - public void onKeyboardShown() { - keyboardShown = true; - Log.i(TAG, "Keyboard shown"); - } + } + + @Override + public void onKeyboardHidden() { + keyboardShown = false; + Log.i(TAG, "Keyboard hidden"); + } + + @Override + public void onKeyboardShown() { + keyboardShown = true; + Log.i(TAG, "Keyboard shown"); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java b/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java index ca3fea5cb..06d0b19e5 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java +++ b/src/main/java/org/thoughtcrime/securesms/components/SearchToolbar.java @@ -1,16 +1,9 @@ package org.thoughtcrime.securesms.components; - import android.animation.Animator; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.os.Build; -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.Toolbar; import android.util.AttributeSet; import android.util.Log; import android.view.MenuItem; @@ -19,7 +12,11 @@ import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.LinearLayout; - +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.Toolbar; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; @@ -56,9 +53,11 @@ private void initialize() { return; } - Drawable drawable = getContext().getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); + Drawable drawable = + getContext().getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); drawable.mutate(); - drawable.setColorFilter(getContext().getResources().getColor(R.color.grey_700), PorterDuff.Mode.SRC_IN); + drawable.setColorFilter( + getContext().getResources().getColor(R.color.grey_700), PorterDuff.Mode.SRC_IN); toolbar.setNavigationIcon(drawable); toolbar.inflateMenu(R.menu.conversation_list_search); @@ -72,53 +71,57 @@ private void initialize() { if (searchText != null) { searchText.setHint(R.string.search); - searchText.setOnEditorActionListener((textView, actionId, keyEvent) -> { - if (EditorInfo.IME_ACTION_DONE == actionId) { - searchView.clearFocus(); - return true; - } - return false; - }); + searchText.setOnEditorActionListener( + (textView, actionId, keyEvent) -> { + if (EditorInfo.IME_ACTION_DONE == actionId) { + searchView.clearFocus(); + return true; + } + return false; + }); } else { searchView.setQueryHint(getResources().getString(R.string.search)); } - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - if (listener != null) listener.onSearchTextChange(query); - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - return onQueryTextSubmit(newText); - } - }); - - searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - hide(); - return true; - } - }); + searchView.setOnQueryTextListener( + new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + if (listener != null) listener.onSearchTextChange(query); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + return onQueryTextSubmit(newText); + } + }); + + searchItem.setOnActionExpandListener( + new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + hide(); + return true; + } + }); MenuItem searchUnread = toolbar.getMenu().findItem(R.id.search_unread); - searchUnread.setOnMenuItemClickListener(item -> { - String t = searchText.getText().toString(); - if (!t.contains("is:unread")) { - t += (t.isEmpty() ? "" : " ") + "is:unread "; - } - searchText.setText(t); - searchText.setSelection(t.length(), t.length()); - return true; - }); + searchUnread.setOnMenuItemClickListener( + item -> { + String t = searchText.getText().toString(); + if (!t.contains("is:unread")) { + t += (t.isEmpty() ? "" : " ") + "is:unread "; + } + searchText.setText(t); + searchText.setSelection(t.length(), t.length()); + return true; + }); toolbar.setNavigationOnClickListener(v -> hide()); } @@ -131,7 +134,8 @@ public void display(float x, float y) { searchItem.expandActionView(); - Animator animator = ViewAnimationUtils.createCircularReveal(this, (int) x, (int) y, 0, getWidth()); + Animator animator = + ViewAnimationUtils.createCircularReveal(this, (int) x, (int) y, 0, getWidth()); animator.setDuration(400); setVisibility(View.VISIBLE); @@ -147,17 +151,18 @@ public void collapse() { private void hide() { if (getVisibility() == View.VISIBLE) { - if (listener != null) listener.onSearchClosed(); - Animator animator = ViewAnimationUtils.createCircularReveal(this, (int) x, (int) y, getWidth(), 0); + Animator animator = + ViewAnimationUtils.createCircularReveal(this, (int) x, (int) y, getWidth(), 0); animator.setDuration(400); - animator.addListener(new AnimationCompleteListener() { - @Override - public void onAnimationEnd(@NonNull Animator animation) { - setVisibility(View.INVISIBLE); - } - }); + animator.addListener( + new AnimationCompleteListener() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + setVisibility(View.INVISIBLE); + } + }); animator.start(); } } @@ -173,7 +178,7 @@ public void setListener(SearchListener listener) { public interface SearchListener { void onSearchTextChange(String text); + void onSearchClosed(); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/SendButton.java b/src/main/java/org/thoughtcrime/securesms/components/SendButton.java index f7a44e13c..7c04d4e35 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/SendButton.java +++ b/src/main/java/org/thoughtcrime/securesms/components/SendButton.java @@ -3,9 +3,7 @@ import android.content.Context; import android.util.AttributeSet; import android.view.View; - import androidx.appcompat.widget.AppCompatImageButton; - import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.TransportOptions; import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; @@ -15,9 +13,8 @@ public class SendButton extends AppCompatImageButton implements TransportOptions.OnTransportChangedListener, - TransportOptionsPopup.SelectedListener, - View.OnLongClickListener -{ + TransportOptionsPopup.SelectedListener, + View.OnLongClickListener { private final TransportOptions transportOptions; diff --git a/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java b/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java index 32eb85051..2b7900100 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java @@ -11,20 +11,19 @@ import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.R; public class ShapeScrim extends View { private enum ShapeType { - CIRCLE, SQUARE + CIRCLE, + SQUARE } - private final Paint eraser; + private final Paint eraser; private final ShapeType shape; - private final float radius; + private final float radius; private Bitmap scrim; private Canvas scrimCanvas; @@ -41,7 +40,8 @@ public ShapeScrim(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); if (attrs != null) { - try (TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeScrim, 0, 0)) { + try (TypedArray typedArray = + context.getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeScrim, 0, 0)) { String shapeName = typedArray.getString(R.styleable.ShapeScrim_shape); if ("square".equalsIgnoreCase(shapeName)) this.shape = ShapeType.SQUARE; @@ -51,7 +51,7 @@ public ShapeScrim(Context context, AttributeSet attrs, int defStyleAttr) { this.radius = typedArray.getFloat(R.styleable.ShapeScrim_radius, 0.4f); } } else { - this.shape = ShapeType.SQUARE; + this.shape = ShapeType.SQUARE; this.radius = 0.4f; } @@ -64,8 +64,8 @@ public ShapeScrim(Context context, AttributeSet attrs, int defStyleAttr) { public void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); - int shortDimension = Math.min(getWidth(), getHeight()); - float drawRadius = shortDimension * radius; + int shortDimension = Math.min(getWidth(), getHeight()); + float drawRadius = shortDimension * radius; if (scrimCanvas == null) { scrim = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); @@ -76,7 +76,7 @@ public void onDraw(@NonNull Canvas canvas) { scrimCanvas.drawColor(Color.parseColor("#55BDBDBD")); if (shape == ShapeType.CIRCLE) drawCircle(scrimCanvas, drawRadius, eraser); - else drawSquare(scrimCanvas, drawRadius, eraser); + else drawSquare(scrimCanvas, drawRadius, eraser); canvas.drawBitmap(scrim, 0, 0, null); } @@ -86,7 +86,7 @@ public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldHeight, oldHeight); if (width != oldWidth || height != oldHeight) { - scrim = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + scrim = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); scrimCanvas = new Canvas(scrim); } } @@ -96,9 +96,9 @@ private void drawCircle(Canvas canvas, float radius, Paint eraser) { } private void drawSquare(Canvas canvas, float radius, Paint eraser) { - float left = (getWidth() / 2 ) - radius; - float top = (getHeight() / 2) - radius; - float right = left + (radius * 2); + float left = (getWidth() / 2) - radius; + float top = (getHeight() / 2) - radius; + float right = left + (radius * 2); float bottom = top + (radius * 2); RectF square = new RectF(left, top, right, bottom); diff --git a/src/main/java/org/thoughtcrime/securesms/components/SquareFrameLayout.java b/src/main/java/org/thoughtcrime/securesms/components/SquareFrameLayout.java index 9517f8ea1..679cd4b0a 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/SquareFrameLayout.java +++ b/src/main/java/org/thoughtcrime/securesms/components/SquareFrameLayout.java @@ -4,7 +4,6 @@ import android.content.res.TypedArray; import android.util.AttributeSet; import android.widget.FrameLayout; - import org.thoughtcrime.securesms.R; public class SquareFrameLayout extends FrameLayout { @@ -23,8 +22,10 @@ public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) super(context, attrs, defStyleAttr); if (attrs != null) { - try (TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SquareFrameLayout, 0, 0)) { - this.squareHeight = typedArray.getBoolean(R.styleable.SquareFrameLayout_square_height, false); + try (TypedArray typedArray = + context.getTheme().obtainStyledAttributes(attrs, R.styleable.SquareFrameLayout, 0, 0)) { + this.squareHeight = + typedArray.getBoolean(R.styleable.SquareFrameLayout_square_height, false); } } else { this.squareHeight = false; @@ -34,6 +35,6 @@ public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (squareHeight) super.onMeasure(heightMeasureSpec, heightMeasureSpec); - else super.onMeasure(widthMeasureSpec, widthMeasureSpec); + else super.onMeasure(widthMeasureSpec, widthMeasureSpec); } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java b/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java index c1e3e7b06..3b5f0c884 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java +++ b/src/main/java/org/thoughtcrime/securesms/components/SwitchPreferenceCompat.java @@ -1,50 +1,49 @@ package org.thoughtcrime.securesms.components; import android.content.Context; - +import android.util.AttributeSet; import androidx.preference.CheckBoxPreference; import androidx.preference.Preference; -import android.util.AttributeSet; - import org.thoughtcrime.securesms.R; public class SwitchPreferenceCompat extends CheckBoxPreference { - private Preference.OnPreferenceClickListener listener; - - public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setLayoutRes(); - } - - public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - setLayoutRes(); - } - - public SwitchPreferenceCompat(Context context, AttributeSet attrs) { - super(context, attrs); - setLayoutRes(); - } - - public SwitchPreferenceCompat(Context context) { - super(context); - setLayoutRes(); - } - - private void setLayoutRes() { - setWidgetLayoutResource(R.layout.switch_compat_preference); - } - - @Override - public void setOnPreferenceClickListener(Preference.OnPreferenceClickListener listener) { - this.listener = listener; - } - - @Override - protected void onClick() { - if (listener == null || !listener.onPreferenceClick(this)) { - super.onClick(); - } + private Preference.OnPreferenceClickListener listener; + + public SwitchPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setLayoutRes(); + } + + public SwitchPreferenceCompat( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setLayoutRes(); + } + + public SwitchPreferenceCompat(Context context, AttributeSet attrs) { + super(context, attrs); + setLayoutRes(); + } + + public SwitchPreferenceCompat(Context context) { + super(context); + setLayoutRes(); + } + + private void setLayoutRes() { + setWidgetLayoutResource(R.layout.switch_compat_preference); + } + + @Override + public void setOnPreferenceClickListener(Preference.OnPreferenceClickListener listener) { + this.listener = listener; + } + + @Override + protected void onClick() { + if (listener == null || !listener.onPreferenceClick(this)) { + super.onClick(); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index f57946ccf..a32293524 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.components; +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; + import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; @@ -10,12 +12,12 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.annotation.UiThread; - +import chat.delta.util.ListenableFuture; +import chat.delta.util.SettableFuture; import com.bumptech.glide.load.engine.DiskCacheStrategy; - +import java.util.Locale; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; @@ -26,33 +28,26 @@ import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.Util; -import java.util.Locale; - -import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; - -import chat.delta.util.ListenableFuture; -import chat.delta.util.SettableFuture; - public class ThumbnailView extends FrameLayout { - private static final String TAG = ThumbnailView.class.getSimpleName(); - private static final int WIDTH = 0; - private static final int HEIGHT = 1; - private static final int MIN_WIDTH = 0; - private static final int MAX_WIDTH = 1; - private static final int MIN_HEIGHT = 2; - private static final int MAX_HEIGHT = 3; + private static final String TAG = ThumbnailView.class.getSimpleName(); + private static final int WIDTH = 0; + private static final int HEIGHT = 1; + private static final int MIN_WIDTH = 0; + private static final int MAX_WIDTH = 1; + private static final int MIN_HEIGHT = 2; + private static final int MAX_HEIGHT = 3; - private final ImageView image; - private final View playOverlay; + private final ImageView image; + private final View playOverlay; private OnClickListener parentClickListener; - private final int[] dimens = new int[2]; - private final int[] bounds = new int[4]; + private final int[] dimens = new int[2]; + private final int[] bounds = new int[4]; private final int[] measureDimens = new int[2]; - private SlideClickListener thumbnailClickListener = null; - private Slide slide = null; + private SlideClickListener thumbnailClickListener = null; + private Slide slide = null; public ThumbnailView(Context context) { this(context, null); @@ -67,20 +62,23 @@ public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) { inflate(context, R.layout.thumbnail_view, this); - this.image = findViewById(R.id.thumbnail_image); + this.image = findViewById(R.id.thumbnail_image); this.playOverlay = findViewById(R.id.play_overlay); super.setOnClickListener(new ThumbnailClickDispatcher()); if (attrs != null) { - try (TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)) { + try (TypedArray typedArray = + context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)) { bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0); bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0); - bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0); - bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0); - // int radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.gallery_thumbnail_radius)); + bounds[MIN_HEIGHT] = + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0); + bounds[MAX_HEIGHT] = + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0); + // int radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, + // getResources().getDimensionPixelSize(R.dimen.gallery_thumbnail_radius)); } } - } public String getDescription() { @@ -99,11 +97,12 @@ protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasure return; } - int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight(); + int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight(); int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom(); - super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)); + super.onMeasure( + MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)); } @SuppressWarnings("SuspiciousNameCombination") @@ -117,62 +116,72 @@ private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds return; } - double naturalWidth = dimens[WIDTH]; + double naturalWidth = dimens[WIDTH]; double naturalHeight = dimens[HEIGHT]; - int minWidth = bounds[MIN_WIDTH]; - int maxWidth = bounds[MAX_WIDTH]; + int minWidth = bounds[MIN_WIDTH]; + int maxWidth = bounds[MAX_WIDTH]; int minHeight = bounds[MIN_HEIGHT]; int maxHeight = bounds[MAX_HEIGHT]; if (dimensFilledCount > 0 && dimensFilledCount < dimens.length) { - throw new IllegalStateException(String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %f x %f", - naturalWidth, naturalHeight)); + throw new IllegalStateException( + String.format( + Locale.ENGLISH, + "Width or height has been specified, but not both. Dimens: %f x %f", + naturalWidth, + naturalHeight)); } if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) { - throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]", - minWidth, maxWidth, minHeight, maxHeight)); + throw new IllegalStateException( + String.format( + Locale.ENGLISH, + "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]", + minWidth, + maxWidth, + minHeight, + maxHeight)); } - double measuredWidth = naturalWidth; + double measuredWidth = naturalWidth; double measuredHeight = naturalHeight; - boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth; + boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth; boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight; if (!widthInBounds || !heightInBounds) { - double minWidthRatio = naturalWidth / minWidth; - double maxWidthRatio = naturalWidth / maxWidth; + double minWidthRatio = naturalWidth / minWidth; + double maxWidthRatio = naturalWidth / maxWidth; double minHeightRatio = naturalHeight / minHeight; double maxHeightRatio = naturalHeight / maxHeight; if (maxWidthRatio > 1 || maxHeightRatio > 1) { if (maxWidthRatio >= maxHeightRatio) { - measuredWidth /= maxWidthRatio; + measuredWidth /= maxWidthRatio; measuredHeight /= maxWidthRatio; } else { - measuredWidth /= maxHeightRatio; + measuredWidth /= maxHeightRatio; measuredHeight /= maxHeightRatio; } - measuredWidth = Math.max(measuredWidth, minWidth); + measuredWidth = Math.max(measuredWidth, minWidth); measuredHeight = Math.max(measuredHeight, minHeight); } else if (minWidthRatio < 1 || minHeightRatio < 1) { if (minWidthRatio <= minHeightRatio) { - measuredWidth /= minWidthRatio; + measuredWidth /= minWidthRatio; measuredHeight /= minWidthRatio; } else { - measuredWidth /= minHeightRatio; + measuredWidth /= minHeightRatio; measuredHeight /= minHeightRatio; } - measuredWidth = Math.min(measuredWidth, maxWidth); + measuredWidth = Math.min(measuredWidth, maxWidth); measuredHeight = Math.min(measuredHeight, maxHeight); } } - targetDimens[WIDTH] = (int) measuredWidth; + targetDimens[WIDTH] = (int) measuredWidth; targetDimens[HEIGHT] = (int) measuredHeight; } @@ -202,20 +211,21 @@ public void setClickable(boolean clickable) { } @UiThread - public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide) - { + public ListenableFuture setImageResource( + @NonNull GlideRequests glideRequests, @NonNull Slide slide) { return setImageResource(glideRequests, slide, 0, 0); } @SuppressLint("StaticFieldLeak") @UiThread - public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - int naturalWidth, int naturalHeight) - { + public ListenableFuture setImageResource( + @NonNull GlideRequests glideRequests, + @NonNull Slide slide, + int naturalWidth, + int naturalHeight) { if (slide.hasPlayOverlay()) { this.playOverlay.setVisibility(View.VISIBLE); - } - else { + } else { this.playOverlay.setVisibility(View.GONE); } @@ -224,30 +234,33 @@ public ListenableFuture setImageResource(@NonNull GlideRequests glideRe return new SettableFuture<>(false); } - if (this.slide != null && this.slide.getFastPreflightId() != null && - this.slide.getFastPreflightId().equals(slide.getFastPreflightId())) - { + if (this.slide != null + && this.slide.getFastPreflightId() != null + && this.slide.getFastPreflightId().equals(slide.getFastPreflightId())) { Log.w(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId()); this.slide = slide; return new SettableFuture<>(false); } - Log.w(TAG, "loading part with id " + slide.asAttachment().getDataUri() - + ", progress " + slide.getTransferState() + ", fast preflight id: " + - slide.asAttachment().getFastPreflightId()); + Log.w( + TAG, + "loading part with id " + + slide.asAttachment().getDataUri() + + ", progress " + + slide.getTransferState() + + ", fast preflight id: " + + slide.asAttachment().getFastPreflightId()); this.slide = slide; - dimens[WIDTH] = naturalWidth; + dimens[WIDTH] = naturalWidth; dimens[HEIGHT] = naturalHeight; invalidate(); SettableFuture result = new SettableFuture<>(); - if (slide.getThumbnailUri() != null) - { - if(slide.hasVideo()) - { + if (slide.getThumbnailUri() != null) { + if (slide.hasVideo()) { Uri dataUri = slide.getUri(); Uri thumbnailUri = slide.getThumbnailUri(); ImageView img = findViewById(R.id.thumbnail_image); @@ -257,31 +270,33 @@ public ListenableFuture setImageResource(@NonNull GlideRequests glideRe protected Boolean doInBackground(Void... params) { return MediaUtil.createVideoThumbnailIfNeeded(context, dataUri, thumbnailUri, null); } + @Override protected void onPostExecute(Boolean success) { - GlideRequest request = glideRequests.load(new DecryptableUri(thumbnailUri)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .transition(withCrossFade()); + GlideRequest request = + glideRequests + .load(new DecryptableUri(thumbnailUri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transition(withCrossFade()); request.into(new GlideDrawableListeningTarget(img, result)); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - if(slide.hasSticker()) - { - GlideRequest request = glideRequests.load(new DecryptableUri(slide.getThumbnailUri())) - .diskCacheStrategy(DiskCacheStrategy.NONE); + if (slide.hasSticker()) { + GlideRequest request = + glideRequests + .load(new DecryptableUri(slide.getThumbnailUri())) + .diskCacheStrategy(DiskCacheStrategy.NONE); request.into(new GlideDrawableListeningTarget(image, result)); - } - else - { - GlideRequest request = glideRequests.load(new DecryptableUri(slide.getThumbnailUri())) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .transition(withCrossFade()); + } else { + GlideRequest request = + glideRequests + .load(new DecryptableUri(slide.getThumbnailUri())) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transition(withCrossFade()); request.into(new GlideDrawableListeningTarget(image, result)); } - } - else - { + } else { glideRequests.clear(image); result.set(false); } @@ -306,11 +321,10 @@ public void setScaleType(@NonNull ImageView.ScaleType scale) { private class ThumbnailClickDispatcher implements View.OnClickListener { @Override public void onClick(View view) { - if (thumbnailClickListener != null && - slide != null && - slide.asAttachment().getDataUri() != null && - slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) - { + if (thumbnailClickListener != null + && slide != null + && slide.asAttachment().getDataUri() != null + && slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) { thumbnailClickListener.onClick(view, slide); } else if (parentClickListener != null) { parentClickListener.onClick(view); diff --git a/src/main/java/org/thoughtcrime/securesms/components/VcardView.java b/src/main/java/org/thoughtcrime/securesms/components/VcardView.java index a5b635bcd..b90d24d00 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/VcardView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/VcardView.java @@ -5,20 +5,17 @@ import android.util.Log; import android.widget.FrameLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.VcardContact; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.VcardSlide; import org.thoughtcrime.securesms.recipients.Recipient; -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; -import chat.delta.rpc.types.VcardContact; - public class VcardView extends FrameLayout { private static final String TAG = VcardView.class.getSimpleName(); @@ -41,23 +38,28 @@ public VcardView(final Context context, AttributeSet attrs, int defStyle) { inflate(context, R.layout.vcard_view, this); - this.avatar = findViewById(R.id.avatar); - this.name = findViewById(R.id.name); + this.avatar = findViewById(R.id.avatar); + this.name = findViewById(R.id.name); - setOnClickListener(v -> { - if (viewListener != null && slide != null) { - viewListener.onClick(v, slide); - } - }); + setOnClickListener( + v -> { + if (viewListener != null && slide != null) { + viewListener.onClick(v, slide); + } + }); } public void setVcardClickListener(@Nullable SlideClickListener listener) { this.viewListener = listener; } - public void setVcard(@NonNull GlideRequests glideRequests, final @NonNull VcardSlide slide, final @NonNull Rpc rpc) { + public void setVcard( + @NonNull GlideRequests glideRequests, + final @NonNull VcardSlide slide, + final @NonNull Rpc rpc) { try { - VcardContact vcardContact = rpc.parseVcard(slide.asAttachment().getRealPath(getContext())).get(0); + VcardContact vcardContact = + rpc.parseVcard(slide.asAttachment().getRealPath(getContext())).get(0); name.setText(vcardContact.displayName); avatar.setAvatar(glideRequests, new Recipient(getContext(), vcardContact), false); this.slide = slide; diff --git a/src/main/java/org/thoughtcrime/securesms/components/WebxdcView.java b/src/main/java/org/thoughtcrime/securesms/components/WebxdcView.java index 84ced7bbe..594e7d4a7 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/WebxdcView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/WebxdcView.java @@ -1,35 +1,29 @@ package org.thoughtcrime.securesms.components; - import android.content.Context; import android.content.res.TypedArray; - -import androidx.annotation.AttrRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageView; - import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; - +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; import com.b44t.messenger.DcMsg; - +import java.io.ByteArrayInputStream; import org.json.JSONObject; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.DocumentSlide; import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.util.JsonUtils; -import java.io.ByteArrayInputStream; - public class WebxdcView extends FrameLayout { private final @NonNull AppCompatImageView icon; - private final @NonNull TextView appName; - private final @NonNull TextView appSubtitle; + private final @NonNull TextView appName; + private final @NonNull TextView appSubtitle; private @Nullable SlideClickListener viewListener; @@ -41,11 +35,13 @@ public WebxdcView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public WebxdcView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + public WebxdcView( + @NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); boolean compact; - try (TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WebxdcView, 0, 0)) { + try (TypedArray a = + context.getTheme().obtainStyledAttributes(attrs, R.styleable.WebxdcView, 0, 0)) { compact = a.getBoolean(R.styleable.WebxdcView_compact, false); } if (compact) { @@ -54,8 +50,8 @@ public WebxdcView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrR inflate(context, R.layout.webxdc_view, this); } - this.icon = findViewById(R.id.webxdc_icon); - this.appName = findViewById(R.id.webxdc_app_name); + this.icon = findViewById(R.id.webxdc_icon); + this.appName = findViewById(R.id.webxdc_app_name); this.appSubtitle = findViewById(R.id.webxdc_subtitle); } @@ -63,8 +59,7 @@ public void setWebxdcClickListener(@Nullable SlideClickListener listener) { this.viewListener = listener; } - public void setWebxdc(final @NonNull DcMsg dcMsg, String defaultSummary) - { + public void setWebxdc(final @NonNull DcMsg dcMsg, String defaultSummary) { JSONObject info = dcMsg.getWebxdcInfo(); setOnClickListener(new OpenClickedListener(getContext(), dcMsg)); @@ -95,10 +90,12 @@ public void setWebxdc(final @NonNull DcMsg dcMsg, String defaultSummary) } public String getDescription() { - String type = getContext().getString( R.string.webxdc_app); + String type = getContext().getString(R.string.webxdc_app); String desc = type; desc += "\n" + appName.getText(); - if (appSubtitle.getText() != null && !appSubtitle.getText().toString().isEmpty() && !appSubtitle.getText().toString().equals(type)) { + if (appSubtitle.getText() != null + && !appSubtitle.getText().toString().isEmpty() + && !appSubtitle.getText().toString().equals(type)) { desc += "\n" + appSubtitle.getText(); } return desc; @@ -118,5 +115,4 @@ public void onClick(View v) { } } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/ZoomingImageView.java b/src/main/java/org/thoughtcrime/securesms/components/ZoomingImageView.java index 3e4b31e71..cd721a89e 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/ZoomingImageView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/ZoomingImageView.java @@ -4,21 +4,21 @@ import android.content.Context; import android.net.Uri; import android.os.AsyncTask; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.view.View; import android.widget.FrameLayout; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.Target; import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import com.davemorrissey.labs.subscaleview.decoder.DecoderFactory; import com.github.chrisbanes.photoview.PhotoView; - +import java.io.IOException; +import java.io.InputStream; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder; import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder; @@ -29,15 +29,11 @@ import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.MediaUtil; -import java.io.IOException; -import java.io.InputStream; - - public class ZoomingImageView extends FrameLayout { private static final String TAG = ZoomingImageView.class.getName(); - private final PhotoView photoView; + private final PhotoView photoView; private final SubsamplingScaleImageView subsamplingImageView; public ZoomingImageView(Context context) { @@ -53,17 +49,17 @@ public ZoomingImageView(Context context, AttributeSet attrs, int defStyleAttr) { inflate(context, R.layout.zooming_image_view, this); - this.photoView = findViewById(R.id.image_view); + this.photoView = findViewById(R.id.image_view); this.subsamplingImageView = findViewById(R.id.subsampling_image_view); this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF); } @SuppressLint("StaticFieldLeak") - public void setImageUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull String contentType) - { - final Context context = getContext(); - final int maxTextureSize = BitmapUtil.getMaxTextureSize(); + public void setImageUri( + @NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull String contentType) { + final Context context = getContext(); + final int maxTextureSize = BitmapUtil.getMaxTextureSize(); Log.w(TAG, "Max texture size: " + maxTextureSize); @@ -82,9 +78,13 @@ public void setImageUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, } protected void onPostExecute(@Nullable Pair dimensions) { - Log.w(TAG, "Dimensions: " + (dimensions == null ? "(null)" : dimensions.first + ", " + dimensions.second)); + Log.w( + TAG, + "Dimensions: " + + (dimensions == null ? "(null)" : dimensions.first + ", " + dimensions.second)); - if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) { + if (dimensions == null + || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) { Log.w(TAG, "Loading in standard image view..."); setImageViewUri(glideRequests, uri); } else { @@ -99,11 +99,12 @@ private void setImageViewUri(@NonNull GlideRequests glideRequests, @NonNull Uri photoView.setVisibility(View.VISIBLE); subsamplingImageView.setVisibility(View.GONE); - glideRequests.load(new DecryptableUri(uri)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .dontTransform() - .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) - .into(photoView); + glideRequests + .load(new DecryptableUri(uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontTransform() + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .into(photoView); } private void setSubsamplingImageViewUri(@NonNull Uri uri) { @@ -121,14 +122,16 @@ public void cleanup() { subsamplingImageView.recycle(); } - private static class AttachmentBitmapDecoderFactory implements DecoderFactory { + private static class AttachmentBitmapDecoderFactory + implements DecoderFactory { @Override public AttachmentBitmapDecoder make() throws IllegalAccessException, InstantiationException { return new AttachmentBitmapDecoder(); } } - private static class AttachmentRegionDecoderFactory implements DecoderFactory { + private static class AttachmentRegionDecoderFactory + implements DecoderFactory { @Override public AttachmentRegionDecoder make() throws IllegalAccessException, InstantiationException { return new AttachmentRegionDecoder(); diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java index 09868ed25..3ad17906a 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackState.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.components.audioplay; import android.net.Uri; - import androidx.annotation.Nullable; public class AudioPlaybackState { @@ -19,11 +18,12 @@ public enum PlaybackStatus { ERROR } - public AudioPlaybackState(int msgId, - @Nullable Uri audioUri, - PlaybackStatus status, - long currentPosition, - long duration) { + public AudioPlaybackState( + int msgId, + @Nullable Uri audioUri, + PlaybackStatus status, + long currentPosition, + long duration) { this.msgId = msgId; this.audioUri = audioUri; this.status = status; diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java index 49d2ee0c6..c75fb38cc 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java @@ -6,7 +6,6 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; - import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -14,7 +13,6 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.session.MediaController; - import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -22,15 +20,16 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; - public class AudioPlaybackViewModel extends ViewModel { private static final String TAG = AudioPlaybackViewModel.class.getSimpleName(); - private static final int NON_MESSAGE_AUDIO_MSG_ID = 0; // Audios not attached to a message doesn't have message id. + private static final int NON_MESSAGE_AUDIO_MSG_ID = + 0; // Audios not attached to a message doesn't have message id. private final MutableLiveData playbackState; - private final MutableLiveData> durations = new MutableLiveData<>(new HashMap<>()); + private final MutableLiveData> durations = + new MutableLiveData<>(new HashMap<>()); private final Set extractionInProgress = new HashSet<>(); private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2); @@ -64,16 +63,15 @@ public void loadAudioAndPlay(int msgId, Uri audioUri) { if (isDifferentAudio(msgId, audioUri)) { updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0); - MediaItem mediaItem = new MediaItem.Builder() - .setMediaId(String.valueOf(msgId)) - .setUri(audioUri) - .build(); + MediaItem mediaItem = + new MediaItem.Builder().setMediaId(String.valueOf(msgId)).setUri(audioUri).build(); mediaController.setMediaItem(mediaItem); mediaController.prepare(); } play(msgId, audioUri); } + private boolean isSameAudio(int msgId, Uri audioUri) { return !isDifferentAudio(msgId, audioUri); } @@ -81,10 +79,10 @@ private boolean isSameAudio(int msgId, Uri audioUri) { private boolean isDifferentAudio(int msgId, Uri audioUri) { AudioPlaybackState currentState = playbackState.getValue(); - return currentState != null && ( - msgId != currentState.getMsgId() || - currentState.getAudioUri() == null || - currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri)); + return currentState != null + && (msgId != currentState.getMsgId() + || currentState.getAudioUri() == null + || currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri)); } public LiveData> getDurations() { @@ -107,35 +105,36 @@ public void ensureDurationLoaded(Context context, int msgId, Uri audioUri) { } // Extract in background - extractionExecutor.execute(() -> { - long duration = extractDurationFromAudio(context, audioUri); - - handler.post(() -> { - Map updatedDurations = new HashMap<>(durations.getValue()); - updatedDurations.put(msgId, duration); - durations.setValue(updatedDurations); - }); - - synchronized (extractionInProgress) { - extractionInProgress.remove(msgId); - } - }); + extractionExecutor.execute( + () -> { + long duration = extractDurationFromAudio(context, audioUri); + + handler.post( + () -> { + Map updatedDurations = new HashMap<>(durations.getValue()); + updatedDurations.put(msgId, duration); + durations.setValue(updatedDurations); + }); + + synchronized (extractionInProgress) { + extractionInProgress.remove(msgId); + } + }); } private long extractDurationFromAudio(Context context, Uri audioUri) { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { retriever.setDataSource(context, audioUri); - String durationStr = retriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_DURATION - ); + String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); return durationStr != null ? Long.parseLong(durationStr) : 0; } catch (Exception e) { return 0; } finally { try { retriever.release(); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } } @@ -192,31 +191,32 @@ public void setUserSeeking(boolean isUserSeeking) { private void setupPlayerListener() { if (mediaController == null) return; - mediaController.addListener(new Player.Listener() { - @Override - public void onEvents(Player player, Player.Events events) { - if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) { - if (player.isPlaying()) { - startUpdateProgress(); - } else { - stopUpdateProgress(); + mediaController.addListener( + new Player.Listener() { + @Override + public void onEvents(Player player, Player.Events events) { + if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) { + if (player.isPlaying()) { + startUpdateProgress(); + } else { + stopUpdateProgress(); + } + updateCurrentState(false); + } + if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) { + if (player.getPlaybackState() == Player.STATE_READY) { + updateCurrentState(false); + } else if (player.getPlaybackState() == Player.STATE_ENDED) { + // This is to prevent automatically playing after the audio + // has been play to the end once, then user dragged the seek bar again + mediaController.setPlayWhenReady(false); + } + } + if (events.containsAny(Player.EVENT_PLAYER_ERROR)) { + updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0); + } } - updateCurrentState(false); - } - if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) { - if (player.getPlaybackState() == Player.STATE_READY) { - updateCurrentState(false); - } else if (player.getPlaybackState() == Player.STATE_ENDED) { - // This is to prevent automatically playing after the audio - // has been play to the end once, then user dragged the seek bar again - mediaController.setPlayWhenReady(false); - } - } - if (events.containsAny(Player.EVENT_PLAYER_ERROR)) { - updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0); - } - } - }); + }); } private void updateCurrentState(boolean queryPlaying) { @@ -226,7 +226,7 @@ private void updateCurrentState(boolean queryPlaying) { if (mediaController.isPlaying()) { status = AudioPlaybackState.PlaybackStatus.PLAYING; } else if (mediaController.getPlaybackState() == Player.STATE_READY - || mediaController.getPlaybackState() == Player.STATE_ENDED) { + || mediaController.getPlaybackState() == Player.STATE_ENDED) { status = AudioPlaybackState.PlaybackStatus.PAUSED; } else { status = AudioPlaybackState.PlaybackStatus.IDLE; @@ -252,18 +252,19 @@ private void updateCurrentState(boolean queryPlaying) { } } updateState( - currentMsgId, - currentUri, - status, - mediaController.getCurrentPosition(), - mediaController.getDuration()); + currentMsgId, + currentUri, + status, + mediaController.getCurrentPosition(), + mediaController.getDuration()); } - private void updateState(int msgId, - Uri audioUri, - AudioPlaybackState.PlaybackStatus status, - long position, - long duration) { + private void updateState( + int msgId, + Uri audioUri, + AudioPlaybackState.PlaybackStatus status, + long position, + long duration) { // Sanitize longs if (position < 0 || position > Integer.MAX_VALUE) { position = 0; @@ -272,14 +273,11 @@ private void updateState(int msgId, duration = 0; } - playbackState.setValue(new AudioPlaybackState( - msgId, audioUri, status, position, duration - )); + playbackState.setValue(new AudioPlaybackState(msgId, audioUri, status, position, duration)); } - private void updateCurrentAudioState(AudioPlaybackState.PlaybackStatus status, - long position, - long duration) { + private void updateCurrentAudioState( + AudioPlaybackState.PlaybackStatus status, long position, long duration) { AudioPlaybackState current = playbackState.getValue(); if (current != null) { @@ -288,17 +286,19 @@ private void updateCurrentAudioState(AudioPlaybackState.PlaybackStatus status, } // Progress tracking - private final Runnable progressRunnable = new Runnable() { - @Override - public void run() { - if (mediaController != null && mediaController.isPlaying() && !isUserSeeking) { - updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.PLAYING, - mediaController.getCurrentPosition(), - mediaController.getDuration()); - handler.postDelayed(this, 100); - } - } - }; + private final Runnable progressRunnable = + new Runnable() { + @Override + public void run() { + if (mediaController != null && mediaController.isPlaying() && !isUserSeeking) { + updateCurrentAudioState( + AudioPlaybackState.PlaybackStatus.PLAYING, + mediaController.getCurrentPosition(), + mediaController.getDuration()); + handler.postDelayed(this, 100); + } + } + }; private void startUpdateProgress() { stopUpdateProgress(); diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java index 9258b6d30..e7b6aa3d9 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java @@ -11,44 +11,40 @@ import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; import androidx.lifecycle.Observer; import androidx.vectordrawable.graphics.drawable.Animatable2Compat; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; - +import java.util.Map; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.util.DateUtils; -import java.util.Map; - - public class AudioView extends FrameLayout { private static final String TAG = AudioView.class.getSimpleName(); - private final @NonNull ImageView playPauseButton; + private final @NonNull ImageView playPauseButton; private final AnimatedVectorDrawableCompat playToPauseDrawable; private final AnimatedVectorDrawableCompat pauseToPlayDrawable; - private final Drawable playDrawable; - private final Drawable pauseDrawable; + private final Drawable playDrawable; + private final Drawable pauseDrawable; private final Animatable2Compat.AnimationCallback animationCallback; - private final @NonNull SeekBar seekBar; - private final @NonNull TextView timestamp; - private final @NonNull TextView title; - private final @NonNull View mask; - private OnActionListener listener; - - private int msgId = -1; - private Uri audioUri; - private int progress; - private int duration; - private AudioPlaybackViewModel viewModel; + private final @NonNull SeekBar seekBar; + private final @NonNull TextView timestamp; + private final @NonNull TextView title; + private final @NonNull View mask; + private OnActionListener listener; + + private int msgId = -1; + private Uri audioUri; + private int progress; + private int duration; + private AudioPlaybackViewModel viewModel; private final Observer stateObserver = this::onPlaybackStateChanged; private final Observer> durationObserver = this::onDurationsChanged; - private boolean isPlaying; + private boolean isPlaying; public AudioView(Context context) { this(context, null); @@ -62,29 +58,30 @@ public AudioView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); inflate(context, R.layout.audio_view, this); - this.playPauseButton = findViewById(R.id.play_pause); - this.seekBar = findViewById(R.id.seek); - this.timestamp = findViewById(R.id.timestamp); - this.title = findViewById(R.id.title); - this.mask = findViewById(R.id.interception_mask); + this.playPauseButton = findViewById(R.id.play_pause); + this.seekBar = findViewById(R.id.seek); + this.timestamp = findViewById(R.id.timestamp); + this.title = findViewById(R.id.title); + this.mask = findViewById(R.id.interception_mask); updateTimestampsAndSeekBar(); // Load drawables once - this.playToPauseDrawable = AnimatedVectorDrawableCompat.create( - getContext(), R.drawable.play_to_pause_animation); - this.pauseToPlayDrawable = AnimatedVectorDrawableCompat.create( - getContext(), R.drawable.pause_to_play_animation); + this.playToPauseDrawable = + AnimatedVectorDrawableCompat.create(getContext(), R.drawable.play_to_pause_animation); + this.pauseToPlayDrawable = + AnimatedVectorDrawableCompat.create(getContext(), R.drawable.pause_to_play_animation); this.playDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.play_icon); this.pauseDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.pause_icon); - this.animationCallback = new Animatable2Compat.AnimationCallback() { - @Override - public void onAnimationEnd(Drawable drawable) { - Drawable endState = isPlaying ? pauseDrawable : playDrawable; - playPauseButton.setImageDrawable(endState); - } - }; + this.animationCallback = + new Animatable2Compat.AnimationCallback() { + @Override + public void onAnimationEnd(Drawable drawable) { + Drawable endState = isPlaying ? pauseDrawable : playDrawable; + playPauseButton.setImageDrawable(endState); + } + }; } @Override @@ -104,51 +101,53 @@ private void setupControls() { viewModel.getDurations().observeForever(durationObserver); } - playPauseButton.setOnClickListener(v -> { - Log.w(TAG, "playPauseButton onClick"); - - if (viewModel == null || audioUri == null) return; - - AudioPlaybackState state = viewModel.getPlaybackState().getValue(); - - if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) { - // Same audio - if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) { - viewModel.pause(msgId, audioUri); - } else { - viewModel.play(msgId, audioUri); - } - } else { - // Different audio - // Note: they can be the same *physical* file, but in different messages - viewModel.loadAudioAndPlay(msgId, audioUri); - } - - if (listener != null) { - listener.onPlayPauseButtonClicked(v); - } - }); - - seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - AudioView.this.progress = progress; - updateTimestampsAndSeekBar(); - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - viewModel.setUserSeeking(true); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - viewModel.setUserSeeking(false); - viewModel.seekTo(seekBar.getProgress(), msgId, audioUri); - } - }); + playPauseButton.setOnClickListener( + v -> { + Log.w(TAG, "playPauseButton onClick"); + + if (viewModel == null || audioUri == null) return; + + AudioPlaybackState state = viewModel.getPlaybackState().getValue(); + + if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) { + // Same audio + if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) { + viewModel.pause(msgId, audioUri); + } else { + viewModel.play(msgId, audioUri); + } + } else { + // Different audio + // Note: they can be the same *physical* file, but in different messages + viewModel.loadAudioAndPlay(msgId, audioUri); + } + + if (listener != null) { + listener.onPlayPauseButtonClicked(v); + } + }); + + seekBar.setOnSeekBarChangeListener( + new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + AudioView.this.progress = progress; + updateTimestampsAndSeekBar(); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + viewModel.setUserSeeking(true); + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + viewModel.setUserSeeking(false); + viewModel.seekTo(seekBar.getProgress(), msgId, audioUri); + } + }); if (playToPauseDrawable != null) { playToPauseDrawable.registerAnimationCallback(animationCallback); @@ -188,8 +187,7 @@ public void setPlaybackViewModel(AudioPlaybackViewModel viewModel) { } } - public void setAudio(final @NonNull AudioSlide audio) - { + public void setAudio(final @NonNull AudioSlide audio) { msgId = audio.getDcMsgId(); audioUri = audio.getUri(); playPauseButton.setImageDrawable(playDrawable); @@ -205,10 +203,9 @@ public void setAudio(final @NonNull AudioSlide audio) viewModel.ensureDurationLoaded(getContext(), msgId, audioUri); } - if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) { + if (audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) { title.setVisibility(View.GONE); - } - else { + } else { title.setText(audio.getFileName().get()); title.setVisibility(View.VISIBLE); } @@ -256,7 +253,7 @@ public String getDescription() { } desc += "\n" + this.timestamp.getText(); if (title.getVisibility() == View.VISIBLE) { - desc += "\n" + this.title.getText(); + desc += "\n" + this.title.getText(); } return desc; } @@ -273,7 +270,7 @@ private void updateProgress(AudioPlaybackState state) { } public void disablePlayer(boolean disable) { - this.mask.setVisibility(disable? View.VISIBLE : View.GONE); + this.mask.setVisibility(disable ? View.VISIBLE : View.GONE); } public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) { @@ -290,9 +287,10 @@ private void togglePlayPause(boolean expectedPlaying) { isAnimating = ((AnimatedVectorDrawableCompat) currentDrawable).isRunning(); } if (!isAnimating && playPauseButton.getDrawable() != expectedDrawable) { - AnimatedVectorDrawableCompat animDrawable = expectedPlaying ? playToPauseDrawable : pauseToPlayDrawable; - String contentDescription = getContext().getString( - expectedPlaying ? R.string.menu_pause : R.string.menu_play); + AnimatedVectorDrawableCompat animDrawable = + expectedPlaying ? playToPauseDrawable : pauseToPlayDrawable; + String contentDescription = + getContext().getString(expectedPlaying ? R.string.menu_pause : R.string.menu_play); if (animDrawable != null) { playPauseButton.setImageDrawable(animDrawable); @@ -343,10 +341,12 @@ private void onDurationsChanged(Map durations) { AudioPlaybackState state = viewModel.getPlaybackState().getValue(); // When there is no playback happening, msgId can be -1 and audioUri is null - if (state != null && - msgId >= 0 && msgId == state.getMsgId() && - audioUri != null && audioUri.equals(state.getAudioUri())) { - return; // Is playing this message + if (state != null + && msgId >= 0 + && msgId == state.getMsgId() + && audioUri != null + && audioUri.equals(state.getAudioUri())) { + return; // Is playing this message } Long duration = durations.get(msgId); diff --git a/src/main/java/org/thoughtcrime/securesms/components/emoji/AutoScaledEmojiTextView.java b/src/main/java/org/thoughtcrime/securesms/components/emoji/AutoScaledEmojiTextView.java index 8ab53699d..7517c1ad8 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/emoji/AutoScaledEmojiTextView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/emoji/AutoScaledEmojiTextView.java @@ -4,16 +4,12 @@ import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.TypedValue; - import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatTextView; - -import org.thoughtcrime.securesms.util.ViewUtil; - import java.text.BreakIterator; import java.util.Locale; import java.util.regex.Pattern; - +import org.thoughtcrime.securesms.util.ViewUtil; public class AutoScaledEmojiTextView extends AppCompatTextView { @@ -22,7 +18,9 @@ public class AutoScaledEmojiTextView extends AppCompatTextView { with spaces, *, # and 0-9 removed and the corresponding emojis added explicitly to avoid matching normal text with such characters */ - private static final Pattern emojiRegex = Pattern.compile("([🏻-🏿😀😃😄😁😆😅🤣😂🙂🙃🫠😉😊😇🥰😍🤩😘😗☺😚😙🥲😋😛😜🤪😝🤑🤗🤭🫢🫣🤫🤔🫡🤐🤨😐😑😶🫥😏😒🙄😬🤥🫨😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕🫤😟🙁☹😮😯😲😳🥺🥹😦-😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀☠💩🤡👹-👻👽👾🤖😺😸😹😻-😽🙀😿😾🙈-🙊💌💘💝💖💗💓💞💕💟❣💔❤🩷🧡💛💚💙🩵💜🤎🖤🩶🤍💋💯💢💥💫💦💨🕳💬🗨🗯💭💤👋🤚🖐✋🖖🫱-🫴🫷🫸👌🤌🤏✌🤞🫰🤟🤘🤙👈👉👆🖕👇☝🫵👍👎✊👊🤛🤜👏🙌🫶👐🤲🤝🙏✍💅🤳💪🦾🦿🦵🦶👂🦻👃🧠🫀🫁🦷🦴👀👁👅👄🫦👶🧒👦👧🧑👱👨🧔👩🧓👴👵🙍🙎🙅🙆💁🙋🧏🙇🤦🤷👮🕵💂🥷👷🫅🤴👸👳👲🧕🤵👰🤰🫃🫄🤱👼🎅🤶🦸🦹🧙-🧟🧌💆💇🚶🧍🧎🏃💃🕺🕴👯🧖🧗🤺🏇⛷🏂🏌🏄🚣🏊⛹🏋🚴🚵🤸🤼-🤾🤹🧘🛀🛌👭👫👬💏💑🗣👤👥🫂👪👣🦰🦱🦳🦲🐵🐒🦍🦧🐶🐕🦮🐩🐺🦊🦝🐱🐈🦁🐯🐅🐆🐴🫎🫏🐎🦄🦓🦌🦬🐮🐂-🐄🐷🐖🐗🐽🐏🐑🐐🐪🐫🦙🦒🐘🦣🦏🦛🐭🐁🐀🐹🐰🐇🐿🦫🦔🦇🐻🐨🐼🦥🦦🦨🦘🦡🐾🦃🐔🐓🐣-🐧🕊🦅🦆🦢🦉🦤🪶🦩🦚🦜🪽🪿🐸🐊🐢🦎🐍🐲🐉🦕🦖🐳🐋🐬🦭🐟-🐡🦈🐙🐚🪸🪼🐌🦋🐛-🐝🪲🐞🦗🪳🕷🕸🦂🦟🪰🪱🦠💐🌸💮🪷🏵🌹🥀🌺-🌼🌷🪻🌱🪴🌲-🌵🌾🌿☘🍀-🍃🪹🪺🍄🍇-🍍🥭🍎-🍓🫐🥝🍅🫒🥥🥑🍆🥔🥕🌽🌶🫑🥒🥬🥦🧄🧅🥜🫘🌰🫚🫛🍞🥐🥖🫓🥨🥯🥞🧇🧀🍖🍗🥩🥓🍔🍟🍕🌭🥪🌮🌯🫔🥙🧆🥚🍳🥘🍲🫕🥣🥗🍿🧈🧂🥫🍱🍘-🍝🍠🍢-🍥🥮🍡🥟-🥡🦀🦞🦐🦑🦪🍦-🍪🎂🍰🧁🥧🍫-🍯🍼🥛☕🫖🍵🍶🍾🍷-🍻🥂🥃🫗🥤🧋🧃🧉🧊🥢🍽🍴🥄🔪🫙🏺🌍-🌐🗺🗾🧭🏔⛰🌋🗻🏕🏖🏜-🏟🏛🏗🧱🪨🪵🛖🏘🏚🏠-🏦🏨-🏭🏯🏰💒🗼🗽⛪🕌🛕🕍⛩🕋⛲⛺🌁🌃🏙🌄-🌇🌉♨🎠🛝🎡🎢💈🎪🚂-🚊🚝🚞🚋-🚎🚐-🚙🛻🚚-🚜🏎🏍🛵🦽🦼🛺🚲🛴🛹🛼🚏🛣🛤🛢⛽🛞🚨🚥🚦🛑🚧⚓🛟⛵🛶🚤🛳⛴🛥🚢✈🛩🛫🛬🪂💺🚁🚟-🚡🛰🚀🛸🛎🧳⌛⏳⌚⏰-⏲🕰🕛🕧🕐🕜🕑🕝🕒🕞🕓🕟🕔🕠🕕🕡🕖🕢🕗🕣🕘🕤🕙🕥🕚🕦🌑-🌜🌡☀🌝🌞🪐⭐🌟🌠🌌☁⛅⛈🌤-🌬🌀🌈🌂☂☔⛱⚡❄☃⛄☄🔥💧🌊🎃🎄🎆🎇🧨✨🎈-🎋🎍-🎑🧧🎀🎁🎗🎟🎫🎖🏆🏅🥇-🥉⚽⚾🥎🏀🏐🏈🏉🎾🥏🎳🏏🏑🏒🥍🏓🏸🥊🥋🥅⛳⛸🎣🤿🎽🎿🛷🥌🎯🪀🪁🔫🎱🔮🪄🎮🕹🎰🎲🧩🧸🪅🪩🪆♠♥♦♣♟🃏🀄🎴🎭🖼🎨🧵🪡🧶🪢👓🕶🥽🥼🦺👔-👖🧣-🧦👗👘🥻🩱-🩳👙👚🪭👛-👝🛍🎒🩴👞👟🥾🥿👠👡🩰👢🪮👑👒🎩🎓🧢🪖⛑📿💄💍💎🔇-🔊📢📣📯🔔🔕🎼🎵🎶🎙-🎛🎤🎧📻🎷🪗🎸-🎻🪕🥁🪘🪇🪈📱📲☎📞-📠🔋🪫🔌💻🖥🖨⌨🖱🖲💽-📀🧮🎥🎞📽🎬📺📷-📹📼🔍🔎🕯💡🔦🏮🪔📔-📚📓📒📃📜📄📰🗞📑🔖🏷💰🪙💴-💸💳🧾💹✉📧-📩📤-📦📫📪📬-📮🗳✏✒🖋🖊🖌🖍📝💼📁📂🗂📅📆🗒🗓📇-📎🖇📏📐✂🗃🗄🗑🔒🔓🔏-🔑🗝🔨🪓⛏⚒🛠🗡⚔💣🪃🏹🛡🪚🔧🪛🔩⚙🗜⚖🦯🔗⛓🪝🧰🧲🪜⚗🧪-🧬🔬🔭📡💉🩸💊🩹🩼🩺🩻🚪🛗🪞🪟🛏🛋🪑🚽🪠🚿🛁🪤🪒🧴🧷🧹-🧻🪣🧼🫧🪥🧽🧯🛒🚬⚰🪦⚱🧿🪬🗿🪧🪪🏧🚮🚰♿🚹-🚼🚾🛂-🛅⚠🚸⛔🚫🚳🚭🚯🚱🚷📵🔞☢☣⬆↗➡↘⬇↙⬅↖↕↔↩↪⤴⤵🔃🔄🔙-🔝🛐⚛🕉✡☸☯✝☦☪☮🕎🔯🪯♈-♓⛎🔀-🔂▶⏩⏭⏯◀⏪⏮🔼⏫🔽⏬⏸-⏺⏏🎦🔅🔆📶🛜📳📴♀♂⚧✖➕-➗🟰♾‼⁉❓-❕❗〰💱💲⚕♻⚜🔱📛🔰⭕✅☑✔❌❎➰➿〽✳✴❇©®™🔟-🔤🅰🆎🅱🆑-🆓ℹ🆔Ⓜ🆕🆖🅾🆗🅿🆘-🆚🈁🈂🈷🈶🈯🉐🈹🈚🈲🉑🈸🈴🈳㊗㊙🈺🈵🔴🟠-🟢🔵🟣🟤⚫⚪🟥🟧-🟩🟦🟪🟫⬛⬜◼◻◾◽▪▫🔶-🔻💠🔘🔳🔲🏁🚩🎌🏴🏳🇦-🇿\uD83E\uDD89\uD83E\uDD8F\uD83E\uDDBE\uD83E\uDDC6\uD83E\uddcd\uD83E\udddf\uD83E\ude99]|#️⃣|\\*️⃣|0️⃣|1️⃣|2️⃣|3️⃣|4️⃣|5️⃣|6️⃣|7️⃣|8️⃣|9️⃣)+.*"); + private static final Pattern emojiRegex = + Pattern.compile( + "([🏻-🏿😀😃😄😁😆😅🤣😂🙂🙃🫠😉😊😇🥰😍🤩😘😗☺😚😙🥲😋😛😜🤪😝🤑🤗🤭🫢🫣🤫🤔🫡🤐🤨😐😑😶🫥😏😒🙄😬🤥🫨😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕🫤😟🙁☹😮😯😲😳🥺🥹😦-😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀☠💩🤡👹-👻👽👾🤖😺😸😹😻-😽🙀😿😾🙈-🙊💌💘💝💖💗💓💞💕💟❣💔❤🩷🧡💛💚💙🩵💜🤎🖤🩶🤍💋💯💢💥💫💦💨🕳💬🗨🗯💭💤👋🤚🖐✋🖖🫱-🫴🫷🫸👌🤌🤏✌🤞🫰🤟🤘🤙👈👉👆🖕👇☝🫵👍👎✊👊🤛🤜👏🙌🫶👐🤲🤝🙏✍💅🤳💪🦾🦿🦵🦶👂🦻👃🧠🫀🫁🦷🦴👀👁👅👄🫦👶🧒👦👧🧑👱👨🧔👩🧓👴👵🙍🙎🙅🙆💁🙋🧏🙇🤦🤷👮🕵💂🥷👷🫅🤴👸👳👲🧕🤵👰🤰🫃🫄🤱👼🎅🤶🦸🦹🧙-🧟🧌💆💇🚶🧍🧎🏃💃🕺🕴👯🧖🧗🤺🏇⛷🏂🏌🏄🚣🏊⛹🏋🚴🚵🤸🤼-🤾🤹🧘🛀🛌👭👫👬💏💑🗣👤👥🫂👪👣🦰🦱🦳🦲🐵🐒🦍🦧🐶🐕🦮🐩🐺🦊🦝🐱🐈🦁🐯🐅🐆🐴🫎🫏🐎🦄🦓🦌🦬🐮🐂-🐄🐷🐖🐗🐽🐏🐑🐐🐪🐫🦙🦒🐘🦣🦏🦛🐭🐁🐀🐹🐰🐇🐿🦫🦔🦇🐻🐨🐼🦥🦦🦨🦘🦡🐾🦃🐔🐓🐣-🐧🕊🦅🦆🦢🦉🦤🪶🦩🦚🦜🪽🪿🐸🐊🐢🦎🐍🐲🐉🦕🦖🐳🐋🐬🦭🐟-🐡🦈🐙🐚🪸🪼🐌🦋🐛-🐝🪲🐞🦗🪳🕷🕸🦂🦟🪰🪱🦠💐🌸💮🪷🏵🌹🥀🌺-🌼🌷🪻🌱🪴🌲-🌵🌾🌿☘🍀-🍃🪹🪺🍄🍇-🍍🥭🍎-🍓🫐🥝🍅🫒🥥🥑🍆🥔🥕🌽🌶🫑🥒🥬🥦🧄🧅🥜🫘🌰🫚🫛🍞🥐🥖🫓🥨🥯🥞🧇🧀🍖🍗🥩🥓🍔🍟🍕🌭🥪🌮🌯🫔🥙🧆🥚🍳🥘🍲🫕🥣🥗🍿🧈🧂🥫🍱🍘-🍝🍠🍢-🍥🥮🍡🥟-🥡🦀🦞🦐🦑🦪🍦-🍪🎂🍰🧁🥧🍫-🍯🍼🥛☕🫖🍵🍶🍾🍷-🍻🥂🥃🫗🥤🧋🧃🧉🧊🥢🍽🍴🥄🔪🫙🏺🌍-🌐🗺🗾🧭🏔⛰🌋🗻🏕🏖🏜-🏟🏛🏗🧱🪨🪵🛖🏘🏚🏠-🏦🏨-🏭🏯🏰💒🗼🗽⛪🕌🛕🕍⛩🕋⛲⛺🌁🌃🏙🌄-🌇🌉♨🎠🛝🎡🎢💈🎪🚂-🚊🚝🚞🚋-🚎🚐-🚙🛻🚚-🚜🏎🏍🛵🦽🦼🛺🚲🛴🛹🛼🚏🛣🛤🛢⛽🛞🚨🚥🚦🛑🚧⚓🛟⛵🛶🚤🛳⛴🛥🚢✈🛩🛫🛬🪂💺🚁🚟-🚡🛰🚀🛸🛎🧳⌛⏳⌚⏰-⏲🕰🕛🕧🕐🕜🕑🕝🕒🕞🕓🕟🕔🕠🕕🕡🕖🕢🕗🕣🕘🕤🕙🕥🕚🕦🌑-🌜🌡☀🌝🌞🪐⭐🌟🌠🌌☁⛅⛈🌤-🌬🌀🌈🌂☂☔⛱⚡❄☃⛄☄🔥💧🌊🎃🎄🎆🎇🧨✨🎈-🎋🎍-🎑🧧🎀🎁🎗🎟🎫🎖🏆🏅🥇-🥉⚽⚾🥎🏀🏐🏈🏉🎾🥏🎳🏏🏑🏒🥍🏓🏸🥊🥋🥅⛳⛸🎣🤿🎽🎿🛷🥌🎯🪀🪁🔫🎱🔮🪄🎮🕹🎰🎲🧩🧸🪅🪩🪆♠♥♦♣♟🃏🀄🎴🎭🖼🎨🧵🪡🧶🪢👓🕶🥽🥼🦺👔-👖🧣-🧦👗👘🥻🩱-🩳👙👚🪭👛-👝🛍🎒🩴👞👟🥾🥿👠👡🩰👢🪮👑👒🎩🎓🧢🪖⛑📿💄💍💎🔇-🔊📢📣📯🔔🔕🎼🎵🎶🎙-🎛🎤🎧📻🎷🪗🎸-🎻🪕🥁🪘🪇🪈📱📲☎📞-📠🔋🪫🔌💻🖥🖨⌨🖱🖲💽-📀🧮🎥🎞📽🎬📺📷-📹📼🔍🔎🕯💡🔦🏮🪔📔-📚📓📒📃📜📄📰🗞📑🔖🏷💰🪙💴-💸💳🧾💹✉📧-📩📤-📦📫📪📬-📮🗳✏✒🖋🖊🖌🖍📝💼📁📂🗂📅📆🗒🗓📇-📎🖇📏📐✂🗃🗄🗑🔒🔓🔏-🔑🗝🔨🪓⛏⚒🛠🗡⚔💣🪃🏹🛡🪚🔧🪛🔩⚙🗜⚖🦯🔗⛓🪝🧰🧲🪜⚗🧪-🧬🔬🔭📡💉🩸💊🩹🩼🩺🩻🚪🛗🪞🪟🛏🛋🪑🚽🪠🚿🛁🪤🪒🧴🧷🧹-🧻🪣🧼🫧🪥🧽🧯🛒🚬⚰🪦⚱🧿🪬🗿🪧🪪🏧🚮🚰♿🚹-🚼🚾🛂-🛅⚠🚸⛔🚫🚳🚭🚯🚱🚷📵🔞☢☣⬆↗➡↘⬇↙⬅↖↕↔↩↪⤴⤵🔃🔄🔙-🔝🛐⚛🕉✡☸☯✝☦☪☮🕎🔯🪯♈-♓⛎🔀-🔂▶⏩⏭⏯◀⏪⏮🔼⏫🔽⏬⏸-⏺⏏🎦🔅🔆📶🛜📳📴♀♂⚧✖➕-➗🟰♾‼⁉❓-❕❗〰💱💲⚕♻⚜🔱📛🔰⭕✅☑✔❌❎➰➿〽✳✴❇©®™🔟-🔤🅰🆎🅱🆑-🆓ℹ🆔Ⓜ🆕🆖🅾🆗🅿🆘-🆚🈁🈂🈷🈶🈯🉐🈹🈚🈲🉑🈸🈴🈳㊗㊙🈺🈵🔴🟠-🟢🔵🟣🟤⚫⚪🟥🟧-🟩🟦🟪🟫⬛⬜◼◻◾◽▪▫🔶-🔻💠🔘🔳🔲🏁🚩🎌🏴🏳🇦-🇿\uD83E\uDD89\uD83E\uDD8F\uD83E\uDDBE\uD83E\uDDC6\uD83E\uddcd\uD83E\udddf\uD83E\ude99]|#️⃣|\\*️⃣|0️⃣|1️⃣|2️⃣|3️⃣|4️⃣|5️⃣|6️⃣|7️⃣|8️⃣|9️⃣)+.*"); private float originalFontSize; public AutoScaledEmojiTextView(Context context) { @@ -35,7 +33,8 @@ public AutoScaledEmojiTextView(Context context, AttributeSet attrs) { public AutoScaledEmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - try (TypedArray typedArray = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize})) { + try (TypedArray typedArray = + context.obtainStyledAttributes(attrs, new int[] {android.R.attr.textSize})) { originalFontSize = ViewUtil.pxToSp(context, typedArray.getDimensionPixelSize(0, 0)); if (originalFontSize == 0) { originalFontSize = 16f; @@ -98,7 +97,9 @@ public static int countEmojis(String text, int max) { // Iterate over the text and count graphemes int start = graphemeIterator.first(); - for (int end = graphemeIterator.next(); end != BreakIterator.DONE; start = end, end = graphemeIterator.next()) { + for (int end = graphemeIterator.next(); + end != BreakIterator.DONE; + start = end, end = graphemeIterator.next()) { String grapheme = text.substring(start, end); if (!emojiRegex.matcher(grapheme).matches()) return -1; if (++graphemeCount > max) return -1; diff --git a/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java b/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java index 851796687..9723e6770 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java +++ b/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java @@ -3,21 +3,18 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; - import androidx.appcompat.widget.AppCompatImageButton; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ResUtil; public class EmojiToggle extends AppCompatImageButton { private Drawable emojiToggle; -// private Drawable stickerToggle; + // private Drawable stickerToggle; private Drawable mediaToggle; private Drawable imeToggle; - public EmojiToggle(Context context) { super(context); initialize(); @@ -42,10 +39,11 @@ public void setToIme() { } private void initialize() { - this.emojiToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_emoji_toggle); -// this.stickerToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_sticker_toggle); - this.imeToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_keyboard_toggle); - this.mediaToggle = emojiToggle; + this.emojiToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_emoji_toggle); + // this.stickerToggle = ResUtil.getDrawable(getContext(), + // R.attr.conversation_sticker_toggle); + this.imeToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_keyboard_toggle); + this.mediaToggle = emojiToggle; setToMedia(); } @@ -59,7 +57,7 @@ public void setStickerMode(boolean stickerMode) { } public boolean isStickerMode() { - //return this.mediaToggle == stickerToggle; + // return this.mediaToggle == stickerToggle; return false; } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java b/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java index 1f83d3c56..0eb641118 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java +++ b/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java @@ -8,22 +8,19 @@ import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Consumer; import androidx.emoji2.emojipicker.EmojiPickerView; import androidx.emoji2.emojipicker.EmojiViewItem; - import com.google.android.material.tabs.TabLayout; - +import java.io.File; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.InputAwareLayout.InputView; import org.thoughtcrime.securesms.util.ResUtil; -import java.io.File; - -public class MediaKeyboard extends LinearLayout implements InputView, Consumer, StickerPickerView.StickerPickerListener { +public class MediaKeyboard extends LinearLayout + implements InputView, Consumer, StickerPickerView.StickerPickerListener { private static final String TAG = MediaKeyboard.class.getSimpleName(); @@ -65,24 +62,23 @@ public void onFinishInflate() { tabLayout.addTab(tabLayout.newTab().setIcon(emojiIcon)); tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_sticker_24)); - tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { - @Override - public void onTabSelected(TabLayout.Tab tab) { - if (tab.getPosition() == 0) { - showEmojiPicker(); - } else { - showStickerPicker(); - } - } - - @Override - public void onTabUnselected(TabLayout.Tab tab) { - } - - @Override - public void onTabReselected(TabLayout.Tab tab) { - } - }); + tabLayout.addOnTabSelectedListener( + new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + if (tab.getPosition() == 0) { + showEmojiPicker(); + } else { + showStickerPicker(); + } + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) {} + + @Override + public void onTabReselected(TabLayout.Tab tab) {} + }); } } @@ -134,7 +130,8 @@ private void showStickerPicker() { private void updateStickerEmptyState() { if (stickerPicker != null && stickerPickerEmpty != null) { - boolean hasStickers = stickerPicker.getAdapter() != null && stickerPicker.getAdapter().getItemCount() > 0; + boolean hasStickers = + stickerPicker.getAdapter() != null && stickerPicker.getAdapter().getItemCount() > 0; stickerPickerEmpty.setVisibility(hasStickers ? View.GONE : View.VISIBLE); } } @@ -172,8 +169,11 @@ public void onStickerDeleted(@NonNull File stickerFile) { public interface MediaKeyboardListener { void onShown(); + void onHidden(); + void onEmojiPicked(String emoji); + void onStickerPicked(Uri stickerUri); } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java b/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java index 8e2b030fa..80c1254ec 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java @@ -6,22 +6,18 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.bumptech.glide.load.engine.DiskCacheStrategy; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.mms.GlideRequests; - import java.io.File; -import java.util.Collections; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.mms.GlideRequests; public class StickerPickerView extends RecyclerView { @@ -48,17 +44,17 @@ private void init(Context context) { public void setStickerPickerListener(@Nullable StickerPickerListener listener) { assert getAdapter() != null; - ((StickerAdapter)getAdapter()).setStickerPickerListener(listener); + ((StickerAdapter) getAdapter()).setStickerPickerListener(listener); } public void loadStickers() { assert getAdapter() != null; - ((StickerAdapter)getAdapter()).changeData(getSavedStickers()); + ((StickerAdapter) getAdapter()).changeData(getSavedStickers()); } private List getSavedStickers() { List stickerFiles = new ArrayList<>(); - + if (!stickerDir.exists()) { return stickerFiles; } @@ -80,6 +76,7 @@ private List getSavedStickers() { public interface StickerPickerListener { void onStickerSelected(@NonNull File stickerFile); + void onStickerDeleted(@NonNull File stickerFile); } @@ -117,9 +114,7 @@ public void onBindViewHolder(@NonNull StickerViewHolder holder, int position) { File stickerFile = stickerFiles.get(position); holder.stickerFile = stickerFile; - glideRequests.load(stickerFile) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(holder.image); + glideRequests.load(stickerFile).diskCacheStrategy(DiskCacheStrategy.NONE).into(holder.image); } @Override @@ -136,22 +131,24 @@ public void onViewRecycled(@NonNull StickerViewHolder holder) { private void deleteSticker(File stickerFile) { if (stickerFile != null && stickerFile.exists()) { new androidx.appcompat.app.AlertDialog.Builder(context) - .setTitle(R.string.delete) - .setMessage(R.string.ask_delete_sticker) - .setPositiveButton(R.string.delete, (dialog, which) -> { - if (stickerFile.delete()) { - int position = stickerFiles.indexOf(stickerFile); - if (position >= 0) { - stickerFiles.remove(position); - notifyItemRemoved(position); - } - if (listener != null) { - listener.onStickerDeleted(stickerFile); - } - } - }) - .setNegativeButton(R.string.cancel, null) - .show(); + .setTitle(R.string.delete) + .setMessage(R.string.ask_delete_sticker) + .setPositiveButton( + R.string.delete, + (dialog, which) -> { + if (stickerFile.delete()) { + int position = stickerFiles.indexOf(stickerFile); + if (position >= 0) { + stickerFiles.remove(position); + notifyItemRemoved(position); + } + if (listener != null) { + listener.onStickerDeleted(stickerFile); + } + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); } } @@ -163,18 +160,20 @@ class StickerViewHolder extends RecyclerView.ViewHolder { StickerViewHolder(View itemView) { super(itemView); image = itemView.findViewById(R.id.sticker_image); - itemView.setOnClickListener(view -> { - if (listener != null && stickerFile != null) { - listener.onStickerSelected(stickerFile); - } - }); - itemView.setOnLongClickListener(view -> { - if (stickerFile != null) { - deleteSticker(stickerFile); - return true; - } - return false; - }); + itemView.setOnClickListener( + view -> { + if (listener != null && stickerFile != null) { + listener.onStickerSelected(stickerFile); + } + }); + itemView.setOnLongClickListener( + view -> { + if (stickerFile != null) { + deleteSticker(stickerFile); + return true; + } + return false; + }); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/recyclerview/DeleteItemAnimator.java b/src/main/java/org/thoughtcrime/securesms/components/recyclerview/DeleteItemAnimator.java index 4fc116565..0ab876f8f 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/recyclerview/DeleteItemAnimator.java +++ b/src/main/java/org/thoughtcrime/securesms/components/recyclerview/DeleteItemAnimator.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.components.recyclerview; - import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.RecyclerView; @@ -17,10 +16,9 @@ public boolean animateAdd(RecyclerView.ViewHolder viewHolder) { } @Override - public boolean animateMove(RecyclerView.ViewHolder viewHolder, int fromX, int fromY, int toX, int toY) { + public boolean animateMove( + RecyclerView.ViewHolder viewHolder, int fromX, int fromY, int toX, int toY) { dispatchMoveFinished(viewHolder); return false; } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/registration/PulsingFloatingActionButton.java b/src/main/java/org/thoughtcrime/securesms/components/registration/PulsingFloatingActionButton.java index 9a0360d3a..6ca2f0159 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/registration/PulsingFloatingActionButton.java +++ b/src/main/java/org/thoughtcrime/securesms/components/registration/PulsingFloatingActionButton.java @@ -1,13 +1,10 @@ package org.thoughtcrime.securesms.components.registration; - import android.animation.Animator; import android.content.Context; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import android.util.AttributeSet; - import androidx.annotation.NonNull; - +import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; public class PulsingFloatingActionButton extends FloatingActionButton { @@ -40,18 +37,30 @@ public void stopPulse() { private void pulse(long periodMillis) { if (!pulsing) return; - this.animate().scaleX(1.2f).scaleY(1.2f).setDuration(150).setListener(new AnimationCompleteListener() { - @Override - public void onAnimationEnd(@NonNull Animator animation) { - clearAnimation(); - animate().scaleX(1.0f).scaleY(1.0f).setDuration(150).setListener(new AnimationCompleteListener() { - @Override - public void onAnimationEnd(@NonNull Animator animation) { - PulsingFloatingActionButton.this.postDelayed(() -> pulse(periodMillis), periodMillis); - } - }).start(); - } - }).start(); + this.animate() + .scaleX(1.2f) + .scaleY(1.2f) + .setDuration(150) + .setListener( + new AnimationCompleteListener() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + clearAnimation(); + animate() + .scaleX(1.0f) + .scaleY(1.0f) + .setDuration(150) + .setListener( + new AnimationCompleteListener() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + PulsingFloatingActionButton.this.postDelayed( + () -> pulse(periodMillis), periodMillis); + } + }) + .start(); + } + }) + .start(); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java b/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java index f0c2a57ad..a34d8b408 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java +++ b/src/main/java/org/thoughtcrime/securesms/components/reminder/DozeReminder.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.components.reminder; - import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; @@ -11,14 +10,11 @@ import android.os.PowerManager; import android.provider.Settings; import android.util.Log; - import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; @@ -30,25 +26,26 @@ public class DozeReminder { private static final String TAG = DozeReminder.class.getSimpleName(); public static boolean isEligible(Context context) { - if(context==null) { + if (context == null) { return false; } - if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return false; } - if(Prefs.getPrompteDozeMsgId(context)!=0) { + if (Prefs.getPrompteDozeMsgId(context) != 0) { return false; } - // If we did never ask directly, we do not want to add a device message yet. First we want to try to ask directly. + // If we did never ask directly, we do not want to add a device message yet. First we want to + // try to ask directly. if (!Prefs.getBooleanPreference(context, Prefs.DOZE_ASKED_DIRECTLY, false)) { return false; } - PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); - if(pm.isIgnoringBatteryOptimizations(context.getPackageName())) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (pm.isIgnoringBatteryOptimizations(context.getPackageName())) { return false; } @@ -59,21 +56,24 @@ public static boolean isEligible(Context context) { if (numberOfChats <= 2) { return false; } - } - catch(Exception e) { + } catch (Exception e) { Log.e(TAG, "Error calling getChatlist()", e); } - return !isPushAvailableAndSufficient(); // yip, asking for disabling battery optimisations makes sense + return !isPushAvailableAndSufficient(); // yip, asking for disabling battery optimisations makes + // sense } public static void addDozeReminderDeviceMsg(Context context) { DcContext dcContext = DcHelper.getContext(context); DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); - msg.setText("\uD83D\uDC49 "+context.getString(R.string.perm_enable_bg_reminder_title)+" \uD83D\uDC48\n\n" - +context.getString(R.string.pref_background_notifications_rationale)); + msg.setText( + "\uD83D\uDC49 " + + context.getString(R.string.perm_enable_bg_reminder_title) + + " \uD83D\uDC48\n\n" + + context.getString(R.string.pref_background_notifications_rationale)); int msgId = dcContext.addDeviceMsg("android.doze-reminder", msg); - if(msgId!=0) { + if (msgId != 0) { Prefs.setPromptedDozeMsgId(context, msgId); } } @@ -85,25 +85,28 @@ public static boolean isDozeReminderMsg(Context context, DcMsg msg) { } public static void dozeReminderTapped(Context context) { - if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return; } - PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); - if(pm.isIgnoringBatteryOptimizations(context.getPackageName())) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (pm.isIgnoringBatteryOptimizations(context.getPackageName())) { new AlertDialog.Builder(context) - .setMessage(R.string.perm_enable_bg_already_done) - .setPositiveButton(android.R.string.ok, null) - .show(); + .setMessage(R.string.perm_enable_bg_already_done) + .setPositiveButton(android.R.string.ok, null) + .show(); return; } - if(ContextCompat.checkSelfPermission(context, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)==PackageManager.PERMISSION_GRANTED) { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + if (ContextCompat.checkSelfPermission( + context, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + == PackageManager.PERMISSION_GRANTED) { + Intent intent = + new Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:" + context.getPackageName())); context.startActivity(intent); - } - else { + } else { Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); context.startActivity(intent); } @@ -111,7 +114,7 @@ public static void dozeReminderTapped(Context context) { private static boolean isPushAvailableAndSufficient() { return ApplicationContext.getDcAccounts().isAllChatmail() - && FcmReceiveService.getToken() != null; + && FcmReceiveService.getToken() != null; } public static void maybeAskDirectly(Context context) { @@ -122,16 +125,23 @@ public static void maybeAskDirectly(Context context) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && !Prefs.getBooleanPreference(context, Prefs.DOZE_ASKED_DIRECTLY, false) - && ContextCompat.checkSelfPermission(context, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_GRANTED - && !((PowerManager) context.getSystemService(Context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName())) { + && ContextCompat.checkSelfPermission( + context, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + == PackageManager.PERMISSION_GRANTED + && !((PowerManager) context.getSystemService(Context.POWER_SERVICE)) + .isIgnoringBatteryOptimizations(context.getPackageName())) { new AlertDialog.Builder(context) .setTitle(R.string.pref_background_notifications) .setMessage(R.string.pref_background_notifications_rationale) - .setPositiveButton(R.string.perm_continue, (dialog, which) -> { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - Uri.parse("package:" + context.getPackageName())); + .setPositiveButton( + R.string.perm_continue, + (dialog, which) -> { + Intent intent = + new Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + context.getPackageName())); context.startActivity(intent); - }) + }) .setCancelable(false) .show(); } @@ -139,7 +149,7 @@ public static void maybeAskDirectly(Context context) { // As long as Prefs.DOZE_ASKED_DIRECTLY is false, isEligible() will return false // and no device message will be added. Prefs.setBooleanPreference(context, Prefs.DOZE_ASKED_DIRECTLY, true); - } catch(Exception e) { + } catch (Exception e) { Log.e(TAG, "Error in maybeAskDirectly()", e); } } diff --git a/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentBitmapDecoder.java b/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentBitmapDecoder.java index 27b133f67..64372c9ad 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentBitmapDecoder.java +++ b/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentBitmapDecoder.java @@ -1,19 +1,15 @@ package org.thoughtcrime.securesms.components.subsampling; - import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; - import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder; import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder; - -import org.thoughtcrime.securesms.mms.PartAuthority; - import java.io.InputStream; +import org.thoughtcrime.securesms.mms.PartAuthority; -public class AttachmentBitmapDecoder implements ImageDecoder{ +public class AttachmentBitmapDecoder implements ImageDecoder { public AttachmentBitmapDecoder() {} @@ -30,12 +26,11 @@ public Bitmap decode(Context context, Uri uri) throws Exception { Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); if (bitmap == null) { - throw new RuntimeException("Skia image region decoder returned null bitmap - image format may not be supported"); + throw new RuntimeException( + "Skia image region decoder returned null bitmap - image format may not be supported"); } return bitmap; } } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentRegionDecoder.java b/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentRegionDecoder.java index cf357f6f1..4c89942ad 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentRegionDecoder.java +++ b/src/main/java/org/thoughtcrime/securesms/components/subsampling/AttachmentRegionDecoder.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.components.subsampling; - import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -9,13 +8,10 @@ import android.graphics.Rect; import android.net.Uri; import android.util.Log; - import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder; import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder; - -import org.thoughtcrime.securesms.mms.PartAuthority; - import java.io.InputStream; +import org.thoughtcrime.securesms.mms.PartAuthority; public class AttachmentRegionDecoder implements ImageRegionDecoder { @@ -49,15 +45,16 @@ public Bitmap decodeRegion(Rect rect, int sampleSize) { return passthrough.decodeRegion(rect, sampleSize); } - synchronized(this) { + synchronized (this) { BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = sampleSize; + options.inSampleSize = sampleSize; options.inPreferredConfig = Bitmap.Config.RGB_565; Bitmap bitmap = bitmapRegionDecoder.decodeRegion(rect, options); if (bitmap == null) { - throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported"); + throw new RuntimeException( + "Skia image decoder returned null bitmap - image format may not be supported"); } return bitmap; @@ -66,8 +63,8 @@ public Bitmap decodeRegion(Rect rect, int sampleSize) { public boolean isReady() { Log.w(TAG, "isReady"); - return (passthrough != null && passthrough.isReady()) || - (bitmapRegionDecoder != null && !bitmapRegionDecoder.isRecycled()); + return (passthrough != null && passthrough.isReady()) + || (bitmapRegionDecoder != null && !bitmapRegionDecoder.isRecycled()); } public void recycle() { diff --git a/src/main/java/org/thoughtcrime/securesms/components/viewpager/ExtendedOnPageChangedListener.java b/src/main/java/org/thoughtcrime/securesms/components/viewpager/ExtendedOnPageChangedListener.java index ae679ddce..8c7222047 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/viewpager/ExtendedOnPageChangedListener.java +++ b/src/main/java/org/thoughtcrime/securesms/components/viewpager/ExtendedOnPageChangedListener.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.components.viewpager; - import androidx.viewpager.widget.ViewPager; public abstract class ExtendedOnPageChangedListener implements ViewPager.OnPageChangeListener { @@ -8,9 +7,7 @@ public abstract class ExtendedOnPageChangedListener implements ViewPager.OnPageC private Integer currentPage = null; @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - - } + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} @Override public void onPageSelected(int position) { @@ -21,9 +18,5 @@ public void onPageSelected(int position) { public abstract void onPageUnselected(int position); @Override - public void onPageScrollStateChanged(int state) { - - } - - + public void onPageScrollStateChanged(int state) {} } diff --git a/src/main/java/org/thoughtcrime/securesms/components/viewpager/HackyViewPager.java b/src/main/java/org/thoughtcrime/securesms/components/viewpager/HackyViewPager.java index a170b7a74..82e2e92f4 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/viewpager/HackyViewPager.java +++ b/src/main/java/org/thoughtcrime/securesms/components/viewpager/HackyViewPager.java @@ -1,21 +1,19 @@ package org.thoughtcrime.securesms.components.viewpager; - import android.content.Context; -import androidx.viewpager.widget.ViewPager; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; +import androidx.viewpager.widget.ViewPager; /** * Hacky fix for http://code.google.com/p/android/issues/detail?id=18990 - *

- * ScaleGestureDetector seems to mess up the touch events, which means that - * ViewGroups which make use of onInterceptTouchEvent throw a lot of - * IllegalArgumentException: pointerIndex out of range. - *

- * There's not much I can do in my code for now, but we can mask the result by - * just catching the problem and ignoring it. + * + *

ScaleGestureDetector seems to mess up the touch events, which means that ViewGroups which make + * use of onInterceptTouchEvent throw a lot of IllegalArgumentException: pointerIndex out of range. + * + *

There's not much I can do in my code for now, but we can mask the result by just catching the + * problem and ignoring it. * * @author Chris Banes */ @@ -40,4 +38,4 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java b/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java index de8901b52..755a52ffc 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/AccountManager.java @@ -4,154 +4,157 @@ import android.content.Context; import android.content.Intent; import android.util.Log; - import androidx.fragment.app.FragmentActivity; import androidx.preference.PreferenceManager; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcAccounts; import com.b44t.messenger.DcContext; - +import java.io.File; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.WelcomeActivity; import org.thoughtcrime.securesms.accounts.AccountSelectionListFragment; -import java.io.File; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - public class AccountManager { - private static final String TAG = AccountManager.class.getSimpleName(); - private static final String LAST_ACCOUNT_ID = "last_account_id"; - private static AccountManager self; + private static final String TAG = AccountManager.class.getSimpleName(); + private static final String LAST_ACCOUNT_ID = "last_account_id"; + private static AccountManager self; - private void resetDcContext(Context context) { - ApplicationContext appContext = (ApplicationContext)context.getApplicationContext(); - appContext.setDcContext(ApplicationContext.getDcAccounts().getSelectedAccount()); - DcHelper.setStockTranslations(context); - DirectShareUtil.resetAllShortcuts(appContext); - } + private void resetDcContext(Context context) { + ApplicationContext appContext = (ApplicationContext) context.getApplicationContext(); + appContext.setDcContext(ApplicationContext.getDcAccounts().getSelectedAccount()); + DcHelper.setStockTranslations(context); + DirectShareUtil.resetAllShortcuts(appContext); + } + // public api - // public api - - public static AccountManager getInstance() { - if (self == null) { - self = new AccountManager(); - } - return self; + public static AccountManager getInstance() { + if (self == null) { + self = new AccountManager(); } - - public void migrateToDcAccounts(ApplicationContext context) { - try { - int selectAccountId = 0; - - File[] files = context.getFilesDir().listFiles(); - if (files != null) { - for (File file : files) { - // old accounts have the pattern "messenger*.db" - if (!file.isDirectory() && file.getName().startsWith("messenger") && file.getName().endsWith(".db")) { - int accountId = ApplicationContext.getDcAccounts().migrateAccount(file.getAbsolutePath()); - if (accountId != 0) { - String selName = PreferenceManager.getDefaultSharedPreferences(context) - .getString("curr_account_db_name", "messenger.db"); - if (file.getName().equals(selName)) { - // postpone selection as it will otherwise be overwritten by the next migrateAccount() call - // (if more than one account needs to be migrated) - selectAccountId = accountId; - } - } - } + return self; + } + + public void migrateToDcAccounts(ApplicationContext context) { + try { + int selectAccountId = 0; + + File[] files = context.getFilesDir().listFiles(); + if (files != null) { + for (File file : files) { + // old accounts have the pattern "messenger*.db" + if (!file.isDirectory() + && file.getName().startsWith("messenger") + && file.getName().endsWith(".db")) { + int accountId = + ApplicationContext.getDcAccounts().migrateAccount(file.getAbsolutePath()); + if (accountId != 0) { + String selName = + PreferenceManager.getDefaultSharedPreferences(context) + .getString("curr_account_db_name", "messenger.db"); + if (file.getName().equals(selName)) { + // postpone selection as it will otherwise be overwritten by the next + // migrateAccount() call + // (if more than one account needs to be migrated) + selectAccountId = accountId; + } } } - - if (selectAccountId != 0) { - ApplicationContext.getDcAccounts().selectAccount(selectAccountId); - } - } catch (Exception e) { - Log.e(TAG, "Error in migrateToDcAccounts()", e); - } - } - - public void switchAccount(Context context, int accountId) { - DcHelper.getAccounts(context).selectAccount(accountId); - resetDcContext(context); - } - - - // add accounts - - public int beginAccountCreation(Context context) { - Rpc rpc = DcHelper.getRpc(context); - DcAccounts accounts = DcHelper.getAccounts(context); - DcContext selectedAccount = accounts.getSelectedAccount(); - if (selectedAccount.isOk()) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(LAST_ACCOUNT_ID, selectedAccount.getAccountId()).apply(); } + } - int id = 0; - try { - id = rpc.addAccount(); - } catch (RpcException e) { - Log.e(TAG, "Error calling rpc.addAccount()", e); + if (selectAccountId != 0) { + ApplicationContext.getDcAccounts().selectAccount(selectAccountId); } - resetDcContext(context); - return id; + } catch (Exception e) { + Log.e(TAG, "Error in migrateToDcAccounts()", e); + } + } + + public void switchAccount(Context context, int accountId) { + DcHelper.getAccounts(context).selectAccount(accountId); + resetDcContext(context); + } + + // add accounts + + public int beginAccountCreation(Context context) { + Rpc rpc = DcHelper.getRpc(context); + DcAccounts accounts = DcHelper.getAccounts(context); + DcContext selectedAccount = accounts.getSelectedAccount(); + if (selectedAccount.isOk()) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(LAST_ACCOUNT_ID, selectedAccount.getAccountId()) + .apply(); } - public boolean canRollbackAccountCreation(Context context) { - return DcHelper.getAccounts(context).getAll().length > 1; + int id = 0; + try { + id = rpc.addAccount(); + } catch (RpcException e) { + Log.e(TAG, "Error calling rpc.addAccount()", e); } + resetDcContext(context); + return id; + } - public void rollbackAccountCreation(Activity activity) { - DcAccounts accounts = DcHelper.getAccounts(activity); + public boolean canRollbackAccountCreation(Context context) { + return DcHelper.getAccounts(context).getAll().length > 1; + } - DcContext selectedAccount = accounts.getSelectedAccount(); - if (selectedAccount.isConfigured() == 0) { - accounts.removeAccount(selectedAccount.getAccountId()); - } + public void rollbackAccountCreation(Activity activity) { + DcAccounts accounts = DcHelper.getAccounts(activity); - int lastAccountId = PreferenceManager.getDefaultSharedPreferences(activity).getInt(LAST_ACCOUNT_ID, 0); - if (lastAccountId == 0 || !accounts.getAccount(lastAccountId).isOk()) { - lastAccountId = accounts.getSelectedAccount().getAccountId(); - } - switchAccountAndStartActivity(activity, lastAccountId); + DcContext selectedAccount = accounts.getSelectedAccount(); + if (selectedAccount.isConfigured() == 0) { + accounts.removeAccount(selectedAccount.getAccountId()); } - public void switchAccountAndStartActivity(Activity activity, int destAccountId) { - if (destAccountId==0) { - beginAccountCreation(activity); - } else { - switchAccount(activity, destAccountId); - } - - activity.finishAffinity(); - if (destAccountId == 0) { - activity.startActivity(new Intent(activity, WelcomeActivity.class)); - } else { - activity.startActivity(new Intent(activity.getApplicationContext(), ConversationListActivity.class)); - } + int lastAccountId = + PreferenceManager.getDefaultSharedPreferences(activity).getInt(LAST_ACCOUNT_ID, 0); + if (lastAccountId == 0 || !accounts.getAccount(lastAccountId).isOk()) { + lastAccountId = accounts.getSelectedAccount().getAccountId(); + } + switchAccountAndStartActivity(activity, lastAccountId); + } + + public void switchAccountAndStartActivity(Activity activity, int destAccountId) { + if (destAccountId == 0) { + beginAccountCreation(activity); + } else { + switchAccount(activity, destAccountId); } - // ui - - public void showSwitchAccountMenu(ConversationListActivity activity, boolean selectOnly) { - AccountSelectionListFragment dialog = AccountSelectionListFragment.newInstance(selectOnly); - dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null); + activity.finishAffinity(); + if (destAccountId == 0) { + activity.startActivity(new Intent(activity, WelcomeActivity.class)); + } else { + activity.startActivity( + new Intent(activity.getApplicationContext(), ConversationListActivity.class)); } + } - public void addAccountFromSecondDevice(Activity activity, String backupQr) { - DcAccounts accounts = DcHelper.getAccounts(activity); - if (accounts.getSelectedAccount().isConfigured() == 1) { - // the selected account is already configured, create a new one - beginAccountCreation(activity); - } + // ui - activity.finishAffinity(); - Intent intent = new Intent(activity, WelcomeActivity.class); - intent.putExtra(WelcomeActivity.BACKUP_QR_EXTRA, backupQr); - activity.startActivity(intent); + public void showSwitchAccountMenu(ConversationListActivity activity, boolean selectOnly) { + AccountSelectionListFragment dialog = AccountSelectionListFragment.newInstance(selectOnly); + dialog.show(((FragmentActivity) activity).getSupportFragmentManager(), null); + } + + public void addAccountFromSecondDevice(Activity activity, String backupQr) { + DcAccounts accounts = DcHelper.getAccounts(activity); + if (accounts.getSelectedAccount().isConfigured() == 1) { + // the selected account is already configured, create a new one + beginAccountCreation(activity); } + + activity.finishAffinity(); + Intent intent = new Intent(activity, WelcomeActivity.class); + intent.putExtra(WelcomeActivity.BACKUP_QR_EXTRA, backupQr); + activity.startActivity(intent); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/AttachmentsContentProvider.java b/src/main/java/org/thoughtcrime/securesms/connect/AttachmentsContentProvider.java index c8ffe406f..b643f1dc2 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/AttachmentsContentProvider.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/AttachmentsContentProvider.java @@ -6,86 +6,81 @@ import android.net.Uri; import android.os.ParcelFileDescriptor; import android.webkit.MimeTypeMap; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcContext; - -import org.thoughtcrime.securesms.util.MediaUtil; - import java.io.File; import java.io.FileNotFoundException; import java.util.Objects; +import org.thoughtcrime.securesms.util.MediaUtil; public class AttachmentsContentProvider extends ContentProvider { - /* We save all attachments in our private files-directory - that cannot be read by other apps. - - When starting an Intent for viewing, we cannot use the paths. - Instead, we give a content://-url that results in calls to this class. - - (An alternative would be to copy files to view to a public directory, however, this would - lead to duplicate data. - Another alternative would be to write all attachments to a public directory, however, this - may lead to security problems as files are system-wide-readable and would also cause problems - if the user or another app deletes these files) */ - - @Override - public ParcelFileDescriptor openFile(Uri uri, @NonNull String mode) throws FileNotFoundException { - DcContext dcContext = DcHelper.getContext(Objects.requireNonNull(getContext())); - - // `uri` originally comes from DcHelper.openForViewOrShare() and - // looks like `content://chat.delta.attachments/ef39a39/text.txt` - // where ef39a39 is the file in the blob directory - // and text.txt is the original name of the file, as returned by `msg.getFilename()`. - // `uri.getPathSegments()` returns ["ef39a39", "text.txt"] in this example. - String file = uri.getPathSegments().get(0); - if (!DcHelper.sharedFiles.containsKey(file)) { - throw new FileNotFoundException("File was not shared before."); - } - - File privateFile = new File(dcContext.getBlobdir(), file); - return ParcelFileDescriptor.open(privateFile, ParcelFileDescriptor.MODE_READ_ONLY); - } - - @Override - public int delete(@NonNull Uri arg0, String arg1, String[] arg2) { - return 0; - } - - @Override - public String getType(Uri uri) { - String file = uri.getPathSegments().get(0); - String mimeType = DcHelper.sharedFiles.get(file); - - return DcHelper.checkMime(uri.toString(), mimeType); - } - - @Override - public String getTypeAnonymous(Uri uri) { - String ext = MediaUtil.getFileExtensionFromUrl(uri.toString()); - return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); - } - - @Override - public Uri insert(@NonNull Uri arg0, ContentValues arg1) { - return null; + /* We save all attachments in our private files-directory + that cannot be read by other apps. + + When starting an Intent for viewing, we cannot use the paths. + Instead, we give a content://-url that results in calls to this class. + + (An alternative would be to copy files to view to a public directory, however, this would + lead to duplicate data. + Another alternative would be to write all attachments to a public directory, however, this + may lead to security problems as files are system-wide-readable and would also cause problems + if the user or another app deletes these files) */ + + @Override + public ParcelFileDescriptor openFile(Uri uri, @NonNull String mode) throws FileNotFoundException { + DcContext dcContext = DcHelper.getContext(Objects.requireNonNull(getContext())); + + // `uri` originally comes from DcHelper.openForViewOrShare() and + // looks like `content://chat.delta.attachments/ef39a39/text.txt` + // where ef39a39 is the file in the blob directory + // and text.txt is the original name of the file, as returned by `msg.getFilename()`. + // `uri.getPathSegments()` returns ["ef39a39", "text.txt"] in this example. + String file = uri.getPathSegments().get(0); + if (!DcHelper.sharedFiles.containsKey(file)) { + throw new FileNotFoundException("File was not shared before."); } - @Override - public boolean onCreate() { - return false; - } - - @Override - public Cursor query(@NonNull Uri arg0, String[] arg1, String arg2, String[] arg3, - String arg4) { - return null; - } - - @Override - public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) { - return 0; - } + File privateFile = new File(dcContext.getBlobdir(), file); + return ParcelFileDescriptor.open(privateFile, ParcelFileDescriptor.MODE_READ_ONLY); + } + + @Override + public int delete(@NonNull Uri arg0, String arg1, String[] arg2) { + return 0; + } + + @Override + public String getType(Uri uri) { + String file = uri.getPathSegments().get(0); + String mimeType = DcHelper.sharedFiles.get(file); + + return DcHelper.checkMime(uri.toString(), mimeType); + } + + @Override + public String getTypeAnonymous(Uri uri) { + String ext = MediaUtil.getFileExtensionFromUrl(uri.toString()); + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); + } + + @Override + public Uri insert(@NonNull Uri arg0, ContentValues arg1) { + return null; + } + + @Override + public boolean onCreate() { + return false; + } + + @Override + public Cursor query(@NonNull Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) { + return null; + } + + @Override + public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) { + return 0; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java b/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java index 3715819f3..93e72f4b1 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java @@ -1,75 +1,79 @@ package org.thoughtcrime.securesms.connect; import android.content.Context; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.util.AsyncLoader; import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.Util; public class DcContactsLoader extends AsyncLoader { - private final int listflags; - private final String query; - private final boolean addScanQRLink; - private final boolean addCreateGroupLinks; - private final boolean addCreateContactLink; - private final boolean blockedContacts; + private final int listflags; + private final String query; + private final boolean addScanQRLink; + private final boolean addCreateGroupLinks; + private final boolean addCreateContactLink; + private final boolean blockedContacts; - public DcContactsLoader(Context context, int listflags, String query, boolean addCreateGroupLinks, boolean addCreateContactLink, boolean addScanQRLink, boolean blockedContacts) { - super(context); - this.listflags = listflags; - this.query = (query==null||query.isEmpty())? null : query; - this.addScanQRLink = addScanQRLink; - this.addCreateGroupLinks = addCreateGroupLinks; - this.addCreateContactLink= addCreateContactLink; - this.blockedContacts = blockedContacts; - } + public DcContactsLoader( + Context context, + int listflags, + String query, + boolean addCreateGroupLinks, + boolean addCreateContactLink, + boolean addScanQRLink, + boolean blockedContacts) { + super(context); + this.listflags = listflags; + this.query = (query == null || query.isEmpty()) ? null : query; + this.addScanQRLink = addScanQRLink; + this.addCreateGroupLinks = addCreateGroupLinks; + this.addCreateContactLink = addCreateContactLink; + this.blockedContacts = blockedContacts; + } - @Override - public @NonNull - DcContactsLoader.Ret loadInBackground() { - DcContext dcContext = DcHelper.getContext(getContext()); - if (blockedContacts) { - int[] blocked_ids = dcContext.getBlockedContacts(); - return new Ret(blocked_ids); - } + @Override + public @NonNull DcContactsLoader.Ret loadInBackground() { + DcContext dcContext = DcHelper.getContext(getContext()); + if (blockedContacts) { + int[] blocked_ids = dcContext.getBlockedContacts(); + return new Ret(blocked_ids); + } - int[] contact_ids = dcContext.getContacts(listflags, query); - int[] additional_items = new int[0]; - if (query == null && addScanQRLink) - { - additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_QR_INVITE); - } - if (addCreateContactLink && !dcContext.isChatmail()) - { - additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT); - } - if (query == null && addCreateGroupLinks) { - additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP); + int[] contact_ids = dcContext.getContacts(listflags, query); + int[] additional_items = new int[0]; + if (query == null && addScanQRLink) { + additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_QR_INVITE); + } + if (addCreateContactLink && !dcContext.isChatmail()) { + additional_items = + Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT); + } + if (query == null && addCreateGroupLinks) { + additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP); - final boolean broadcastsEnabled = Prefs.isNewBroadcastAvailable(getContext()); - if (broadcastsEnabled) additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST); + final boolean broadcastsEnabled = Prefs.isNewBroadcastAvailable(getContext()); + if (broadcastsEnabled) + additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST); - if (!dcContext.isChatmail()) { - additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP); - } - } - int[] all_ids = new int[contact_ids.length + additional_items.length]; - System.arraycopy(additional_items, 0, all_ids, 0, additional_items.length); - System.arraycopy(contact_ids, 0, all_ids, additional_items.length, contact_ids.length); - return new Ret(all_ids); + if (!dcContext.isChatmail()) { + additional_items = + Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP); + } } + int[] all_ids = new int[contact_ids.length + additional_items.length]; + System.arraycopy(additional_items, 0, all_ids, 0, additional_items.length); + System.arraycopy(contact_ids, 0, all_ids, additional_items.length, contact_ids.length); + return new Ret(all_ids); + } - public static class Ret { - public final int[] ids; + public static class Ret { + public final int[] ids; - Ret(int[] ids) { - this.ids = ids; - } + Ret(int[] ids) { + this.ids = ids; } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java b/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java index c9082e2d2..281cf41a1 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DcHelper.java @@ -220,10 +220,10 @@ public static void setStockTranslations(Context context) { dcContext.setStockTranslation(220, context.getString(R.string.proxy_enabled)); dcContext.setStockTranslation(221, context.getString(R.string.proxy_enabled_hint)); dcContext.setStockTranslation(230, context.getString(R.string.chat_unencrypted_explanation)); - dcContext.setStockTranslation(232, context.getString(R.string.outgoing_audio_call)); - dcContext.setStockTranslation(233, context.getString(R.string.outgoing_video_call)); - dcContext.setStockTranslation(234, context.getString(R.string.incoming_audio_call)); - dcContext.setStockTranslation(235, context.getString(R.string.incoming_video_call)); + dcContext.setStockTranslation(232, context.getString(R.string.audio_call)); + dcContext.setStockTranslation(233, context.getString(R.string.video_call)); + dcContext.setStockTranslation(234, context.getString(R.string.audio_call)); + dcContext.setStockTranslation(235, context.getString(R.string.video_call)); dcContext.setStockTranslation(240, context.getString(R.string.chat_description_changed_by_you)); dcContext.setStockTranslation(241, context.getString(R.string.chat_description_changed_by_other)); } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DirectShareUtil.java b/src/main/java/org/thoughtcrime/securesms/connect/DirectShareUtil.java index 08238abfd..2f26310c3 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DirectShareUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DirectShareUtil.java @@ -7,18 +7,19 @@ import android.graphics.Bitmap; import android.os.Build; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContext; import com.bumptech.glide.load.engine.DiskCacheStrategy; - +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutionException; import org.thoughtcrime.securesms.ShareActivity; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.mms.GlideApp; @@ -28,18 +29,13 @@ import org.thoughtcrime.securesms.util.DrawableUtil; import org.thoughtcrime.securesms.util.Util; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutionException; - /** * The Signal code has a similar class called ConversationUtil. * - * This class uses the Sharing Shortcuts API to publish dynamic launcher shortcuts (the ones that + *

This class uses the Sharing Shortcuts API to publish dynamic launcher shortcuts (the ones that * appear when you long-press on an app) and direct-sharing-shortcuts. * - * It replaces the class DirectShareService, because DirectShareService used the + *

It replaces the class DirectShareService, because DirectShareService used the * ChooserTargetService API, which was replaced by the Sharing Shortcuts API. */ public class DirectShareUtil { @@ -49,55 +45,61 @@ public class DirectShareUtil { public static void clearShortcut(@NonNull Context context, int chatId) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Util.runOnAnyBackgroundThread(() -> { - try { - ShortcutManagerCompat.removeDynamicShortcuts(context, Collections.singletonList(Integer.toString(chatId))); - } catch (Exception e) { - Log.e(TAG, "Clearing shortcut failed", e); - } - }); + Util.runOnAnyBackgroundThread( + () -> { + try { + ShortcutManagerCompat.removeDynamicShortcuts( + context, Collections.singletonList(Integer.toString(chatId))); + } catch (Exception e) { + Log.e(TAG, "Clearing shortcut failed", e); + } + }); } } public static void resetAllShortcuts(@NonNull Context context) { - Util.runOnBackground(() -> { - try { - ShortcutManagerCompat.removeAllDynamicShortcuts(context); - triggerRefreshDirectShare(context); - } catch (Exception e) { - Log.e(TAG, "Resetting shortcuts failed", e); - } - }); + Util.runOnBackground( + () -> { + try { + ShortcutManagerCompat.removeAllDynamicShortcuts(context); + triggerRefreshDirectShare(context); + } catch (Exception e) { + Log.e(TAG, "Resetting shortcuts failed", e); + } + }); } public static void triggerRefreshDirectShare(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Util.runOnBackgroundDelayed(() -> { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 + Util.runOnBackgroundDelayed( + () -> { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && context.getSystemService(ShortcutManager.class).isRateLimitingActive()) { - return; - } + return; + } - int maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context); - List currentShortcuts = ShortcutManagerCompat.getDynamicShortcuts(context); - List newShortcuts = getChooserTargets(context); + int maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context); + List currentShortcuts = + ShortcutManagerCompat.getDynamicShortcuts(context); + List newShortcuts = getChooserTargets(context); - if (maxShortcuts > 0 + if (maxShortcuts > 0 && currentShortcuts.size() + newShortcuts.size() > maxShortcuts) { - ShortcutManagerCompat.removeAllDynamicShortcuts(context); - } - - boolean success = ShortcutManagerCompat.addDynamicShortcuts(context, newShortcuts); - Log.i(TAG, "Updated dynamic shortcuts, success: " + success); - } catch(Exception e) { - Log.e(TAG, "Updating dynamic shortcuts failed: " + e); - } - - // Wait 1500ms, this is called by onResume(), and we want to make sure that refreshing - // shortcuts does not delay loading of the chatlist - }, 1500); + ShortcutManagerCompat.removeAllDynamicShortcuts(context); + } + + boolean success = ShortcutManagerCompat.addDynamicShortcuts(context, newShortcuts); + Log.i(TAG, "Updated dynamic shortcuts, success: " + success); + } catch (Exception e) { + Log.e(TAG, "Updating dynamic shortcuts failed: " + e); + } + + // Wait 1500ms, this is called by onResume(), and we want to make sure that refreshing + // shortcuts does not delay loading of the chatlist + }, + 1500); } } @@ -106,11 +108,9 @@ private static List getChooserTargets(Context context) { List results = new LinkedList<>(); DcContext dcContext = DcHelper.getContext(context); - DcChatlist chatlist = dcContext.getChatlist( - DcContext.DC_GCL_FOR_FORWARDING | DcContext.DC_GCL_NO_SPECIALS, - null, - 0 - ); + DcChatlist chatlist = + dcContext.getChatlist( + DcContext.DC_GCL_FOR_FORWARDING | DcContext.DC_GCL_NO_SPECIALS, null, 0); int max = 5; if (chatlist.getCnt() < max) { max = chatlist.getCnt(); @@ -128,10 +128,12 @@ private static List getChooserTargets(Context context) { Recipient recipient = new Recipient(context, chat); Bitmap avatar = getIconForShortcut(context, recipient); - results.add(new ShortcutInfoCompat.Builder(context, "chat-" + dcContext.getAccountId() + "-" + chat.getId()) + results.add( + new ShortcutInfoCompat.Builder( + context, "chat-" + dcContext.getAccountId() + "-" + chat.getId()) .setShortLabel(chat.getName()) .setLongLived(true) - .setRank(i+1) + .setRank(i + 1) .setIcon(IconCompat.createWithAdaptiveBitmap(avatar)) .setCategories(Collections.singleton(SHORTCUT_CATEGORY)) .setIntent(intent) @@ -150,22 +152,32 @@ public static Bitmap getIconForShortcut(@NonNull Context context, @NonNull Recip } } - private static @NonNull Bitmap getShortcutInfoBitmap(@NonNull Context context, @NonNull Recipient recipient) throws ExecutionException, InterruptedException { - return DrawableUtil.wrapBitmapForShortcutInfo(request(GlideApp.with(context).asBitmap(), context, recipient).submit().get()); + private static @NonNull Bitmap getShortcutInfoBitmap( + @NonNull Context context, @NonNull Recipient recipient) + throws ExecutionException, InterruptedException { + return DrawableUtil.wrapBitmapForShortcutInfo( + request(GlideApp.with(context).asBitmap(), context, recipient).submit().get()); } private static Bitmap getFallbackDrawable(Context context, @NonNull Recipient recipient) { - return BitmapUtil.createFromDrawable(recipient.getFallbackAvatarDrawable(context, false), - context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), - context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)); + return BitmapUtil.createFromDrawable( + recipient.getFallbackAvatarDrawable(context, false), + context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), + context + .getResources() + .getDimensionPixelSize(android.R.dimen.notification_large_icon_height)); } - private static GlideRequest request(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient) { + private static GlideRequest request( + @NonNull GlideRequest glideRequest, + @NonNull Context context, + @NonNull Recipient recipient) { final ContactPhoto photo; photo = recipient.getContactPhoto(context); - return glideRequest.load(photo) - .error(getFallbackDrawable(context, recipient)) - .diskCacheStrategy(DiskCacheStrategy.ALL); + return glideRequest + .load(photo) + .error(getFallbackDrawable(context, recipient)) + .diskCacheStrategy(DiskCacheStrategy.ALL); } } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/FetchWorker.java b/src/main/java/org/thoughtcrime/securesms/connect/FetchWorker.java index 355f55b6c..a57a66e03 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/FetchWorker.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/FetchWorker.java @@ -2,40 +2,36 @@ import android.content.Context; import android.util.Log; - import androidx.annotation.NonNull; import androidx.work.Worker; import androidx.work.WorkerParameters; - import org.thoughtcrime.securesms.util.Util; public class FetchWorker extends Worker { - private final @NonNull Context context; - - public FetchWorker( - @NonNull Context context, - @NonNull WorkerParameters params) { - super(context, params); - this.context = context; - } - - // doWork() is called in a background thread; - // once we return, Worker is considered to have finished and will be destroyed, - // this does not necessarily mean, that the app is killed, we may or may not keep running, - // therefore we do not stopIo() here. - @Override - public @NonNull Result doWork() { - Log.i("DeltaChat", "++++++++++++++++++ FetchWorker.doWork() started ++++++++++++++++++"); - DcHelper.getAccounts(context).startIo(); - - // as we do not know when startIo() has done it's work or if is even doable in one step, - // we go the easy way and just wait for some amount of time. - // the core has to handle interrupts at any point anyway, - // and work also maybe continued when doWork() returns. - // however, we should not wait too long here to avoid getting bad battery ratings. - Util.sleep(60 * 1000); - - Log.i("DeltaChat", "++++++++++++++++++ FetchWorker.doWork() will return ++++++++++++++++++"); - return Result.success(); - } + private final @NonNull Context context; + + public FetchWorker(@NonNull Context context, @NonNull WorkerParameters params) { + super(context, params); + this.context = context; + } + + // doWork() is called in a background thread; + // once we return, Worker is considered to have finished and will be destroyed, + // this does not necessarily mean, that the app is killed, we may or may not keep running, + // therefore we do not stopIo() here. + @Override + public @NonNull Result doWork() { + Log.i("DeltaChat", "++++++++++++++++++ FetchWorker.doWork() started ++++++++++++++++++"); + DcHelper.getAccounts(context).startIo(); + + // as we do not know when startIo() has done it's work or if is even doable in one step, + // we go the easy way and just wait for some amount of time. + // the core has to handle interrupts at any point anyway, + // and work also maybe continued when doWork() returns. + // however, we should not wait too long here to avoid getting bad battery ratings. + Util.sleep(60 * 1000); + + Log.i("DeltaChat", "++++++++++++++++++ FetchWorker.doWork() will return ++++++++++++++++++"); + return Result.success(); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/ForegroundDetector.java b/src/main/java/org/thoughtcrime/securesms/connect/ForegroundDetector.java index a88fdcf26..ca8d2c828 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/ForegroundDetector.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/ForegroundDetector.java @@ -5,86 +5,85 @@ import android.app.Application; import android.os.Bundle; import android.util.Log; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.ApplicationContext; @SuppressLint("NewApi") public class ForegroundDetector implements Application.ActivityLifecycleCallbacks { - private int refs = 0; - private static ForegroundDetector Instance = null; - private final ApplicationContext application; - - public static ForegroundDetector getInstance() { - return Instance; + private int refs = 0; + private static ForegroundDetector Instance = null; + private final ApplicationContext application; + + public static ForegroundDetector getInstance() { + return Instance; + } + + public ForegroundDetector(ApplicationContext application) { + Instance = this; + this.application = application; + application.registerActivityLifecycleCallbacks(this); + } + + public boolean isForeground() { + return refs > 0; + } + + public boolean isBackground() { + return refs == 0; + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + if (refs == 0) { + Log.i( + "DeltaChat", + "++++++++++++++++++ first ForegroundDetector.onActivityStarted() ++++++++++++++++++"); + DcHelper.getAccounts(application).startIo(); + if (DcHelper.isNetworkConnected(application)) { + new Thread( + () -> { + Log.i("DeltaChat", "calling maybeNetwork()"); + DcHelper.getAccounts(application).maybeNetwork(); + Log.i("DeltaChat", "maybeNetwork() returned"); + }) + .start(); + } } - public ForegroundDetector(ApplicationContext application) { - Instance = this; - this.application = application; - application.registerActivityLifecycleCallbacks(this); - } + refs++; + } - public boolean isForeground() { - return refs > 0; + @Override + public void onActivityStopped(@NonNull Activity activity) { + if (refs <= 0) { + Log.w("DeltaChat", "invalid call to ForegroundDetector.onActivityStopped()"); + return; } - public boolean isBackground() { - return refs == 0; - } + refs--; - @Override - public void onActivityStarted(@NonNull Activity activity) { - if (refs == 0) { - Log.i("DeltaChat", "++++++++++++++++++ first ForegroundDetector.onActivityStarted() ++++++++++++++++++"); - DcHelper.getAccounts(application).startIo(); - if (DcHelper.isNetworkConnected(application)) { - new Thread(() -> { - Log.i("DeltaChat", "calling maybeNetwork()"); - DcHelper.getAccounts(application).maybeNetwork(); - Log.i("DeltaChat", "maybeNetwork() returned"); - }).start(); - } - } - - refs++; + if (refs == 0) { + Log.i( + "DeltaChat", + "++++++++++++++++++ last ForegroundDetector.onActivityStopped() ++++++++++++++++++"); } + } + @Override + public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) {} - @Override - public void onActivityStopped(@NonNull Activity activity) { - if( refs <= 0 ) { - Log.w("DeltaChat", "invalid call to ForegroundDetector.onActivityStopped()"); - return; - } - - refs--; + @Override + public void onActivityResumed(@NonNull Activity activity) {} - if (refs == 0) { - Log.i("DeltaChat", "++++++++++++++++++ last ForegroundDetector.onActivityStopped() ++++++++++++++++++"); - } - } + @Override + public void onActivityPaused(@NonNull Activity activity) { + // pause/resume will also be called when the app is partially covered by a dialog + } - @Override - public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) { - } + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} - @Override - public void onActivityResumed(@NonNull Activity activity) { - } - - @Override - public void onActivityPaused(@NonNull Activity activity) { - // pause/resume will also be called when the app is partially covered by a dialog - } - - @Override - public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { - } - - @Override - public void onActivityDestroyed(@NonNull Activity activity) { - } + @Override + public void onActivityDestroyed(@NonNull Activity activity) {} } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/KeepAliveService.java b/src/main/java/org/thoughtcrime/securesms/connect/KeepAliveService.java index 06a17dc82..a427480fc 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/KeepAliveService.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/KeepAliveService.java @@ -11,10 +11,8 @@ import android.os.Build; import android.os.IBinder; import android.util.Log; - import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; - import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.notifications.NotificationCenter; @@ -23,113 +21,117 @@ public class KeepAliveService extends Service { - private static final String TAG = KeepAliveService.class.getSimpleName(); - - static KeepAliveService s_this = null; - - public static void maybeStartSelf(Context context) { - // note, that unfortunately, the check for isIgnoringBatteryOptimizations() is not sufficient, - // this checks only stock-android settings, several os have additional "optimizers" that ignore this setting. - // therefore, the most reliable way to not get killed is a permanent-foreground-notification. - if (Prefs.reliableService(context)) { - startSelf(context); - } - } + private static final String TAG = KeepAliveService.class.getSimpleName(); - public static void startSelf(Context context) - { - try { - ContextCompat.startForegroundService(context, new Intent(context, KeepAliveService.class)); - } - catch(Exception e) { - Log.i(TAG, "Error calling ContextCompat.startForegroundService()", e); - } - } + static KeepAliveService s_this = null; - @Override - public void onCreate() { - Log.i("DeltaChat", "*** KeepAliveService.onCreate()"); - // there's nothing more to do here as all initialisation stuff is already done in - // ApplicationLoader.onCreate() which is called before this broadcast is sended. - s_this = this; - - // set self as foreground - try { - stopForeground(true); - startForeground(NotificationCenter.ID_PERMANENT, createNotification()); - } - catch (Exception e) { - Log.i(TAG, "Error in onCreate()", e); - } + public static void maybeStartSelf(Context context) { + // note, that unfortunately, the check for isIgnoringBatteryOptimizations() is not sufficient, + // this checks only stock-android settings, several os have additional "optimizers" that ignore + // this setting. + // therefore, the most reliable way to not get killed is a permanent-foreground-notification. + if (Prefs.reliableService(context)) { + startSelf(context); } + } - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - // START_STICKY ensured, the service is recreated as soon it is terminated for any reasons. - // as ApplicationLoader.onCreate() is called before a service starts, there is no more to do here, - // the app is just running fine. - Log.i("DeltaChat", "*** KeepAliveService.onStartCommand()"); - return START_STICKY; + public static void startSelf(Context context) { + try { + ContextCompat.startForegroundService(context, new Intent(context, KeepAliveService.class)); + } catch (Exception e) { + Log.i(TAG, "Error calling ContextCompat.startForegroundService()", e); } - - @Override - public IBinder onBind(Intent intent) { - return null; + } + + @Override + public void onCreate() { + Log.i("DeltaChat", "*** KeepAliveService.onCreate()"); + // there's nothing more to do here as all initialisation stuff is already done in + // ApplicationLoader.onCreate() which is called before this broadcast is sended. + s_this = this; + + // set self as foreground + try { + stopForeground(true); + startForeground(NotificationCenter.ID_PERMANENT, createNotification()); + } catch (Exception e) { + Log.i(TAG, "Error in onCreate()", e); } - - @Override - public void onDestroy() { - Log.i("DeltaChat", "*** KeepAliveService.onDestroy()"); - // the service will be restarted due to START_STICKY automatically, there's nothing more to do. + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // START_STICKY ensured, the service is recreated as soon it is terminated for any reasons. + // as ApplicationLoader.onCreate() is called before a service starts, there is no more to do + // here, + // the app is just running fine. + Log.i("DeltaChat", "*** KeepAliveService.onStartCommand()"); + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + Log.i("DeltaChat", "*** KeepAliveService.onDestroy()"); + // the service will be restarted due to START_STICKY automatically, there's nothing more to do. + } + + @Override + public void onTimeout(int startId, int fgsType) { + stopSelf(); + } + + public static KeepAliveService getInstance() { + return s_this; // may be null + } + + /* The notification + * A notification is required for a foreground service; and without a foreground service, + * ArcaneChat won't get new messages reliable + **********************************************************************************************/ + + private Notification createNotification() { + Intent intent = new Intent(this, ConversationListActivity.class); + PendingIntent contentIntent = + PendingIntent.getActivity( + this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE()); + // a notification _must_ contain a small icon, a title and a text, see + // https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Required + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + + builder.setContentTitle(getString(R.string.app_name)); + builder.setContentText(getString(R.string.notify_background_connection_enabled)); + + builder.setPriority(NotificationCompat.PRIORITY_MIN); + builder.setWhen(0); + builder.setContentIntent(contentIntent); + builder.setSmallIcon(R.drawable.notification_permanent); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createFgNotificationChannel(this); + builder.setChannelId(NotificationCenter.CH_PERMANENT); } - - @Override - public void onTimeout(int startId, int fgsType) { - stopSelf(); - } - - static public KeepAliveService getInstance() - { - return s_this; // may be null - } - - /* The notification - * A notification is required for a foreground service; and without a foreground service, - * ArcaneChat won't get new messages reliable - **********************************************************************************************/ - - private Notification createNotification() - { - Intent intent = new Intent(this, ConversationListActivity.class); - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE()); - // a notification _must_ contain a small icon, a title and a text, see https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Required - NotificationCompat.Builder builder = new NotificationCompat.Builder(this); - - builder.setContentTitle(getString(R.string.app_name)); - builder.setContentText(getString(R.string.notify_background_connection_enabled)); - - builder.setPriority(NotificationCompat.PRIORITY_MIN); - builder.setWhen(0); - builder.setContentIntent(contentIntent); - builder.setSmallIcon(R.drawable.notification_permanent); - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O) { - createFgNotificationChannel(this); - builder.setChannelId(NotificationCenter.CH_PERMANENT); - } - return builder.build(); - } - - private static boolean ch_created = false; - @TargetApi(Build.VERSION_CODES.O) - static private void createFgNotificationChannel(Context context) { - if(!ch_created) { - ch_created = true; - NotificationChannel channel = new NotificationChannel(NotificationCenter.CH_PERMANENT, - "Receive messages in background.", NotificationManager.IMPORTANCE_MIN); // IMPORTANCE_DEFAULT will play a sound - channel.setDescription("Ensure reliable message receiving."); - channel.setShowBadge(false); - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } + return builder.build(); + } + + private static boolean ch_created = false; + + @TargetApi(Build.VERSION_CODES.O) + private static void createFgNotificationChannel(Context context) { + if (!ch_created) { + ch_created = true; + NotificationChannel channel = + new NotificationChannel( + NotificationCenter.CH_PERMANENT, + "Receive messages in background.", + NotificationManager.IMPORTANCE_MIN); // IMPORTANCE_DEFAULT will play a sound + channel.setDescription("Ensure reliable message receiving."); + channel.setShowBadge(false); + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/NetworkStateReceiver.java b/src/main/java/org/thoughtcrime/securesms/connect/NetworkStateReceiver.java index 0a1d87b71..dcf39dd6e 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/NetworkStateReceiver.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/NetworkStateReceiver.java @@ -10,33 +10,36 @@ public class NetworkStateReceiver extends BroadcastReceiver { - private static final String TAG = NetworkStateReceiver.class.getSimpleName(); - private int debugConnectedCount; + private static final String TAG = NetworkStateReceiver.class.getSimpleName(); + private int debugConnectedCount; - @Override - public void onReceive(Context context, Intent intent) { + @Override + public void onReceive(Context context, Intent intent) { - try { - ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo ni = manager.getActiveNetworkInfo(); + try { + ConnectivityManager manager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = manager.getActiveNetworkInfo(); - if (ni != null && ni.getState() == NetworkInfo.State.CONNECTED) { - Log.i("DeltaChat", "++++++++++++++++++ Connected #" + debugConnectedCount++); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - new Thread(() -> { + if (ni != null && ni.getState() == NetworkInfo.State.CONNECTED) { + Log.i("DeltaChat", "++++++++++++++++++ Connected #" + debugConnectedCount++); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + new Thread( + () -> { // call dc_maybe_network() from a worker thread. - // theoretically, dc_maybe_network() can be called from the main thread and returns at once, + // theoretically, dc_maybe_network() can be called from the main thread and + // returns at once, // however, in reality, it does currently halt things for some seconds. // this is a workaround that make things usable for now. Log.i("DeltaChat", "calling maybeNetwork()"); DcHelper.getAccounts(context).maybeNetwork(); Log.i("DeltaChat", "maybeNetwork() returned"); - }).start(); - } - } - } - catch (Exception e) { - Log.i(TAG, "Error in onReceive()", e); + }) + .start(); } + } + } catch (Exception e) { + Log.i(TAG, "Error in onReceive()", e); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java index ebe027a96..1ced4abc4 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -1,18 +1,16 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.contacts; @@ -20,28 +18,24 @@ import android.database.Cursor; import android.provider.ContactsContract; import android.util.Log; - -import org.thoughtcrime.securesms.ContactSelectionListFragment; -import org.thoughtcrime.securesms.util.Hash; -import org.thoughtcrime.securesms.util.Prefs; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.thoughtcrime.securesms.ContactSelectionListFragment; +import org.thoughtcrime.securesms.util.Hash; +import org.thoughtcrime.securesms.util.Prefs; /** - * This class was originally a layer of indirection between - * ContactAccessorNewApi and ContactAccesorOldApi, which corresponded - * to the API changes between 1.x and 2.x. + * This class was originally a layer of indirection between ContactAccessorNewApi and + * ContactAccesorOldApi, which corresponded to the API changes between 1.x and 2.x. * - * Now that we no longer support 1.x, this class mostly serves as a place - * to encapsulate Contact-related logic. It's still a singleton, mostly - * just because that's how it's currently called from everywhere. + *

Now that we no longer support 1.x, this class mostly serves as a place to encapsulate + * Contact-related logic. It's still a singleton, mostly just because that's how it's currently + * called from everywhere. * * @author Moxie Marlinspike */ - public class ContactAccessor { private static final String TAG = ContactSelectionListFragment.class.getSimpleName(); @@ -58,8 +52,14 @@ public static synchronized ContactAccessor getInstance() { } public Cursor getAllSystemContacts(Context context) { - String[] projection = {ContactsContract.Data.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.ADDRESS, ContactsContract.Data.CONTACT_ID}; - return context.getContentResolver().query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, projection, null, null, null); + String[] projection = { + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Email.ADDRESS, + ContactsContract.Data.CONTACT_ID + }; + return context + .getContentResolver() + .query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, projection, null, null, null); } public String getAllSystemContactsAsString(Context context) { @@ -78,7 +78,7 @@ public String getAllSystemContactsAsString(Context context) { } else { name = ""; } - } catch(Exception e) { + } catch (Exception e) { Log.e(TAG, "Can't get contact name: " + e); name = ""; } @@ -90,7 +90,7 @@ public String getAllSystemContactsAsString(Context context) { mail = mail.replace("\r", ""); // remove characters later used as field separator mail = mail.replace("\n", ""); } - } catch(Exception e) { + } catch (Exception e) { Log.e(TAG, "Can't get contact addr: " + e); } @@ -101,11 +101,11 @@ public String getAllSystemContactsAsString(Context context) { contactPhotoIdentifiers.add(hashedIdentifierAndId); } if (mail != null && !mail.isEmpty() && !mailList.contains(mail)) { - mailList.add(mail); - if (name.isEmpty()) { - name = mail; - } - result.append(name).append("\n").append(mail).append("\n"); + mailList.add(mail); + if (name.isEmpty()) { + name = mail; + } + result.append(name).append("\n").append(mail).append("\n"); } } Prefs.setSystemContactPhotos(context, contactPhotoIdentifiers); diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index 300afece7..e0448389a 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -1,18 +1,16 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.contacts; @@ -22,49 +20,45 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.connect.DcContactsLoader; -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.util.LRUCache; - import java.lang.ref.SoftReference; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.connect.DcContactsLoader; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.util.LRUCache; /** * List adapter to display all contacts and their related information * * @author Jake McGinty */ -public class ContactSelectionListAdapter extends RecyclerView.Adapter -{ +public class ContactSelectionListAdapter + extends RecyclerView.Adapter { private static final int VIEW_TYPE_CONTACT = 0; private static final int MAX_CACHE_SIZE = 100; - private final Map> recordCache = - Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); + private final Map> recordCache = + Collections.synchronizedMap(new LRUCache>(MAX_CACHE_SIZE)); - private final @NonNull Context context; - private final @NonNull DcContext dcContext; - private @NonNull int[] dcContactList = new int[0]; - private final boolean multiSelect; - private final boolean longPressSelect; - private final LayoutInflater li; - private final ItemClickListener clickListener; - private final GlideRequests glideRequests; - private final Set selectedContacts = new HashSet<>(); - private final SparseIntArray actionModeSelection = new SparseIntArray(); + private final @NonNull Context context; + private final @NonNull DcContext dcContext; + private @NonNull int[] dcContactList = new int[0]; + private final boolean multiSelect; + private final boolean longPressSelect; + private final LayoutInflater li; + private final ItemClickListener clickListener; + private final GlideRequests glideRequests; + private final Set selectedContacts = new HashSet<>(); + private final SparseIntArray actionModeSelection = new SparseIntArray(); @Override public int getItemCount() { @@ -72,7 +66,7 @@ public int getItemCount() { } private @NonNull DcContact getContact(int position) { - if(position<0 || position>=dcContactList.length) { + if (position < 0 || position >= dcContactList.length) { return new DcContact(0); } @@ -89,21 +83,21 @@ public int getItemCount() { return fromDb; } - public void resetActionModeSelection() { - actionModeSelection.clear(); - notifyDataSetChanged(); - } + public void resetActionModeSelection() { + actionModeSelection.clear(); + notifyDataSetChanged(); + } - public void selectAll() { - actionModeSelection.clear(); - for(int index = 0; index < dcContactList.length; index++) { - int value = dcContactList[index]; - if (value > 0) { - actionModeSelection.put(index, value); - } - } - notifyDataSetChanged(); + public void selectAll() { + actionModeSelection.clear(); + for (int index = 0; index < dcContactList.length; index++) { + int value = dcContactList[index]; + if (value > 0) { + actionModeSelection.put(index, value); + } } + notifyDataSetChanged(); + } private boolean isActionModeEnabled() { return actionModeSelection.size() != 0; @@ -115,38 +109,52 @@ public ViewHolder(View itemView) { super(itemView); } - public abstract void bind(@NonNull GlideRequests glideRequests, int type, DcContact contact, String name, String number, String label, boolean multiSelect, boolean enabled); + public abstract void bind( + @NonNull GlideRequests glideRequests, + int type, + DcContact contact, + String name, + String number, + String label, + boolean multiSelect, + boolean enabled); + public abstract void unbind(@NonNull GlideRequests glideRequests); + public abstract void setChecked(boolean checked); + public abstract void setSelected(boolean enabled); + public abstract void setEnabled(boolean enabled); - } + } public class ContactViewHolder extends ViewHolder { - ContactViewHolder(@NonNull final View itemView, - @Nullable final ItemClickListener clickListener) { + ContactViewHolder( + @NonNull final View itemView, @Nullable final ItemClickListener clickListener) { super(itemView); - itemView.setOnClickListener(view -> { - if (clickListener != null) { - if (isActionModeEnabled()) { - toggleSelection(); - clickListener.onItemClick(getView(), true); - } else { - clickListener.onItemClick(getView(), false); - } - } - }); - itemView.setOnLongClickListener(view -> { - if (clickListener != null) { - int contactId = getContactId(getAdapterPosition()); - if (contactId > 0) { - toggleSelection(); - clickListener.onItemLongClick(getView()); + itemView.setOnClickListener( + view -> { + if (clickListener != null) { + if (isActionModeEnabled()) { + toggleSelection(); + clickListener.onItemClick(getView(), true); + } else { + clickListener.onItemClick(getView(), false); + } } - } - return true; - }); + }); + itemView.setOnLongClickListener( + view -> { + if (clickListener != null) { + int contactId = getContactId(getAdapterPosition()); + if (contactId > 0) { + toggleSelection(); + clickListener.onItemLongClick(getView()); + } + } + return true; + }); } private int getContactId(int adapterPosition) { @@ -163,7 +171,7 @@ private void toggleSelection() { boolean enabled = actionModeSelection.indexOfKey(adapterPosition) > -1; if (enabled) { ContactSelectionListAdapter.this.actionModeSelection.delete(adapterPosition); - } else { + } else { ContactSelectionListAdapter.this.actionModeSelection.put(adapterPosition, contactId); } notifyDataSetChanged(); @@ -173,7 +181,15 @@ public ContactSelectionListItem getView() { return (ContactSelectionListItem) itemView; } - public void bind(@NonNull GlideRequests glideRequests, int type, DcContact contact, String name, String addr, String label, boolean multiSelect, boolean enabled) { + public void bind( + @NonNull GlideRequests glideRequests, + int type, + DcContact contact, + String name, + String addr, + String label, + boolean multiSelect, + boolean enabled) { getView().set(glideRequests, type, contact, name, addr, label, multiSelect, enabled); } @@ -208,7 +224,15 @@ public static class DividerViewHolder extends ViewHolder { } @Override - public void bind(@NonNull GlideRequests glideRequests, int type, DcContact contact, String name, String number, String label, boolean multiSelect, boolean enabled) { + public void bind( + @NonNull GlideRequests glideRequests, + int type, + DcContact contact, + String name, + String number, + String label, + boolean multiSelect, + boolean enabled) { this.label.setText(name); } @@ -219,38 +243,38 @@ public void unbind(@NonNull GlideRequests glideRequests) {} public void setChecked(boolean checked) {} @Override - public void setSelected(boolean enabled) { - } + public void setSelected(boolean enabled) {} @Override - public void setEnabled(boolean enabled) { - - } + public void setEnabled(boolean enabled) {} } - public ContactSelectionListAdapter(@NonNull Context context, - @NonNull GlideRequests glideRequests, - @Nullable ItemClickListener clickListener, - boolean multiSelect, - boolean longPressSelect) - { + public ContactSelectionListAdapter( + @NonNull Context context, + @NonNull GlideRequests glideRequests, + @Nullable ItemClickListener clickListener, + boolean multiSelect, + boolean longPressSelect) { super(); - this.context = context; - this.dcContext = DcHelper.getContext(context); - this.li = LayoutInflater.from(context); + this.context = context; + this.dcContext = DcHelper.getContext(context); + this.li = LayoutInflater.from(context); this.glideRequests = glideRequests; - this.multiSelect = multiSelect; + this.multiSelect = multiSelect; this.clickListener = clickListener; this.longPressSelect = longPressSelect; } @NonNull @Override - public ContactSelectionListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + public ContactSelectionListAdapter.ViewHolder onCreateViewHolder( + @NonNull ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_CONTACT) { - return new ContactViewHolder(li.inflate(R.layout.contact_selection_list_item, parent, false), clickListener); + return new ContactViewHolder( + li.inflate(R.layout.contact_selection_list_item, parent, false), clickListener); } else { - return new DividerViewHolder(li.inflate(R.layout.contact_selection_list_divider, parent, false)); + return new DividerViewHolder( + li.inflate(R.layout.contact_selection_list_divider, parent, false)); } } @@ -265,7 +289,8 @@ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { if (id == DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT) { name = context.getString(R.string.menu_new_classic_contact); - itemMultiSelect = false; // the item creates a new contact in the list that will be selected instead + itemMultiSelect = + false; // the item creates a new contact in the list that will be selected instead } else if (id == DcContact.DC_CONTACT_ID_NEW_GROUP) { name = context.getString(R.string.menu_new_group); } else if (id == DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP) { @@ -317,7 +342,7 @@ public interface ItemClickListener { } public void changeData(DcContactsLoader.Ret loaderRet) { - this.dcContactList = loaderRet==null? new int[0] : loaderRet.ids; + this.dcContactList = loaderRet == null ? new int[0] : loaderRet.ids; recordCache.clear(); notifyDataSetChanged(); } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java b/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java index 20a187223..7454406be 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java @@ -7,35 +7,32 @@ import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcContact; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AvatarView; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; +import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; public class ContactSelectionListItem extends LinearLayout implements RecipientModifiedListener { - private AvatarView avatar; - private View numberContainer; - private TextView numberView; - private TextView nameView; - private TextView labelView; - private CheckBox checkBox; - - private int specialId; - private String name; - private String number; - private Recipient recipient; + private AvatarView avatar; + private View numberContainer; + private TextView numberView; + private TextView nameView; + private TextView labelView; + private CheckBox checkBox; + + private int specialId; + private String name; + private String number; + private Recipient recipient; private GlideRequests glideRequests; public ContactSelectionListItem(Context context) { @@ -49,31 +46,38 @@ public ContactSelectionListItem(Context context, AttributeSet attrs) { @Override protected void onFinishInflate() { super.onFinishInflate(); - this.avatar = findViewById(R.id.avatar); - this.numberContainer = findViewById(R.id.number_container); - this.numberView = findViewById(R.id.number); - this.labelView = findViewById(R.id.label); - this.nameView = findViewById(R.id.name); - this.checkBox = findViewById(R.id.check_box); + this.avatar = findViewById(R.id.avatar); + this.numberContainer = findViewById(R.id.number_container); + this.numberView = findViewById(R.id.number); + this.labelView = findViewById(R.id.label); + this.nameView = findViewById(R.id.name); + this.checkBox = findViewById(R.id.check_box); ViewUtil.setTextViewGravityStart(this.nameView, getContext()); } - public void set(@NonNull GlideRequests glideRequests, int specialId, DcContact contact, String name, String number, String label, boolean multiSelect, boolean enabled) { + public void set( + @NonNull GlideRequests glideRequests, + int specialId, + DcContact contact, + String name, + String number, + String label, + boolean multiSelect, + boolean enabled) { this.glideRequests = glideRequests; - this.specialId = specialId; - this.name = name; - this.number = number; - - if (specialId==DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT - || specialId==DcContact.DC_CONTACT_ID_NEW_GROUP - || specialId==DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP - || specialId==DcContact.DC_CONTACT_ID_NEW_BROADCAST - || specialId==DcContact.DC_CONTACT_ID_ADD_MEMBER - || specialId==DcContact.DC_CONTACT_ID_QR_INVITE) { + this.specialId = specialId; + this.name = name; + this.number = number; + + if (specialId == DcContact.DC_CONTACT_ID_NEW_CLASSIC_CONTACT + || specialId == DcContact.DC_CONTACT_ID_NEW_GROUP + || specialId == DcContact.DC_CONTACT_ID_NEW_UNENCRYPTED_GROUP + || specialId == DcContact.DC_CONTACT_ID_NEW_BROADCAST + || specialId == DcContact.DC_CONTACT_ID_ADD_MEMBER + || specialId == DcContact.DC_CONTACT_ID_QR_INVITE) { this.nameView.setTypeface(null, Typeface.BOLD); - } - else { + } else { this.recipient = new Recipient(getContext(), contact); this.recipient.addListener(this); if (this.recipient.getName() != null) { @@ -82,7 +86,9 @@ public void set(@NonNull GlideRequests glideRequests, int specialId, DcContact c this.nameView.setTypeface(null, Typeface.NORMAL); } if (specialId == DcContact.DC_CONTACT_ID_QR_INVITE) { - this.avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_qr_code_24).asDrawable(getContext(), ThemeUtil.getDummyContactColor(getContext()))); + this.avatar.setImageDrawable( + new ResourceContactPhoto(R.drawable.ic_qr_code_24) + .asDrawable(getContext(), ThemeUtil.getDummyContactColor(getContext()))); } else { this.avatar.setAvatar(glideRequests, recipient, false); } @@ -92,7 +98,7 @@ public void set(@NonNull GlideRequests glideRequests, int specialId, DcContact c setEnabled(enabled); if (multiSelect) this.checkBox.setVisibility(View.VISIBLE); - else this.checkBox.setVisibility(View.GONE); + else this.checkBox.setVisibility(View.GONE); } public void setChecked(boolean selected) { @@ -110,7 +116,7 @@ public void unbind(GlideRequests glideRequests) { private void setText(String name, String number, String label, DcContact contact) { this.nameView.setEnabled(true); - this.nameView.setText(name==null? "#" : name); + this.nameView.setText(name == null ? "#" : name); if (contact != null) { if (contact.isBot()) { @@ -120,17 +126,20 @@ private void setText(String name, String number, String label, DcContact contact } else { long timestamp = contact.getLastSeen(); if (timestamp != 0) { - number = getContext().getString(R.string.last_seen_at, DateUtils.getExtendedTimeSpanString(getContext(), timestamp)); + number = + getContext() + .getString( + R.string.last_seen_at, + DateUtils.getExtendedTimeSpanString(getContext(), timestamp)); } } } - if(number!=null) { + if (number != null) { this.numberView.setText(number); - this.labelView.setText(label==null? "" : label); + this.labelView.setText(label == null ? "" : label); this.numberContainer.setVisibility(View.VISIBLE); - } - else { + } else { this.numberContainer.setVisibility(View.GONE); } } @@ -162,12 +171,13 @@ public int getContactId() { @Override public void onModified(final Recipient recipient) { if (this.recipient == recipient) { - Util.runOnMain(() -> { - avatar.setAvatar(glideRequests, recipient, false); - DcContact contact = recipient.getDcContact(); - avatar.setSeenRecently(contact != null && contact.wasSeenRecently()); - nameView.setText(recipient.toShortString()); - }); + Util.runOnMain( + () -> { + avatar.setAvatar(glideRequests, recipient, false); + DcContact contact = recipient.getDcContact(); + avatar.setSeenRecently(contact != null && contact.wasSeenRecently()); + nameView.setText(recipient.toShortString()); + }); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java b/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java index c3459619a..5fb33d4dd 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/NewContactActivity.java @@ -6,15 +6,14 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; - +import chat.delta.rpc.types.SecurejoinSource; +import chat.delta.rpc.types.SecurejoinUiPath; import com.b44t.messenger.DcContext; import com.google.android.material.textfield.TextInputEditText; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - import org.thoughtcrime.securesms.ConversationActivity; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.R; @@ -22,11 +21,7 @@ import org.thoughtcrime.securesms.qr.QrCodeHandler; import org.thoughtcrime.securesms.util.ViewUtil; -import chat.delta.rpc.types.SecurejoinSource; -import chat.delta.rpc.types.SecurejoinUiPath; - -public class NewContactActivity extends PassphraseRequiredActionBarActivity -{ +public class NewContactActivity extends PassphraseRequiredActionBarActivity { public static final String ADDR_EXTRA = "contact_addr"; public static final String CONTACT_ID_EXTRA = "contact_id"; @@ -53,12 +48,13 @@ protected void onCreate(Bundle state, boolean ready) { nameInput = ViewUtil.findById(this, R.id.name_text); addrInput = ViewUtil.findById(this, R.id.email_text); addrInput.setText(getIntent().getStringExtra(ADDR_EXTRA)); - addrInput.setOnFocusChangeListener((view, focused) -> { - String addr = addrInput.getText() == null? "" : addrInput.getText().toString(); - if(!focused && !dcContext.mayBeValidAddr(addr)) { - addrInput.setError(getString(R.string.login_error_mail)); - } - }); + addrInput.setOnFocusChangeListener( + (view, focused) -> { + String addr = addrInput.getText() == null ? "" : addrInput.getText().toString(); + if (!focused && !dcContext.mayBeValidAddr(addr)) { + addrInput.setError(getString(R.string.login_error_mail)); + } + }); } @Override @@ -108,7 +104,8 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && requestCode == IntentIntegrator.REQUEST_CODE) { IntentResult scanResult = IntentIntegrator.parseActivityResult(resultCode, data); QrCodeHandler qrCodeHandler = new QrCodeHandler(this); - qrCodeHandler.handleOnlySecureJoinQr(scanResult.getContents(), SecurejoinSource.Scan, SecurejoinUiPath.NewContact); + qrCodeHandler.handleOnlySecureJoinQr( + scanResult.getContents(), SecurejoinSource.Scan, SecurejoinUiPath.NewContact); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ContactPhoto.java index 8c9120d34..abe9c81dd 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ContactPhoto.java @@ -1,14 +1,10 @@ package org.thoughtcrime.securesms.contacts.avatars; - import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.bumptech.glide.load.Key; - import java.io.IOException; import java.io.InputStream; @@ -16,8 +12,8 @@ public interface ContactPhoto extends Key { InputStream openInputStream(Context context) throws IOException; - @Nullable Uri getUri(@NonNull Context context); + @Nullable + Uri getUri(@NonNull Context context); boolean isProfilePhoto(); - } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java index d33323098..8cba7cd55 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java @@ -6,6 +6,6 @@ public interface FallbackContactPhoto { public Drawable asDrawable(Context context, int color); - public Drawable asCallCard(Context context); + public Drawable asCallCard(Context context); } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java index 2e87281e9..faa0fb844 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java @@ -3,15 +3,11 @@ import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; - import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; - import com.amulyakhare.textdrawable.TextDrawable; - -import org.thoughtcrime.securesms.R; - import java.util.regex.Pattern; +import org.thoughtcrime.securesms.R; public class GeneratedContactPhoto implements FallbackContactPhoto { @@ -20,7 +16,7 @@ public class GeneratedContactPhoto implements FallbackContactPhoto { private final String name; public GeneratedContactPhoto(@NonNull String name) { - this.name = name; + this.name = name; } @Override @@ -29,17 +25,21 @@ public Drawable asDrawable(Context context, int color) { } public Drawable asDrawable(Context context, int color, boolean roundShape) { - int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - - TextDrawable.IShapeBuilder builder = TextDrawable.builder() - .beginConfig() - .width(targetSize) - .height(targetSize) - .textColor(Color.WHITE) - .bold() - .toUpperCase() - .endConfig(); - return roundShape? builder.buildRound(getCharacter(name), color) : builder.buildRect(getCharacter(name), color); + int targetSize = + context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); + + TextDrawable.IShapeBuilder builder = + TextDrawable.builder() + .beginConfig() + .width(targetSize) + .height(targetSize) + .textColor(Color.WHITE) + .bold() + .toUpperCase() + .endConfig(); + return roundShape + ? builder.buildRound(getCharacter(name), color) + : builder.buildRect(getCharacter(name), color); } private String getCharacter(String name) { @@ -55,6 +55,5 @@ private String getCharacter(String name) { @Override public Drawable asCallCard(Context context) { return AppCompatResources.getDrawable(context, R.drawable.ic_person_large); - } } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java index 00b24e5c3..b04d9cde3 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java @@ -1,31 +1,28 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; - import com.b44t.messenger.DcChat; - import org.thoughtcrime.securesms.database.Address; public class GroupRecordContactPhoto extends LocalFileContactPhoto { - public GroupRecordContactPhoto(Context context, Address address, DcChat dcChat) { - super(context, address, dcChat, null); - } - - @Override - public boolean isProfilePhoto() { - return false; - } - - @Override - int getId() { - return address.getDcChatId(); - } - - @Override - public String getPath(Context context) { - String profileImage = dcChat.getProfileImage(); - return profileImage != null ? profileImage : ""; - } - + public GroupRecordContactPhoto(Context context, Address address, DcChat dcChat) { + super(context, address, dcChat, null); + } + + @Override + public boolean isProfilePhoto() { + return false; + } + + @Override + int getId() { + return address.getDcChatId(); + } + + @Override + public String getPath(Context context) { + String profileImage = dcChat.getProfileImage(); + return profileImage != null ? profileImage : ""; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java index dac672756..455a4e0b7 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/LocalFileContactPhoto.java @@ -2,72 +2,67 @@ import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; - -import org.thoughtcrime.securesms.database.Address; -import org.thoughtcrime.securesms.profiles.AvatarHelper; -import org.thoughtcrime.securesms.util.Conversions; - import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; +import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.profiles.AvatarHelper; +import org.thoughtcrime.securesms.util.Conversions; public abstract class LocalFileContactPhoto implements ContactPhoto { - final Address address; - final DcChat dcChat; - final DcContact dcContact; - - private final int id; + final Address address; + final DcChat dcChat; + final DcContact dcContact; - private final String path; + private final int id; - LocalFileContactPhoto(Context context, Address address, DcChat dcChat, DcContact dcContact) { - this.address = address; - this.dcChat = dcChat; - this.dcContact = dcContact; - id = getId(); - path = getPath(context); - } + private final String path; - @Override - public InputStream openInputStream(Context context) throws IOException { - return new FileInputStream(path); - } + LocalFileContactPhoto(Context context, Address address, DcChat dcChat, DcContact dcContact) { + this.address = address; + this.dcChat = dcChat; + this.dcContact = dcContact; + id = getId(); + path = getPath(context); + } - @Override - public @Nullable Uri getUri(@NonNull Context context) { - return isProfilePhoto() ? Uri.fromFile(AvatarHelper.getSelfAvatarFile(context)) : null; - } + @Override + public InputStream openInputStream(Context context) throws IOException { + return new FileInputStream(path); + } - @Override - public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update(address.serialize().getBytes()); - messageDigest.update(Conversions.longToByteArray(id)); - messageDigest.update(path.getBytes()); - } + @Override + public @Nullable Uri getUri(@NonNull Context context) { + return isProfilePhoto() ? Uri.fromFile(AvatarHelper.getSelfAvatarFile(context)) : null; + } - @Override - public boolean equals(Object other) { - if (!(other instanceof LocalFileContactPhoto)) return false; + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update(address.serialize().getBytes()); + messageDigest.update(Conversions.longToByteArray(id)); + messageDigest.update(path.getBytes()); + } - LocalFileContactPhoto that = (LocalFileContactPhoto) other; - return this.address.equals(that.address) && this.id == that.id && this.path.equals(that.path); - } + @Override + public boolean equals(Object other) { + if (!(other instanceof LocalFileContactPhoto)) return false; - @Override - public int hashCode() { - return this.address.hashCode() ^ id; - } + LocalFileContactPhoto that = (LocalFileContactPhoto) other; + return this.address.equals(that.address) && this.id == that.id && this.path.equals(that.path); + } - abstract int getId(); + @Override + public int hashCode() { + return this.address.hashCode() ^ id; + } - abstract public String getPath(Context context); + abstract int getId(); + public abstract String getPath(Context context); } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java index 767d99a5d..1c7f5745c 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/MyProfileContactPhoto.java @@ -1,62 +1,57 @@ package org.thoughtcrime.securesms.contacts.avatars; - import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.profiles.AvatarHelper; - import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; +import org.thoughtcrime.securesms.profiles.AvatarHelper; public class MyProfileContactPhoto implements ContactPhoto { - private final @NonNull String address; - private final @NonNull String avatarObject; - - public MyProfileContactPhoto(@NonNull String address, @NonNull String avatarObject) { - this.address = address; - this.avatarObject = avatarObject; - } - - @Override - public InputStream openInputStream(Context context) throws IOException { - return new FileInputStream(AvatarHelper.getSelfAvatarFile(context)); - } - - @Override - public @Nullable - Uri getUri(@NonNull Context context) { - return Uri.fromFile(AvatarHelper.getSelfAvatarFile(context)); - } - - @Override - public boolean isProfilePhoto() { - return true; - } - - @Override - public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update(address.getBytes()); - messageDigest.update(avatarObject.getBytes()); - } - - @Override - public boolean equals(Object other) { - if (other == null || !(other instanceof MyProfileContactPhoto)) return false; - - MyProfileContactPhoto that = (MyProfileContactPhoto) other; - - return this.address.equals(that.address) && this.avatarObject.equals(that.avatarObject); - } - - @Override - public int hashCode() { - return address.hashCode() ^ avatarObject.hashCode(); - } + private final @NonNull String address; + private final @NonNull String avatarObject; + + public MyProfileContactPhoto(@NonNull String address, @NonNull String avatarObject) { + this.address = address; + this.avatarObject = avatarObject; + } + + @Override + public InputStream openInputStream(Context context) throws IOException { + return new FileInputStream(AvatarHelper.getSelfAvatarFile(context)); + } + + @Override + public @Nullable Uri getUri(@NonNull Context context) { + return Uri.fromFile(AvatarHelper.getSelfAvatarFile(context)); + } + + @Override + public boolean isProfilePhoto() { + return true; + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update(address.getBytes()); + messageDigest.update(avatarObject.getBytes()); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof MyProfileContactPhoto)) return false; + + MyProfileContactPhoto that = (MyProfileContactPhoto) other; + + return this.address.equals(that.address) && this.avatarObject.equals(that.avatarObject); + } + + @Override + public int hashCode() { + return address.hashCode() ^ avatarObject.hashCode(); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java index 45c8bef00..0280e987b 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java @@ -1,31 +1,28 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; - import com.b44t.messenger.DcContact; - import org.thoughtcrime.securesms.database.Address; public class ProfileContactPhoto extends LocalFileContactPhoto { - public ProfileContactPhoto(Context context, Address address, DcContact dcContact) { - super(context, address, null, dcContact); - } - - @Override - public boolean isProfilePhoto() { - return true; - } - - @Override - int getId() { - return address.getDcContactId(); - } - - @Override - public String getPath(Context context) { - String profileImage = dcContact.getProfileImage(); - return profileImage != null ? profileImage : ""; - } - + public ProfileContactPhoto(Context context, Address address, DcContact dcContact) { + super(context, address, null, dcContact); + } + + @Override + public boolean isProfilePhoto() { + return true; + } + + @Override + int getId() { + return address.getDcContactId(); + } + + @Override + public String getPath(Context context) { + String profileImage = dcContact.getProfileImage(); + return profileImage != null ? profileImage : ""; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java index 0bd296147..8d371a0e7 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java @@ -4,10 +4,8 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.widget.ImageView; - import androidx.annotation.DrawableRes; import androidx.appcompat.content.res.AppCompatResources; - import com.amulyakhare.textdrawable.TextDrawable; import com.makeramen.roundedimageview.RoundedDrawable; @@ -21,14 +19,16 @@ public ResourceContactPhoto(@DrawableRes int resourceId) { } public ResourceContactPhoto(@DrawableRes int resourceId, @DrawableRes int callCardResourceId) { - this.resourceId = resourceId; + this.resourceId = resourceId; this.callCardResourceId = callCardResourceId; } @Override public Drawable asDrawable(Context context, int color) { - Drawable background = TextDrawable.builder().buildRound(" ", color); - RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId)); + Drawable background = TextDrawable.builder().buildRound(" ", color); + RoundedDrawable foreground = + (RoundedDrawable) + RoundedDrawable.fromDrawable(context.getResources().getDrawable(resourceId)); foreground.setScaleType(ImageView.ScaleType.CENTER); @@ -55,5 +55,4 @@ public int getIntrinsicHeight() { return -1; } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java index 8db2a7a5e..87f7242e2 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/SystemContactPhoto.java @@ -1,34 +1,32 @@ package org.thoughtcrime.securesms.contacts.avatars; - import android.content.Context; import android.net.Uri; import android.provider.ContactsContract; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.database.Address; -import org.thoughtcrime.securesms.util.Conversions; - import java.io.InputStream; import java.security.MessageDigest; +import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.util.Conversions; public class SystemContactPhoto implements ContactPhoto { private final @NonNull Address address; - private final @NonNull Uri contactPhotoUri; - private final long lastModifiedTime; + private final @NonNull Uri contactPhotoUri; + private final long lastModifiedTime; - public SystemContactPhoto(@NonNull Address address, @NonNull Uri contactPhotoUri, long lastModifiedTime) { - this.address = address; - this.contactPhotoUri = contactPhotoUri; + public SystemContactPhoto( + @NonNull Address address, @NonNull Uri contactPhotoUri, long lastModifiedTime) { + this.address = address; + this.contactPhotoUri = contactPhotoUri; this.lastModifiedTime = lastModifiedTime; } @Override - public InputStream openInputStream(Context context) { - return ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), contactPhotoUri, true); + public InputStream openInputStream(Context context) { + return ContactsContract.Contacts.openContactPhotoInputStream( + context.getContentResolver(), contactPhotoUri, true); } @Nullable @@ -53,14 +51,15 @@ public void updateDiskCacheKey(MessageDigest messageDigest) { public boolean equals(Object other) { if (other == null || !(other instanceof SystemContactPhoto)) return false; - SystemContactPhoto that = (SystemContactPhoto)other; + SystemContactPhoto that = (SystemContactPhoto) other; - return this.address.equals(that.address) && this.contactPhotoUri.equals(that.contactPhotoUri) && this.lastModifiedTime == that.lastModifiedTime; + return this.address.equals(that.address) + && this.contactPhotoUri.equals(that.contactPhotoUri) + && this.lastModifiedTime == that.lastModifiedTime; } @Override public int hashCode() { - return address.hashCode() ^ contactPhotoUri.hashCode() ^ (int)lastModifiedTime; + return address.hashCode() ^ contactPhotoUri.hashCode() ^ (int) lastModifiedTime; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java index a15fbc697..c5d399012 100644 --- a/src/main/java/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java +++ b/src/main/java/org/thoughtcrime/securesms/contacts/avatars/VcardContactPhoto.java @@ -2,19 +2,15 @@ import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.util.JsonUtils; - +import chat.delta.rpc.types.VcardContact; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.security.MessageDigest; - -import chat.delta.rpc.types.VcardContact; +import org.thoughtcrime.securesms.util.JsonUtils; public class VcardContactPhoto implements ContactPhoto { private final VcardContact vContact; @@ -26,7 +22,7 @@ public VcardContactPhoto(VcardContact vContact) { @Override public InputStream openInputStream(Context context) throws IOException { byte[] blob = JsonUtils.decodeBase64(vContact.profileImage); - return (blob == null)? null : new ByteArrayInputStream(blob); + return (blob == null) ? null : new ByteArrayInputStream(blob); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecret.java b/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecret.java index bdd425e0c..6f5871d96 100644 --- a/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecret.java +++ b/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecret.java @@ -1,11 +1,8 @@ package org.thoughtcrime.securesms.crypto; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.util.Hex; - import java.io.IOException; +import org.thoughtcrime.securesms.util.Hex; public class DatabaseSecret { @@ -18,7 +15,7 @@ public DatabaseSecret(@NonNull byte[] key) { } public DatabaseSecret(@NonNull String encoded) throws IOException { - this.key = Hex.fromStringCondensed(encoded); + this.key = Hex.fromStringCondensed(encoded); this.encoded = encoded; } diff --git a/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java b/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java index 3336b2993..027b7a1aa 100644 --- a/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java +++ b/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java @@ -1,16 +1,12 @@ package org.thoughtcrime.securesms.crypto; - import android.content.Context; import android.os.Build; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.util.Prefs; - import java.io.IOException; import java.security.SecureRandom; import java.util.concurrent.ConcurrentHashMap; +import org.thoughtcrime.securesms.util.Prefs; /** * It can be rather expensive to read from the keystore, so this class caches the key in memory @@ -18,7 +14,8 @@ */ public final class DatabaseSecretProvider { - private static final ConcurrentHashMap instances = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap instances = + new ConcurrentHashMap<>(); public static DatabaseSecret getOrCreateDatabaseSecret(@NonNull Context context, int accountId) { if (instances.get(accountId) == null) { @@ -32,20 +29,20 @@ public static DatabaseSecret getOrCreateDatabaseSecret(@NonNull Context context, return instances.get(accountId); } - private DatabaseSecretProvider() { - } + private DatabaseSecretProvider() {} private static @NonNull DatabaseSecret getOrCreate(@NonNull Context context, int accountId) { String unencryptedSecret = Prefs.getDatabaseUnencryptedSecret(context, accountId); - String encryptedSecret = Prefs.getDatabaseEncryptedSecret(context, accountId); + String encryptedSecret = Prefs.getDatabaseEncryptedSecret(context, accountId); - if (unencryptedSecret != null) return getUnencryptedDatabaseSecret(context, unencryptedSecret, accountId); - else if (encryptedSecret != null) return getEncryptedDatabaseSecret(encryptedSecret); - else return createAndStoreDatabaseSecret(context, accountId); + if (unencryptedSecret != null) + return getUnencryptedDatabaseSecret(context, unencryptedSecret, accountId); + else if (encryptedSecret != null) return getEncryptedDatabaseSecret(encryptedSecret); + else return createAndStoreDatabaseSecret(context, accountId); } - private static @NonNull DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret, int accountId) - { + private static @NonNull DatabaseSecret getUnencryptedDatabaseSecret( + @NonNull Context context, @NonNull String unencryptedSecret, int accountId) { try { DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret); @@ -64,18 +61,22 @@ private DatabaseSecretProvider() { } } - private static @NonNull DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) { + private static @NonNull DatabaseSecret getEncryptedDatabaseSecret( + @NonNull String serializedEncryptedSecret) { if (Build.VERSION.SDK_INT < 23) { - throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!"); + throw new AssertionError( + "OS downgrade not supported. KeyStore sealed data exists on platform < M!"); } else { - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret); + KeyStoreHelper.SealedData encryptedSecret = + KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret); return new DatabaseSecret(KeyStoreHelper.unseal(encryptedSecret)); } } - private static @NonNull DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context, int accountId) { + private static @NonNull DatabaseSecret createAndStoreDatabaseSecret( + @NonNull Context context, int accountId) { SecureRandom random = new SecureRandom(); - byte[] secret = new byte[32]; + byte[] secret = new byte[32]; random.nextBytes(secret); DatabaseSecret databaseSecret = new DatabaseSecret(secret); diff --git a/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java b/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java index 5a957c368..1ff8b3d20 100644 --- a/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java +++ b/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java @@ -1,14 +1,11 @@ package org.thoughtcrime.securesms.crypto; - import android.os.Build; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.util.Base64; - import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; - import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -18,9 +15,6 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import org.thoughtcrime.securesms.util.JsonUtils; - import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -31,7 +25,6 @@ import java.security.UnrecoverableEntryException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -39,11 +32,12 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; +import org.thoughtcrime.securesms.util.JsonUtils; public final class KeyStoreHelper { private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; - private static final String KEY_ALIAS = "DeltaSecret"; + private static final String KEY_ALIAS = "DeltaSecret"; @RequiresApi(Build.VERSION_CODES.M) public static SealedData seal(@NonNull byte[] input) { @@ -53,11 +47,15 @@ public static SealedData seal(@NonNull byte[] input) { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] iv = cipher.getIV(); + byte[] iv = cipher.getIV(); byte[] data = cipher.doFinal(input); return new SealedData(iv, data); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + } catch (NoSuchAlgorithmException + | NoSuchPaddingException + | InvalidKeyException + | IllegalBlockSizeException + | BadPaddingException e) { throw new AssertionError(e); } } @@ -71,7 +69,12 @@ public static byte[] unseal(@NonNull SealedData sealedData) { cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); return cipher.doFinal(sealedData.data); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { + } catch (NoSuchAlgorithmException + | NoSuchPaddingException + | InvalidKeyException + | InvalidAlgorithmParameterException + | IllegalBlockSizeException + | BadPaddingException e) { throw new AssertionError(e); } } @@ -79,14 +82,17 @@ public static byte[] unseal(@NonNull SealedData sealedData) { @RequiresApi(Build.VERSION_CODES.M) private static SecretKey getOrCreateKeyStoreEntry() { if (hasKeyStoreEntry()) return getKeyStoreEntry(); - else return createKeyStoreEntry(); + else return createKeyStoreEntry(); } @RequiresApi(Build.VERSION_CODES.M) private static SecretKey createKeyStoreEntry() { try { - KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE); - KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + KeyGenerator keyGenerator = + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE); + KeyGenParameterSpec keyGenParameterSpec = + new KeyGenParameterSpec.Builder( + KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .build(); @@ -94,7 +100,9 @@ private static SecretKey createKeyStoreEntry() { keyGenerator.init(keyGenParameterSpec); return keyGenerator.generateKey(); - } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { + } catch (NoSuchAlgorithmException + | NoSuchProviderException + | InvalidAlgorithmParameterException e) { throw new AssertionError(e); } } @@ -143,7 +151,8 @@ private static boolean hasKeyStoreEntry() { KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE); ks.load(null); - return ks.containsAlias(KEY_ALIAS) && ks.entryInstanceOf(KEY_ALIAS, KeyStore.SecretKeyEntry.class); + return ks.containsAlias(KEY_ALIAS) + && ks.entryInstanceOf(KEY_ALIAS, KeyStore.SecretKeyEntry.class); } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { throw new AssertionError(e); } @@ -162,12 +171,14 @@ public static class SealedData { private byte[] data; SealedData(@NonNull byte[] iv, @NonNull byte[] data) { - this.iv = iv; + this.iv = iv; this.data = data; } @SuppressWarnings("unused") - public SealedData() { /* needed by JsonUtils.fromJson() */ } + public SealedData() { + /* needed by JsonUtils.fromJson() */ + } public String serialize() { try { @@ -187,7 +198,8 @@ public static SealedData fromString(@NonNull String value) { private static class ByteArraySerializer extends JsonSerializer { @Override - public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING)); } } @@ -199,7 +211,5 @@ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING); } } - } - } diff --git a/src/main/java/org/thoughtcrime/securesms/database/Address.java b/src/main/java/org/thoughtcrime/securesms/database/Address.java index a114946a7..96a5c969a 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/Address.java +++ b/src/main/java/org/thoughtcrime/securesms/database/Address.java @@ -1,27 +1,26 @@ package org.thoughtcrime.securesms.database; - import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; public class Address implements Parcelable, Comparable

{ - public static final Parcelable.Creator
CREATOR = new Parcelable.Creator
() { - public Address createFromParcel(Parcel in) { - return new Address(in); - } + public static final Parcelable.Creator
CREATOR = + new Parcelable.Creator
() { + public Address createFromParcel(Parcel in) { + return new Address(in); + } - public Address[] newArray(int size) { - return new Address[size]; - } - }; + public Address[] newArray(int size) { + return new Address[size]; + } + }; public static final Address UNKNOWN = new Address("Unknown"); - private final static String DC_CHAT_PREFIX = "dc:"; - private final static String DC_CONTACT_PREFIX = "dcc:"; + private static final String DC_CHAT_PREFIX = "dc:"; + private static final String DC_CONTACT_PREFIX = "dcc:"; private final String address; @@ -46,17 +45,23 @@ public Address(Parcel in) { return new Address(serialized); } - public boolean isDcChat() { return address.startsWith(DC_CHAT_PREFIX); }; + public boolean isDcChat() { + return address.startsWith(DC_CHAT_PREFIX); + } + ; - public boolean isDcContact() { return address.startsWith(DC_CONTACT_PREFIX); }; + public boolean isDcContact() { + return address.startsWith(DC_CONTACT_PREFIX); + } + ; public int getDcChatId() { - if(!isDcChat()) throw new AssertionError("Not dc chat: " + address); + if (!isDcChat()) throw new AssertionError("Not dc chat: " + address); return Integer.valueOf(address.substring(DC_CHAT_PREFIX.length())); } public int getDcContactId() { - if(!isDcContact()) throw new AssertionError("Not dc contact: " + address); + if (!isDcContact()) throw new AssertionError("Not dc contact: " + address); return Integer.valueOf(address.substring(DC_CONTACT_PREFIX.length())); } diff --git a/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 95cbb3bfa..2fbb119d8 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -17,7 +17,7 @@ package org.thoughtcrime.securesms.database; public class AttachmentDatabase { - public static final int TRANSFER_PROGRESS_DONE = 0; + public static final int TRANSFER_PROGRESS_DONE = 0; public static final int TRANSFER_PROGRESS_STARTED = 1; - public static final int TRANSFER_PROGRESS_FAILED = 3; + public static final int TRANSFER_PROGRESS_FAILED = 3; } diff --git a/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java b/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java index e517390ca..918254088 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/database/CursorRecyclerViewAdapter.java @@ -1,18 +1,16 @@ /** * Copyright (C) 2015 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.database; @@ -21,7 +19,6 @@ import android.database.DataSetObserver; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -29,21 +26,23 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder; /** - * RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in ListView/GridView. + * RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in + * ListView/GridView. */ -public abstract class CursorRecyclerViewAdapter extends RecyclerView.Adapter { +public abstract class CursorRecyclerViewAdapter + extends RecyclerView.Adapter { private final Context context; private final DataSetObserver observer = new AdapterDataSetObserver(); - @VisibleForTesting static final int HEADER_TYPE = Integer.MIN_VALUE; - @VisibleForTesting static final int FOOTER_TYPE = Integer.MIN_VALUE + 1; - @VisibleForTesting static final long HEADER_ID = Long.MIN_VALUE; - @VisibleForTesting static final long FOOTER_ID = Long.MIN_VALUE + 1; + @VisibleForTesting static final int HEADER_TYPE = Integer.MIN_VALUE; + @VisibleForTesting static final int FOOTER_TYPE = Integer.MIN_VALUE + 1; + @VisibleForTesting static final long HEADER_ID = Long.MIN_VALUE; + @VisibleForTesting static final long FOOTER_ID = Long.MIN_VALUE + 1; - private Cursor cursor; - private boolean valid; - private @Nullable View header; - private @Nullable View footer; + private Cursor cursor; + private boolean valid; + private @Nullable View header; + private @Nullable View footer; private static class HeaderFooterViewHolder extends RecyclerView.ViewHolder { public HeaderFooterViewHolder(View itemView) { @@ -108,16 +107,16 @@ public int getItemCount() { if (!isActiveCursor()) return 0; return cursor.getCount() - + getFastAccessSize() - + (hasHeaderView() ? 1 : 0) - + (hasFooterView() ? 1 : 0); + + getFastAccessSize() + + (hasHeaderView() ? 1 : 0) + + (hasFooterView() ? 1 : 0); } @SuppressWarnings("unchecked") @Override public final void onViewRecycled(@NonNull ViewHolder holder) { if (!(holder instanceof HeaderFooterViewHolder)) { - onItemViewRecycled((VH)holder); + onItemViewRecycled((VH) holder); } } @@ -127,9 +126,12 @@ public void onItemViewRecycled(VH holder) {} @Override public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { - case HEADER_TYPE: return new HeaderFooterViewHolder(header); - case FOOTER_TYPE: return new HeaderFooterViewHolder(footer); - default: return onCreateItemViewHolder(parent, viewType); + case HEADER_TYPE: + return new HeaderFooterViewHolder(header); + case FOOTER_TYPE: + return new HeaderFooterViewHolder(footer); + default: + return onCreateItemViewHolder(parent, viewType); } } @@ -139,21 +141,19 @@ public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewTy @Override public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (!isHeaderPosition(position) && !isFooterPosition(position)) { - if (isFastAccessPosition(position)) onBindFastAccessItemViewHolder((VH)viewHolder, position); - else onBindItemViewHolder((VH)viewHolder, getCursorAtPositionOrThrow(position)); + if (isFastAccessPosition(position)) onBindFastAccessItemViewHolder((VH) viewHolder, position); + else onBindItemViewHolder((VH) viewHolder, getCursorAtPositionOrThrow(position)); } } public abstract void onBindItemViewHolder(VH viewHolder, @NonNull Cursor cursor); - protected void onBindFastAccessItemViewHolder(VH viewHolder, int position) { - - } + protected void onBindFastAccessItemViewHolder(VH viewHolder, int position) {} @Override public final int getItemViewType(int position) { - if (isHeaderPosition(position)) return HEADER_TYPE; - if (isFooterPosition(position)) return FOOTER_TYPE; + if (isHeaderPosition(position)) return HEADER_TYPE; + if (isFooterPosition(position)) return FOOTER_TYPE; if (isFastAccessPosition(position)) return getFastAccessItemViewType(position); return getItemViewType(getCursorAtPositionOrThrow(position)); } @@ -164,8 +164,8 @@ public int getItemViewType(@NonNull Cursor cursor) { @Override public final long getItemId(int position) { - if (isHeaderPosition(position)) return HEADER_ID; - if (isFooterPosition(position)) return FOOTER_ID; + if (isHeaderPosition(position)) return HEADER_ID; + if (isFooterPosition(position)) return FOOTER_ID; if (isFastAccessPosition(position)) return getFastAccessItemId(position); long itemId = getItemId(getCursorAtPositionOrThrow(position)); return itemId <= Long.MIN_VALUE + 1 ? itemId + 2 : itemId; diff --git a/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java b/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java index ee60b0d79..a50cacd41 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/database/loaders/BucketedThreadMediaLoader.java @@ -1,18 +1,10 @@ package org.thoughtcrime.securesms.database.loaders; - import android.content.Context; - import androidx.annotation.NonNull; import androidx.loader.content.AsyncTaskLoader; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.util.Util; - import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; @@ -22,15 +14,20 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.util.Util; -public class BucketedThreadMediaLoader extends AsyncTaskLoader { +public class BucketedThreadMediaLoader + extends AsyncTaskLoader { private final int chatId; private final int msgType1; private final int msgType2; private final int msgType3; - public BucketedThreadMediaLoader(@NonNull Context context, int chatId, int msgType1, int msgType2, int msgType3) { + public BucketedThreadMediaLoader( + @NonNull Context context, int chatId, int msgType1, int msgType2, int msgType3) { super(context); this.chatId = chatId; this.msgType1 = msgType1; @@ -53,16 +50,15 @@ protected void onStopLoading() { } @Override - protected void onAbandon() { - } + protected void onAbandon() {} @Override public BucketedThreadMedia loadInBackground() { - BucketedThreadMedia result = new BucketedThreadMedia(getContext()); + BucketedThreadMedia result = new BucketedThreadMedia(getContext()); DcContext context = DcHelper.getContext(getContext()); - if(chatId!=-1 /*0=all, -1=none*/) { + if (chatId != -1 /*0=all, -1=none*/) { int[] messages = context.getChatMedia(chatId, msgType1, msgType2, msgType3); - for(int nextId : messages) { + for (int nextId : messages) { result.add(context.getMsg(nextId)); } } @@ -72,37 +68,59 @@ public BucketedThreadMedia loadInBackground() { public static class BucketedThreadMedia { - private final TimeBucket TODAY; - private final TimeBucket YESTERDAY; - private final TimeBucket THIS_WEEK; - private final TimeBucket LAST_WEEK; - private final TimeBucket THIS_MONTH; - private final TimeBucket LAST_MONTH; + private final TimeBucket TODAY; + private final TimeBucket YESTERDAY; + private final TimeBucket THIS_WEEK; + private final TimeBucket LAST_WEEK; + private final TimeBucket THIS_MONTH; + private final TimeBucket LAST_MONTH; private final MonthBuckets OLDER; private final TimeBucket[] TIME_SECTIONS; public BucketedThreadMedia(@NonNull Context context) { // from today midnight until the end of human time - this.TODAY = new TimeBucket(context.getString(R.string.today), - addToCalendarFromTodayMidnight(Calendar.DAY_OF_YEAR, 0), Long.MAX_VALUE); + this.TODAY = + new TimeBucket( + context.getString(R.string.today), + addToCalendarFromTodayMidnight(Calendar.DAY_OF_YEAR, 0), + Long.MAX_VALUE); // from yesterday midnight until today midnight - this.YESTERDAY = new TimeBucket(context.getString(R.string.yesterday), - addToCalendarFromTodayMidnight(Calendar.DAY_OF_YEAR, -1), TODAY.startTime); - // from the closest start of week until yesterday midnight (that can be a negative timespace and thus be empty) - this.THIS_WEEK = new TimeBucket(context.getString(R.string.this_week), - setInCalendarFromTodayMidnight(Calendar.DAY_OF_WEEK, getCalendar().getFirstDayOfWeek()), YESTERDAY.startTime); + this.YESTERDAY = + new TimeBucket( + context.getString(R.string.yesterday), + addToCalendarFromTodayMidnight(Calendar.DAY_OF_YEAR, -1), + TODAY.startTime); + // from the closest start of week until yesterday midnight (that can be a negative timespace + // and thus be empty) + this.THIS_WEEK = + new TimeBucket( + context.getString(R.string.this_week), + setInCalendarFromTodayMidnight( + Calendar.DAY_OF_WEEK, getCalendar().getFirstDayOfWeek()), + YESTERDAY.startTime); // from the closest start of week one week back until the closest start of week. - this.LAST_WEEK = new TimeBucket(context.getString(R.string.last_week), - addToCalendarFrom(THIS_WEEK.startTime, Calendar.WEEK_OF_YEAR, -1), THIS_WEEK.startTime); - // from the closest 1st of a month until one week prior to the closest start of week (can be negative and thus empty) - this.THIS_MONTH = new TimeBucket(context.getString(R.string.this_month), - setInCalendarFromTodayMidnight(Calendar.DAY_OF_MONTH, 1), LAST_WEEK.startTime); + this.LAST_WEEK = + new TimeBucket( + context.getString(R.string.last_week), + addToCalendarFrom(THIS_WEEK.startTime, Calendar.WEEK_OF_YEAR, -1), + THIS_WEEK.startTime); + // from the closest 1st of a month until one week prior to the closest start of week (can be + // negative and thus empty) + this.THIS_MONTH = + new TimeBucket( + context.getString(R.string.this_month), + setInCalendarFromTodayMidnight(Calendar.DAY_OF_MONTH, 1), + LAST_WEEK.startTime); // from the closest 1st of a month, one month back to the closest 1st of a month - this.LAST_MONTH = new TimeBucket(context.getString(R.string.last_month), - addToCalendarFrom(THIS_MONTH.startTime, Calendar.MONTH, -1), LAST_WEEK.startTime); - this.TIME_SECTIONS = new TimeBucket[]{TODAY, YESTERDAY, THIS_WEEK, LAST_WEEK, THIS_MONTH, LAST_MONTH}; - this.OLDER = new MonthBuckets(); + this.LAST_MONTH = + new TimeBucket( + context.getString(R.string.last_month), + addToCalendarFrom(THIS_MONTH.startTime, Calendar.MONTH, -1), + LAST_WEEK.startTime); + this.TIME_SECTIONS = + new TimeBucket[] {TODAY, YESTERDAY, THIS_WEEK, LAST_WEEK, THIS_MONTH, LAST_MONTH}; + this.OLDER = new MonthBuckets(); } public void add(DcMsg imageMessage) { @@ -141,7 +159,7 @@ public int getSectionItemCount(int section) { } if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItemCount(); - else return OLDER.getSectionItemCount(section - activeTimeBuckets.size()); + else return OLDER.getSectionItemCount(section - activeTimeBuckets.size()); } public DcMsg get(int section, int item) { @@ -151,7 +169,7 @@ public DcMsg get(int section, int item) { } if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItem(item); - else return OLDER.getItem(section - activeTimeBuckets.size(), item); + else return OLDER.getItem(section - activeTimeBuckets.size(), item); } public String getName(int section) { @@ -161,7 +179,7 @@ public String getName(int section) { } if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getName(); - else return OLDER.getName(section - activeTimeBuckets.size()); + else return OLDER.getName(section - activeTimeBuckets.size()); } // tests should override this function to deliver a preset calendar. @@ -201,12 +219,12 @@ private static class TimeBucket { private final LinkedList records = new LinkedList<>(); - private final long startTime; + private final long startTime; private final long endTime; private final String name; TimeBucket(String name, long startTime, long endTime) { - this.name = name; + this.name = name; this.startTime = startTime; this.endTime = endTime; } @@ -244,9 +262,9 @@ void add(DcMsg record) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(record.getTimestamp()); - int year = calendar.get(Calendar.YEAR) - 1900; - int month = calendar.get(Calendar.MONTH); - Date date = new Date(year, month, 1); + int year = calendar.get(Calendar.YEAR) - 1900; + int month = calendar.get(Calendar.MONTH); + Date date = new Date(year, month, 1); if (months.containsKey(date)) { months.get(date).addFirst(record); diff --git a/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java b/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java index 24f366cc3..65547290e 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java @@ -1,16 +1,12 @@ package org.thoughtcrime.securesms.database.loaders; - import android.content.Context; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMediaGalleryElement; import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.AsyncLoader; @@ -18,12 +14,12 @@ public class PagingMediaLoader extends AsyncLoader { private static final String TAG = PagingMediaLoader.class.getSimpleName(); - private final DcMsg msg; - private final boolean leftIsRecent; + private final DcMsg msg; + private final boolean leftIsRecent; public PagingMediaLoader(@NonNull Context context, @NonNull DcMsg msg, boolean leftIsRecent) { super(context); - this.msg = msg; + this.msg = msg; this.leftIsRecent = leftIsRecent; } @@ -31,19 +27,28 @@ public PagingMediaLoader(@NonNull Context context, @NonNull DcMsg msg, boolean l @Override public DcMediaGalleryElement loadInBackground() { DcContext context = DcHelper.getContext(getContext()); - int[] mediaMessages = context.getChatMedia(msg.getChatId(), DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO); + int[] mediaMessages = + context.getChatMedia( + msg.getChatId(), DcMsg.DC_MSG_IMAGE, DcMsg.DC_MSG_GIF, DcMsg.DC_MSG_VIDEO); // first id is the oldest message. int currentIndex = -1; - for(int ii = 0; ii < mediaMessages.length; ii++) { - if(mediaMessages[ii] == msg.getId()) { + for (int ii = 0; ii < mediaMessages.length; ii++) { + if (mediaMessages[ii] == msg.getId()) { currentIndex = ii; break; } } - if(currentIndex == -1) { + if (currentIndex == -1) { currentIndex = 0; DcMsg unfound = context.getMsg(msg.getId()); - Log.e(TAG, "did not find message in list: " + unfound.getId() + " / " + unfound.getFile() + " / " + unfound.getText()); + Log.e( + TAG, + "did not find message in list: " + + unfound.getId() + + " / " + + unfound.getFile() + + " / " + + unfound.getText()); } return new DcMediaGalleryElement(mediaMessages, currentIndex, context, leftIsRecent); } diff --git a/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java b/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java index ebbdad799..f1a73e8fe 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/database/loaders/RecentPhotosLoader.java @@ -1,30 +1,28 @@ package org.thoughtcrime.securesms.database.loaders; - import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; - import androidx.loader.content.CursorLoader; - import org.thoughtcrime.securesms.permissions.Permissions; public class RecentPhotosLoader extends CursorLoader { public static final Uri BASE_URL = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - private static final String[] PROJECTION = new String[] { - MediaStore.Images.ImageColumns._ID, - MediaStore.Images.ImageColumns.DATE_TAKEN, - MediaStore.Images.ImageColumns.DATE_MODIFIED, - MediaStore.Images.ImageColumns.ORIENTATION, - MediaStore.Images.ImageColumns.MIME_TYPE - }; + private static final String[] PROJECTION = + new String[] { + MediaStore.Images.ImageColumns._ID, + MediaStore.Images.ImageColumns.DATE_TAKEN, + MediaStore.Images.ImageColumns.DATE_MODIFIED, + MediaStore.Images.ImageColumns.ORIENTATION, + MediaStore.Images.ImageColumns.MIME_TYPE + }; - private static final String SELECTION = Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.IS_PENDING + " != 1" - : null; + private static final String SELECTION = + Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.IS_PENDING + " != 1" : null; private final Context context; @@ -36,13 +34,16 @@ public RecentPhotosLoader(Context context) { @Override public Cursor loadInBackground() { if (Permissions.hasAll(context, Permissions.galleryPermissions())) { - return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - PROJECTION, SELECTION, null, - MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC"); + return context + .getContentResolver() + .query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + PROJECTION, + SELECTION, + null, + MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC"); } else { return null; } } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 428da4776..cc237790e 100644 --- a/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -20,53 +20,51 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.style.StyleSpan; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcLot; - import org.thoughtcrime.securesms.recipients.Recipient; /** * The message record model which represents thread heading messages. * * @author Moxie Marlinspike - * */ public class ThreadRecord { - private final Recipient recipient; - private final long dateReceived; - private final long threadId; - private final String body; - - private final int unreadCount; - private final int visibility; - private final boolean isSendingLocations; - private final boolean isMuted; - private final boolean isContactRequest; - private @Nullable final DcLot dcSummary; - - public ThreadRecord(@NonNull String body, - @NonNull Recipient recipient, long dateReceived, int unreadCount, - long threadId, - int visibility, - boolean isSendingLocations, - boolean isMuted, - boolean isContactRequest, - @Nullable DcLot dcSummary) - { - this.threadId = threadId; - this.recipient = recipient; - this.dateReceived = dateReceived; - this.body = body; - this.unreadCount = unreadCount; - this.visibility = visibility; + private final Recipient recipient; + private final long dateReceived; + private final long threadId; + private final String body; + + private final int unreadCount; + private final int visibility; + private final boolean isSendingLocations; + private final boolean isMuted; + private final boolean isContactRequest; + private @Nullable final DcLot dcSummary; + + public ThreadRecord( + @NonNull String body, + @NonNull Recipient recipient, + long dateReceived, + int unreadCount, + long threadId, + int visibility, + boolean isSendingLocations, + boolean isMuted, + boolean isContactRequest, + @Nullable DcLot dcSummary) { + this.threadId = threadId; + this.recipient = recipient; + this.dateReceived = dateReceived; + this.body = body; + this.unreadCount = unreadCount; + this.visibility = visibility; this.isSendingLocations = isSendingLocations; - this.isMuted = isMuted; + this.isMuted = isMuted; this.isContactRequest = isContactRequest; - this.dcSummary = dcSummary; + this.dcSummary = dcSummary; } public @NonNull String getBody() { @@ -86,7 +84,7 @@ public long getThreadId() { } public SpannableString getDisplayBody() { - if(dcSummary!=null && dcSummary.getText1Meaning()==DcLot.DC_TEXT1_DRAFT) { + if (dcSummary != null && dcSummary.getText1Meaning() == DcLot.DC_TEXT1_DRAFT) { String draftText = dcSummary.getText1() + ":"; return emphasisAdded(draftText + " " + dcSummary.getText2(), 0, draftText.length()); } else { @@ -96,8 +94,11 @@ public SpannableString getDisplayBody() { private SpannableString emphasisAdded(String sequence, int start, int end) { SpannableString spannable = new SpannableString(sequence); - spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), - start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(android.graphics.Typeface.ITALIC), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } @@ -118,11 +119,11 @@ public int getVisibility() { } public boolean isSendingLocations() { - return isSendingLocations; + return isSendingLocations; } public boolean isMuted() { - return isMuted; + return isMuted; } public boolean isContactRequest() { diff --git a/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocation.java b/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocation.java index 23ce38eb4..614823e8b 100644 --- a/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocation.java +++ b/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocation.java @@ -2,129 +2,127 @@ import android.location.Location; import android.util.Log; - import java.util.Observable; public class DcLocation extends Observable { - private static final String TAG = DcLocation.class.getSimpleName(); - private Location lastLocation; - private static DcLocation instance; - private static final int TIMEOUT = 1000 * 15; - private static final int EARTH_RADIUS = 6371; - - private DcLocation() { - lastLocation = getDefault(); + private static final String TAG = DcLocation.class.getSimpleName(); + private Location lastLocation; + private static DcLocation instance; + private static final int TIMEOUT = 1000 * 15; + private static final int EARTH_RADIUS = 6371; + + private DcLocation() { + lastLocation = getDefault(); + } + + public static DcLocation getInstance() { + if (instance == null) { + instance = new DcLocation(); } + return instance; + } - public static DcLocation getInstance() { - if (instance == null) { - instance = new DcLocation(); - } - return instance; - } + public Location getLastLocation() { + return lastLocation; + } - public Location getLastLocation() { - return lastLocation; - } + public boolean isValid() { + return !"?".equals(lastLocation.getProvider()); + } + void updateLocation(Location location) { + if (isBetterLocation(location, lastLocation)) { + lastLocation = location; - public boolean isValid() { - return !"?".equals(lastLocation.getProvider()); + instance.setChanged(); + instance.notifyObservers(); } - - void updateLocation(Location location) { - if (isBetterLocation(location, lastLocation)) { - lastLocation = location; - - instance.setChanged(); - instance.notifyObservers(); - } + } + + void reset() { + updateLocation(getDefault()); + } + + private Location getDefault() { + return new Location("?"); + } + + /** + * https://developer.android.com/guide/topics/location/strategies Determines whether one Location + * reading is better than the current Location fix + * + * @param location The new Location that you want to evaluate + * @param currentBestLocation The current Location fix, to which you want to compare the new one + */ + private boolean isBetterLocation(Location location, Location currentBestLocation) { + if (currentBestLocation == null) { + // A new location is always better than no location + return true; } - void reset() { - updateLocation(getDefault()); - - } + // Check whether the new location fix is newer or older + long timeDelta = location.getTime() - currentBestLocation.getTime(); + boolean isSignificantlyOlder = timeDelta < -TIMEOUT; - private Location getDefault() { - return new Location("?"); + if (isSignificantlyOlder) { + return false; } - /** https://developer.android.com/guide/topics/location/strategies - * Determines whether one Location reading is better than the current Location fix - * @param location The new Location that you want to evaluate - * @param currentBestLocation The current Location fix, to which you want to compare the new one - */ - private boolean isBetterLocation(Location location, Location currentBestLocation) { - if (currentBestLocation == null) { - // A new location is always better than no location - return true; - } - - // Check whether the new location fix is newer or older - long timeDelta = location.getTime() - currentBestLocation.getTime(); - boolean isSignificantlyOlder = timeDelta < -TIMEOUT; - - if (isSignificantlyOlder) { - return false; - } - - // Check whether the new location fix is more or less accurate - int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); - Log.d(TAG, "accuracyDelta: " + accuracyDelta); - boolean isSignificantlyMoreAccurate = accuracyDelta > 50; - boolean isSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider()); - - if (isSignificantlyMoreAccurate && isSameProvider) { - return true; - } - - boolean isMoreAccurate = accuracyDelta > 0; - double distance = distance(location, currentBestLocation); - return hasLocationChanged(distance) && isMoreAccurate || - hasLocationSignificantlyChanged(distance); + // Check whether the new location fix is more or less accurate + int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); + Log.d(TAG, "accuracyDelta: " + accuracyDelta); + boolean isSignificantlyMoreAccurate = accuracyDelta > 50; + boolean isSameProvider = + isSameProvider(location.getProvider(), currentBestLocation.getProvider()); + if (isSignificantlyMoreAccurate && isSameProvider) { + return true; } - private boolean hasLocationSignificantlyChanged(double distance) { - return distance > 30D; - } + boolean isMoreAccurate = accuracyDelta > 0; + double distance = distance(location, currentBestLocation); + return hasLocationChanged(distance) && isMoreAccurate + || hasLocationSignificantlyChanged(distance); + } - private boolean hasLocationChanged(double distance) { - return distance > 10D; - } + private boolean hasLocationSignificantlyChanged(double distance) { + return distance > 30D; + } - private double distance(Location location, Location currentBestLocation) { + private boolean hasLocationChanged(double distance) { + return distance > 10D; + } - double startLat = location.getLatitude(); - double startLong = location.getLongitude(); - double endLat = currentBestLocation.getLatitude(); - double endLong = currentBestLocation.getLongitude(); + private double distance(Location location, Location currentBestLocation) { - double dLat = Math.toRadians(endLat - startLat); - double dLong = Math.toRadians(endLong - startLong); + double startLat = location.getLatitude(); + double startLong = location.getLongitude(); + double endLat = currentBestLocation.getLatitude(); + double endLong = currentBestLocation.getLongitude(); - startLat = Math.toRadians(startLat); - endLat = Math.toRadians(endLat); + double dLat = Math.toRadians(endLat - startLat); + double dLong = Math.toRadians(endLong - startLong); - double a = haversin(dLat) + Math.cos(startLat) * Math.cos(endLat) * haversin(dLong); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + startLat = Math.toRadians(startLat); + endLat = Math.toRadians(endLat); - double distance = EARTH_RADIUS * c * 1000; - Log.d(TAG, "Distance between location updates: " + distance); - return distance; - } + double a = haversin(dLat) + Math.cos(startLat) * Math.cos(endLat) * haversin(dLong); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - private double haversin(double val) { - return Math.pow(Math.sin(val / 2), 2); - } + double distance = EARTH_RADIUS * c * 1000; + Log.d(TAG, "Distance between location updates: " + distance); + return distance; + } - /** Checks whether two providers are the same */ - private boolean isSameProvider(String provider1, String provider2) { - if (provider1 == null) { - return provider2 == null; - } - return provider1.equals(provider2); - } + private double haversin(double val) { + return Math.pow(Math.sin(val / 2), 2); + } + /** Checks whether two providers are the same */ + private boolean isSameProvider(String provider1, String provider2) { + if (provider1 == null) { + return provider2 == null; + } + return provider1.equals(provider2); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java b/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java index c54e81455..6a12d06c2 100644 --- a/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java +++ b/src/main/java/org/thoughtcrime/securesms/geolocation/DcLocationManager.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.geolocation; +import static android.content.Context.BIND_AUTO_CREATE; + import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -7,111 +9,117 @@ import android.location.Location; import android.os.IBinder; import android.util.Log; - -import org.thoughtcrime.securesms.connect.DcHelper; - +import com.b44t.messenger.DcContext; import java.util.LinkedList; import java.util.Observable; import java.util.Observer; - -import static android.content.Context.BIND_AUTO_CREATE; - -import com.b44t.messenger.DcContext; +import org.thoughtcrime.securesms.connect.DcHelper; public class DcLocationManager implements Observer { - private static final String TAG = DcLocationManager.class.getSimpleName(); - private LocationBackgroundService.LocationBackgroundServiceBinder serviceBinder; - private final Context context; - private DcLocation dcLocation = DcLocation.getInstance(); - private final LinkedList pendingShareLastLocation = new LinkedList<>(); - private final ServiceConnection serviceConnection = new ServiceConnection() { + private static final String TAG = DcLocationManager.class.getSimpleName(); + private LocationBackgroundService.LocationBackgroundServiceBinder serviceBinder; + private final Context context; + private DcLocation dcLocation = DcLocation.getInstance(); + private final LinkedList pendingShareLastLocation = new LinkedList<>(); + private final ServiceConnection serviceConnection = + new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - Log.d(TAG, "background service connected"); - serviceBinder = (LocationBackgroundService.LocationBackgroundServiceBinder) service; - while (!pendingShareLastLocation.isEmpty()) { - shareLastLocation(pendingShareLastLocation.pop()); - } + Log.d(TAG, "background service connected"); + serviceBinder = (LocationBackgroundService.LocationBackgroundServiceBinder) service; + while (!pendingShareLastLocation.isEmpty()) { + shareLastLocation(pendingShareLastLocation.pop()); + } } @Override public void onServiceDisconnected(ComponentName name) { - Log.d(TAG, "background service disconnected"); - serviceBinder = null; + Log.d(TAG, "background service disconnected"); + serviceBinder = null; } - }; + }; - public DcLocationManager(Context context, DcContext dcContext) { - this.context = context.getApplicationContext(); - DcLocation.getInstance().addObserver(this); - if (dcContext.isSendingLocationsToChat(0)) { - startLocationEngine(); - } + public DcLocationManager(Context context, DcContext dcContext) { + this.context = context.getApplicationContext(); + DcLocation.getInstance().addObserver(this); + if (dcContext.isSendingLocationsToChat(0)) { + startLocationEngine(); } + } - public void startLocationEngine() { - if (serviceBinder == null) { - Intent intent = new Intent(context.getApplicationContext(), LocationBackgroundService.class); - context.bindService(intent, serviceConnection, BIND_AUTO_CREATE); - } + public void startLocationEngine() { + if (serviceBinder == null) { + Intent intent = new Intent(context.getApplicationContext(), LocationBackgroundService.class); + context.bindService(intent, serviceConnection, BIND_AUTO_CREATE); } + } - public void stopLocationEngine() { - if (serviceBinder == null) { - return; - } - context.unbindService(serviceConnection); - serviceBinder.stop(); - serviceBinder = null; + public void stopLocationEngine() { + if (serviceBinder == null) { + return; } - - public void stopSharingLocation(int chatId) { - DcHelper.getContext(context).sendLocationsToChat(chatId, 0); - if(!DcHelper.getContext(context).isSendingLocationsToChat(0)) { - stopLocationEngine(); - } + context.unbindService(serviceConnection); + serviceBinder.stop(); + serviceBinder = null; + } + + public void stopSharingLocation(int chatId) { + DcHelper.getContext(context).sendLocationsToChat(chatId, 0); + if (!DcHelper.getContext(context).isSendingLocationsToChat(0)) { + stopLocationEngine(); } - - public void shareLocation(int duration, int chatId) { - startLocationEngine(); - Log.d(TAG, String.format("Share location in chat %d for %d seconds", chatId, duration)); - DcHelper.getContext(context).sendLocationsToChat(chatId, duration); - if (dcLocation.isValid()) { - writeDcLocationUpdateMessage(); - } + } + + public void shareLocation(int duration, int chatId) { + startLocationEngine(); + Log.d(TAG, String.format("Share location in chat %d for %d seconds", chatId, duration)); + DcHelper.getContext(context).sendLocationsToChat(chatId, duration); + if (dcLocation.isValid()) { + writeDcLocationUpdateMessage(); } + } - public void shareLastLocation(int chatId) { - if (serviceBinder == null) { - pendingShareLastLocation.push(chatId); - startLocationEngine(); - return; - } - - if (dcLocation.isValid()) { - DcHelper.getContext(context).sendLocationsToChat(chatId, 1); - writeDcLocationUpdateMessage(); - } + public void shareLastLocation(int chatId) { + if (serviceBinder == null) { + pendingShareLastLocation.push(chatId); + startLocationEngine(); + return; } - @Override - public void update(Observable o, Object arg) { - if (o instanceof DcLocation) { - dcLocation = (DcLocation) o; - if (dcLocation.isValid()) { - writeDcLocationUpdateMessage(); - } - } + if (dcLocation.isValid()) { + DcHelper.getContext(context).sendLocationsToChat(chatId, 1); + writeDcLocationUpdateMessage(); } - - private void writeDcLocationUpdateMessage() { - Log.d(TAG, "Share location: " + dcLocation.getLastLocation().getLatitude() + ", " + dcLocation.getLastLocation().getLongitude()); - Location lastLocation = dcLocation.getLastLocation(); - - boolean continueLocationStreaming = DcHelper.getContext(context).setLocation((float) lastLocation.getLatitude(), (float) lastLocation.getLongitude(), lastLocation.getAccuracy()); - if (!continueLocationStreaming) { - stopLocationEngine(); - } + } + + @Override + public void update(Observable o, Object arg) { + if (o instanceof DcLocation) { + dcLocation = (DcLocation) o; + if (dcLocation.isValid()) { + writeDcLocationUpdateMessage(); + } + } + } + + private void writeDcLocationUpdateMessage() { + Log.d( + TAG, + "Share location: " + + dcLocation.getLastLocation().getLatitude() + + ", " + + dcLocation.getLastLocation().getLongitude()); + Location lastLocation = dcLocation.getLastLocation(); + + boolean continueLocationStreaming = + DcHelper.getContext(context) + .setLocation( + (float) lastLocation.getLatitude(), + (float) lastLocation.getLongitude(), + lastLocation.getAccuracy()); + if (!continueLocationStreaming) { + stopLocationEngine(); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java b/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java index 4b17f1d27..a4fc4adbe 100644 --- a/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java +++ b/src/main/java/org/thoughtcrime/securesms/geolocation/LocationBackgroundService.java @@ -11,130 +11,132 @@ import android.os.Bundle; import android.os.IBinder; import android.util.Log; - import androidx.annotation.NonNull; public class LocationBackgroundService extends Service { - private static final int INITIAL_TIMEOUT = 1000 * 60 * 2; - private static final String TAG = LocationBackgroundService.class.getSimpleName(); - private LocationManager locationManager = null; - private static final int LOCATION_INTERVAL = 1000; - private static final float LOCATION_DISTANCE = 25F; - ServiceLocationListener locationListener; - - private final IBinder mBinder = new LocationBackgroundServiceBinder(); - - @Override - public boolean bindService(Intent service, ServiceConnection conn, int flags) { - return super.bindService(service, conn, flags); + private static final int INITIAL_TIMEOUT = 1000 * 60 * 2; + private static final String TAG = LocationBackgroundService.class.getSimpleName(); + private LocationManager locationManager = null; + private static final int LOCATION_INTERVAL = 1000; + private static final float LOCATION_DISTANCE = 25F; + ServiceLocationListener locationListener; + + private final IBinder mBinder = new LocationBackgroundServiceBinder(); + + @Override + public boolean bindService(Intent service, ServiceConnection conn, int flags) { + return super.bindService(service, conn, flags); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onCreate() { + locationManager = + (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE); + if (locationManager == null) { + Log.e(TAG, "Unable to initialize location service"); + return; } - @Override - public IBinder onBind(Intent intent) { - return mBinder; + locationListener = new ServiceLocationListener(); + Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (lastLocation != null) { + long locationAge = System.currentTimeMillis() - lastLocation.getTime(); + if (locationAge <= 600 * 1000) { // not older than 10 minutes + DcLocation.getInstance().updateLocation(lastLocation); + } } - - @Override - public void onCreate() { - locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE); - if (locationManager == null) { - Log.e(TAG, "Unable to initialize location service"); - return; - } - - locationListener = new ServiceLocationListener(); - Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - if (lastLocation != null) { - long locationAge = System.currentTimeMillis() - lastLocation.getTime(); - if (locationAge <= 600 * 1000) { // not older than 10 minutes - DcLocation.getInstance().updateLocation(lastLocation); - } - } - //requestLocationUpdate(LocationManager.NETWORK_PROVIDER); - requestLocationUpdate(LocationManager.GPS_PROVIDER); - initialLocationUpdate(); + // requestLocationUpdate(LocationManager.NETWORK_PROVIDER); + requestLocationUpdate(LocationManager.GPS_PROVIDER); + initialLocationUpdate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (locationManager == null) { + return; } - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - return START_STICKY; + try { + locationManager.removeUpdates(locationListener); + } catch (Exception ex) { + Log.i(TAG, "fail to remove location listeners, ignore", ex); + } + } + + private void requestLocationUpdate(String provider) { + try { + locationManager.requestLocationUpdates( + provider, LOCATION_INTERVAL, LOCATION_DISTANCE, locationListener); + } catch (SecurityException | IllegalArgumentException ex) { + Log.e( + TAG, + String.format("Unable to request %s provider based location updates.", provider), + ex); } + } + + private void initialLocationUpdate() { + try { + Location gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (gpsLocation != null + && System.currentTimeMillis() - gpsLocation.getTime() < INITIAL_TIMEOUT) { + locationListener.onLocationChanged(gpsLocation); + } + + } catch (NullPointerException | SecurityException e) { + e.printStackTrace(); + } + } - @Override - public void onDestroy() { - super.onDestroy(); - - if (locationManager == null) { - return; - } - - try { - locationManager.removeUpdates(locationListener); - } catch (Exception ex) { - Log.i(TAG, "fail to remove location listeners, ignore", ex); - } + class LocationBackgroundServiceBinder extends Binder { + LocationBackgroundServiceBinder getService() { + return LocationBackgroundServiceBinder.this; } - private void requestLocationUpdate(String provider) { - try { - locationManager.requestLocationUpdates( - provider, LOCATION_INTERVAL, LOCATION_DISTANCE, - locationListener); - } catch (SecurityException | IllegalArgumentException ex) { - Log.e(TAG, String.format("Unable to request %s provider based location updates.", provider), ex); - } + void stop() { + DcLocation.getInstance().reset(); + stopSelf(); } + } - private void initialLocationUpdate() { - try { - Location gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - if (gpsLocation != null && System.currentTimeMillis() - gpsLocation.getTime() < INITIAL_TIMEOUT) { - locationListener.onLocationChanged(gpsLocation); - } + private class ServiceLocationListener implements LocationListener { - } catch (NullPointerException | SecurityException e) { - e.printStackTrace(); - } + @Override + public void onLocationChanged(@NonNull Location location) { + Log.d(TAG, "onLocationChanged: " + location); + if (location == null) { + return; + } + DcLocation.getInstance().updateLocation(location); } - class LocationBackgroundServiceBinder extends Binder { - LocationBackgroundServiceBinder getService() { - return LocationBackgroundServiceBinder.this; - } - - void stop() { - DcLocation.getInstance().reset(); - stopSelf(); - } + @Override + public void onProviderDisabled(@NonNull String provider) { + Log.e(TAG, "onProviderDisabled: " + provider); } - private class ServiceLocationListener implements LocationListener { - - @Override - public void onLocationChanged(@NonNull Location location) { - Log.d(TAG, "onLocationChanged: " + location); - if (location == null) { - return; - } - DcLocation.getInstance().updateLocation(location); - } - - @Override - public void onProviderDisabled(@NonNull String provider) { - Log.e(TAG, "onProviderDisabled: " + provider); - } - - @Override - public void onProviderEnabled(@NonNull String provider) { - Log.e(TAG, "onProviderEnabled: " + provider); - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - Log.e(TAG, "onStatusChanged: " + provider + " status: " + status); - } + @Override + public void onProviderEnabled(@NonNull String provider) { + Log.e(TAG, "onProviderEnabled: " + provider); } + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + Log.e(TAG, "onStatusChanged: " + provider + " status: " + status); + } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java b/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java index 56db549e7..ff0a259b7 100644 --- a/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java +++ b/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java @@ -1,28 +1,23 @@ package org.thoughtcrime.securesms.glide; - import android.content.Context; - import androidx.annotation.NonNull; - import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; - -import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; - import java.io.IOException; import java.io.InputStream; +import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; class ContactPhotoFetcher implements DataFetcher { - private final Context context; + private final Context context; private final ContactPhoto contactPhoto; private InputStream inputStream; ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) { - this.context = context.getApplicationContext(); + this.context = context.getApplicationContext(); this.contactPhoto = contactPhoto; } @@ -40,13 +35,12 @@ public void loadData(@NonNull Priority priority, DataCallback { @@ -24,7 +20,8 @@ private ContactPhotoLoader(Context context) { @Nullable @Override - public LoadData buildLoadData(@NonNull ContactPhoto contactPhoto, int width, int height, @NonNull Options options) { + public LoadData buildLoadData( + @NonNull ContactPhoto contactPhoto, int width, int height, @NonNull Options options) { return new LoadData<>(contactPhoto, new ContactPhotoFetcher(context, contactPhoto)); } @@ -43,7 +40,8 @@ public Factory(Context context) { @NonNull @Override - public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { + public ModelLoader build( + @NonNull MultiModelLoaderFactory multiFactory) { return new ContactPhotoLoader(context); } diff --git a/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDecoder.java b/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDecoder.java index c425f6694..7c8dbbe9d 100644 --- a/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDecoder.java +++ b/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDecoder.java @@ -1,16 +1,13 @@ package org.thoughtcrime.securesms.glide.lottie; import androidx.annotation.NonNull; - import com.airbnb.lottie.LottieComposition; import com.airbnb.lottie.LottieCompositionFactory; import com.airbnb.lottie.LottieResult; - import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.resource.SimpleResource; - import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; @@ -26,7 +23,8 @@ public Resource decode( @NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException { try { - LottieResult result = LottieCompositionFactory.fromJsonInputStreamSync(new GZIPInputStream(source), null); + LottieResult result = + LottieCompositionFactory.fromJsonInputStreamSync(new GZIPInputStream(source), null); return new SimpleResource<>(result.getValue()); } catch (Exception ex) { throw new IOException("Cannot load Lottie animation from stream", ex); diff --git a/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDrawableTranscoder.java b/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDrawableTranscoder.java index 39ffaf020..71d89e594 100644 --- a/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDrawableTranscoder.java +++ b/src/main/java/org/thoughtcrime/securesms/glide/lottie/LottieDrawableTranscoder.java @@ -2,16 +2,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.airbnb.lottie.LottieComposition; import com.airbnb.lottie.LottieDrawable; - import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.resource.SimpleResource; import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; -public class LottieDrawableTranscoder implements ResourceTranscoder { +public class LottieDrawableTranscoder + implements ResourceTranscoder { @Nullable @Override public Resource transcode( diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/Bounds.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/Bounds.java index 95437c920..5de1e24d5 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/Bounds.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/Bounds.java @@ -2,33 +2,35 @@ import android.graphics.Matrix; import android.graphics.RectF; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** - * The local extent of a {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement}. - * i.e. all {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement}s have a bounding rectangle from: - *

- * {@link #LEFT} to {@link #RIGHT} and from {@link #TOP} to {@link #BOTTOM}. + * The local extent of a {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement}. i.e. + * all {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement}s have a bounding rectangle + * from: + * + *

{@link #LEFT} to {@link #RIGHT} and from {@link #TOP} to {@link #BOTTOM}. */ public final class Bounds { - public static final float LEFT = -1000f; - public static final float RIGHT = 1000f; + public static final float LEFT = -1000f; + public static final float RIGHT = 1000f; - public static final float TOP = -1000f; - public static final float BOTTOM = 1000f; + public static final float TOP = -1000f; + public static final float BOTTOM = 1000f; - public static final float CENTRE_X = (LEFT + RIGHT) / 2f; - public static final float CENTRE_Y = (TOP + BOTTOM) / 2f; + public static final float CENTRE_X = (LEFT + RIGHT) / 2f; + public static final float CENTRE_Y = (TOP + BOTTOM) / 2f; - public static final float[] CENTRE = new float[]{ CENTRE_X, CENTRE_Y }; + public static final float[] CENTRE = new float[] {CENTRE_X, CENTRE_Y}; - private static final float[] POINTS = { Bounds.LEFT, Bounds.TOP, - Bounds.RIGHT, Bounds.TOP, - Bounds.RIGHT, Bounds.BOTTOM, - Bounds.LEFT, Bounds.BOTTOM }; + private static final float[] POINTS = { + Bounds.LEFT, Bounds.TOP, + Bounds.RIGHT, Bounds.TOP, + Bounds.RIGHT, Bounds.BOTTOM, + Bounds.LEFT, Bounds.BOTTOM + }; static RectF newFullBounds() { return new RectF(LEFT, TOP, RIGHT, BOTTOM); @@ -37,12 +39,15 @@ static RectF newFullBounds() { public static final RectF FULL_BOUNDS = newFullBounds(); public static boolean contains(float x, float y) { - return x >= FULL_BOUNDS.left && x <= FULL_BOUNDS.right && - y >= FULL_BOUNDS.top && y <= FULL_BOUNDS.bottom; + return x >= FULL_BOUNDS.left + && x <= FULL_BOUNDS.right + && y >= FULL_BOUNDS.top + && y <= FULL_BOUNDS.bottom; } /** - * Maps all the points of bounds with the supplied matrix and determines whether they are still in bounds. + * Maps all the points of bounds with the supplied matrix and determines whether they are still in + * bounds. * * @param matrix matrix to transform points by, null is treated as identity. * @return true iff all points remain in bounds after transformation. diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java index 7b12689d6..93e0a4cac 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java @@ -3,28 +3,27 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; - import androidx.annotation.NonNull; /** * Tracks the current matrix for a canvas. - *

- * This is because you cannot reliably call {@link Canvas#setMatrix(Matrix)}. - * {@link Canvas#getMatrix()} provides this hint in its documentation: - * "track relevant transform state outside of the canvas." - *

- * To achieve this, any changes to the canvas matrix must be done via this class, including save and - * restore operations where the matrix was altered in between. + * + *

This is because you cannot reliably call {@link Canvas#setMatrix(Matrix)}. {@link + * Canvas#getMatrix()} provides this hint in its documentation: "track relevant transform state + * outside of the canvas." + * + *

To achieve this, any changes to the canvas matrix must be done via this class, including save + * and restore operations where the matrix was altered in between. */ public final class CanvasMatrix { - private final static int STACK_HEIGHT_LIMIT = 16; + private static final int STACK_HEIGHT_LIMIT = 16; - private final Canvas canvas; - private final Matrix canvasMatrix = new Matrix(); - private final Matrix temp = new Matrix(); - private final Matrix[] stack = new Matrix[STACK_HEIGHT_LIMIT]; - private int stackHeight; + private final Canvas canvas; + private final Matrix canvasMatrix = new Matrix(); + private final Matrix temp = new Matrix(); + private final Matrix[] stack = new Matrix[STACK_HEIGHT_LIMIT]; + private int stackHeight; CanvasMatrix(Canvas canvas) { this.canvas = canvas; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java index 11706dfc0..4e3247a1c 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java @@ -4,8 +4,8 @@ /** * A renderer that can have its color changed. - *

- * For example, Lines and Text can change color. + * + *

For example, Lines and Text can change color. */ public interface ColorableRenderer extends Renderer { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java index 7fe7c0dd6..135e238da 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java @@ -2,25 +2,25 @@ import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.renderers.BezierDrawingRenderer; -/** - * Passes touch events into a {@link BezierDrawingRenderer}. - */ +/** Passes touch events into a {@link BezierDrawingRenderer}. */ class DrawingSession extends ElementEditSession { private final BezierDrawingRenderer renderer; - private DrawingSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix, @NonNull BezierDrawingRenderer renderer) { + private DrawingSession( + @NonNull EditorElement selected, + @NonNull Matrix inverseMatrix, + @NonNull BezierDrawingRenderer renderer) { super(selected, inverseMatrix); this.renderer = renderer; } - public static EditSession start(EditorElement element, BezierDrawingRenderer renderer, Matrix inverseMatrix, PointF point) { + public static EditSession start( + EditorElement element, BezierDrawingRenderer renderer, Matrix inverseMatrix, PointF point) { DrawingSession drawingSession = new DrawingSession(element, inverseMatrix, renderer); drawingSession.setScreenStartPoint(0, point); renderer.setFirstPoint(drawingSession.startPointElement[0]); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java index 3ea2148c7..5bcae29e8 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java @@ -2,21 +2,20 @@ import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; /** * Represents an underway edit of the image. - *

- * Accepts new touch positions, new touch points, released touch points and when complete can commit the edit. - *

- * Examples of edit session implementations are, Drag, Draw, Resize: - *

- * {@link ElementDragEditSession} for dragging with a single finger. - * {@link ElementScaleEditSession} for resize/dragging with two fingers. - * {@link DrawingSession} for drawing with a single finger. + * + *

Accepts new touch positions, new touch points, released touch points and when complete can + * commit the edit. + * + *

Examples of edit session implementations are, Drag, Draw, Resize: + * + *

{@link ElementDragEditSession} for dragging with a single finger. {@link + * ElementScaleEditSession} for resize/dragging with two fingers. {@link DrawingSession} for drawing + * with a single finger. */ interface EditSession { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java index 23bd5e961..651f17001 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java @@ -2,9 +2,7 @@ import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; final class ElementDragEditSession extends ElementEditSession { @@ -13,10 +11,14 @@ private ElementDragEditSession(@NonNull EditorElement selected, @NonNull Matrix super(selected, inverseMatrix); } - static ElementDragEditSession startDrag(@NonNull EditorElement selected, @NonNull Matrix inverseViewModelMatrix, @NonNull PointF point) { + static ElementDragEditSession startDrag( + @NonNull EditorElement selected, + @NonNull Matrix inverseViewModelMatrix, + @NonNull PointF point) { if (!selected.getFlags().isEditable()) return null; - ElementDragEditSession elementDragEditSession = new ElementDragEditSession(selected, inverseViewModelMatrix); + ElementDragEditSession elementDragEditSession = + new ElementDragEditSession(selected, inverseViewModelMatrix); elementDragEditSession.setScreenStartPoint(0, point); elementDragEditSession.setScreenEndPoint(0, point); @@ -27,8 +29,11 @@ static ElementDragEditSession startDrag(@NonNull EditorElement selected, @NonNul public void movePoint(int p, @NonNull PointF point) { setScreenEndPoint(p, point); - selected.getEditorMatrix() - .setTranslate(endPointElement[0].x - startPointElement[0].x, endPointElement[0].y - startPointElement[0].y); + selected + .getEditorMatrix() + .setTranslate( + endPointElement[0].x - startPointElement[0].x, + endPointElement[0].y - startPointElement[0].y); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java index 6bf0e229d..3969d5a51 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java @@ -2,9 +2,7 @@ import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; abstract class ElementEditSession implements EditSession { @@ -14,9 +12,9 @@ abstract class ElementEditSession implements EditSession { final EditorElement selected; final PointF[] startPointElement = newTwoPointArray(); - final PointF[] endPointElement = newTwoPointArray(); - final PointF[] startPointScreen = newTwoPointArray(); - final PointF[] endPointScreen = newTwoPointArray(); + final PointF[] endPointElement = newTwoPointArray(); + final PointF[] startPointScreen = newTwoPointArray(); + final PointF[] endPointScreen = newTwoPointArray(); ElementEditSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix) { this.selected = selected; @@ -57,12 +55,12 @@ private static PointF[] newTwoPointArray() { /** * Map src to dst using the matrix. * - * @param dst Output point. + * @param dst Output point. * @param matrix Matrix to transform point with. - * @param src Input point. + * @param src Input point. */ private static void mapPoint(@NonNull PointF dst, @NonNull Matrix matrix, @NonNull PointF src) { - float[] in = { src.x, src.y }; + float[] in = {src.x, src.y}; float[] out = new float[2]; matrix.mapPoints(out, in); dst.set(out[0], out[1]); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java index d18934b3d..3c75c652e 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java @@ -2,9 +2,7 @@ import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; final class ElementScaleEditSession extends ElementEditSession { @@ -13,9 +11,14 @@ private ElementScaleEditSession(@NonNull EditorElement selected, @NonNull Matrix super(selected, inverseMatrix); } - static ElementScaleEditSession startScale(@NonNull ElementDragEditSession session, @NonNull Matrix inverseMatrix, @NonNull PointF point, int p) { + static ElementScaleEditSession startScale( + @NonNull ElementDragEditSession session, + @NonNull Matrix inverseMatrix, + @NonNull PointF point, + int p) { session.commit(); - ElementScaleEditSession newSession = new ElementScaleEditSession(session.selected, inverseMatrix); + ElementScaleEditSession newSession = + new ElementScaleEditSession(session.selected, inverseMatrix); newSession.setScreenStartPoint(1 - p, session.endPointScreen[0]); newSession.setScreenEndPoint(1 - p, session.endPointScreen[0]); newSession.setScreenStartPoint(p, point); @@ -37,7 +40,9 @@ public void movePoint(int p, @NonNull PointF point) { editorMatrix.postTranslate(-startPointElement[0].x, -startPointElement[0].y); editorMatrix.postScale(scale, scale); - double angle = angle(endPointElement[0], endPointElement[1]) - angle(startPointElement[0], startPointElement[1]); + double angle = + angle(endPointElement[0], endPointElement[1]) + - angle(startPointElement[0], startPointElement[1]); if (!selected.getFlags().isRotateLocked()) { editorMatrix.postRotate((float) Math.toDegrees(angle)); @@ -47,8 +52,12 @@ public void movePoint(int p, @NonNull PointF point) { } else { editorMatrix.postTranslate(-startPointElement[0].x, -startPointElement[0].y); - float scaleX = (endPointElement[1].x - endPointElement[0].x) / (startPointElement[1].x - startPointElement[0].x); - float scaleY = (endPointElement[1].y - endPointElement[0].y) / (startPointElement[1].y - startPointElement[0].y); + float scaleX = + (endPointElement[1].x - endPointElement[0].x) + / (startPointElement[1].x - startPointElement[0].x); + float scaleY = + (endPointElement[1].y - endPointElement[0].y) + / (startPointElement[1].y - startPointElement[0].y); editorMatrix.postScale(scaleX, scaleY); @@ -78,18 +87,16 @@ private ElementDragEditSession convertToDrag(int p, @NonNull Matrix inverse) { * Find relative distance between an old and new set of Points. * * @param from Pair of points. - * @param to New pair of points. + * @param to New pair of points. * @return Scale */ private static double findScale(@NonNull PointF[] from, @NonNull PointF[] to) { float originalD2 = getDistanceSquared(from[0], from[1]); - float newD2 = getDistanceSquared(to[0], to[1]); + float newD2 = getDistanceSquared(to[0], to[1]); return Math.sqrt(newD2 / originalD2); } - /** - * Distance between two points squared. - */ + /** Distance between two points squared. */ private static float getDistanceSquared(@NonNull PointF a, @NonNull PointF b) { float dx = a.x - b.x; float dy = a.y - b.y; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java index 32a2fc51e..1b5ec70cd 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java @@ -10,33 +10,25 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; -/** - * Invisible {@link android.widget.EditText} that is used during in-image text editing. - */ +/** Invisible {@link android.widget.EditText} that is used during in-image text editing. */ final class HiddenEditText extends AppCompatEditText { @SuppressLint("InlinedApi") private static final int INCOGNITO_KEYBOARD_IME = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING; - @Nullable - private EditorElement currentTextEditorElement; + @Nullable private EditorElement currentTextEditorElement; - @Nullable - private MultiLineTextRenderer currentTextEntity; + @Nullable private MultiLineTextRenderer currentTextEntity; - @Nullable - private Runnable onEndEdit; + @Nullable private Runnable onEndEdit; - @Nullable - private OnEditOrSelectionChange onEditOrSelectionChange; + @Nullable private OnEditOrSelectionChange onEditOrSelectionChange; public HiddenEditText(Context context) { super(context); @@ -87,21 +79,26 @@ private void endEdit() { } private void postEditOrSelectionChange() { - if (currentTextEditorElement != null && currentTextEntity != null && onEditOrSelectionChange != null) { + if (currentTextEditorElement != null + && currentTextEntity != null + && onEditOrSelectionChange != null) { onEditOrSelectionChange.onChange(currentTextEditorElement, currentTextEntity); } } - @Nullable MultiLineTextRenderer getCurrentTextEntity() { + @Nullable + MultiLineTextRenderer getCurrentTextEntity() { return currentTextEntity; } - @Nullable EditorElement getCurrentTextEditorElement() { + @Nullable + EditorElement getCurrentTextEditorElement() { return currentTextEditorElement; } public void setCurrentTextEditorElement(@Nullable EditorElement currentTextEditorElement) { - if (currentTextEditorElement != null && currentTextEditorElement.getRenderer() instanceof MultiLineTextRenderer) { + if (currentTextEditorElement != null + && currentTextEditorElement.getRenderer() instanceof MultiLineTextRenderer) { this.currentTextEditorElement = currentTextEditorElement; setCurrentTextEntity((MultiLineTextRenderer) currentTextEditorElement.getRenderer()); } else { @@ -143,10 +140,12 @@ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { if (currentTextEntity != null && focus) { currentTextEntity.setFocused(true); - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT); if (!imm.isAcceptingText()) { - imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY); + imm.toggleSoftInput( + InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY); } } @@ -154,24 +153,29 @@ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { } public void hideKeyboard() { - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); } public void setIncognitoKeyboardEnabled(boolean incognitoKeyboardEnabled) { - setImeOptions(incognitoKeyboardEnabled ? getImeOptions() | INCOGNITO_KEYBOARD_IME - : getImeOptions() & ~INCOGNITO_KEYBOARD_IME); + setImeOptions( + incognitoKeyboardEnabled + ? getImeOptions() | INCOGNITO_KEYBOARD_IME + : getImeOptions() & ~INCOGNITO_KEYBOARD_IME); } public void setOnEndEdit(@Nullable Runnable onEndEdit) { this.onEndEdit = onEndEdit; } - public void setOnEditOrSelectionChange(@Nullable OnEditOrSelectionChange onEditOrSelectionChange) { + public void setOnEditOrSelectionChange( + @Nullable OnEditOrSelectionChange onEditOrSelectionChange) { this.onEditOrSelectionChange = onEditOrSelectionChange; } public interface OnEditOrSelectionChange { - void onChange(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer); + void onChange( + @NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer); } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java index 0eab881ee..6c2457375 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java @@ -1,29 +1,28 @@ package org.thoughtcrime.securesms.imageeditor; import android.content.Context; - import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.util.Util; public class ImageEditorMediaConstraints extends MediaConstraints { - private static final int MAX_IMAGE_DIMEN_LOWMEM = 768; - private static final int MAX_IMAGE_DIMEN = 1536; - private static final int KB = 1024; - private static final int MB = 1024 * KB; + private static final int MAX_IMAGE_DIMEN_LOWMEM = 768; + private static final int MAX_IMAGE_DIMEN = 1536; + private static final int KB = 1024; + private static final int MB = 1024 * KB; - @Override - public int getImageMaxWidth(Context context) { - return Util.isLowMemory(context) ? MAX_IMAGE_DIMEN_LOWMEM : MAX_IMAGE_DIMEN; - } + @Override + public int getImageMaxWidth(Context context) { + return Util.isLowMemory(context) ? MAX_IMAGE_DIMEN_LOWMEM : MAX_IMAGE_DIMEN; + } - @Override - public int getImageMaxHeight(Context context) { - return getImageMaxWidth(context); - } + @Override + public int getImageMaxHeight(Context context) { + return getImageMaxWidth(context); + } - @Override - public int getImageMaxSize(Context context) { - return 4 * MB; - } + @Override + public int getImageMaxSize(Context context) { + return 4 * MB; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java index fcdc2f13a..af749c0c4 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java @@ -11,12 +11,10 @@ import android.view.GestureDetector; import android.view.MotionEvent; import android.widget.FrameLayout; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.GestureDetectorCompat; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; import org.thoughtcrime.securesms.imageeditor.model.ThumbRenderer; @@ -25,56 +23,53 @@ /** * ImageEditorView - *

- * Android {@link android.view.View} that allows manipulation of a base image, rotate/flip/crop and - * addition and manipulation of text/drawing/and other image layers that move with the base image. - *

- * Drawing - *

- * Drawing is achieved by setting the {@link #color} and putting the view in {@link Mode#Draw}. - * Touch events are then passed to a new {@link BezierDrawingRenderer} on a new {@link EditorElement}. - *

- * New images - *

- * To add new images to the base image add via the {@link EditorModel#addElementCentered(EditorElement, float)} - * which centers the new item in the current crop area. + * + *

Android {@link android.view.View} that allows manipulation of a base image, rotate/flip/crop + * and addition and manipulation of text/drawing/and other image layers that move with the base + * image. + * + *

Drawing + * + *

Drawing is achieved by setting the {@link #color} and putting the view in {@link Mode#Draw}. + * Touch events are then passed to a new {@link BezierDrawingRenderer} on a new {@link + * EditorElement}. + * + *

New images + * + *

To add new images to the base image add via the {@link + * EditorModel#addElementCentered(EditorElement, float)} which centers the new item in the current + * crop area. */ public final class ImageEditorView extends FrameLayout { private HiddenEditText editText; - @NonNull - private Mode mode = Mode.MoveAndResize; + @NonNull private Mode mode = Mode.MoveAndResize; - @ColorInt - private int color = 0xff000000; + @ColorInt private int color = 0xff000000; private float thickness = 0.02f; - @NonNull - private Paint.Cap cap = Paint.Cap.ROUND; + @NonNull private Paint.Cap cap = Paint.Cap.ROUND; private EditorModel model; private GestureDetectorCompat doubleTap; - @Nullable - private DrawingChangedListener drawingChangedListener; + @Nullable private DrawingChangedListener drawingChangedListener; - @Nullable - private UndoRedoStackListener undoRedoStackListener; + @Nullable private UndoRedoStackListener undoRedoStackListener; - private final Matrix viewMatrix = new Matrix(); - private final RectF viewPort = Bounds.newFullBounds(); - private final RectF visibleViewPort = Bounds.newFullBounds(); - private final RectF screen = new RectF(); + private final Matrix viewMatrix = new Matrix(); + private final RectF viewPort = Bounds.newFullBounds(); + private final RectF visibleViewPort = Bounds.newFullBounds(); + private final RectF screen = new RectF(); - private TapListener tapListener; + private TapListener tapListener; private RendererContext rendererContext; - @Nullable - private EditSession editSession; - private boolean moreThanOnePointerUsedInSession; + @Nullable private EditSession editSession; + private boolean moreThanOnePointerUsedInSession; public ImageEditorView(Context context) { super(context); @@ -111,7 +106,8 @@ private HiddenEditText createAHiddenTextEntryField() { return editText; } - public void startTextEditing(@NonNull EditorElement editorElement, boolean incognitoKeyboardEnabled, boolean selectAll) { + public void startTextEditing( + @NonNull EditorElement editorElement, boolean incognitoKeyboardEnabled, boolean selectAll) { if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { editText.setIncognitoKeyboardEnabled(incognitoKeyboardEnabled); editText.setCurrentTextEditorElement(editorElement); @@ -122,8 +118,9 @@ public void startTextEditing(@NonNull EditorElement editorElement, boolean incog } } - private void zoomToFitText(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer) { - getModel().zoomToTextElement(editorElement, textRenderer); + private void zoomToFitText( + @NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer) { + getModel().zoomToTextElement(editorElement, textRenderer); } public boolean isTextEditing() { @@ -144,7 +141,8 @@ public void doneTextEditing() { @Override protected void onDraw(Canvas canvas) { if (rendererContext == null || rendererContext.canvas != canvas) { - rendererContext = new RendererContext(getContext(), canvas, rendererReady, rendererInvalidate); + rendererContext = + new RendererContext(getContext(), canvas, rendererReady, rendererInvalidate); } rendererContext.save(); try { @@ -156,13 +154,15 @@ protected void onDraw(Canvas canvas) { } } - private final RendererContext.Ready rendererReady = new RendererContext.Ready() { - @Override - public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { - model.onReady(renderer, cropMatrix, size); - invalidate(); - } - }; + private final RendererContext.Ready rendererReady = + new RendererContext.Ready() { + @Override + public void onReady( + @NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { + model.onReady(renderer, cropMatrix, size); + invalidate(); + } + }; private final RendererContext.Invalidate rendererInvalidate = renderer -> invalidate(); @@ -218,100 +218,110 @@ public void setModel(@NonNull EditorModel model) { @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - Matrix inverse = new Matrix(); - PointF point = getPoint(event); - EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse); + case MotionEvent.ACTION_DOWN: + { + Matrix inverse = new Matrix(); + PointF point = getPoint(event); + EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse); - moreThanOnePointerUsedInSession = false; - model.pushUndoPoint(); - editSession = startEdit(inverse, point, selected); + moreThanOnePointerUsedInSession = false; + model.pushUndoPoint(); + editSession = startEdit(inverse, point, selected); - if (tapListener != null && allowTaps()) { - if (editSession != null) { - tapListener.onEntityDown(editSession.getSelected()); - } else { - tapListener.onEntityDown(null); + if (tapListener != null && allowTaps()) { + if (editSession != null) { + tapListener.onEntityDown(editSession.getSelected()); + } else { + tapListener.onEntityDown(null); + } } + + return true; } + case MotionEvent.ACTION_MOVE: + { + if (editSession != null) { + int historySize = event.getHistorySize(); + int pointerCount = Math.min(2, event.getPointerCount()); - return true; - } - case MotionEvent.ACTION_MOVE: { - if (editSession != null) { - int historySize = event.getHistorySize(); - int pointerCount = Math.min(2, event.getPointerCount()); + for (int h = 0; h < historySize; h++) { + for (int p = 0; p < pointerCount; p++) { + editSession.movePoint(p, getHistoricalPoint(event, p, h)); + } + } - for (int h = 0; h < historySize; h++) { for (int p = 0; p < pointerCount; p++) { - editSession.movePoint(p, getHistoricalPoint(event, p, h)); + editSession.movePoint(p, getPoint(event, p)); } + model.moving(editSession.getSelected()); + invalidate(); + return true; } - - for (int p = 0; p < pointerCount; p++) { - editSession.movePoint(p, getPoint(event, p)); - } - model.moving(editSession.getSelected()); - invalidate(); - return true; + break; } - break; - } - case MotionEvent.ACTION_POINTER_DOWN: { - if (editSession != null && event.getPointerCount() == 2) { - moreThanOnePointerUsedInSession = true; - editSession.commit(); - model.pushUndoPoint(); - - Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); - if (newInverse != null) { - editSession = editSession.newPoint(newInverse, getPoint(event, event.getActionIndex()), event.getActionIndex()); - } else { - editSession = null; + case MotionEvent.ACTION_POINTER_DOWN: + { + if (editSession != null && event.getPointerCount() == 2) { + moreThanOnePointerUsedInSession = true; + editSession.commit(); + model.pushUndoPoint(); + + Matrix newInverse = + model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); + if (newInverse != null) { + editSession = + editSession.newPoint( + newInverse, getPoint(event, event.getActionIndex()), event.getActionIndex()); + } else { + editSession = null; + } + if (editSession == null) { + dragDropRelease(); + } + return true; } - if (editSession == null) { + break; + } + case MotionEvent.ACTION_POINTER_UP: + { + if (editSession != null && event.getActionIndex() < 2) { + editSession.commit(); + model.pushUndoPoint(); dragDropRelease(); + + Matrix newInverse = + model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); + if (newInverse != null) { + editSession = editSession.removePoint(newInverse, event.getActionIndex()); + } else { + editSession = null; + } + return true; } - return true; + break; } - break; - } - case MotionEvent.ACTION_POINTER_UP: { - if (editSession != null && event.getActionIndex() < 2) { - editSession.commit(); - model.pushUndoPoint(); - dragDropRelease(); + case MotionEvent.ACTION_UP: + { + if (editSession != null) { + editSession.commit(); + dragDropRelease(); - Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); - if (newInverse != null) { - editSession = editSession.removePoint(newInverse, event.getActionIndex()); - } else { editSession = null; + model.postEdit(moreThanOnePointerUsedInSession); + invalidate(); + return true; + } else { + model.postEdit(moreThanOnePointerUsedInSession); } - return true; + break; } - break; - } - case MotionEvent.ACTION_UP: { - if (editSession != null) { - editSession.commit(); - dragDropRelease(); - - editSession = null; - model.postEdit(moreThanOnePointerUsedInSession); - invalidate(); - return true; - } else { - model.postEdit(moreThanOnePointerUsedInSession); - } - break; - } } return super.onTouchEvent(event); } - private @Nullable EditSession startEdit(@NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { + private @Nullable EditSession startEdit( + @NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { if (mode == Mode.Draw || mode == Mode.Blur) { return startADrawingSession(point); } else { @@ -320,8 +330,11 @@ public boolean onTouchEvent(MotionEvent event) { } private EditSession startADrawingSession(@NonNull PointF point) { - BezierDrawingRenderer renderer = new BezierDrawingRenderer(color, thickness * Bounds.FULL_BOUNDS.width(), cap, model.findCropRelativeToRoot()); - EditorElement element = new EditorElement(renderer, mode == Mode.Blur ? EditorModel.Z_MASK : EditorModel.Z_DRAWING); + BezierDrawingRenderer renderer = + new BezierDrawingRenderer( + color, thickness * Bounds.FULL_BOUNDS.width(), cap, model.findCropRelativeToRoot()); + EditorElement element = + new EditorElement(renderer, mode == Mode.Blur ? EditorModel.Z_MASK : EditorModel.Z_DRAWING); model.addElementCentered(element, 1); Matrix elementInverseMatrix = model.findElementInverseMatrix(element, viewMatrix); @@ -329,7 +342,8 @@ private EditSession startADrawingSession(@NonNull PointF point) { return DrawingSession.start(element, renderer, elementInverseMatrix, point); } - private EditSession startAMoveAndResizeSession(@NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { + private EditSession startAMoveAndResizeSession( + @NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { Matrix elementInverseMatrix; if (selected == null) return null; @@ -342,7 +356,8 @@ private EditSession startAMoveAndResizeSession(@NonNull Matrix inverse, @NonNull elementInverseMatrix = model.findElementInverseMatrix(selected, viewMatrix); if (elementInverseMatrix != null) { - return ThumbDragEditSession.startDrag(selected, elementInverseMatrix, thumb.getControlPoint(), point); + return ThumbDragEditSession.startDrag( + selected, elementInverseMatrix, thumb.getControlPoint(), point); } else { return null; } @@ -357,7 +372,7 @@ public void setMode(@NonNull Mode mode) { public void startDrawing(float thickness, @NonNull Paint.Cap cap, boolean blur) { this.thickness = thickness; - this.cap = cap; + this.cap = cap; setMode(blur ? Mode.Blur : Mode.Draw); } @@ -381,8 +396,8 @@ private static PointF getPoint(MotionEvent event, int p) { } private static PointF getHistoricalPoint(MotionEvent event, int p, int historicalIndex) { - return new PointF(event.getHistoricalX(p, historicalIndex), - event.getHistoricalY(p, historicalIndex)); + return new PointF( + event.getHistoricalX(p, historicalIndex), event.getHistoricalY(p, historicalIndex)); } public EditorModel getModel() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java index 9b431e45f..ff50ab3ac 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java @@ -1,13 +1,14 @@ package org.thoughtcrime.securesms.imageeditor; import android.os.Parcelable; - import androidx.annotation.NonNull; /** - * Responsible for rendering a single {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement} to the canvas. - *

- * Because it knows the most about the whereabouts of the image it is also responsible for hit detection. + * Responsible for rendering a single {@link + * org.thoughtcrime.securesms.imageeditor.model.EditorElement} to the canvas. + * + *

Because it knows the most about the whereabouts of the image it is also responsible for hit + * detection. */ public interface Renderer extends Parcelable { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java index f814699f1..2f0347767 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java @@ -6,38 +6,30 @@ import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.imageeditor.model.EditorElement; - import java.util.Collections; import java.util.List; +import org.thoughtcrime.securesms.imageeditor.model.EditorElement; /** * Contains all of the information required for a {@link Renderer} to do its job. - *

- * Includes a {@link #canvas}, preconfigured with the correct matrix. - *

- * The {@link #canvasMatrix} should further matrix manipulation be required. + * + *

Includes a {@link #canvas}, preconfigured with the correct matrix. + * + *

The {@link #canvasMatrix} should further matrix manipulation be required. */ public final class RendererContext { - @NonNull - public final Context context; + @NonNull public final Context context; - @NonNull - public final Canvas canvas; + @NonNull public final Canvas canvas; - @NonNull - public final CanvasMatrix canvasMatrix; + @NonNull public final CanvasMatrix canvasMatrix; - @NonNull - public final Ready rendererReady; + @NonNull public final Ready rendererReady; - @NonNull - public final Invalidate invalidate; + @NonNull public final Invalidate invalidate; private boolean blockingLoad; @@ -46,14 +38,18 @@ public final class RendererContext { private boolean isEditing = true; private List children = Collections.emptyList(); - private Paint maskPaint; - - public RendererContext(@NonNull Context context, @NonNull Canvas canvas, @NonNull Ready rendererReady, @NonNull Invalidate invalidate) { - this.context = context; - this.canvas = canvas; - this.canvasMatrix = new CanvasMatrix(canvas); + private Paint maskPaint; + + public RendererContext( + @NonNull Context context, + @NonNull Canvas canvas, + @NonNull Ready rendererReady, + @NonNull Invalidate invalidate) { + this.context = context; + this.canvas = canvas; + this.canvasMatrix = new CanvasMatrix(canvas); this.rendererReady = rendererReady; - this.invalidate = invalidate; + this.invalidate = invalidate; } public void setBlockingLoad(boolean blockingLoad) { @@ -61,12 +57,14 @@ public void setBlockingLoad(boolean blockingLoad) { } /** - * {@link Renderer}s generally run in the foreground but can load any data they require in the background. - *

- * If they do so, they can use the {@link #invalidate} callback when ready to inform the view it needs to be redrawn. - *

- * However, when isBlockingLoad is true, the renderer is running in the background for the final render - * and must load the data immediately and block the render until done so. + * {@link Renderer}s generally run in the foreground but can load any data they require in the + * background. + * + *

If they do so, they can use the {@link #invalidate} callback when ready to inform the view + * it needs to be redrawn. + * + *

However, when isBlockingLoad is true, the renderer is running in the background for the + * final render and must load the data immediately and block the render until done so. */ public boolean isBlockingLoad() { return blockingLoad; @@ -99,9 +97,7 @@ public void save() { canvasMatrix.save(); } - /** - * Restore the current state from the stack, must match a call to {@link #save()}. - */ + /** Restore the current state from the stack, must match a call to {@link #save()}. */ public void restore() { canvasMatrix.restore(); } @@ -128,16 +124,14 @@ public void setMaskPaint(@Nullable Paint maskPaint) { public interface Ready { - Ready NULL = (renderer, cropMatrix, size) -> { - }; + Ready NULL = (renderer, cropMatrix, size) -> {}; void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size); } public interface Invalidate { - Invalidate NULL = (renderer) -> { - }; + Invalidate NULL = (renderer) -> {}; void onInvalidate(@NonNull Renderer renderer); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java index 860883e56..ed44dd04b 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java @@ -2,26 +2,31 @@ import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.model.ThumbRenderer; class ThumbDragEditSession extends ElementEditSession { - @NonNull - private final ThumbRenderer.ControlPoint controlPoint; + @NonNull private final ThumbRenderer.ControlPoint controlPoint; - private ThumbDragEditSession(@NonNull EditorElement selected, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull Matrix inverseMatrix) { + private ThumbDragEditSession( + @NonNull EditorElement selected, + @NonNull ThumbRenderer.ControlPoint controlPoint, + @NonNull Matrix inverseMatrix) { super(selected, inverseMatrix); this.controlPoint = controlPoint; } - static EditSession startDrag(@NonNull EditorElement selected, @NonNull Matrix inverseViewModelMatrix, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull PointF point) { + static EditSession startDrag( + @NonNull EditorElement selected, + @NonNull Matrix inverseViewModelMatrix, + @NonNull ThumbRenderer.ControlPoint controlPoint, + @NonNull PointF point) { if (!selected.getFlags().isEditable()) return null; - ElementEditSession elementDragEditSession = new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix); + ElementEditSession elementDragEditSession = + new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix); elementDragEditSession.setScreenStartPoint(0, point); elementDragEditSession.setScreenEndPoint(0, point); return elementDragEditSession; @@ -48,13 +53,20 @@ public void movePoint(int p, @NonNull PointF point) { float defaultScale = aspectLocked ? 2 : 1; - float scaleX = controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x); - float scaleY = controlPoint.isHorizontalCenter() ? defaultScale : (yEnd - y) / (controlPoint.getY() - y); + float scaleX = + controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x); + float scaleY = + controlPoint.isHorizontalCenter() ? defaultScale : (yEnd - y) / (controlPoint.getY() - y); scale(editorMatrix, aspectLocked, scaleX, scaleY, controlPoint.opposite()); } - private void scale(Matrix editorMatrix, boolean aspectLocked, float scaleX, float scaleY, ThumbRenderer.ControlPoint around) { + private void scale( + Matrix editorMatrix, + boolean aspectLocked, + float scaleX, + float scaleY, + ThumbRenderer.ControlPoint around) { float x = around.getX(); float y = around.getY(); editorMatrix.postTranslate(-x, -y); @@ -76,4 +88,4 @@ public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, i public EditSession removePoint(@NonNull Matrix newInverse, int p) { return null; } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java index 6aae9f88a..0bef9ac3f 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java @@ -1,25 +1,25 @@ package org.thoughtcrime.securesms.imageeditor.model; import android.animation.ValueAnimator; -import androidx.annotation.Nullable; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; +import androidx.annotation.Nullable; final class AlphaAnimation { - private final static Interpolator interpolator = new LinearInterpolator(); + private static final Interpolator interpolator = new LinearInterpolator(); - final static AlphaAnimation NULL_1 = new AlphaAnimation(1); + static final AlphaAnimation NULL_1 = new AlphaAnimation(1); - private final float from; - private final float to; + private final float from; + private final float to; private final Runnable invalidate; - private final boolean canAnimate; - private float animatedFraction; + private final boolean canAnimate; + private float animatedFraction; private AlphaAnimation(float from, float to, @Nullable Runnable invalidate) { - this.from = from; - this.to = to; + this.from = from; + this.to = to; this.invalidate = invalidate; this.canAnimate = invalidate != null; } @@ -47,10 +47,11 @@ private void start() { ValueAnimator animator = ValueAnimator.ofFloat(from, to); animator.setDuration(200); animator.setInterpolator(interpolator); - animator.addUpdateListener(animation -> { - animatedFraction = (float) animation.getAnimatedValue(); - invalidate.run(); - }); + animator.addUpdateListener( + animation -> { + animatedFraction = (float) animation.getAnimatedValue(); + invalidate.run(); + }); animator.start(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java index 202821cc3..0cb454405 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java @@ -2,22 +2,19 @@ import android.animation.ValueAnimator; import android.graphics.Matrix; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.view.animation.CycleInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.thoughtcrime.securesms.imageeditor.CanvasMatrix; -/** - * Animation Matrix provides a matrix that animates over time down to the identity matrix. - */ +/** Animation Matrix provides a matrix that animates over time down to the identity matrix. */ final class AnimationMatrix { - private final static float[] iValues = new float[9]; - private final static Interpolator interpolator = new DecelerateInterpolator(); - private final static Interpolator pulseInterpolator = inverse(new CycleInterpolator(0.5f)); + private static final float[] iValues = new float[9]; + private static final Interpolator interpolator = new DecelerateInterpolator(); + private static final Interpolator pulseInterpolator = inverse(new CycleInterpolator(0.5f)); static final AnimationMatrix NULL = new AnimationMatrix(); @@ -26,14 +23,14 @@ final class AnimationMatrix { } private final Runnable invalidate; - private final boolean canAnimate; - private final float[] undoValues = new float[9]; + private final boolean canAnimate; + private final float[] undoValues = new float[9]; - private final Matrix temp = new Matrix(); - private final float[] tempValues = new float[9]; + private final Matrix temp = new Matrix(); + private final float[] tempValues = new float[9]; private ValueAnimator animator; - private float animatedFraction; + private float animatedFraction; private AnimationMatrix(@NonNull Matrix undo, @NonNull Runnable invalidate) { this.invalidate = invalidate; @@ -46,7 +43,8 @@ private AnimationMatrix() { invalidate = null; } - static @NonNull AnimationMatrix animate(@NonNull Matrix from, @NonNull Matrix to, @Nullable Runnable invalidate) { + static @NonNull AnimationMatrix animate( + @NonNull Matrix from, @NonNull Matrix to, @Nullable Runnable invalidate) { if (invalidate == null) { return NULL; } @@ -65,10 +63,9 @@ private AnimationMatrix() { } } - /** - * Animate applying a matrix and then animate removing. - */ - static @NonNull AnimationMatrix singlePulse(@NonNull Matrix pulse, @Nullable Runnable invalidate) { + /** Animate applying a matrix and then animate removing. */ + static @NonNull AnimationMatrix singlePulse( + @NonNull Matrix pulse, @Nullable Runnable invalidate) { if (invalidate == null) { return NULL; } @@ -84,10 +81,11 @@ private void start(@NonNull Interpolator interpolator) { animator = ValueAnimator.ofFloat(1, 0); animator.setDuration(250); animator.setInterpolator(interpolator); - animator.addUpdateListener(animation -> { - animatedFraction = (float) animation.getAnimatedValue(); - invalidate.run(); - }); + animator.addUpdateListener( + animation -> { + animatedFraction = (float) animation.getAnimatedValue(); + invalidate.run(); + }); animator.start(); } } @@ -97,18 +95,14 @@ void stop() { if (animator != null) animator.cancel(); } - /** - * Append the current animation value. - */ + /** Append the current animation value. */ void preConcatValueTo(@NonNull Matrix onTo) { if (!canAnimate) return; onTo.preConcat(buildTemp()); } - /** - * Append the current animation value. - */ + /** Append the current animation value. */ void preConcatValueTo(@NonNull CanvasMatrix canvasMatrix) { if (!canAnimate) return; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java index 35f3115b0..2cf05fc86 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java @@ -11,7 +11,7 @@ final class Bisect { private static final int MAX_ITERATIONS = 16; interface Predicate { - boolean test(); + boolean test(); } interface ModifyElement { @@ -19,95 +19,101 @@ interface ModifyElement { } /** - * Given a predicate function, attempts to finds the boundary between predicate true and predicate false. - * If it returns true, it will animate the element to the closest true value found to that boundary. + * Given a predicate function, attempts to finds the boundary between predicate true and predicate + * false. If it returns true, it will animate the element to the closest true value found to that + * boundary. * - * @param element The element to modify. - * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a translate. - * @param atMost A value believed to be in bounds. - * @param predicate The out of bounds predicate. - * @param modifyElement Apply the latest value to the element local matrix. - * @param invalidate For animation if finds a result. + * @param element The element to modify. + * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a + * translate. + * @param atMost A value believed to be in bounds. + * @param predicate The out of bounds predicate. + * @param modifyElement Apply the latest value to the element local matrix. + * @param invalidate For animation if finds a result. * @return true iff finds a result. */ - static boolean bisectToTest(@NonNull EditorElement element, - float outOfBoundsValue, - float atMost, - @NonNull Predicate predicate, - @NonNull ModifyElement modifyElement, - @NonNull Runnable invalidate) - { - Matrix closestSuccesful = bisectToTest(element, outOfBoundsValue, atMost, predicate, modifyElement); - - if (closestSuccesful != null) { - element.animateLocalTo(closestSuccesful, invalidate); - return true; - } else { - return false; - } + static boolean bisectToTest( + @NonNull EditorElement element, + float outOfBoundsValue, + float atMost, + @NonNull Predicate predicate, + @NonNull ModifyElement modifyElement, + @NonNull Runnable invalidate) { + Matrix closestSuccesful = + bisectToTest(element, outOfBoundsValue, atMost, predicate, modifyElement); + + if (closestSuccesful != null) { + element.animateLocalTo(closestSuccesful, invalidate); + return true; + } else { + return false; + } } /** - * Given a predicate function, attempts to finds the boundary between predicate true and predicate false. - * Returns new local matrix for the element if a solution is found. + * Given a predicate function, attempts to finds the boundary between predicate true and predicate + * false. Returns new local matrix for the element if a solution is found. * - * @param element The element to modify. - * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a translate. - * @param atMost A value believed to be in bounds. - * @param predicate The out of bounds predicate. - * @param modifyElement Apply the latest value to the element local matrix. + * @param element The element to modify. + * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a + * translate. + * @param atMost A value believed to be in bounds. + * @param predicate The out of bounds predicate. + * @param modifyElement Apply the latest value to the element local matrix. * @return matrix to replace local matrix iff finds a result, null otherwise. */ - static @Nullable Matrix bisectToTest(@NonNull EditorElement element, - float outOfBoundsValue, - float atMost, - @NonNull Predicate predicate, - @NonNull ModifyElement modifyElement) - { - Matrix elementMatrix = element.getLocalMatrix(); - Matrix original = new Matrix(elementMatrix); - Matrix closestSuccessful = new Matrix(); - boolean haveResult = false; - int attempt = 0; - float successValue = 0; - float inBoundsValue = atMost; - float nextValueToTry = inBoundsValue; - - do { - attempt++; - - modifyElement.applyFactor(elementMatrix, nextValueToTry); - try { - - if (predicate.test()) { - inBoundsValue = nextValueToTry; - - // if first success or closer to out of bounds than the current closest - if (!haveResult || Math.abs(nextValueToTry - outOfBoundsValue) < Math.abs(successValue - outOfBoundsValue)) { - haveResult = true; - successValue = nextValueToTry; - closestSuccessful.set(elementMatrix); - } - } else { - if (attempt == 1) { - // failure on first attempt means inBoundsValue is actually out of bounds and so no solution - return null; - } - outOfBoundsValue = nextValueToTry; - } - } finally { - // reset - elementMatrix.set(original); - } - - nextValueToTry = (inBoundsValue + outOfBoundsValue) / 2f; - - } while (attempt < MAX_ITERATIONS && Math.abs(inBoundsValue - outOfBoundsValue) > ACCURACY); - - if (haveResult) { - return closestSuccessful; - } - return null; - } - + static @Nullable Matrix bisectToTest( + @NonNull EditorElement element, + float outOfBoundsValue, + float atMost, + @NonNull Predicate predicate, + @NonNull ModifyElement modifyElement) { + Matrix elementMatrix = element.getLocalMatrix(); + Matrix original = new Matrix(elementMatrix); + Matrix closestSuccessful = new Matrix(); + boolean haveResult = false; + int attempt = 0; + float successValue = 0; + float inBoundsValue = atMost; + float nextValueToTry = inBoundsValue; + + do { + attempt++; + + modifyElement.applyFactor(elementMatrix, nextValueToTry); + try { + + if (predicate.test()) { + inBoundsValue = nextValueToTry; + + // if first success or closer to out of bounds than the current closest + if (!haveResult + || Math.abs(nextValueToTry - outOfBoundsValue) + < Math.abs(successValue - outOfBoundsValue)) { + haveResult = true; + successValue = nextValueToTry; + closestSuccessful.set(elementMatrix); + } + } else { + if (attempt == 1) { + // failure on first attempt means inBoundsValue is actually out of bounds and so no + // solution + return null; + } + outOfBoundsValue = nextValueToTry; + } + } finally { + // reset + elementMatrix.set(original); + } + + nextValueToTry = (inBoundsValue + outOfBoundsValue) / 2f; + + } while (attempt < MAX_ITERATIONS && Math.abs(inBoundsValue - outOfBoundsValue) > ACCURACY); + + if (haveResult) { + return closestSuccessful; + } + return null; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java index 34f3a8b0b..0f61ad903 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java @@ -3,31 +3,29 @@ import android.graphics.Matrix; import android.os.Parcel; import androidx.annotation.NonNull; - +import java.util.UUID; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; -import java.util.UUID; - /** * Hit tests a circle that is {@link R.dimen#crop_area_renderer_edge_size} in radius on the screen. - *

- * Does not draw anything. + * + *

Does not draw anything. */ class CropThumbRenderer implements Renderer, ThumbRenderer { private final ControlPoint controlPoint; - private final UUID toControl; + private final UUID toControl; - private final float[] centreOnScreen = new float[2]; - private final Matrix matrix = new Matrix(); - private int size; + private final float[] centreOnScreen = new float[2]; + private final Matrix matrix = new Matrix(); + private int size; CropThumbRenderer(@NonNull ControlPoint controlPoint, @NonNull UUID toControl) { this.controlPoint = controlPoint; - this.toControl = toControl; + this.toControl = toControl; } @Override @@ -44,13 +42,17 @@ public UUID getElementToControl() { public void render(@NonNull RendererContext rendererContext) { rendererContext.canvasMatrix.mapPoints(centreOnScreen, Bounds.CENTRE); rendererContext.canvasMatrix.copyTo(matrix); - size = rendererContext.context.getResources().getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size); + size = + rendererContext + .context + .getResources() + .getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size); } @Override public boolean hitTest(float x, float y) { float[] hitPointOnScreen = new float[2]; - matrix.mapPoints(hitPointOnScreen, new float[]{ x, y }); + matrix.mapPoints(hitPointOnScreen, new float[] {x, y}); float dx = centreOnScreen[0] - hitPointOnScreen[0]; float dy = centreOnScreen[1] - hitPointOnScreen[1]; @@ -63,17 +65,19 @@ public int describeContents() { return 0; } - public static final Creator CREATOR = new Creator() { - @Override - public CropThumbRenderer createFromParcel(Parcel in) { - return new CropThumbRenderer(ControlPoint.values()[in.readInt()], ParcelUtils.readUUID(in)); - } - - @Override - public CropThumbRenderer[] newArray(int size) { - return new CropThumbRenderer[size]; - } - }; + public static final Creator CREATOR = + new Creator() { + @Override + public CropThumbRenderer createFromParcel(Parcel in) { + return new CropThumbRenderer( + ControlPoint.values()[in.readInt()], ParcelUtils.readUUID(in)); + } + + @Override + public CropThumbRenderer[] newArray(int size) { + return new CropThumbRenderer[size]; + } + }; @Override public void writeToParcel(Parcel dest, int flags) { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java index 26d10d855..c8a0f5f59 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java @@ -3,13 +3,8 @@ import android.graphics.Matrix; import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.imageeditor.Renderer; -import org.thoughtcrime.securesms.imageeditor.RendererContext; - import java.util.Collections; import java.util.Comparator; import java.util.Iterator; @@ -17,65 +12,62 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import org.thoughtcrime.securesms.imageeditor.Renderer; +import org.thoughtcrime.securesms.imageeditor.RendererContext; /** * An image consists of a tree of {@link EditorElement}s. - *

- * Each element has some persisted state: - * - An optional {@link Renderer} so that it can draw itself. - * - A list of child elements that make the tree possible. - * - Its own transformation matrix, which applies to itself and all its children. - * - A set of flags controlling visibility, selectablity etc. - *

- * Then some temporary state. - * - A editor matrix for displaying as yet uncommitted edits. - * - An animation matrix for animating from one matrix to another. - * - Deleted children to allow them to fade out on delete. - * - Temporary flags, for temporary visibility, selectablity etc. + * + *

Each element has some persisted state: - An optional {@link Renderer} so that it can draw + * itself. - A list of child elements that make the tree possible. - Its own transformation matrix, + * which applies to itself and all its children. - A set of flags controlling visibility, + * selectablity etc. + * + *

Then some temporary state. - A editor matrix for displaying as yet uncommitted edits. - An + * animation matrix for animating from one matrix to another. - Deleted children to allow them to + * fade out on delete. - Temporary flags, for temporary visibility, selectablity etc. */ public final class EditorElement implements Parcelable { - private static final Comparator Z_ORDER_COMPARATOR = (e1, e2) -> Integer.compare(e1.zOrder, e2.zOrder); + private static final Comparator Z_ORDER_COMPARATOR = + (e1, e2) -> Integer.compare(e1.zOrder, e2.zOrder); - private final UUID id; + private final UUID id; private final EditorFlags flags; - private final Matrix localMatrix = new Matrix(); - private final Matrix editorMatrix = new Matrix(); - private final int zOrder; + private final Matrix localMatrix = new Matrix(); + private final Matrix editorMatrix = new Matrix(); + private final int zOrder; - @Nullable - private final Renderer renderer; + @Nullable private final Renderer renderer; private final Matrix temp = new Matrix(); private final Matrix tempMatrix = new Matrix(); - private final List children = new LinkedList<>(); + private final List children = new LinkedList<>(); private final List deletedChildren = new LinkedList<>(); - @NonNull - private AnimationMatrix animationMatrix = AnimationMatrix.NULL; + @NonNull private AnimationMatrix animationMatrix = AnimationMatrix.NULL; - @NonNull - private AlphaAnimation alphaAnimation = AlphaAnimation.NULL_1; + @NonNull private AlphaAnimation alphaAnimation = AlphaAnimation.NULL_1; public EditorElement(@Nullable Renderer renderer) { this(renderer, 0); } public EditorElement(@Nullable Renderer renderer, int zOrder) { - this.id = UUID.randomUUID(); - this.flags = new EditorFlags(); + this.id = UUID.randomUUID(); + this.flags = new EditorFlags(); this.renderer = renderer; - this.zOrder = zOrder; + this.zOrder = zOrder; } private EditorElement(Parcel in) { - id = ParcelUtils.readUUID(in); - flags = new EditorFlags(in.readInt()); + id = ParcelUtils.readUUID(in); + flags = new EditorFlags(in.readInt()); ParcelUtils.readMatrix(localMatrix, in); renderer = in.readParcelable(Renderer.class.getClassLoader()); - zOrder = in.readInt(); + zOrder = in.readInt(); in.readTypedList(children, EditorElement.CREATOR); } @@ -88,12 +80,12 @@ UUID getId() { } /** - * Iff Visible, - * Renders tree with the following localMatrix: - *

- * viewModelMatrix * localMatrix * editorMatrix * animationMatrix - *

- * Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * localMatrix * editorMatrix * animationMatrix + * Iff Visible, Renders tree with the following localMatrix: + * + *

viewModelMatrix * localMatrix * editorMatrix * animationMatrix + * + *

Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * localMatrix * + * editorMatrix * animationMatrix * * @param rendererContext Canvas to draw on to. */ @@ -132,7 +124,8 @@ private void drawSelf(@NonNull RendererContext rendererContext) { renderer.render(rendererContext); } - private static void drawChildren(@NonNull List children, @NonNull RendererContext rendererContext) { + private static void drawChildren( + @NonNull List children, @NonNull RendererContext rendererContext) { for (EditorElement element : children) { if (element.zOrder >= 0) { element.draw(rendererContext); @@ -153,23 +146,34 @@ public Matrix getEditorMatrix() { return editorMatrix; } - EditorElement findElement(@NonNull EditorElement toFind, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { - return findElement(viewMatrix, outInverseModelMatrix, (element, inverseMatrix) -> toFind == element); + EditorElement findElement( + @NonNull EditorElement toFind, + @NonNull Matrix viewMatrix, + @NonNull Matrix outInverseModelMatrix) { + return findElement( + viewMatrix, outInverseModelMatrix, (element, inverseMatrix) -> toFind == element); } - EditorElement findElementAt(float x, float y, @NonNull Matrix viewModelMatrix, @NonNull Matrix outInverseModelMatrix) { + EditorElement findElementAt( + float x, float y, @NonNull Matrix viewModelMatrix, @NonNull Matrix outInverseModelMatrix) { final float[] dst = new float[2]; - final float[] src = { x, y }; - - return findElement(viewModelMatrix, outInverseModelMatrix, (element, inverseMatrix) -> { - Renderer renderer = element.renderer; - if (renderer == null) return false; - inverseMatrix.mapPoints(dst, src); - return element.flags.isSelectable() && renderer.hitTest(dst[0], dst[1]); - }); - } - - public EditorElement findElement(@NonNull Matrix viewModelMatrix, @NonNull Matrix outInverseModelMatrix, @NonNull FindElementPredicate predicate) { + final float[] src = {x, y}; + + return findElement( + viewModelMatrix, + outInverseModelMatrix, + (element, inverseMatrix) -> { + Renderer renderer = element.renderer; + if (renderer == null) return false; + inverseMatrix.mapPoints(dst, src); + return element.flags.isSelectable() && renderer.hitTest(dst[0], dst[1]); + }); + } + + public EditorElement findElement( + @NonNull Matrix viewModelMatrix, + @NonNull Matrix outInverseModelMatrix, + @NonNull FindElementPredicate predicate) { temp.set(viewModelMatrix); temp.preConcat(localMatrix); @@ -178,7 +182,8 @@ public EditorElement findElement(@NonNull Matrix viewModelMatrix, @NonNull Matri if (temp.invert(tempMatrix)) { for (int i = children.size() - 1; i >= 0; i--) { - EditorElement elementAt = children.get(i).findElement(temp, outInverseModelMatrix, predicate); + EditorElement elementAt = + children.get(i).findElement(temp, outInverseModelMatrix, predicate); if (elementAt != null) { return elementAt; } @@ -235,7 +240,8 @@ void animateFadeIn(@Nullable Runnable invalidate) { alphaAnimation = AlphaAnimation.animate(0, 1, invalidate); } - @Nullable EditorElement parentOf(@NonNull EditorElement element) { + @Nullable + EditorElement parentOf(@NonNull EditorElement element) { if (children.contains(element)) { return this; } @@ -304,10 +310,11 @@ void animateLocalTo(@NonNull Matrix newLocalMatrix, @Nullable Runnable invalidat /** * @param destination Matrix to change - * @param source Matrix value to set - * @param invalidate Callback to allow animation + * @param source Matrix value to set + * @param invalidate Callback to allow animation */ - private void setMatrixWithAnimation(@NonNull Matrix destination, @NonNull Matrix source, @Nullable Runnable invalidate) { + private void setMatrixWithAnimation( + @NonNull Matrix destination, @NonNull Matrix source, @Nullable Runnable invalidate) { Matrix old = new Matrix(destination); animationMatrix.stop(); animationMatrix.preConcatValueTo(old); @@ -325,17 +332,18 @@ void stopAnimation() { animationMatrix.stop(); } - public static final Creator CREATOR = new Creator() { - @Override - public EditorElement createFromParcel(Parcel in) { - return new EditorElement(in); - } + public static final Creator CREATOR = + new Creator() { + @Override + public EditorElement createFromParcel(Parcel in) { + return new EditorElement(in); + } - @Override - public EditorElement[] newArray(int size) { - return new EditorElement[size]; - } - }; + @Override + public EditorElement[] newArray(int size) { + return new EditorElement[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java index cab52e3f4..97fe4f328 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java @@ -4,10 +4,8 @@ import android.graphics.Point; import android.graphics.PointF; import android.graphics.RectF; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.renderers.CropAreaRenderer; @@ -16,31 +14,17 @@ /** * Creates and handles a strict EditorElement Hierarchy. - *

- * root - always square, contains only temporary zooms for editing. e.g. when the whole editor zooms out for cropping - * | - * |- view - contains persisted adjustments for crops - * | | - * | |- flipRotate - contains persisted adjustments for flip and rotate operations, ensures operations are centered within the current view - * | | - * | |- imageRoot - * | | |- mainImage - * | | |- stickers/drawings/text - * | | - * | |- overlay - always square - * | | |- imageCrop - a crop to match the aspect of the main image - * | | | |- cropEditorElement - user crop, not always square, but upright, the area of the view - * | | | | | All children do not move/scale or rotate. - * | | | | |- blackout - * | | | | |- thumbs - * | | | | | |- Center left thumb - * | | | | | |- Center right thumb - * | | | | | |- Top center thumb - * | | | | | |- Bottom center thumb - * | | | | | |- Top left thumb - * | | | | | |- Top right thumb - * | | | | | |- Bottom left thumb - * | | | | | |- Bottom right thumb + * + *

root - always square, contains only temporary zooms for editing. e.g. when the whole editor + * zooms out for cropping | |- view - contains persisted adjustments for crops | | | |- flipRotate - + * contains persisted adjustments for flip and rotate operations, ensures operations are centered + * within the current view | | | |- imageRoot | | |- mainImage | | |- stickers/drawings/text | | | + * |- overlay - always square | | |- imageCrop - a crop to match the aspect of the main image | | | + * |- cropEditorElement - user crop, not always square, but upright, the area of the view | | | | | + * All children do not move/scale or rotate. | | | | |- blackout | | | | |- thumbs | | | | | |- + * Center left thumb | | | | | |- Center right thumb | | | | | |- Top center thumb | | | | | |- + * Bottom center thumb | | | | | |- Top left thumb | | | | | |- Top right thumb | | | | | |- Bottom + * left thumb | | | | | |- Bottom right thumb */ final class EditorElementHierarchy { @@ -67,15 +51,15 @@ final class EditorElementHierarchy { private final EditorElement thumbs; private EditorElementHierarchy(@NonNull EditorElement root) { - this.root = root; - this.view = this.root.getChild(0); - this.flipRotate = this.view.getChild(0); - this.imageRoot = this.flipRotate.getChild(0); - this.overlay = this.flipRotate.getChild(1); - this.imageCrop = this.overlay.getChild(0); + this.root = root; + this.view = this.root.getChild(0); + this.flipRotate = this.view.getChild(0); + this.imageRoot = this.flipRotate.getChild(0); + this.overlay = this.flipRotate.getChild(1); + this.imageCrop = this.overlay.getChild(0); this.cropEditorElement = this.imageCrop.getChild(0); - this.blackout = this.cropEditorElement.getChild(0); - this.thumbs = this.cropEditorElement.getChild(1); + this.blackout = this.cropEditorElement.getChild(0); + this.thumbs = this.cropEditorElement.getChild(1); } private static @NonNull EditorElement createRoot(boolean circleEdit) { @@ -96,32 +80,32 @@ private EditorElementHierarchy(@NonNull EditorElement root) { EditorElement imageCrop = new EditorElement(null); overlay.addElement(imageCrop); - EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, !circleEdit)); + EditorElement cropEditorElement = + new EditorElement( + new CropAreaRenderer(R.color.crop_area_renderer_outer_color, !circleEdit)); - cropEditorElement.getFlags() - .setRotateLocked(true) - .setAspectLocked(true) - .setSelectable(false) - .setVisible(false) - .persist(); + cropEditorElement + .getFlags() + .setRotateLocked(true) + .setAspectLocked(true) + .setSelectable(false) + .setVisible(false) + .persist(); imageCrop.addElement(cropEditorElement); EditorElement blackout = new EditorElement(new InverseFillRenderer(0xff000000)); - blackout.getFlags() - .setSelectable(false) - .setEditable(false) - .persist(); + blackout.getFlags().setSelectable(false).setEditable(false).persist(); cropEditorElement.addElement(blackout); cropEditorElement.addElement(createThumbs(cropEditorElement, !circleEdit)); if (circleEdit) { - EditorElement circle = new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color)); - circle.getFlags().setSelectable(false) - .persist(); + EditorElement circle = + new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color)); + circle.getFlags().setSelectable(false).persist(); cropEditorElement.addElement(circle); } @@ -129,14 +113,11 @@ private EditorElementHierarchy(@NonNull EditorElement root) { return root; } - private static @NonNull EditorElement createThumbs(EditorElement cropEditorElement, boolean centerThumbs) { + private static @NonNull EditorElement createThumbs( + EditorElement cropEditorElement, boolean centerThumbs) { EditorElement thumbs = new EditorElement(null); - thumbs.getFlags() - .setChildrenVisible(false) - .setSelectable(false) - .setVisible(false) - .persist(); + thumbs.getFlags().setChildrenVisible(false).setSelectable(false).setVisible(false).persist(); if (centerThumbs) { thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_LEFT)); @@ -154,12 +135,12 @@ private EditorElementHierarchy(@NonNull EditorElement root) { return thumbs; } - private static @NonNull EditorElement newThumb(@NonNull EditorElement toControl, @NonNull ThumbRenderer.ControlPoint controlPoint) { - EditorElement element = new EditorElement(new CropThumbRenderer(controlPoint, toControl.getId())); + private static @NonNull EditorElement newThumb( + @NonNull EditorElement toControl, @NonNull ThumbRenderer.ControlPoint controlPoint) { + EditorElement element = + new EditorElement(new CropThumbRenderer(controlPoint, toControl.getId())); - element.getFlags() - .setSelectable(false) - .persist(); + element.getFlags().setSelectable(false).persist(); element.getLocalMatrix().preTranslate(controlPoint.getX(), controlPoint.getY()); @@ -174,10 +155,9 @@ EditorElement getImageRoot() { return imageRoot; } - /** - * The main image, null if not yet set. - */ - @Nullable EditorElement getMainImage() { + /** The main image, null if not yet set. */ + @Nullable + EditorElement getMainImage() { return imageRoot.getChildCount() > 0 ? imageRoot.getChild(0) : null; } @@ -198,20 +178,17 @@ EditorElement getFlipRotate() { } void startCrop(@NonNull Runnable invalidate) { - Matrix editor = new Matrix(); - float scaleInForCrop = 0.8f; + Matrix editor = new Matrix(); + float scaleInForCrop = 0.8f; editor.postScale(scaleInForCrop, scaleInForCrop); root.animateEditorTo(editor, invalidate); - cropEditorElement.getFlags() - .setVisible(true); + cropEditorElement.getFlags().setVisible(true); - blackout.getFlags() - .setVisible(false); + blackout.getFlags().setVisible(false); - thumbs.getFlags() - .setChildrenVisible(true); + thumbs.getFlags().setChildrenVisible(true); thumbs.forAllInTree(element -> element.getFlags().setSelectable(true)); @@ -252,10 +229,11 @@ void updateViewToCrop(@NonNull RectF visibleViewPort, @Nullable Runnable invalid /** * Returns a matrix that maps points from the crop on to the visible image. - *

- * i.e. if a mapped point is in bounds, then the point is on the visible image. + * + *

i.e. if a mapped point is in bounds, then the point is on the visible image. */ - @Nullable Matrix imageMatrixRelativeToCrop() { + @Nullable + Matrix imageMatrixRelativeToCrop() { EditorElement mainImage = getMainImage(); if (mainImage == null) return null; @@ -286,7 +264,12 @@ RectF getCropRect() { return dst; } - void flipRotate(int degrees, int scaleX, int scaleY, @NonNull RectF visibleViewPort, @Nullable Runnable invalidate) { + void flipRotate( + int degrees, + int scaleX, + int scaleY, + @NonNull RectF visibleViewPort, + @Nullable Runnable invalidate) { Matrix newLocal = new Matrix(flipRotate.getLocalMatrix()); if (degrees != 0) { newLocal.postRotate(degrees); @@ -296,9 +279,7 @@ void flipRotate(int degrees, int scaleX, int scaleY, @NonNull RectF visibleViewP updateViewToCrop(visibleViewPort, invalidate); } - /** - * The full matrix for the {@link #getMainImage()} from {@link #root} down. - */ + /** The full matrix for the {@link #getMainImage()} from {@link #root} down. */ Matrix getMainImageFullMatrix() { Matrix matrix = new Matrix(); @@ -308,9 +289,7 @@ Matrix getMainImageFullMatrix() { return matrix; } - /** - * The full matrix for the {@link #getMainImage()} from {@link #flipRotate} down. - */ + /** The full matrix for the {@link #getMainImage()} from {@link #flipRotate} down. */ Matrix getMainImageFullMatrixFromFlipRotate() { Matrix matrix = new Matrix(); @@ -339,22 +318,21 @@ PointF getOutputSize(@NonNull Point inputSize) { matrix.preConcat(cropEditorElement.getEditorMatrix()); EditorElement mainImage = getMainImage(); if (mainImage != null) { - float xScale = 1f / (xScale(mainImage.getLocalMatrix()) * xScale(mainImage.getEditorMatrix())); + float xScale = + 1f / (xScale(mainImage.getLocalMatrix()) * xScale(mainImage.getEditorMatrix())); matrix.preScale(xScale, xScale); } float[] dst = new float[4]; - matrix.mapPoints(dst, new float[]{ 0, 0, inputSize.x, inputSize.y }); + matrix.mapPoints(dst, new float[] {0, 0, inputSize.x, inputSize.y}); - float widthF = Math.abs(dst[0] - dst[2]); + float widthF = Math.abs(dst[0] - dst[2]); float heightF = Math.abs(dst[1] - dst[3]); return new PointF(widthF, heightF); } - /** - * Extract the x scale from a matrix, which is the length of the first column. - */ + /** Extract the x scale from a matrix, which is the length of the first column. */ static float xScale(@NonNull Matrix matrix) { float[] values = new float[9]; matrix.getValues(values); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java index 77e4dfedf..ad59a714c 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java @@ -4,19 +4,20 @@ /** * Flags for an {@link EditorElement}. - *

- * Values you set are not persisted unless you call {@link #persist()}. - *

- * This allows temporary state for editing and an easy way to revert to the persisted state via {@link #reset()}. + * + *

Values you set are not persisted unless you call {@link #persist()}. + * + *

This allows temporary state for editing and an easy way to revert to the persisted state via + * {@link #reset()}. */ public final class EditorFlags { - private static final int ASPECT_LOCK = 1; - private static final int ROTATE_LOCK = 2; - private static final int SELECTABLE = 4; - private static final int VISIBLE = 8; + private static final int ASPECT_LOCK = 1; + private static final int ROTATE_LOCK = 2; + private static final int SELECTABLE = 4; + private static final int VISIBLE = 8; private static final int CHILDREN_VISIBLE = 16; - private static final int EDITABLE = 32; + private static final int EDITABLE = 32; private int flags; private int markedFlags; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java index f9dc113d7..862d6da92 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java @@ -9,11 +9,14 @@ import android.graphics.RectF; import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; - +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.Renderer; @@ -21,46 +24,38 @@ import org.thoughtcrime.securesms.imageeditor.UndoRedoStackListener; import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - /** * Contains a reference to the root {@link EditorElement}, maintains undo and redo stacks and has a * reference to the {@link EditorElementHierarchy}. - *

- * As such it is the entry point for all operations that change the image. + * + *

As such it is the entry point for all operations that change the image. */ public final class EditorModel implements Parcelable, RendererContext.Ready { - public static final int Z_MASK = -1; - public static final int Z_DRAWING = 0; + public static final int Z_MASK = -1; + public static final int Z_DRAWING = 0; public static final int Z_STICKERS = 0; - public static final int Z_TEXT = 1; + public static final int Z_TEXT = 1; - private static final Runnable NULL_RUNNABLE = () -> { - }; + private static final Runnable NULL_RUNNABLE = () -> {}; private static final int MINIMUM_OUTPUT_WIDTH = 1024; - private static final int MINIMUM_CROP_PIXEL_COUNT = 100; - private static final Point MINIMUM_RATIO = new Point(15, 1); + private static final int MINIMUM_CROP_PIXEL_COUNT = 100; + private static final Point MINIMUM_RATIO = new Point(15, 1); - @NonNull - private Runnable invalidate = NULL_RUNNABLE; + @NonNull private Runnable invalidate = NULL_RUNNABLE; private UndoRedoStackListener undoRedoStackListener; private final UndoRedoStacks undoRedoStacks; private final UndoRedoStacks cropUndoRedoStacks; - private final InBoundsMemory inBoundsMemory = new InBoundsMemory(); + private final InBoundsMemory inBoundsMemory = new InBoundsMemory(); private EditorElementHierarchy editorElementHierarchy; - private final RectF visibleViewPort = new RectF(); - private final Point size; + private final RectF visibleViewPort = new RectF(); + private final Point size; private final boolean circleEditing; public EditorModel() { @@ -68,21 +63,22 @@ public EditorModel() { } private EditorModel(@NonNull Parcel in) { - ClassLoader classLoader = getClass().getClassLoader(); - this.circleEditing = in.readByte() == 1; - this.size = new Point(in.readInt(), in.readInt()); + ClassLoader classLoader = getClass().getClassLoader(); + this.circleEditing = in.readByte() == 1; + this.size = new Point(in.readInt(), in.readInt()); //noinspection ConstantConditions this.editorElementHierarchy = EditorElementHierarchy.create(in.readParcelable(classLoader)); - this.undoRedoStacks = in.readParcelable(classLoader); - this.cropUndoRedoStacks = in.readParcelable(classLoader); + this.undoRedoStacks = in.readParcelable(classLoader); + this.cropUndoRedoStacks = in.readParcelable(classLoader); } - public EditorModel(boolean circleEditing, @NonNull EditorElementHierarchy editorElementHierarchy) { - this.circleEditing = circleEditing; - this.size = new Point(1024, 1024); + public EditorModel( + boolean circleEditing, @NonNull EditorElementHierarchy editorElementHierarchy) { + this.circleEditing = circleEditing; + this.size = new Point(1024, 1024); this.editorElementHierarchy = editorElementHierarchy; - this.undoRedoStacks = new UndoRedoStacks(50); - this.cropUndoRedoStacks = new UndoRedoStacks(50); + this.undoRedoStacks = new UndoRedoStacks(50); + this.cropUndoRedoStacks = new UndoRedoStacks(50); } public static EditorModel create() { @@ -90,7 +86,8 @@ public static EditorModel create() { } public static EditorModel createForCircleEditing() { - EditorModel editorModel = new EditorModel(true, EditorElementHierarchy.createForCircleEditing()); + EditorModel editorModel = + new EditorModel(true, EditorElementHierarchy.createForCircleEditing()); editorModel.setCropAspectLock(true); return editorModel; } @@ -107,13 +104,13 @@ public void setUndoRedoStackListener(UndoRedoStackListener undoRedoStackListener /** * Renders tree with the following matrix: - *

- * viewModelMatrix * matrix * editorMatrix - *

- * Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * matrix * editorMatrix + * + *

viewModelMatrix * matrix * editorMatrix + * + *

Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * matrix * editorMatrix * * @param rendererContext Canvas to draw on to. - * @param renderOnTop This element will appear on top of the overlay. + * @param renderOnTop This element will appear on top of the overlay. */ public void draw(@NonNull RendererContext rendererContext, @Nullable EditorElement renderOnTop) { EditorElement root = editorElementHierarchy.getRoot(); @@ -139,7 +136,8 @@ public void draw(@NonNull RendererContext rendererContext, @Nullable EditorEleme } } - public @Nullable Matrix findElementInverseMatrix(@NonNull EditorElement element, @NonNull Matrix viewMatrix) { + public @Nullable Matrix findElementInverseMatrix( + @NonNull EditorElement element, @NonNull Matrix viewMatrix) { Matrix inverse = new Matrix(); if (findElement(element, viewMatrix, inverse)) { return inverse; @@ -147,7 +145,8 @@ public void draw(@NonNull RendererContext rendererContext, @Nullable EditorEleme return null; } - private @Nullable Matrix findElementMatrix(@NonNull EditorElement element, @NonNull Matrix viewMatrix) { + private @Nullable Matrix findElementMatrix( + @NonNull EditorElement element, @NonNull Matrix viewMatrix) { Matrix inverse = findElementInverseMatrix(element, viewMatrix); if (inverse != null) { Matrix regular = new Matrix(); @@ -157,12 +156,19 @@ public void draw(@NonNull RendererContext rendererContext, @Nullable EditorEleme return null; } - public EditorElement findElementAtPoint(@NonNull PointF point, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { - return editorElementHierarchy.getRoot().findElementAt(point.x, point.y, viewMatrix, outInverseModelMatrix); + public EditorElement findElementAtPoint( + @NonNull PointF point, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { + return editorElementHierarchy + .getRoot() + .findElementAt(point.x, point.y, viewMatrix, outInverseModelMatrix); } - private boolean findElement(@NonNull EditorElement element, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { - return editorElementHierarchy.getRoot().findElement(element, viewMatrix, outInverseModelMatrix) == element; + private boolean findElement( + @NonNull EditorElement element, + @NonNull Matrix viewMatrix, + @NonNull Matrix outInverseModelMatrix) { + return editorElementHierarchy.getRoot().findElement(element, viewMatrix, outInverseModelMatrix) + == element; } public void pushUndoPoint() { @@ -175,8 +181,8 @@ public void pushUndoPoint() { } public void undo() { - boolean cropping = isCropping(); - UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); + boolean cropping = isCropping(); + UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); undoRedo(stacks.getUndoStack(), stacks.getRedoStack(), cropping); @@ -184,35 +190,42 @@ public void undo() { } public void redo() { - boolean cropping = isCropping(); - UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); + boolean cropping = isCropping(); + UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); undoRedo(stacks.getRedoStack(), stacks.getUndoStack(), cropping); updateUndoRedoAvailableState(stacks); } - private void undoRedo(@NonNull ElementStack fromStack, @NonNull ElementStack toStack, boolean keepEditorState) { + private void undoRedo( + @NonNull ElementStack fromStack, @NonNull ElementStack toStack, boolean keepEditorState) { final EditorElement oldRootElement = editorElementHierarchy.getRoot(); - final EditorElement popped = fromStack.pop(oldRootElement); + final EditorElement popped = fromStack.pop(oldRootElement); if (popped != null) { editorElementHierarchy = EditorElementHierarchy.create(popped); toStack.tryPush(oldRootElement); - restoreStateWithAnimations(oldRootElement, editorElementHierarchy.getRoot(), invalidate, keepEditorState); + restoreStateWithAnimations( + oldRootElement, editorElementHierarchy.getRoot(), invalidate, keepEditorState); invalidate.run(); // re-zoom image root as the view port might be different now editorElementHierarchy.updateViewToCrop(visibleViewPort, invalidate); - inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); + inBoundsMemory.push( + editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); } } - private static void restoreStateWithAnimations(@NonNull EditorElement fromRootElement, @NonNull EditorElement toRootElement, @NonNull Runnable onInvalidate, boolean keepEditorState) { + private static void restoreStateWithAnimations( + @NonNull EditorElement fromRootElement, + @NonNull EditorElement toRootElement, + @NonNull Runnable onInvalidate, + boolean keepEditorState) { Map fromMap = getElementMap(fromRootElement); - Map toMap = getElementMap(toRootElement); + Map toMap = getElementMap(toRootElement); for (EditorElement fromElement : fromMap.values()) { fromElement.stopAnimation(); @@ -249,7 +262,8 @@ private void updateUndoRedoAvailableState(UndoRedoStacks currentStack) { EditorElement root = editorElementHierarchy.getRoot(); - undoRedoStackListener.onAvailabilityChanged(currentStack.canUndo(root), currentStack.canRedo(root)); + undoRedoStackListener.onAvailabilityChanged( + currentStack.canUndo(root), currentStack.canRedo(root)); } private static Map getElementMap(@NonNull EditorElement element) { @@ -262,7 +276,8 @@ public void startCrop() { pushUndoPoint(); cropUndoRedoStacks.clear(editorElementHierarchy.getRoot()); editorElementHierarchy.startCrop(invalidate); - inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); + inBoundsMemory.push( + editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); updateUndoRedoAvailableState(cropUndoRedoStacks); } @@ -273,11 +288,10 @@ public void doneCrop() { public void setCropAspectLock(boolean locked) { EditorFlags flags = editorElementHierarchy.getCropEditorElement().getFlags(); - int currentState = flags.setAspectLocked(locked).getCurrentState(); + int currentState = flags.setAspectLocked(locked).getCurrentState(); flags.reset(); - flags.setAspectLocked(locked) - .persist(); + flags.setAspectLocked(locked).persist(); flags.restoreState(currentState); } @@ -329,51 +343,62 @@ private void ensureFitsBounds(boolean allowScaleToRepairCrop) { /** * Attempts to scale the supplied element such that {@link #cropIsWithinMainImageBounds} is true. - *

- * Does not respect minimum scale, so does need a further check to {@link #currentCropIsAcceptable} afterwards. * - * @param element The element to be scaled. If successful, it will be animated to the correct position. - * @param scaleAtMost The amount of scale to apply at most. Use < 1 for the crop, and > 1 for the image. + *

Does not respect minimum scale, so does need a further check to {@link + * #currentCropIsAcceptable} afterwards. + * + * @param element The element to be scaled. If successful, it will be animated to the correct + * position. + * @param scaleAtMost The amount of scale to apply at most. Use < 1 for the crop, and > 1 for the + * image. * @return true if successfully scaled the element. false if the element was left unchanged. */ private boolean tryToScaleToFit(@NonNull EditorElement element, float scaleAtMost) { - return Bisect.bisectToTest(element, - 1, - scaleAtMost, - this::cropIsWithinMainImageBounds, - (matrix, scale) -> matrix.preScale(scale, scale), - invalidate); + return Bisect.bisectToTest( + element, + 1, + scaleAtMost, + this::cropIsWithinMainImageBounds, + (matrix, scale) -> matrix.preScale(scale, scale), + invalidate); } /** - * Attempts to translate the supplied element such that {@link #cropIsWithinMainImageBounds} is true. - * If you supply both x and y, it will attempt to find a fit on the diagonal with vector x, y. + * Attempts to translate the supplied element such that {@link #cropIsWithinMainImageBounds} is + * true. If you supply both x and y, it will attempt to find a fit on the diagonal with vector x, + * y. * - * @param element The element to be translated. If successful, it will be animated to the correct position. + * @param element The element to be translated. If successful, it will be animated to the correct + * position. * @param translateXAtMost The maximum translation to apply in the x axis. * @param translateYAtMost The maximum translation to apply in the y axis. - * @return a matrix if successfully translated the element. null if the element unable to be translated to fit. + * @return a matrix if successfully translated the element. null if the element unable to be + * translated to fit. */ - private Matrix tryToTranslateToFit(@NonNull EditorElement element, float translateXAtMost, float translateYAtMost) { - return Bisect.bisectToTest(element, - 0, - 1, - this::cropIsWithinMainImageBounds, - (matrix, factor) -> matrix.postTranslate(factor * translateXAtMost, factor * translateYAtMost)); + private Matrix tryToTranslateToFit( + @NonNull EditorElement element, float translateXAtMost, float translateYAtMost) { + return Bisect.bisectToTest( + element, + 0, + 1, + this::cropIsWithinMainImageBounds, + (matrix, factor) -> + matrix.postTranslate(factor * translateXAtMost, factor * translateYAtMost)); } /** * Tries to fix an element that is out of bounds by adjusting it's translation. * - * @param element Element to move. + * @param element Element to move. * @param lastKnownGoodPosition Last known good position of element. * @return true iff fixed the element. */ - private boolean tryToFixTranslationOutOfBounds(@NonNull EditorElement element, @NonNull Matrix lastKnownGoodPosition) { - final Matrix elementMatrix = element.getLocalMatrix(); - final Matrix original = new Matrix(elementMatrix); - final float[] current = new float[9]; - final float[] lastGood = new float[9]; + private boolean tryToFixTranslationOutOfBounds( + @NonNull EditorElement element, @NonNull Matrix lastKnownGoodPosition) { + final Matrix elementMatrix = element.getLocalMatrix(); + final Matrix original = new Matrix(elementMatrix); + final float[] current = new float[9]; + final float[] lastGood = new float[9]; Matrix matrix; elementMatrix.getValues(current); @@ -442,13 +467,13 @@ public void dragDropRelease() { } /** - * Pixel count must be no smaller than {@link #MINIMUM_CROP_PIXEL_COUNT} (unless its original size was less than that) - * and all points must be within the bounds. + * Pixel count must be no smaller than {@link #MINIMUM_CROP_PIXEL_COUNT} (unless its original size + * was less than that) and all points must be within the bounds. */ private boolean currentCropIsAcceptable() { - Point outputSize = getOutputSize(); - int outputPixelCount = outputSize.x * outputSize.y; - int minimumPixelCount = Math.min(size.x * size.y, MINIMUM_CROP_PIXEL_COUNT); + Point outputSize = getOutputSize(); + int outputPixelCount = outputSize.x * outputSize.y; + int minimumPixelCount = Math.min(size.x * size.y, MINIMUM_CROP_PIXEL_COUNT); Point thinnestRatio = MINIMUM_RATIO; @@ -457,20 +482,19 @@ private boolean currentCropIsAcceptable() { thinnestRatio = size; } - return compareRatios(outputSize, thinnestRatio) >= 0 && - outputPixelCount >= minimumPixelCount && - cropIsWithinMainImageBounds(); + return compareRatios(outputSize, thinnestRatio) >= 0 + && outputPixelCount >= minimumPixelCount + && cropIsWithinMainImageBounds(); } /** - * -1 iff a is a narrower ratio than b. - * +1 iff a is a squarer ratio than b. - * 0 if the ratios are the same. + * -1 iff a is a narrower ratio than b. +1 iff a is a squarer ratio than b. 0 if the ratios are + * the same. */ private static int compareRatios(@NonNull Point a, @NonNull Point b) { int smallA = Math.min(a.x, a.y); int largeA = Math.max(a.x, a.y); - + int smallB = Math.min(b.x, b.y); int largeB = Math.max(b.x, b.y); @@ -484,9 +508,7 @@ private boolean cropIsWithinMainImageBounds() { return Bounds.boundsRemainInBounds(editorElementHierarchy.imageMatrixRelativeToCrop()); } - /** - * Called as edits are underway. - */ + /** Called as edits are underway. */ public void moving(@NonNull EditorElement editorElement) { if (!isCropping()) return; @@ -508,27 +530,31 @@ public void setVisibleViewPort(@NonNull RectF visibleViewPort) { public Set getUniqueColorsIgnoringAlpha() { final Set colors = new LinkedHashSet<>(); - editorElementHierarchy.getRoot().forAllInTree(element -> { - Renderer renderer = element.getRenderer(); - if (renderer instanceof ColorableRenderer) { - colors.add(((ColorableRenderer) renderer).getColor() | 0xff000000); - } - }); + editorElementHierarchy + .getRoot() + .forAllInTree( + element -> { + Renderer renderer = element.getRenderer(); + if (renderer instanceof ColorableRenderer) { + colors.add(((ColorableRenderer) renderer).getColor() | 0xff000000); + } + }); return colors; } - public static final Creator CREATOR = new Creator() { - @Override - public EditorModel createFromParcel(Parcel in) { - return new EditorModel(in); - } + public static final Creator CREATOR = + new Creator() { + @Override + public EditorModel createFromParcel(Parcel in) { + return new EditorModel(in); + } - @Override - public EditorModel[] newArray(int size) { - return new EditorModel[size]; - } - }; + @Override + public EditorModel[] newArray(int size) { + return new EditorModel[size]; + } + }; @Override public int describeContents() { @@ -545,27 +571,25 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(cropUndoRedoStacks, flags); } - /** - * Blocking render of the model. - */ + /** Blocking render of the model. */ @WorkerThread public @NonNull Bitmap render(@NonNull Context context) { return render(context, null); } - /** - * Blocking render of the model. - */ + /** Blocking render of the model. */ @WorkerThread public @NonNull Bitmap render(@NonNull Context context, @Nullable Point size) { - EditorElement image = editorElementHierarchy.getFlipRotate(); - RectF cropRect = editorElementHierarchy.getCropRect(); - Point outputSize = size != null ? size : getOutputSize(); + EditorElement image = editorElementHierarchy.getFlipRotate(); + RectF cropRect = editorElementHierarchy.getCropRect(); + Point outputSize = size != null ? size : getOutputSize(); Bitmap bitmap = Bitmap.createBitmap(outputSize.x, outputSize.y, Bitmap.Config.ARGB_8888); try { Canvas canvas = new Canvas(bitmap); - RendererContext rendererContext = new RendererContext(context, canvas, RendererContext.Ready.NULL, RendererContext.Invalidate.NULL); + RendererContext rendererContext = + new RendererContext( + context, canvas, RendererContext.Ready.NULL, RendererContext.Invalidate.NULL); RectF bitmapArea = new RectF(); bitmapArea.right = bitmap.getWidth(); @@ -597,7 +621,7 @@ public void writeToParcel(Parcel dest, int flags) { private Point getOutputSize() { PointF outputSize = editorElementHierarchy.getOutputSize(size); - int width = (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x); + int width = (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x); int height = (int) (width * outputSize.y / outputSize.x); return new Point(width, height); @@ -607,22 +631,23 @@ private Point getOutputSize() { public Point getOutputSizeMaxWidth(int maxDimension) { PointF outputSize = editorElementHierarchy.getOutputSize(size); - int width = Math.min(maxDimension, (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x)); + int width = Math.min(maxDimension, (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x)); int height = (int) (width * outputSize.y / outputSize.x); if (height > maxDimension) { height = maxDimension; - width = (int) (height * outputSize.x / outputSize.y); + width = (int) (height * outputSize.x / outputSize.y); } return new Point(width, height); } @Override - public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { + public void onReady( + @NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { if (cropMatrix != null && size != null && isRendererOfMainImage(renderer)) { - boolean changedBefore = isChanged(); - Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix(); + boolean changedBefore = isChanged(); + Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix(); this.size.set(size.x, size.y); if (imageCropMatrix.isIdentity()) { imageCropMatrix.set(cropMatrix); @@ -650,8 +675,8 @@ public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nu } private boolean isRendererOfMainImage(@NonNull Renderer renderer) { - EditorElement mainImage = editorElementHierarchy.getMainImage(); - Renderer mainImageRenderer = mainImage != null ? mainImage.getRenderer() : null; + EditorElement mainImage = editorElementHierarchy.getMainImage(); + Renderer mainImageRenderer = mainImage != null ? mainImage.getRenderer() : null; return mainImageRenderer == renderer; } @@ -659,7 +684,7 @@ private boolean isRendererOfMainImage(@NonNull Renderer renderer) { * Add a new {@link EditorElement} centered in the current visible crop area. * * @param element New element to add. - * @param scale Initial scale for new element. + * @param scale Initial scale for new element. */ public void addElementCentered(@NonNull EditorElement element, float scale) { Matrix localMatrix = element.getLocalMatrix(); @@ -671,7 +696,8 @@ public void addElementCentered(@NonNull EditorElement element, float scale) { } /** - * Add an element to the main image, or if there is no main image, make the new element the main image. + * Add an element to the main image, or if there is no main image, make the new element the main + * image. * * @param element New element to add. */ @@ -682,7 +708,7 @@ public void addElement(@NonNull EditorElement element) { public void addElementWithoutPushUndo(@NonNull EditorElement element) { EditorElement mainImage = editorElementHierarchy.getMainImage(); - EditorElement parent = mainImage != null ? mainImage : editorElementHierarchy.getImageRoot(); + EditorElement parent = mainImage != null ? mainImage : editorElementHierarchy.getImageRoot(); parent.addElement(element); @@ -716,15 +742,17 @@ RectF findRelativeBounds(EditorElement from, EditorElement to) { } /** - * Returns a matrix that maps points in the {@param from} element in to points in the {@param to} element. + * Returns a matrix that maps points in the {@param from} element in to points in the {@param to} + * element. * * @param from * @param to * @return */ - @Nullable Matrix findRelativeMatrix(@NonNull EditorElement from, @NonNull EditorElement to) { + @Nullable + Matrix findRelativeMatrix(@NonNull EditorElement from, @NonNull EditorElement to) { Matrix matrix = findElementInverseMatrix(to, new Matrix()); - Matrix outOf = findElementMatrix(from, new Matrix()); + Matrix outOf = findElementMatrix(from, new Matrix()); if (outOf != null && matrix != null) { matrix.preConcat(outOf); @@ -764,7 +792,9 @@ public EditorElement getMainImage() { } public void delete(@NonNull EditorElement editorElement) { - editorElementHierarchy.getImageRoot().forAllInTree(element -> element.deleteChild(editorElement, invalidate)); + editorElementHierarchy + .getImageRoot() + .forAllInTree(element -> element.deleteChild(editorElement, invalidate)); } public @Nullable EditorElement findById(@NonNull UUID uuid) { @@ -774,11 +804,12 @@ public void delete(@NonNull EditorElement editorElement) { /** * Changes the temporary view so that the text element is centered in it. * - * @param entity Entity to center on. + * @param entity Entity to center on. * @param textRenderer The text renderer, which can make additional adjustments to the zoom matrix - * to leave space for the keyboard for example. + * to leave space for the keyboard for example. */ - public void zoomToTextElement(@NonNull EditorElement entity, @NonNull MultiLineTextRenderer textRenderer) { + public void zoomToTextElement( + @NonNull EditorElement entity, @NonNull MultiLineTextRenderer textRenderer) { Matrix elementInverseMatrix = findElementInverseMatrix(entity, new Matrix()); if (elementInverseMatrix != null) { EditorElement root = editorElementHierarchy.getRoot(); @@ -803,12 +834,12 @@ public boolean isCropping() { return editorElementHierarchy.getCropEditorElement().getFlags().isVisible(); } - /** - * Returns a matrix that maps bounds to the crop area. - */ + /** Returns a matrix that maps bounds to the crop area. */ public Matrix getInverseCropPosition() { Matrix matrix = new Matrix(); - matrix.set(findRelativeMatrix(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement())); + matrix.set( + findRelativeMatrix( + editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement())); matrix.postConcat(editorElementHierarchy.getFlipRotate().getLocalMatrix()); Matrix positionRelativeToCrop = new Matrix(); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java index 468e288f0..7d5bf0778 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java @@ -4,22 +4,22 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.Arrays; import java.util.Stack; /** * Contains a stack of elements for undo and redo stacks. - *

- * Elements are mutable, so this stack serializes the element and keeps a stack of serialized data. - *

- * The stack has a {@link #limit} and if it exceeds that limit during a push the second to earliest item - * is removed so that it can always go back to the first state. Effectively collapsing the history for - * the start of the stack. + * + *

Elements are mutable, so this stack serializes the element and keeps a stack of serialized + * data. + * + *

The stack has a {@link #limit} and if it exceeds that limit during a push the second to + * earliest item is removed so that it can always go back to the first state. Effectively collapsing + * the history for the start of the stack. */ final class ElementStack implements Parcelable { - private final int limit; + private final int limit; private final Stack stack = new Stack<>(); ElementStack(int limit) { @@ -37,15 +37,15 @@ private ElementStack(@NonNull Parcel in) { /** * Pushes an element to the stack iff the element's serialized value is different to any found at * the top of the stack. - *

- * Removes the second to earliest item if it is overflowing. + * + *

Removes the second to earliest item if it is overflowing. * * @param element new editor element state. * @return true iff the pushed item was different to the top item. */ boolean tryPush(@NonNull EditorElement element) { - byte[] bytes = getBytes(element); - boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek()); + byte[] bytes = getBytes(element); + boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek()); if (push) { stack.push(bytes); @@ -68,14 +68,13 @@ static byte[] getBytes(@NonNull Parcelable parcelable) { return bytes; } - /** - * Pops the first different state from the supplied element. - */ - @Nullable EditorElement pop(@NonNull EditorElement element) { + /** Pops the first different state from the supplied element. */ + @Nullable + EditorElement pop(@NonNull EditorElement element) { if (stack.empty()) return null; byte[] elementBytes = getBytes(element); - byte[] stackData = null; + byte[] stackData = null; while (!stack.empty() && stackData == null) { byte[] topData = stack.pop(); @@ -101,17 +100,18 @@ void clear() { stack.clear(); } - public static final Creator CREATOR = new Creator() { - @Override - public ElementStack createFromParcel(Parcel in) { - return new ElementStack(in); - } - - @Override - public ElementStack[] newArray(int size) { - return new ElementStack[size]; - } - }; + public static final Creator CREATOR = + new Creator() { + @Override + public ElementStack createFromParcel(Parcel in) { + return new ElementStack(in); + } + + @Override + public ElementStack[] newArray(int size) { + return new ElementStack[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java index e048261ef..962a430f4 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java @@ -6,7 +6,7 @@ final class InBoundsMemory { - private final Matrix lastGoodUserCrop = new Matrix(); + private final Matrix lastGoodUserCrop = new Matrix(); private final Matrix lastGoodMainImage = new Matrix(); void push(@Nullable EditorElement mainImage, @NonNull EditorElement userCrop) { @@ -21,7 +21,10 @@ void push(@Nullable EditorElement mainImage, @NonNull EditorElement userCrop) { lastGoodUserCrop.preConcat(userCrop.getEditorMatrix()); } - void restore(@Nullable EditorElement mainImage, @NonNull EditorElement cropEditorElement, @Nullable Runnable invalidate) { + void restore( + @Nullable EditorElement mainImage, + @NonNull EditorElement cropEditorElement, + @Nullable Runnable invalidate) { if (mainImage != null) { mainImage.animateLocalTo(lastGoodMainImage, invalidate); } @@ -31,4 +34,4 @@ void restore(@Nullable EditorElement mainImage, @NonNull EditorElement cropEdito Matrix getLastKnownGoodMainImageMatrix() { return new Matrix(lastGoodMainImage); } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java index aad9eed7e..d43a87b47 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java @@ -4,13 +4,11 @@ import android.graphics.RectF; import android.os.Parcel; import androidx.annotation.NonNull; - import java.util.UUID; public final class ParcelUtils { - private ParcelUtils() { - } + private ParcelUtils() {} public static void writeMatrix(@NonNull Parcel dest, @NonNull Matrix matrix) { float[] values = new float[9]; @@ -38,9 +36,9 @@ public static void writeRect(@NonNull Parcel dest, @NonNull RectF rect) { } public static @NonNull RectF readRectF(@NonNull Parcel in) { - float left = in.readFloat(); - float top = in.readFloat(); - float right = in.readFloat(); + float left = in.readFloat(); + float top = in.readFloat(); + float right = in.readFloat(); float bottom = in.readFloat(); return new RectF(left, top, right, bottom); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java index 144766449..a529927e3 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java @@ -1,31 +1,31 @@ package org.thoughtcrime.securesms.imageeditor.model; +import java.util.UUID; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; -import java.util.UUID; - /** * A special {@link Renderer} that controls another {@link EditorElement}. - *

- * It has a reference to the {@link EditorElement#getId()} and a {@link ControlPoint} which it is in control of. - *

- * The presence of this interface on the selected element is used to launch a ThumbDragEditSession. + * + *

It has a reference to the {@link EditorElement#getId()} and a {@link ControlPoint} which it is + * in control of. + * + *

The presence of this interface on the selected element is used to launch a + * ThumbDragEditSession. */ public interface ThumbRenderer extends Renderer { enum ControlPoint { + CENTER_LEFT(Bounds.LEFT, Bounds.CENTRE_Y), + CENTER_RIGHT(Bounds.RIGHT, Bounds.CENTRE_Y), - CENTER_LEFT (Bounds.LEFT, Bounds.CENTRE_Y), - CENTER_RIGHT (Bounds.RIGHT, Bounds.CENTRE_Y), - - TOP_CENTER (Bounds.CENTRE_X, Bounds.TOP), - BOTTOM_CENTER (Bounds.CENTRE_X, Bounds.BOTTOM), + TOP_CENTER(Bounds.CENTRE_X, Bounds.TOP), + BOTTOM_CENTER(Bounds.CENTRE_X, Bounds.BOTTOM), - TOP_LEFT (Bounds.LEFT, Bounds.TOP), - TOP_RIGHT (Bounds.RIGHT, Bounds.TOP), - BOTTOM_LEFT (Bounds.LEFT, Bounds.BOTTOM), - BOTTOM_RIGHT (Bounds.RIGHT, Bounds.BOTTOM); + TOP_LEFT(Bounds.LEFT, Bounds.TOP), + TOP_RIGHT(Bounds.RIGHT, Bounds.TOP), + BOTTOM_LEFT(Bounds.LEFT, Bounds.BOTTOM), + BOTTOM_RIGHT(Bounds.RIGHT, Bounds.BOTTOM); private final float x; private final float y; @@ -45,14 +45,22 @@ public float getY() { public ControlPoint opposite() { switch (this) { - case CENTER_LEFT: return CENTER_RIGHT; - case CENTER_RIGHT: return CENTER_LEFT; - case TOP_CENTER: return BOTTOM_CENTER; - case BOTTOM_CENTER: return TOP_CENTER; - case TOP_LEFT: return BOTTOM_RIGHT; - case TOP_RIGHT: return BOTTOM_LEFT; - case BOTTOM_LEFT: return TOP_RIGHT; - case BOTTOM_RIGHT: return TOP_LEFT; + case CENTER_LEFT: + return CENTER_RIGHT; + case CENTER_RIGHT: + return CENTER_LEFT; + case TOP_CENTER: + return BOTTOM_CENTER; + case BOTTOM_CENTER: + return TOP_CENTER; + case TOP_LEFT: + return BOTTOM_RIGHT; + case TOP_RIGHT: + return BOTTOM_LEFT; + case BOTTOM_LEFT: + return TOP_RIGHT; + case BOTTOM_RIGHT: + return TOP_LEFT; default: throw new RuntimeException(); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java index 8f680bdd0..51ebab173 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java @@ -4,7 +4,6 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.Arrays; final class UndoRedoStacks implements Parcelable { @@ -12,34 +11,34 @@ final class UndoRedoStacks implements Parcelable { private final ElementStack undoStack; private final ElementStack redoStack; - @NonNull - private byte[] unchangedState; + @NonNull private byte[] unchangedState; UndoRedoStacks(int limit) { this(new ElementStack(limit), new ElementStack(limit), null); } - private UndoRedoStacks(ElementStack undoStack, ElementStack redoStack, @Nullable byte[] unchangedState) { + private UndoRedoStacks( + ElementStack undoStack, ElementStack redoStack, @Nullable byte[] unchangedState) { this.undoStack = undoStack; this.redoStack = redoStack; this.unchangedState = unchangedState != null ? unchangedState : new byte[0]; } - public static final Creator CREATOR = new Creator() { - @Override - public UndoRedoStacks createFromParcel(Parcel in) { - return new UndoRedoStacks( - in.readParcelable(ElementStack.class.getClassLoader()), - in.readParcelable(ElementStack.class.getClassLoader()), - in.createByteArray() - ); - } - - @Override - public UndoRedoStacks[] newArray(int size) { - return new UndoRedoStacks[size]; - } - }; + public static final Creator CREATOR = + new Creator() { + @Override + public UndoRedoStacks createFromParcel(Parcel in) { + return new UndoRedoStacks( + in.readParcelable(ElementStack.class.getClassLoader()), + in.readParcelable(ElementStack.class.getClassLoader()), + in.createByteArray()); + } + + @Override + public UndoRedoStacks[] newArray(int size) { + return new UndoRedoStacks[size]; + } + }; @Override public void writeToParcel(Parcel dest, int flags) { @@ -77,16 +76,12 @@ boolean isChanged(@NonNull EditorElement element) { return !Arrays.equals(ElementStack.getBytes(element), unchangedState); } - /** - * As long as there is something different in the stack somewhere, then we can undo. - */ + /** As long as there is something different in the stack somewhere, then we can undo. */ boolean canUndo(@NonNull EditorElement currentState) { return undoStack.stackContainsStateDifferentFrom(currentState); } - /** - * As long as there is something different in the stack somewhere, then we can redo. - */ + /** As long as there is something different in the stack somewhere, then we can redo. */ boolean canRedo(@NonNull EditorElement currentState) { return redoStack.stackContainsStateDifferentFrom(currentState); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java index dedbbc3ef..210e881f4 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java @@ -5,20 +5,18 @@ import android.graphics.Path; import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.Arrays; /** * Given points for a line to go though, automatically finds control points. - *

- * Based on http://www.particleincell.com/2012/bezier-splines/ - *

- * Can then draw that line to a {@link Canvas} given a {@link Paint}. - *

- * Allocation efficient so that adding new points does not result in lots of array allocations. + * + *

Based on http://www.particleincell.com/2012/bezier-splines/ + * + *

Can then draw that line to a {@link Canvas} given a {@link Paint}. + * + *

Allocation efficient so that adding new points does not result in lots of array allocations. */ final class AutomaticControlPointBezierLine implements Parcelable { @@ -57,8 +55,8 @@ void reset() { /** * Adds a new point to the end of the line but ignores points that are too close to the last. * - * @param x new x point - * @param y new y point + * @param x new x point + * @param y new y point * @param thickness the maximum distance to allow, line thickness is recommended. */ void addPointFiltered(float x, float y, float thickness) { @@ -135,7 +133,7 @@ private void recalculateControlPoints() { * Draw the line. * * @param canvas The canvas to draw on. - * @param paint The paint to use. + * @param paint The paint to use. */ void draw(@NonNull Canvas canvas, @NonNull Paint paint) { canvas.drawPath(path, paint); @@ -148,11 +146,11 @@ void draw(@NonNull Canvas canvas, @NonNull Paint paint) { private float[] r; /** - * Based on http://www.particleincell.com/2012/bezier-splines/ + * Based on http://www.particleincell.com/2012/bezier-splines/ * - * @param k knots x or y, must be at least 2 entries - * @param p1 corresponding first control point x or y - * @param p2 corresponding second control point x or y + * @param k knots x or y, must be at least 2 entries + * @param p1 corresponding first control point x or y + * @param p2 corresponding second control point x or y * @param count number of k to process */ private void computeControlPoints(float[] k, float[] p1, float[] p2, int count) { @@ -198,19 +196,20 @@ private void computeControlPoints(float[] k, float[] p1, float[] p2, int count) p2[n - 1] = 0.5f * (k[n] + p1[n - 1]); } - public static final Creator CREATOR = new Creator() { - @Override - public AutomaticControlPointBezierLine createFromParcel(Parcel in) { - float[] x = in.createFloatArray(); - float[] y = in.createFloatArray(); - return new AutomaticControlPointBezierLine(x, y, x != null ? x.length : 0); - } + public static final Creator CREATOR = + new Creator() { + @Override + public AutomaticControlPointBezierLine createFromParcel(Parcel in) { + float[] x = in.createFloatArray(); + float[] y = in.createFloatArray(); + return new AutomaticControlPointBezierLine(x, y, x != null ? x.length : 0); + } - @Override - public AutomaticControlPointBezierLine[] newArray(int size) { - return new AutomaticControlPointBezierLine[size]; - } - }; + @Override + public AutomaticControlPointBezierLine[] newArray(int size) { + return new AutomaticControlPointBezierLine[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java index 30423b9c4..dd341b8d3 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java @@ -5,41 +5,46 @@ import android.graphics.PointF; import android.graphics.RectF; import android.os.Parcel; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; /** - * Renders a {@link AutomaticControlPointBezierLine} with {@link #thickness}, {@link #color} and {@link #cap} end type. + * Renders a {@link AutomaticControlPointBezierLine} with {@link #thickness}, {@link #color} and + * {@link #cap} end type. */ -public final class BezierDrawingRenderer extends InvalidateableRenderer implements ColorableRenderer { +public final class BezierDrawingRenderer extends InvalidateableRenderer + implements ColorableRenderer { - private final Paint paint; + private final Paint paint; private final AutomaticControlPointBezierLine bezierLine; - private final Paint.Cap cap; - - @Nullable - private final RectF clipRect; - - private int color; - private float thickness; - - private BezierDrawingRenderer(int color, float thickness, @NonNull Paint.Cap cap, @Nullable AutomaticControlPointBezierLine bezierLine, @Nullable RectF clipRect) { - this.paint = new Paint(); - this.color = color; - this.thickness = thickness; - this.cap = cap; - this.clipRect = clipRect; + private final Paint.Cap cap; + + @Nullable private final RectF clipRect; + + private int color; + private float thickness; + + private BezierDrawingRenderer( + int color, + float thickness, + @NonNull Paint.Cap cap, + @Nullable AutomaticControlPointBezierLine bezierLine, + @Nullable RectF clipRect) { + this.paint = new Paint(); + this.color = color; + this.thickness = thickness; + this.cap = cap; + this.clipRect = clipRect; this.bezierLine = bezierLine != null ? bezierLine : new AutomaticControlPointBezierLine(); updatePaint(); } - public BezierDrawingRenderer(int color, float thickness, @NonNull Paint.Cap cap, @Nullable RectF clipRect) { - this(color, thickness, cap,null, clipRect != null ? new RectF(clipRect) : null); + public BezierDrawingRenderer( + int color, float thickness, @NonNull Paint.Cap cap, @Nullable RectF clipRect) { + this(color, thickness, cap, null, clipRect != null ? new RectF(clipRect) : null); } @Override @@ -99,7 +104,10 @@ public void render(@NonNull RendererContext rendererContext) { int alpha = paint.getAlpha(); paint.setAlpha(rendererContext.getAlpha(alpha)); - paint.setXfermode(rendererContext.getMaskPaint() != null ? rendererContext.getMaskPaint().getXfermode() : null); + paint.setXfermode( + rendererContext.getMaskPaint() != null + ? rendererContext.getMaskPaint().getXfermode() + : null); bezierLine.draw(canvas, paint); @@ -112,23 +120,25 @@ public boolean hitTest(float x, float y) { return false; } - public static final Creator CREATOR = new Creator() { - @Override - public BezierDrawingRenderer createFromParcel(Parcel in) { - int color = in.readInt(); - float thickness = in.readFloat(); - Paint.Cap cap = Paint.Cap.values()[in.readInt()]; - AutomaticControlPointBezierLine bezierLine = in.readParcelable(AutomaticControlPointBezierLine.class.getClassLoader()); - RectF clipRect = in.readParcelable(RectF.class.getClassLoader()); - - return new BezierDrawingRenderer(color, thickness, cap, bezierLine, clipRect); - } - - @Override - public BezierDrawingRenderer[] newArray(int size) { - return new BezierDrawingRenderer[size]; - } - }; + public static final Creator CREATOR = + new Creator() { + @Override + public BezierDrawingRenderer createFromParcel(Parcel in) { + int color = in.readInt(); + float thickness = in.readFloat(); + Paint.Cap cap = Paint.Cap.values()[in.readInt()]; + AutomaticControlPointBezierLine bezierLine = + in.readParcelable(AutomaticControlPointBezierLine.class.getClassLoader()); + RectF clipRect = in.readParcelable(RectF.class.getClassLoader()); + + return new BezierDrawingRenderer(color, thickness, cap, bezierLine, clipRect); + } + + @Override + public BezierDrawingRenderer[] newArray(int size) { + return new BezierDrawingRenderer[size]; + } + }; @Override public int describeContents() { @@ -143,5 +153,4 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(bezierLine, flags); dest.writeParcelable(clipRect, flags); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java index a397e06e4..f0973d0ae 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java @@ -6,40 +6,38 @@ import android.graphics.Path; import android.graphics.RectF; import android.os.Parcel; - import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.core.content.res.ResourcesCompat; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; /** - * Renders a box outside of the current crop area using {@link R.color#crop_area_renderer_outer_color} - * and around the edge it renders the markers for the thumbs using {@link R.color#crop_area_renderer_edge_color}, - * {@link R.dimen#crop_area_renderer_edge_thickness} and {@link R.dimen#crop_area_renderer_edge_size}. - *

- * Hit tests outside of the bounds. + * Renders a box outside of the current crop area using {@link + * R.color#crop_area_renderer_outer_color} and around the edge it renders the markers for the thumbs + * using {@link R.color#crop_area_renderer_edge_color}, {@link + * R.dimen#crop_area_renderer_edge_thickness} and {@link R.dimen#crop_area_renderer_edge_size}. + * + *

Hit tests outside of the bounds. */ public final class CropAreaRenderer implements Renderer { - @ColorRes - private final int color; + @ColorRes private final int color; private final boolean renderCenterThumbs; - private final Path cropClipPath = new Path(); + private final Path cropClipPath = new Path(); private final Path screenClipPath = new Path(); - private final RectF dst = new RectF(); + private final RectF dst = new RectF(); private final Paint paint = new Paint(); @Override public void render(@NonNull RendererContext rendererContext) { rendererContext.save(); - Canvas canvas = rendererContext.canvas; + Canvas canvas = rendererContext.canvas; Resources resources = rendererContext.context.getResources(); canvas.clipPath(cropClipPath); @@ -47,10 +45,16 @@ public void render(@NonNull RendererContext rendererContext) { rendererContext.mapRect(dst, Bounds.FULL_BOUNDS); - final int thickness = resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_thickness); - final int size = (int) Math.min(resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size), Math.min(dst.width(), dst.height()) / 3f - 10); + final int thickness = + resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_thickness); + final int size = + (int) + Math.min( + resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size), + Math.min(dst.width(), dst.height()) / 3f - 10); - paint.setColor(ResourcesCompat.getColor(resources, R.color.crop_area_renderer_edge_color, null)); + paint.setColor( + ResourcesCompat.getColor(resources, R.color.crop_area_renderer_edge_color, null)); rendererContext.canvasMatrix.setToIdentity(); screenClipPath.reset(); @@ -92,7 +96,7 @@ public void render(@NonNull RendererContext rendererContext) { } public CropAreaRenderer(@ColorRes int color, boolean renderCenterThumbs) { - this.color = color; + this.color = color; this.renderCenterThumbs = renderCenterThumbs; cropClipPath.toggleInverseFillType(); @@ -109,18 +113,18 @@ public boolean hitTest(float x, float y) { return !Bounds.contains(x, y); } - public static final Creator CREATOR = new Creator() { - @Override - public @NonNull CropAreaRenderer createFromParcel(@NonNull Parcel in) { - return new CropAreaRenderer(in.readInt(), - in.readByte() == 1); - } - - @Override - public @NonNull CropAreaRenderer[] newArray(int size) { - return new CropAreaRenderer[size]; - } - }; + public static final Creator CREATOR = + new Creator() { + @Override + public @NonNull CropAreaRenderer createFromParcel(@NonNull Parcel in) { + return new CropAreaRenderer(in.readInt(), in.readByte() == 1); + } + + @Override + public @NonNull CropAreaRenderer[] newArray(int size) { + return new CropAreaRenderer[size]; + } + }; @Override public void writeToParcel(Parcel dest, int flags) { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java index fd255e4d2..ff44f93be 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java @@ -1,14 +1,13 @@ package org.thoughtcrime.securesms.imageeditor.renderers; import androidx.annotation.NonNull; - +import java.lang.ref.WeakReference; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; -import java.lang.ref.WeakReference; - /** - * Maintains a weak reference to the an invalidate callback allowing future invalidation without memory leak risk. + * Maintains a weak reference to the an invalidate callback allowing future invalidation without + * memory leak risk. */ abstract class InvalidateableRenderer implements Renderer { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java index 6c662e9d1..b110360fc 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java @@ -4,15 +4,14 @@ import android.os.Parcel; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; /** * Renders the {@link color} outside of the {@link Bounds}. - *

- * Hit tests outside of the bounds. + * + *

Hit tests outside of the bounds. */ public final class InverseFillRenderer implements Renderer { @@ -47,17 +46,18 @@ public boolean hitTest(float x, float y) { return !Bounds.contains(x, y); } - public static final Creator CREATOR = new Creator() { - @Override - public InverseFillRenderer createFromParcel(Parcel in) { - return new InverseFillRenderer(in); - } + public static final Creator CREATOR = + new Creator() { + @Override + public InverseFillRenderer createFromParcel(Parcel in) { + return new InverseFillRenderer(in); + } - @Override - public InverseFillRenderer[] newArray(int size) { - return new InverseFillRenderer[size]; - } - }; + @Override + public InverseFillRenderer[] newArray(int size) { + return new InverseFillRenderer[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java index b5b7f350e..59651e0af 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java @@ -10,44 +10,40 @@ import android.graphics.Typeface; import android.os.Parcel; import android.view.animation.Interpolator; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import java.util.ArrayList; +import java.util.List; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; -import java.util.ArrayList; -import java.util.List; - /** * Renders multiple lines of {@link #text} in the specified {@link #color}. - *

- * Scales down the text size of long lines to fit inside the {@link Bounds} width. + * + *

Scales down the text size of long lines to fit inside the {@link Bounds} width. */ -public final class MultiLineTextRenderer extends InvalidateableRenderer implements ColorableRenderer { +public final class MultiLineTextRenderer extends InvalidateableRenderer + implements ColorableRenderer { - @NonNull - private String text = ""; + @NonNull private String text = ""; - @ColorInt - private int color; + @ColorInt private int color; - private final Paint paint = new Paint(); + private final Paint paint = new Paint(); private final Paint selectionPaint = new Paint(); private final float textScale; - private int selStart; - private int selEnd; + private int selStart; + private int selEnd; private boolean hasFocus; private List lines = emptyList(); private ValueAnimator cursorAnimator; - private float cursorAnimatedValue; + private float cursorAnimatedValue; private final Matrix recommendedEditorMatrix = new Matrix(); @@ -133,20 +129,20 @@ private void createLinesForText() { } private class Line { - private final Matrix accentMatrix = new Matrix(); - private final Matrix decentMatrix = new Matrix(); - private final Matrix projectionMatrix = new Matrix(); + private final Matrix accentMatrix = new Matrix(); + private final Matrix decentMatrix = new Matrix(); + private final Matrix projectionMatrix = new Matrix(); private final Matrix inverseProjectionMatrix = new Matrix(); - private final RectF selectionBounds = new RectF(); - private final RectF textBounds = new RectF(); + private final RectF selectionBounds = new RectF(); + private final RectF textBounds = new RectF(); private String text; - private int selStart; - private int selEnd; - private float ascentInBounds; - private float descentInBounds; - private float scale = 1f; - private float heightInBounds; + private int selStart; + private int selEnd; + private float ascentInBounds; + private float descentInBounds; + private float scale = 1f; + private float heightInBounds; Line(String text) { this.text = text; @@ -155,7 +151,7 @@ private class Line { private void recalculate() { RectF maxTextBounds = new RectF(); - Rect temp = new Rect(); + Rect temp = new Rect(); getTextBoundsWithoutTrim(text, 0, text.length(), temp); textBounds.set(temp); @@ -183,23 +179,24 @@ private void recalculate() { paint.getTextBounds("|", 0, 1, temp); int width = temp.width(); - temp.left -= width; + temp.left -= width; temp.right -= width; } - temp.left += startTemp.right; + temp.left += startTemp.right; temp.right += startTemp.right; selectionBounds.set(temp); } - projectionMatrix.setRectToRect(new RectF(maxTextBounds), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); + projectionMatrix.setRectToRect( + new RectF(maxTextBounds), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); removeTranslate(projectionMatrix); - float[] pts = { 0, paint.ascent(), 0, paint.descent() }; + float[] pts = {0, paint.ascent(), 0, paint.descent()}; projectionMatrix.mapPoints(pts); - ascentInBounds = pts[1]; + ascentInBounds = pts[1]; descentInBounds = pts[3]; - heightInBounds = descentInBounds - ascentInBounds; + heightInBounds = descentInBounds - ascentInBounds; projectionMatrix.preTranslate(-textBounds.centerX(), 0); projectionMatrix.invert(inverseProjectionMatrix); @@ -220,20 +217,20 @@ private void removeTranslate(Matrix matrix) { } private boolean showSelectionOrCursor() { - return (selStart >= 0 || selEnd >= 0) && - (selStart <= text.length() || selEnd <= text.length()); + return (selStart >= 0 || selEnd >= 0) + && (selStart <= text.length() || selEnd <= text.length()); } private boolean containsSelectionEnd() { - return (selEnd >= 0) && - (selEnd <= text.length()); + return (selEnd >= 0) && (selEnd <= text.length()); } private void getTextBoundsWithoutTrim(String text, int start, int end, Rect result) { - Rect extra = new Rect(); + Rect extra = new Rect(); Rect xBounds = new Rect(); - String cannotBeTrimmed = "x" + text.substring(Math.max(0, start), Math.min(text.length(), end)) + "x"; + String cannotBeTrimmed = + "x" + text.substring(Math.max(0, start), Math.min(text.length(), end)) + "x"; paint.getTextBounds(cannotBeTrimmed, 0, cannotBeTrimmed.length(), extra); paint.getTextBounds("x", 0, 1, xBounds); @@ -241,14 +238,14 @@ private void getTextBoundsWithoutTrim(String text, int start, int end, Rect resu result.right -= 2 * xBounds.width(); int temp = result.left; - result.left -= temp; + result.left -= temp; result.right -= temp; } public boolean contains(float x, float y) { float[] dst = new float[2]; - inverseProjectionMatrix.mapPoints(dst, new float[]{ x, y }); + inverseProjectionMatrix.mapPoints(dst, new float[] {x, y}); return textBounds.contains(dst[0], dst[1]); } @@ -293,7 +290,7 @@ public void render(@NonNull RendererContext rendererContext) { void setSelection(int selStart, int selEnd) { if (selStart != this.selStart || selEnd != this.selEnd) { this.selStart = selStart; - this.selEnd = selEnd; + this.selEnd = selEnd; recalculate(); } } @@ -333,7 +330,7 @@ public void setSelection(int selStart, int selEnd) { int length = line.text.length() + 1; // one for new line selStart -= length; - selEnd -= length; + selEnd -= length; } } @@ -349,10 +346,11 @@ public void setFocused(boolean hasFocus) { cursorAnimator.setInterpolator(pulseInterpolator()); cursorAnimator.setRepeatCount(ValueAnimator.INFINITE); cursorAnimator.setDuration(1000); - cursorAnimator.addUpdateListener(animation -> { - cursorAnimatedValue = (float) animation.getAnimatedValue(); - invalidate(); - }); + cursorAnimator.addUpdateListener( + animation -> { + cursorAnimatedValue = (float) animation.getAnimatedValue(); + invalidate(); + }); cursorAnimator.start(); } else { invalidate(); @@ -360,17 +358,18 @@ public void setFocused(boolean hasFocus) { } } - public static final Creator CREATOR = new Creator() { - @Override - public MultiLineTextRenderer createFromParcel(Parcel in) { - return new MultiLineTextRenderer(in.readString(), in.readInt()); - } + public static final Creator CREATOR = + new Creator() { + @Override + public MultiLineTextRenderer createFromParcel(Parcel in) { + return new MultiLineTextRenderer(in.readString(), in.readInt()); + } - @Override - public MultiLineTextRenderer[] newArray(int size) { - return new MultiLineTextRenderer[size]; - } - }; + @Override + public MultiLineTextRenderer[] newArray(int size) { + return new MultiLineTextRenderer[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java index 643e83882..4f14a7c04 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java @@ -5,11 +5,9 @@ import android.graphics.Paint; import android.graphics.RectF; import android.os.Parcel; - import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; @@ -17,8 +15,8 @@ /** * Renders an oval inside of the {@link Bounds}. - *

- * Hit tests outside of the bounds. + * + *

Hit tests outside of the bounds. */ public final class OvalGuideRenderer implements Renderer { @@ -32,16 +30,20 @@ public final class OvalGuideRenderer implements Renderer { public void render(@NonNull RendererContext rendererContext) { rendererContext.save(); - Canvas canvas = rendererContext.canvas; - Context context = rendererContext.context; - int stroke = context.getResources().getDimensionPixelSize(R.dimen.oval_guide_stroke_width); - float halfStroke = stroke / 2f; + Canvas canvas = rendererContext.canvas; + Context context = rendererContext.context; + int stroke = context.getResources().getDimensionPixelSize(R.dimen.oval_guide_stroke_width); + float halfStroke = stroke / 2f; this.paint.setStrokeWidth(stroke); paint.setColor(ContextCompat.getColor(context, ovalGuideColor)); rendererContext.mapRect(dst, Bounds.FULL_BOUNDS); - dst.set(dst.left + halfStroke, dst.top + halfStroke, dst.right - halfStroke, dst.bottom - halfStroke); + dst.set( + dst.left + halfStroke, + dst.top + halfStroke, + dst.right - halfStroke, + dst.bottom - halfStroke); rendererContext.canvasMatrix.setToIdentity(); canvas.drawOval(dst, paint); @@ -62,17 +64,18 @@ public boolean hitTest(float x, float y) { return !Bounds.contains(x, y); } - public static final Creator CREATOR = new Creator() { - @Override - public @NonNull OvalGuideRenderer createFromParcel(@NonNull Parcel in) { - return new OvalGuideRenderer(in.readInt()); - } - - @Override - public @NonNull OvalGuideRenderer[] newArray(int size) { - return new OvalGuideRenderer[size]; - } - }; + public static final Creator CREATOR = + new Creator() { + @Override + public @NonNull OvalGuideRenderer createFromParcel(@NonNull Parcel in) { + return new OvalGuideRenderer(in.readInt()); + } + + @Override + public @NonNull OvalGuideRenderer[] newArray(int size) { + return new OvalGuideRenderer[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java b/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java index bfd145104..6b176bcd8 100644 --- a/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java @@ -1,36 +1,32 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.jobmanager; import android.os.PowerManager; - -import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; - import java.io.Serializable; +import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; /** - * An abstract class representing a unit of work that can be scheduled with - * the JobManager. This should be extended to implement tasks. + * An abstract class representing a unit of work that can be scheduled with the JobManager. This + * should be extended to implement tasks. */ public abstract class Job implements Serializable { private final JobParameters parameters; - private transient int runIteration; + private transient int runIteration; private transient PowerManager.WakeLock wakeLock; public Job(JobParameters parameters) { @@ -90,20 +86,21 @@ public void onRetry() { } /** - * Called after a job has been added to the JobManager queue. If it's a persistent job, - * the state has been persisted to disk before this method is called. + * Called after a job has been added to the JobManager queue. If it's a persistent job, the state + * has been persisted to disk before this method is called. */ public abstract void onAdded(); /** * Called to actually execute the job. + * * @throws Exception */ protected abstract void onRun() throws Exception; /** - * If onRun() throws an exception, this method will be called to determine whether the - * job should be retried. + * If onRun() throws an exception, this method will be called to determine whether the job should + * be retried. * * @param exception The exception onRun() threw. * @return true if onRun() should be called again, false otherwise. @@ -115,7 +112,4 @@ public void onRetry() { * the job's configured retry count. */ public abstract void onCanceled(); - - - } diff --git a/src/main/java/org/thoughtcrime/securesms/jobmanager/JobConsumer.java b/src/main/java/org/thoughtcrime/securesms/jobmanager/JobConsumer.java index 9ff187f20..e390a1535 100644 --- a/src/main/java/org/thoughtcrime/securesms/jobmanager/JobConsumer.java +++ b/src/main/java/org/thoughtcrime/securesms/jobmanager/JobConsumer.java @@ -1,23 +1,21 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.jobmanager; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; class JobConsumer extends Thread { @@ -29,17 +27,17 @@ enum JobResult { DEFERRED } - private final JobQueue jobQueue; + private final JobQueue jobQueue; public JobConsumer(String name, JobQueue jobQueue) { super(name); - this.jobQueue = jobQueue; + this.jobQueue = jobQueue; } @Override public void run() { while (true) { - Job job = jobQueue.getNext(); + Job job = jobQueue.getNext(); JobResult result = runJob(job); if (result == JobResult.DEFERRED) { @@ -68,7 +66,7 @@ private JobResult runJob(Job job) { } catch (Exception exception) { Log.w(TAG, exception); if (exception instanceof RuntimeException) { - throw (RuntimeException)exception; + throw (RuntimeException) exception; } else if (!job.onShouldRetry(exception)) { return JobResult.FAILURE; } diff --git a/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java b/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java index 15e468363..2997594b1 100644 --- a/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -2,27 +2,25 @@ import android.content.Context; import android.os.PowerManager; - import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** - * A JobManager allows you to enqueue {@link org.thoughtcrime.securesms.jobmanager.Job} tasks - * that are executed once a Job's {@link org.thoughtcrime.securesms.jobmanager.requirements.Requirement}s + * A JobManager allows you to enqueue {@link org.thoughtcrime.securesms.jobmanager.Job} tasks that + * are executed once a Job's {@link org.thoughtcrime.securesms.jobmanager.requirements.Requirement}s * are met. */ public class JobManager { - private final JobQueue jobQueue = new JobQueue(); - private final Executor eventExecutor = Executors.newSingleThreadExecutor(); + private final JobQueue jobQueue = new JobQueue(); + private final Executor eventExecutor = Executors.newSingleThreadExecutor(); - private final Context context; + private final Context context; - public JobManager(Context context, int consumers) - { - this.context = context; + public JobManager(Context context, int consumers) { + this.context = context; - for (int i=0;iThis 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.jobmanager; -import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; - import java.io.Serializable; import java.util.LinkedList; import java.util.List; +import org.thoughtcrime.securesms.jobmanager.requirements.Requirement; -/** - * The set of parameters that describe a {@link org.thoughtcrime.securesms.jobmanager.Job}. - */ +/** The set of parameters that describe a {@link org.thoughtcrime.securesms.jobmanager.Job}. */ public class JobParameters implements Serializable { private static final long serialVersionUID = 4880456378402584584L; private final List requirements; - private final int retryCount; - private final long retryUntil; - private final String groupId; - private final boolean wakeLock; - private final long wakeLockTimeout; - - private JobParameters(List requirements, - String groupId, - int retryCount, long retryUntil, boolean wakeLock, - long wakeLockTimeout) - { - this.requirements = requirements; - this.groupId = groupId; - this.retryCount = retryCount; - this.retryUntil = retryUntil; - this.wakeLock = wakeLock; + private final int retryCount; + private final long retryUntil; + private final String groupId; + private final boolean wakeLock; + private final long wakeLockTimeout; + + private JobParameters( + List requirements, + String groupId, + int retryCount, + long retryUntil, + boolean wakeLock, + long wakeLockTimeout) { + this.requirements = requirements; + this.groupId = groupId; + this.retryCount = retryCount; + this.retryUntil = retryUntil; + this.wakeLock = wakeLock; this.wakeLockTimeout = wakeLockTimeout; } @@ -81,15 +78,15 @@ public long getWakeLockTimeout() { } public static class Builder { - private final List requirements = new LinkedList<>(); - private final int retryCount = 100; - private final long retryDuration = 0; - private String groupId = null; - private final boolean wakeLock = false; - private final long wakeLockTimeout = 0; + private final List requirements = new LinkedList<>(); + private final int retryCount = 100; + private final long retryDuration = 0; + private String groupId = null; + private final boolean wakeLock = false; + private final long wakeLockTimeout = 0; /** - * Specify a groupId the job should belong to. Jobs with the same groupId are guaranteed to be + * Specify a groupId the job should belong to. Jobs with the same groupId are guaranteed to be * executed serially. * * @param groupId The job's groupId. @@ -104,7 +101,13 @@ public Builder withGroupId(String groupId) { * @return the JobParameters instance that describes a Job. */ public JobParameters create() { - return new JobParameters(requirements, groupId, retryCount, System.currentTimeMillis() + retryDuration, wakeLock, wakeLockTimeout); + return new JobParameters( + requirements, + groupId, + retryCount, + System.currentTimeMillis() + retryDuration, + wakeLock, + wakeLockTimeout); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/jobmanager/JobQueue.java b/src/main/java/org/thoughtcrime/securesms/jobmanager/JobQueue.java index 1b01bd6e6..0a13abdba 100644 --- a/src/main/java/org/thoughtcrime/securesms/jobmanager/JobQueue.java +++ b/src/main/java/org/thoughtcrime/securesms/jobmanager/JobQueue.java @@ -1,23 +1,20 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.jobmanager; import androidx.annotation.NonNull; - import java.util.HashMap; import java.util.LinkedList; import java.util.ListIterator; @@ -26,7 +23,7 @@ class JobQueue { private final Map activeGroupIds = new HashMap<>(); - private final LinkedList jobQueue = new LinkedList<>(); + private final LinkedList jobQueue = new LinkedList<>(); synchronized void add(Job job) { jobQueue.add(job); @@ -91,7 +88,9 @@ private boolean isJobActive(@NonNull Job job) { private boolean isGroupIdAvailable(@NonNull Job requester) { String groupId = requester.getGroupId(); - return groupId == null || !activeGroupIds.containsKey(groupId) || activeGroupIds.get(groupId).equals(requester); + return groupId == null + || !activeGroupIds.containsKey(groupId) + || activeGroupIds.get(groupId).equals(requester); } private void setGroupIdUnavailable(@NonNull Job job) { diff --git a/src/main/java/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java b/src/main/java/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java index 7a09260b6..b96562ce4 100644 --- a/src/main/java/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java +++ b/src/main/java/org/thoughtcrime/securesms/jobmanager/requirements/Requirement.java @@ -1,30 +1,24 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.jobmanager.requirements; import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.jobmanager.Job; - import java.io.Serializable; +import org.thoughtcrime.securesms.jobmanager.Job; -/** - * A Requirement that must be satisfied before a Job can run. - */ +/** A Requirement that must be satisfied before a Job can run. */ public interface Requirement extends Serializable { /** * @return true if the requirement is satisfied, false otherwise. diff --git a/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.java b/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.java index d666d0cce..25b71e3a2 100644 --- a/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.java +++ b/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.java @@ -3,17 +3,15 @@ import android.content.Context; import android.util.AttributeSet; import android.widget.Button; - import androidx.appcompat.widget.AppCompatTextView; import androidx.constraintlayout.widget.ConstraintLayout; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ViewUtil; public class MessageRequestsBottomView extends ConstraintLayout { private AppCompatTextView question; - private Button accept; + private Button accept; private Button block; public MessageRequestsBottomView(Context context) { @@ -35,8 +33,8 @@ protected void onFinishInflate() { inflate(getContext(), R.layout.message_request_bottom_bar, this); question = findViewById(R.id.message_request_question); - accept = findViewById(R.id.message_request_accept); - block = findViewById(R.id.message_request_block); + accept = findViewById(R.id.message_request_accept); + block = findViewById(R.id.message_request_block); } public void setAcceptOnClickListener(OnClickListener acceptOnClickListener) { diff --git a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index aef294ee7..8c22adb0e 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -33,14 +33,20 @@ import android.util.Pair; import android.view.View; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; - +import chat.delta.rpc.RpcException; +import chat.delta.util.ListenableFuture; +import chat.delta.util.SettableFuture; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutionException; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.R; @@ -67,44 +73,31 @@ import org.thoughtcrime.securesms.util.guava.Optional; import org.thoughtcrime.securesms.util.views.Stub; -import java.io.File; -import java.io.IOException; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import chat.delta.rpc.RpcException; -import chat.delta.util.ListenableFuture; -import chat.delta.util.SettableFuture; - - public class AttachmentManager { - private final static String TAG = AttachmentManager.class.getSimpleName(); + private static final String TAG = AttachmentManager.class.getSimpleName(); - private final @NonNull Context context; - private final @NonNull Stub attachmentViewStub; - private final @NonNull - AttachmentListener attachmentListener; + private final @NonNull Context context; + private final @NonNull Stub attachmentViewStub; + private final @NonNull AttachmentListener attachmentListener; private RemovableEditableMediaView removableMediaView; - private ThumbnailView thumbnail; - private AudioView audioView; - private DocumentView documentView; - private WebxdcView webxdcView; - private VcardView vcardView; - //private SignalMapView mapView; - - private final @NonNull List garbage = new LinkedList<>(); - private @NonNull Optional slide = Optional.absent(); - private @Nullable Uri imageCaptureUri; - private @Nullable Uri videoCaptureUri; - private boolean attachmentPresent; - private boolean hidden; + private ThumbnailView thumbnail; + private AudioView audioView; + private DocumentView documentView; + private WebxdcView webxdcView; + private VcardView vcardView; + // private SignalMapView mapView; + + private final @NonNull List garbage = new LinkedList<>(); + private @NonNull Optional slide = Optional.absent(); + private @Nullable Uri imageCaptureUri; + private @Nullable Uri videoCaptureUri; + private boolean attachmentPresent; + private boolean hidden; public AttachmentManager(@NonNull Activity activity, @NonNull AttachmentListener listener) { - this.context = activity; + this.context = activity; this.attachmentListener = listener; this.attachmentViewStub = ViewUtil.findStubById(activity, R.id.attachment_editor_stub); } @@ -113,37 +106,37 @@ private void inflateStub() { if (!attachmentViewStub.resolved()) { View root = attachmentViewStub.get(); - this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail); - this.audioView = ViewUtil.findById(root, R.id.attachment_audio); - this.documentView = ViewUtil.findById(root, R.id.attachment_document); - this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc); - this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard); - //this.mapView = ViewUtil.findById(root, R.id.attachment_location); + this.thumbnail = ViewUtil.findById(root, R.id.attachment_thumbnail); + this.audioView = ViewUtil.findById(root, R.id.attachment_audio); + this.documentView = ViewUtil.findById(root, R.id.attachment_document); + this.webxdcView = ViewUtil.findById(root, R.id.attachment_webxdc); + this.vcardView = ViewUtil.findById(root, R.id.attachment_vcard); + // this.mapView = ViewUtil.findById(root, R.id.attachment_location); this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view); removableMediaView.addRemoveClickListener(new RemoveButtonListener()); removableMediaView.setEditClickListener(new EditButtonListener()); thumbnail.setOnClickListener(new ThumbnailClickListener()); } - } public void clear(@NonNull GlideRequests glideRequests, boolean animate) { if (attachmentViewStub.resolved()) { if (animate) { - ViewUtil.fadeOut(attachmentViewStub.get(), 200).addListener(new ListenableFuture.Listener() { - @Override - public void onSuccess(Boolean result) { - thumbnail.clear(glideRequests); - setAttachmentPresent(false); - attachmentListener.onAttachmentChanged(); - } - - @Override - public void onFailure(ExecutionException e) { - } - }); + ViewUtil.fadeOut(attachmentViewStub.get(), 200) + .addListener( + new ListenableFuture.Listener() { + @Override + public void onSuccess(Boolean result) { + thumbnail.clear(glideRequests); + setAttachmentPresent(false); + attachmentListener.onAttachmentChanged(); + } + + @Override + public void onFailure(ExecutionException e) {} + }); } else { thumbnail.clear(glideRequests); setAttachmentPresent(false); @@ -162,7 +155,7 @@ public void cleanup() { imageCaptureUri = null; videoCaptureUri = null; - slide = Optional.absent(); + slide = Optional.absent(); Iterator iterator = garbage.listIterator(); @@ -187,13 +180,15 @@ private void markGarbage(@Nullable Uri uri) { } private void setSlide(@NonNull Slide slide) { - if (getSlideUri() != null) cleanup(getSlideUri()); - if (imageCaptureUri != null && !imageCaptureUri.equals(slide.getUri())) cleanup(imageCaptureUri); - if (videoCaptureUri != null && !videoCaptureUri.equals(slide.getUri())) cleanup(videoCaptureUri); + if (getSlideUri() != null) cleanup(getSlideUri()); + if (imageCaptureUri != null && !imageCaptureUri.equals(slide.getUri())) + cleanup(imageCaptureUri); + if (videoCaptureUri != null && !videoCaptureUri.equals(slide.getUri())) + cleanup(videoCaptureUri); this.imageCaptureUri = null; this.videoCaptureUri = null; - this.slide = Optional.of(slide); + this.slide = Optional.of(slide); } /* @@ -227,15 +222,15 @@ public void onSuccess(@NonNull Bitmap result) { */ @SuppressLint("StaticFieldLeak") - public ListenableFuture setMedia(@NonNull final GlideRequests glideRequests, - @NonNull final Uri uri, - @Nullable final DcMsg msg, - @NonNull final MediaType mediaType, - final int width, - final int height, - final int chatId, - AudioPlaybackViewModel playbackViewModel) - { + public ListenableFuture setMedia( + @NonNull final GlideRequests glideRequests, + @NonNull final Uri uri, + @Nullable final DcMsg msg, + @NonNull final MediaType mediaType, + final int width, + final int height, + final int chatId, + AudioPlaybackViewModel playbackViewModel) { inflateStub(); final SettableFuture result = new SettableFuture<>(); @@ -252,14 +247,13 @@ protected void onPreExecute() { try { if (msg != null && msg.getType() == DcMsg.DC_MSG_WEBXDC) { return new DocumentSlide(context, msg); - } - else if (PartAuthority.isLocalUri(uri)) { + } else if (PartAuthority.isLocalUri(uri)) { return getManuallyCalculatedSlideInfo(uri, width, height, msg); } else { Slide result = getContentResolverSlideInfo(uri, width, height, chatId); if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height, msg); - else return result; + else return result; } } catch (IOException e) { Log.w(TAG, e); @@ -287,20 +281,23 @@ protected void onPostExecute(@Nullable final Slide slide) { audioView.setPlaybackViewModel(playbackViewModel); audioView.setAudio((AudioSlide) slide); removableMediaView.display(audioView, false); - removableMediaView.addRemoveClickListener(v -> { - playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri()); - }); + removableMediaView.addRemoveClickListener( + v -> { + playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri()); + }); result.set(true); } else if (slide.isVcard()) { - vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context)); - removableMediaView.display(vcardView, false); + vcardView.setVcard(glideRequests, (VcardSlide) slide, DcHelper.getRpc(context)); + removableMediaView.display(vcardView, false); } else if (slide.hasDocument()) { if (slide.isWebxdcDocument()) { - DcMsg instance = msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId); + DcMsg instance = + msg != null ? msg : DcHelper.getContext(context).getMsg(slide.dcMsgId); webxdcView.setWebxdc(instance, context.getString(R.string.webxdc_draft_hint)); - webxdcView.setWebxdcClickListener((v, s) -> { - WebxdcActivity.openWebxdcActivity(context, instance); - }); + webxdcView.setWebxdcClickListener( + (v, s) -> { + WebxdcActivity.openWebxdcActivity(context, instance); + }); removableMediaView.display(webxdcView, false); } else { documentView.setDocument((DocumentSlide) slide); @@ -309,7 +306,9 @@ protected void onPostExecute(@Nullable final Slide slide) { result.set(true); } else { Attachment attachment = slide.asAttachment(); - result.deferTo(thumbnail.setImageResource(glideRequests, slide, attachment.getWidth(), attachment.getHeight())); + result.deferTo( + thumbnail.setImageResource( + glideRequests, slide, attachment.getWidth(), attachment.getHeight())); removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE); } @@ -317,13 +316,15 @@ protected void onPostExecute(@Nullable final Slide slide) { } } - private @Nullable Slide getContentResolverSlideInfo(Uri uri, int width, int height, int chatId) { + private @Nullable Slide getContentResolverSlideInfo( + Uri uri, int width, int height, int chatId) { - long start = System.currentTimeMillis(); + long start = System.currentTimeMillis(); try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); + String fileName = + cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); String mimeType = context.getContentResolver().getType(uri); @@ -333,17 +334,25 @@ protected void onPostExecute(@Nullable final Slide slide) { height = dimens.second; } - Log.w(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, fileSize, width, height, chatId); + Log.w( + TAG, + "remote slide with size " + + fileSize + + " took " + + (System.currentTimeMillis() - start) + + "ms"); + return mediaType.createSlide( + context, uri, fileName, mimeType, fileSize, width, height, chatId); } } return null; } - private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri, int width, int height, @Nullable DcMsg msg) throws IOException { - long start = System.currentTimeMillis(); - Long mediaSize = null; + private @NonNull Slide getManuallyCalculatedSlideInfo( + Uri uri, int width, int height, @Nullable DcMsg msg) throws IOException { + long start = System.currentTimeMillis(); + Long mediaSize = null; String fileName = null; String mimeType = null; @@ -368,20 +377,27 @@ protected void onPostExecute(@Nullable final Slide slide) { if (width == 0 || height == 0) { Pair dimens = MediaUtil.getDimensions(context, mimeType, uri); - width = dimens.first; + width = dimens.first; height = dimens.second; } if (fileName == null) { try { fileName = new File(uri.getPath()).getName(); - } catch(Exception e) { + } catch (Exception e) { Log.w(TAG, "Could not get file name from uri: " + e); } } - Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height, chatId); + Log.w( + TAG, + "local slide with size " + + mediaSize + + " took " + + (System.currentTimeMillis() - start) + + "ms"); + return mediaType.createSlide( + context, uri, fileName, mimeType, mediaSize, width, height, chatId); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -394,7 +410,9 @@ public void onResume() { if (slide.isPresent()) { if (slide.get().isWebxdcDocument()) { if (webxdcView != null) { - webxdcView.setWebxdc(DcHelper.getContext(context).getMsg(slide.get().dcMsgId), context.getString(R.string.webxdc_draft_hint)); + webxdcView.setWebxdc( + DcHelper.getContext(context).getMsg(slide.get().dcMsgId), + context.getString(R.string.webxdc_draft_hint)); } } } @@ -413,7 +431,10 @@ public boolean isAttachmentPresent() { public static @Nullable String getFileName(Context context, Uri uri) { String result = null; if ("content".equals(uri.getScheme())) { - try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) { + try (Cursor cursor = + context + .getContentResolver() + .query(uri, new String[] {OpenableColumns.DISPLAY_NAME}, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { result = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); } @@ -436,24 +457,36 @@ public static void selectWebxdc(Activity activity, int requestCode) { public static void selectGallery(Activity activity, int requestCode) { // to enable camera roll, - // we're asking for "gallery permissions" also on newer systems that do not strictly require that. - // (asking directly after tapping "attachment" would be not-so-good as the user may want to attach sth. else + // we're asking for "gallery permissions" also on newer systems that do not strictly require + // that. + // (asking directly after tapping "attachment" would be not-so-good as the user may want to + // attach sth. else // and asking for permissions is better done on-point) Permissions.with(activity) - .request(Permissions.galleryPermissions()) - .ifNecessary() - .withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied)) - .onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode, null, true)) - .execute(); + .request(Permissions.galleryPermissions()) + .ifNecessary() + .withPermanentDenialDialog( + activity.getString(R.string.perm_explain_access_to_storage_denied)) + .onAllGranted( + () -> + selectMediaType( + activity, + "image/*", + new String[] {"image/*", "video/*"}, + requestCode, + null, + true)) + .execute(); } public static void selectImage(Activity activity, int requestCode) { Permissions.with(activity) - .request(Permissions.galleryPermissions()) - .ifNecessary() - .withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_storage_denied)) - .onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode)) - .execute(); + .request(Permissions.galleryPermissions()) + .ifNecessary() + .withPermanentDenialDialog( + activity.getString(R.string.perm_explain_access_to_storage_denied)) + .onAllGranted(() -> selectMediaType(activity, "image/*", null, requestCode)) + .execute(); } public static void selectLocation(Activity activity, int chatId) { @@ -465,25 +498,37 @@ public static void selectLocation(Activity activity, int chatId) { return; } - // see https://support.google.com/googleplay/android-developer/answer/9799150#zippy=%2Cstep-provide-prominent-in-app-disclosure + // see + // https://support.google.com/googleplay/android-developer/answer/9799150#zippy=%2Cstep-provide-prominent-in-app-disclosure // for rationale dialog requirements - Permissions.PermissionsBuilder permissionsBuilder = Permissions.with(activity) + Permissions.PermissionsBuilder permissionsBuilder = + Permissions.with(activity) .ifNecessary() - .withRationaleDialog("To share your live location with chat members, allow ArcaneChat to use your location data.\n\nTo make live location work gaplessly, location data is used even when the app is closed or not in use.", R.drawable.ic_location_on_white_24dp) - .withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_location_denied)) - .onAllGranted(() -> { - ShareLocationDialog.show(activity, durationInSeconds -> { - if (durationInSeconds == 1) { - dcLocationManager.shareLastLocation(chatId); - } else { - dcLocationManager.shareLocation(durationInSeconds, chatId); - } - }); - }); + .withRationaleDialog( + "To share your live location with chat members, allow ArcaneChat to use your location data.\n\nTo make live location work gaplessly, location data is used even when the app is closed or not in use.", + R.drawable.ic_location_on_white_24dp) + .withPermanentDenialDialog( + activity.getString(R.string.perm_explain_access_to_location_denied)) + .onAllGranted( + () -> { + ShareLocationDialog.show( + activity, + durationInSeconds -> { + if (durationInSeconds == 1) { + dcLocationManager.shareLastLocation(chatId); + } else { + dcLocationManager.shareLocation(durationInSeconds, chatId); + } + }); + }); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { - permissionsBuilder.request(Manifest.permission.ACCESS_BACKGROUND_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION); + permissionsBuilder.request( + Manifest.permission.ACCESS_BACKGROUND_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION); } else { - permissionsBuilder.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION); + permissionsBuilder.request( + Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION); } permissionsBuilder.execute(); } @@ -502,67 +547,87 @@ public static void selectLocation(Activity activity, int chatId) { public void capturePhoto(Activity activity, int requestCode) { Permissions.with(activity) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied)) - .onAllGranted(() -> { - try { - Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { - if (imageCaptureUri == null) { - imageCaptureUri = PersistentBlobProvider.getInstance().createForExternal(context, MediaUtil.IMAGE_JPEG); - } - Log.w(TAG, "imageCaptureUri path is " + imageCaptureUri.getPath()); - captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageCaptureUri); - activity.startActivityForResult(captureIntent, requestCode); - } - } catch (Exception e) { - Log.w(TAG, e); - } - }) - .execute(); + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withPermanentDenialDialog( + activity.getString(R.string.perm_explain_access_to_camera_denied)) + .onAllGranted( + () -> { + try { + Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { + if (imageCaptureUri == null) { + imageCaptureUri = + PersistentBlobProvider.getInstance() + .createForExternal(context, MediaUtil.IMAGE_JPEG); + } + Log.w(TAG, "imageCaptureUri path is " + imageCaptureUri.getPath()); + captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageCaptureUri); + activity.startActivityForResult(captureIntent, requestCode); + } + } catch (Exception e) { + Log.w(TAG, e); + } + }) + .execute(); } public void captureVideo(Activity activity, int requestCode) { Permissions.with(activity) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(activity.getString(R.string.perm_explain_access_to_camera_denied)) - .onAllGranted(() -> { - try { - Intent captureIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { - if (videoCaptureUri==null) { - videoCaptureUri = PersistentBlobProvider.getInstance().createForExternal(context, "video/mp4"); + .withPermanentDenialDialog( + activity.getString(R.string.perm_explain_access_to_camera_denied)) + .onAllGranted( + () -> { + try { + Intent captureIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { + if (videoCaptureUri == null) { + videoCaptureUri = + PersistentBlobProvider.getInstance() + .createForExternal(context, "video/mp4"); + } + Log.w(TAG, "videoCaptureUri path is " + videoCaptureUri.getPath()); + captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoCaptureUri); + captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + captureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + activity.startActivityForResult(captureIntent, requestCode); + } else { + new AlertDialog.Builder(activity) + .setCancelable(false) + .setMessage("Video recording not available") + .setPositiveButton(android.R.string.ok, null) + .show(); + } + } catch (Exception e) { + Log.w(TAG, e); } - Log.w(TAG, "videoCaptureUri path is " + videoCaptureUri.getPath()); - captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoCaptureUri); - captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - captureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - activity.startActivityForResult(captureIntent, requestCode); - } else { - new AlertDialog.Builder(activity) - .setCancelable(false) - .setMessage("Video recording not available") - .setPositiveButton(android.R.string.ok, null) - .show(); - } - } catch (Exception e) { - Log.w(TAG, e); - } - }) + }) .execute(); } - public static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) { + public static void selectMediaType( + Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode) { selectMediaType(activity, type, extraMimeType, requestCode, null, false); } - public static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode, @Nullable Uri initialUri) { + public static void selectMediaType( + Activity activity, + @NonNull String type, + @Nullable String[] extraMimeType, + int requestCode, + @Nullable Uri initialUri) { selectMediaType(activity, type, extraMimeType, requestCode, initialUri, false); } - public static void selectMediaType(Activity activity, @NonNull String type, @Nullable String[] extraMimeType, int requestCode, @Nullable Uri initialUri, boolean allowMultiple) { + public static void selectMediaType( + Activity activity, + @NonNull String type, + @Nullable String[] extraMimeType, + int requestCode, + @Nullable Uri initialUri, + boolean allowMultiple) { final Intent intent = new Intent(); intent.setType(type); @@ -640,53 +705,75 @@ public interface AttachmentListener { } public enum MediaType { - IMAGE, GIF, AUDIO, VIDEO, DOCUMENT; - - public @NonNull Slide createSlide(@NonNull Context context, - @NonNull Uri uri, - @Nullable String fileName, - @Nullable String mimeType, - long dataSize, - int width, - int height, - int chatId) - { + IMAGE, + GIF, + AUDIO, + VIDEO, + DOCUMENT; + + public @NonNull Slide createSlide( + @NonNull Context context, + @NonNull Uri uri, + @Nullable String fileName, + @Nullable String mimeType, + long dataSize, + int width, + int height, + int chatId) { if (mimeType == null) { mimeType = "application/octet-stream"; } switch (this) { - case IMAGE: return new ImageSlide(context, uri, fileName, dataSize, width, height); - case GIF: return new GifSlide(context, uri, fileName, dataSize, width, height); - case AUDIO: return new AudioSlide(context, uri, dataSize, false, fileName); - case VIDEO: return new VideoSlide(context, uri, fileName, dataSize); - case DOCUMENT: - // We have to special-case Webxdc slides: The user can interact with them as soon as a draft - // is set. Therefore we need to create a DcMsg already now. - if (fileName != null && fileName.endsWith(".xdc")) { - DcContext dcContext = DcHelper.getContext(context); - DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC); - Attachment attachment = new UriAttachment(uri, null, MediaUtil.WEBXDC, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, 0, 0, 0, fileName, null, false); - String path = attachment.getRealPath(context); - msg.setFileAndDeduplicate(path, fileName, MediaUtil.WEBXDC); - dcContext.setDraft(chatId, msg); - return new DocumentSlide(context, msg); - } + case IMAGE: + return new ImageSlide(context, uri, fileName, dataSize, width, height); + case GIF: + return new GifSlide(context, uri, fileName, dataSize, width, height); + case AUDIO: + return new AudioSlide(context, uri, dataSize, false, fileName); + case VIDEO: + return new VideoSlide(context, uri, fileName, dataSize); + case DOCUMENT: + // We have to special-case Webxdc slides: The user can interact with them as soon as a + // draft + // is set. Therefore we need to create a DcMsg already now. + if (fileName != null && fileName.endsWith(".xdc")) { + DcContext dcContext = DcHelper.getContext(context); + DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_WEBXDC); + Attachment attachment = + new UriAttachment( + uri, + null, + MediaUtil.WEBXDC, + AttachmentDatabase.TRANSFER_PROGRESS_STARTED, + 0, + 0, + 0, + fileName, + null, + false); + String path = attachment.getRealPath(context); + msg.setFileAndDeduplicate(path, fileName, MediaUtil.WEBXDC); + dcContext.setDraft(chatId, msg); + return new DocumentSlide(context, msg); + } - if (mimeType.equals(MediaUtil.VCARD) || (fileName != null && (fileName.endsWith(".vcf") || fileName.endsWith(".vcard")))) { - VcardSlide slide = new VcardSlide(context, uri, dataSize, fileName); - String path = slide.asAttachment().getRealPath(context); - try { - if (DcHelper.getRpc(context).parseVcard(path).size() == 1) { - return slide; + if (mimeType.equals(MediaUtil.VCARD) + || (fileName != null && (fileName.endsWith(".vcf") || fileName.endsWith(".vcard")))) { + VcardSlide slide = new VcardSlide(context, uri, dataSize, fileName); + String path = slide.asAttachment().getRealPath(context); + try { + if (DcHelper.getRpc(context).parseVcard(path).size() == 1) { + return slide; + } + } catch (RpcException e) { + Log.e(TAG, "Error in call to rpc.parseVcard()", e); } - } catch (RpcException e) { - Log.e(TAG, "Error in call to rpc.parseVcard()", e); } - } - return new DocumentSlide(context, uri, mimeType, dataSize, fileName); - default: throw new AssertionError("unrecognized enum"); + return new DocumentSlide(context, uri, mimeType, dataSize, fileName); + default: + throw new AssertionError("unrecognized enum"); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java index 48a9a904b..e619d8708 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -1,35 +1,29 @@ -/** +/** * 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 + *

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 . + * + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.mms; import android.content.Context; import android.net.Uri; - import androidx.annotation.Nullable; - import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.attachments.DcAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.StorageUtil; - public class AudioSlide extends Slide { public AudioSlide(Context context, DcMsg dcMsg) { @@ -38,18 +32,42 @@ public AudioSlide(Context context, DcMsg dcMsg) { } public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote, String fileName) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, null, StorageUtil.getCleanFileName(fileName), voiceNote)); + super( + context, + constructAttachmentFromUri( + context, + uri, + MediaUtil.AUDIO_UNSPECIFIED, + dataSize, + 0, + 0, + null, + StorageUtil.getCleanFileName(fileName), + voiceNote)); } - public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { - super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote)); + public AudioSlide( + Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { + super( + context, + new UriAttachment( + uri, + null, + contentType, + AttachmentDatabase.TRANSFER_PROGRESS_STARTED, + dataSize, + 0, + 0, + null, + null, + voiceNote)); } @Override public boolean equals(Object other) { - if (other == null) return false; - if (!(other instanceof AudioSlide)) return false; - return this.getDcMsgId() == ((AudioSlide)other).getDcMsgId(); + if (other == null) return false; + if (!(other instanceof AudioSlide)) return false; + return this.getDcMsgId() == ((AudioSlide) other).getDcMsgId(); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java b/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java index 1a1cacf4f..0d9ae5419 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java @@ -4,9 +4,7 @@ import android.content.Context; import android.net.Uri; import android.util.Log; - import com.bumptech.glide.load.data.StreamLocalUriFetcher; - import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -19,11 +17,12 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { DecryptableStreamLocalUriFetcher(Context context, Uri uri) { super(context.getContentResolver(), uri); - this.context = context; + this.context = context; } @Override - protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { + protected InputStream loadResource(Uri uri, ContentResolver contentResolver) + throws FileNotFoundException { try { return PartAuthority.getAttachmentStream(context, uri); } catch (IOException ioe) { diff --git a/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java b/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java index 4f136c8ca..4905f9fac 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java @@ -2,20 +2,16 @@ import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; - -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; - import java.io.InputStream; import java.security.MessageDigest; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; public class DecryptableStreamUriLoader implements ModelLoader { @@ -27,8 +23,10 @@ private DecryptableStreamUriLoader(Context context) { @Nullable @Override - public LoadData buildLoadData(@NonNull DecryptableUri decryptableUri, int width, int height, @NonNull Options options) { - return new LoadData<>(decryptableUri, new DecryptableStreamLocalUriFetcher(context, decryptableUri.uri)); + public LoadData buildLoadData( + @NonNull DecryptableUri decryptableUri, int width, int height, @NonNull Options options) { + return new LoadData<>( + decryptableUri, new DecryptableStreamLocalUriFetcher(context, decryptableUri.uri)); } @Override @@ -45,7 +43,8 @@ static class Factory implements ModelLoaderFactory } @Override - public @NonNull ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { + public @NonNull ModelLoader build( + @NonNull MultiModelLoaderFactory multiFactory) { return new DecryptableStreamUriLoader(context); } @@ -72,10 +71,9 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - DecryptableUri that = (DecryptableUri)o; + DecryptableUri that = (DecryptableUri) o; return uri.equals(that.uri); - } @Override @@ -84,4 +82,3 @@ public int hashCode() { } } } - diff --git a/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java b/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java index 6b495947f..39b6214dc 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/DocumentSlide.java @@ -1,14 +1,10 @@ package org.thoughtcrime.securesms.mms; - import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.attachments.DcAttachment; import org.thoughtcrime.securesms.util.StorageUtil; @@ -21,11 +17,24 @@ public DocumentSlide(Context context, DcMsg dcMsg) { dcMsgType = dcMsg.getType(); } - public DocumentSlide(@NonNull Context context, @NonNull Uri uri, - @NonNull String contentType, long size, - @Nullable String fileName) - { - super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, uri, StorageUtil.getCleanFileName(fileName), false)); + public DocumentSlide( + @NonNull Context context, + @NonNull Uri uri, + @NonNull String contentType, + long size, + @Nullable String fileName) { + super( + context, + constructAttachmentFromUri( + context, + uri, + contentType, + size, + 0, + 0, + uri, + StorageUtil.getCleanFileName(fileName), + false)); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java b/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java index 2811be6e3..1098f2b5b 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java @@ -2,9 +2,7 @@ import android.content.Context; import android.net.Uri; - import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.util.MediaUtil; public class GifSlide extends ImageSlide { @@ -14,7 +12,9 @@ public GifSlide(Context context, DcMsg dcMsg) { } public GifSlide(Context context, Uri uri, String fileName, long size, int width, int height) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, uri, fileName, false)); + super( + context, + constructAttachmentFromUri( + context, uri, MediaUtil.IMAGE_GIF, size, width, height, uri, fileName, false)); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java index 705822282..4d52594cf 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -1,6 +1,6 @@ /* * 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 @@ -10,7 +10,7 @@ * 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 . */ @@ -18,12 +18,9 @@ import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DcAttachment; import org.thoughtcrime.securesms.util.MediaUtil; @@ -40,7 +37,10 @@ public ImageSlide(@NonNull Context context, @NonNull Attachment attachment) { } public ImageSlide(Context context, Uri uri, String fileName, long size, int width, int height) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, uri, fileName, false)); + super( + context, + constructAttachmentFromUri( + context, uri, MediaUtil.IMAGE_JPEG, size, width, height, uri, fileName, false)); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java b/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java index 01ccd9357..fc4fdaf4b 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java @@ -4,6 +4,8 @@ public abstract class MediaConstraints { public abstract int getImageMaxWidth(Context context); + public abstract int getImageMaxHeight(Context context); + public abstract int getImageMaxSize(Context context); } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java index b0ed5a7f1..abd5e5bda 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -4,15 +4,12 @@ import android.content.Context; import android.content.UriMatcher; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; -import org.thoughtcrime.securesms.providers.SingleUseBlobProvider; - import java.io.IOException; import java.io.InputStream; +import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import org.thoughtcrime.securesms.providers.SingleUseBlobProvider; public class PartAuthority { @@ -23,20 +20,24 @@ public class PartAuthority { static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW); - uriMatcher.addURI(PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW); + uriMatcher.addURI( + PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW); + uriMatcher.addURI( + PersistentBlobProvider.AUTHORITY, PersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW); uriMatcher.addURI(SingleUseBlobProvider.AUTHORITY, SingleUseBlobProvider.PATH, SINGLE_USE_ROW); } public static InputStream getAttachmentStream(@NonNull Context context, @NonNull Uri uri) - throws IOException - { + throws IOException { int match = uriMatcher.match(uri); try { switch (match) { - case PERSISTENT_ROW: return PersistentBlobProvider.getInstance().getStream(context, ContentUris.parseId(uri)); - case SINGLE_USE_ROW: return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); - default: return context.getContentResolver().openInputStream(uri); + case PERSISTENT_ROW: + return PersistentBlobProvider.getInstance().getStream(context, ContentUris.parseId(uri)); + case SINGLE_USE_ROW: + return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); + default: + return context.getContentResolver().openInputStream(uri); } } catch (SecurityException se) { throw new IOException(se); @@ -47,11 +48,11 @@ public static InputStream getAttachmentStream(@NonNull Context context, @NonNull int match = uriMatcher.match(uri); switch (match) { - case PERSISTENT_ROW: - return PersistentBlobProvider.getFileName(context, uri); - case SINGLE_USE_ROW: - default: - return null; + case PERSISTENT_ROW: + return PersistentBlobProvider.getFileName(context, uri); + case SINGLE_USE_ROW: + default: + return null; } } @@ -67,7 +68,8 @@ public static InputStream getAttachmentStream(@NonNull Context context, @NonNull } } - public static @Nullable String getAttachmentContentType(@NonNull Context context, @NonNull Uri uri) { + public static @Nullable String getAttachmentContentType( + @NonNull Context context, @NonNull Uri uri) { int match = uriMatcher.match(uri); switch (match) { @@ -82,9 +84,9 @@ public static InputStream getAttachmentStream(@NonNull Context context, @NonNull public static boolean isLocalUri(final @NonNull Uri uri) { int match = uriMatcher.match(uri); switch (match) { - case PERSISTENT_ROW: - case SINGLE_USE_ROW: - return true; + case PERSISTENT_ROW: + case SINGLE_USE_ROW: + return true; } return false; } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java b/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java index 3b6655c61..3cace1c4d 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java @@ -1,25 +1,22 @@ package org.thoughtcrime.securesms.mms; - import androidx.annotation.Nullable; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcMsg; - -import org.thoughtcrime.securesms.attachments.Attachment; - import java.util.List; +import org.thoughtcrime.securesms.attachments.Attachment; public class QuoteModel { private final DcContact author; - private final String text; + private final String text; private final List attachments; private final DcMsg quotedMsg; - public QuoteModel(DcContact author, String text, @Nullable List attachments, DcMsg quotedMsg) { - this.author = author; - this.text = text; + public QuoteModel( + DcContact author, String text, @Nullable List attachments, DcMsg quotedMsg) { + this.author = author; + this.text = text; this.attachments = attachments; this.quotedMsg = quotedMsg; } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java index fabe6e5f0..8604029d3 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -2,23 +2,19 @@ import android.content.Context; import android.graphics.drawable.PictureDrawable; -import android.graphics.drawable.Drawable; import android.util.Log; - import androidx.annotation.NonNull; - import com.airbnb.lottie.LottieComposition; import com.airbnb.lottie.LottieDrawable; - import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.model.UnitModelLoader; import com.bumptech.glide.module.AppGlideModule; - import com.caverock.androidsvg.SVG; - +import java.io.File; +import java.io.InputStream; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.glide.ContactPhotoLoader; import org.thoughtcrime.securesms.glide.lottie.LottieDecoder; @@ -27,9 +23,6 @@ import org.thoughtcrime.securesms.glide.svg.SvgDrawableTranscoder; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; -import java.io.File; -import java.io.InputStream; - @GlideModule public class SignalGlideModule extends AppGlideModule { @@ -41,25 +34,34 @@ public boolean isManifestParsingEnabled() { @Override public void applyOptions(@NonNull Context context, GlideBuilder builder) { builder.setLogLevel(Log.ERROR); -// builder.setDiskCache(new NoopDiskCacheFactory()); + // builder.setDiskCache(new NoopDiskCacheFactory()); } @Override - public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - //AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); - //byte[] secret = attachmentSecret.getModernKey(); + public void registerComponents( + @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + // AttachmentSecret attachmentSecret = + // AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + // byte[] secret = attachmentSecret.getModernKey(); registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance()); - //registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); - //registry.prepend(File.class, Bitmap.class, new EncryptedBitmapCacheDecoder(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); - //registry.prepend(File.class, GifDrawable.class, new EncryptedGifCacheDecoder(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); - - //registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); - //registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); + // registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); + // registry.prepend(File.class, Bitmap.class, new EncryptedBitmapCacheDecoder(secret, new + // StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), + // context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), + // glide.getArrayPool()))); + // registry.prepend(File.class, GifDrawable.class, new EncryptedGifCacheDecoder(secret, new + // StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, + // registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), + // glide.getArrayPool()))); + + // registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); + // registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context)); - registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); - //registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); + registry.append( + DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); + // registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); registry .register(LottieComposition.class, LottieDrawable.class, new LottieDrawableTranscoder()) diff --git a/src/main/java/org/thoughtcrime/securesms/mms/Slide.java b/src/main/java/org/thoughtcrime/securesms/mms/Slide.java index 4b3c1ea9a..7256459ea 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/Slide.java @@ -1,27 +1,25 @@ -/** +/** * 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 + *

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 . + * + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.mms; import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.database.AttachmentDatabase; @@ -29,19 +27,15 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.guava.Optional; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - public abstract class Slide { - protected int dcMsgId; + protected int dcMsgId; protected final Attachment attachment; - protected final Context context; + protected final Context context; public Slide(@NonNull Context context, @NonNull Attachment attachment) { - this.context = context; + this.context = context; this.attachment = attachment; - } public int getDcMsgId() { @@ -78,14 +72,17 @@ public long getFileSize() { /* Return true if this slide has a thumbnail when being quoted, false otherwise */ public boolean hasQuoteThumbnail() { - return (hasImage() || hasVideo() || hasSticker() || isWebxdcDocument() || isVcard()) && getUri() != null; + return (hasImage() || hasVideo() || hasSticker() || isWebxdcDocument() || isVcard()) + && getUri() != null; } public boolean hasImage() { return false; } - public boolean hasSticker() { return false; } + public boolean hasSticker() { + return false; + } public boolean hasVideo() { return false; @@ -123,29 +120,31 @@ public boolean hasPlayOverlay() { return false; } - protected static Attachment constructAttachmentFromUri(@NonNull Context context, - @NonNull Uri uri, - @NonNull String defaultMime, - long size, - int width, - int height, - @Nullable Uri thumbnailUri, - @Nullable String fileName, - boolean voiceNote) - { + protected static Attachment constructAttachmentFromUri( + @NonNull Context context, + @NonNull Uri uri, + @NonNull String defaultMime, + long size, + int width, + int height, + @Nullable Uri thumbnailUri, + @Nullable String fileName, + boolean voiceNote) { try { - String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime); - String fastPreflightId = String.valueOf(SecureRandom.getInstance("SHA1PRNG").nextLong()); - return new UriAttachment(uri, - thumbnailUri, - resolvedType, - AttachmentDatabase.TRANSFER_PROGRESS_STARTED, - size, - width, - height, - fileName, - fastPreflightId, - voiceNote); + String resolvedType = + Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime); + String fastPreflightId = String.valueOf(SecureRandom.getInstance("SHA1PRNG").nextLong()); + return new UriAttachment( + uri, + thumbnailUri, + resolvedType, + AttachmentDatabase.TRANSFER_PROGRESS_STARTED, + size, + width, + height, + fileName, + fastPreflightId, + voiceNote); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } @@ -153,23 +152,29 @@ protected static Attachment constructAttachmentFromUri(@NonNull Context context @Override public boolean equals(Object other) { - if (other == null) return false; + if (other == null) return false; if (!(other instanceof Slide)) return false; - Slide that = (Slide)other; + Slide that = (Slide) other; - return Util.equals(this.getContentType(), that.getContentType()) && - this.hasAudio() == that.hasAudio() && - this.hasImage() == that.hasImage() && - this.hasVideo() == that.hasVideo() && - this.getTransferState() == that.getTransferState() && - Util.equals(this.getUri(), that.getUri()) && - Util.equals(this.getThumbnailUri(), that.getThumbnailUri()); + return Util.equals(this.getContentType(), that.getContentType()) + && this.hasAudio() == that.hasAudio() + && this.hasImage() == that.hasImage() + && this.hasVideo() == that.hasVideo() + && this.getTransferState() == that.getTransferState() + && Util.equals(this.getUri(), that.getUri()) + && Util.equals(this.getThumbnailUri(), that.getThumbnailUri()); } @Override public int hashCode() { - return Util.hashCode(getContentType(), hasAudio(), hasImage(), - hasVideo(), getUri(), getThumbnailUri(), getTransferState()); + return Util.hashCode( + getContentType(), + hasAudio(), + hasImage(), + hasVideo(), + getUri(), + getThumbnailUri(), + getTransferState()); } } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java index beee26df8..cf0301108 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -1,35 +1,30 @@ -/** +/** * 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 + *

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 . + * + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.mms; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.attachments.Attachment; - import java.util.LinkedList; import java.util.List; +import org.thoughtcrime.securesms.attachments.Attachment; public class SlideDeck { private final List slides = new LinkedList<>(); - public SlideDeck() { - } + public SlideDeck() {} public void clear() { slides.clear(); @@ -64,18 +59,19 @@ public boolean containsMediaSlide() { } public @Nullable DocumentSlide getDocumentSlide() { - for (Slide slide: slides) { + for (Slide slide : slides) { if (slide.hasDocument()) { - return (DocumentSlide)slide; + return (DocumentSlide) slide; } } return null; } - // Webxdc requires draft-ids to be used; this function returns the previously used draft-id, if any. + // Webxdc requires draft-ids to be used; this function returns the previously used draft-id, if + // any. public int getWebxdctDraftId() { - for (Slide slide: slides) { + for (Slide slide : slides) { if (slide.isWebxdcDocument()) { return slide.dcMsgId; } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java b/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java index 356bc9f42..025cbebef 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java @@ -1,14 +1,10 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.attachments.DcAttachment; - public class StickerSlide extends Slide { public StickerSlide(@NonNull Context context, @NonNull DcMsg dcMsg) { diff --git a/src/main/java/org/thoughtcrime/securesms/mms/VcardSlide.java b/src/main/java/org/thoughtcrime/securesms/mms/VcardSlide.java index 01f1d7625..a2201a77e 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/VcardSlide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/VcardSlide.java @@ -1,12 +1,9 @@ package org.thoughtcrime.securesms.mms; - import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcMsg; public class VcardSlide extends DocumentSlide { @@ -15,7 +12,8 @@ public VcardSlide(Context context, DcMsg dcMsg) { super(context, dcMsg); } - public VcardSlide(@NonNull Context context, @NonNull Uri uri, long size, @Nullable String fileName) { + public VcardSlide( + @NonNull Context context, @NonNull Uri uri, long size, @Nullable String fileName) { super(context, uri, "text/vcard", size, fileName); } diff --git a/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java b/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java index c8d02b2b3..55b5690ba 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/VideoSlide.java @@ -1,41 +1,47 @@ -/** +/** * 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 + *

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 . + * + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.mms; import android.content.Context; import android.net.Uri; - import com.b44t.messenger.DcMsg; - +import java.io.File; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DcAttachment; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.MediaUtil; -import java.io.File; - public class VideoSlide extends Slide { - private static Attachment constructVideoAttachment(Context context, Uri uri, String fileName, long dataSize) - { - Uri thumbnailUri = Uri.fromFile(new File(DcHelper.getBlobdirFile(DcHelper.getContext(context), "temp-preview.jpg"))); + private static Attachment constructVideoAttachment( + Context context, Uri uri, String fileName, long dataSize) { + Uri thumbnailUri = + Uri.fromFile( + new File(DcHelper.getBlobdirFile(DcHelper.getContext(context), "temp-preview.jpg"))); MediaUtil.ThumbnailSize retWh = new MediaUtil.ThumbnailSize(0, 0); MediaUtil.createVideoThumbnailIfNeeded(context, uri, thumbnailUri, retWh); - return constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, retWh.width, retWh.height, thumbnailUri, fileName, false); + return constructAttachmentFromUri( + context, + uri, + MediaUtil.VIDEO_UNSPECIFIED, + dataSize, + retWh.width, + retWh.height, + thumbnailUri, + fileName, + false); } public VideoSlide(Context context, Uri uri, String fileName, long dataSize) { diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java b/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java index 373d7bc09..549e014b9 100644 --- a/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java +++ b/src/main/java/org/thoughtcrime/securesms/notifications/InChatSounds.java @@ -4,55 +4,55 @@ import android.media.AudioAttributes; import android.media.SoundPool; import android.util.Log; - import org.thoughtcrime.securesms.R; public class InChatSounds { - private static final String TAG = InChatSounds.class.getSimpleName(); - private static volatile InChatSounds instance; + private static final String TAG = InChatSounds.class.getSimpleName(); + private static volatile InChatSounds instance; - private SoundPool soundPool = null; - private int soundIn = 0; - private int soundOut = 0; + private SoundPool soundPool = null; + private int soundIn = 0; + private int soundOut = 0; - static public InChatSounds getInstance(Context context) { + public static InChatSounds getInstance(Context context) { + if (instance == null) { + synchronized (InChatSounds.class) { if (instance == null) { - synchronized (InChatSounds.class) { - if (instance == null) { - instance = new InChatSounds(context); - } - } + instance = new InChatSounds(context); } - return instance; + } } + return instance; + } - private InChatSounds(Context context) { - try { - AudioAttributes audioAttrs = new AudioAttributes.Builder() + private InChatSounds(Context context) { + try { + AudioAttributes audioAttrs = + new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); - soundPool = new SoundPool.Builder().setMaxStreams(3).setAudioAttributes(audioAttrs).build(); - soundIn = soundPool.load(context, R.raw.sound_in, 1); - soundOut = soundPool.load(context, R.raw.sound_out, 1); - } catch(Exception e) { - Log.e(TAG, "cannot initialize sounds", e); - } + soundPool = new SoundPool.Builder().setMaxStreams(3).setAudioAttributes(audioAttrs).build(); + soundIn = soundPool.load(context, R.raw.sound_in, 1); + soundOut = soundPool.load(context, R.raw.sound_out, 1); + } catch (Exception e) { + Log.e(TAG, "cannot initialize sounds", e); } + } - public void playSendSound() { - try { - soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f); - } catch(Exception e) { - Log.e(TAG, "cannot play send sound", e); - } + public void playSendSound() { + try { + soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f); + } catch (Exception e) { + Log.e(TAG, "cannot play send sound", e); } + } - public void playIncomingSound() { - try { - soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f); - } catch(Exception e) { - Log.e(TAG, "cannot play incoming sound", e); - } + public void playIncomingSound() { + try { + soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f); + } catch (Exception e) { + Log.e(TAG, "cannot play incoming sound", e); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 3e569c744..0c4b2e75a 100644 --- a/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -7,18 +7,17 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; - import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.Util; public class MarkReadReceiver extends BroadcastReceiver { - public static final String MARK_NOTICED_ACTION = "org.thoughtcrime.securesms.notifications.MARK_NOTICED"; - public static final String CANCEL_ACTION = "org.thoughtcrime.securesms.notifications.CANCEL"; - public static final String ACCOUNT_ID_EXTRA = "account_id"; - public static final String CHAT_ID_EXTRA = "chat_id"; - public static final String MSG_ID_EXTRA = "msg_id"; + public static final String MARK_NOTICED_ACTION = + "org.thoughtcrime.securesms.notifications.MARK_NOTICED"; + public static final String CANCEL_ACTION = "org.thoughtcrime.securesms.notifications.CANCEL"; + public static final String ACCOUNT_ID_EXTRA = "account_id"; + public static final String CHAT_ID_EXTRA = "chat_id"; + public static final String MSG_ID_EXTRA = "msg_id"; @Override public void onReceive(final Context context, Intent intent) { @@ -34,13 +33,14 @@ public void onReceive(final Context context, Intent intent) { return; } - Util.runOnAnyBackgroundThread(() -> { - DcHelper.getNotificationCenter(context).removeNotifications(accountId, chatId); - if (markNoticed) { - DcContext dcContext = DcHelper.getAccounts(context).getAccount(accountId); - dcContext.marknoticedChat(chatId); - dcContext.markseenMsgs(new int[]{msgId}); - } - }); + Util.runOnAnyBackgroundThread( + () -> { + DcHelper.getNotificationCenter(context).removeNotifications(accountId, chatId); + if (markNoticed) { + DcContext dcContext = DcHelper.getAccounts(context).getAccount(accountId); + dcContext.marknoticedChat(chatId); + dcContext.markseenMsgs(new int[] {msgId}); + } + }); } } diff --git a/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 21c8de8b0..d036dc092 100644 --- a/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -24,23 +24,17 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; - import androidx.core.app.RemoteInput; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.Util; - -/** - * Get the response text from the Wearable Device and sends an message as a reply - */ +/** Get the response text from the Wearable Device and sends an message as a reply */ public class RemoteReplyReceiver extends BroadcastReceiver { - public static final String TAG = RemoteReplyReceiver.class.getSimpleName(); - public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.WEAR_REPLY"; + public static final String TAG = RemoteReplyReceiver.class.getSimpleName(); + public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.WEAR_REPLY"; public static final String ACCOUNT_ID_EXTRA = "account_id"; public static final String CHAT_ID_EXTRA = "chat_id"; public static final String MSG_ID_EXTRA = "msg_id"; @@ -60,20 +54,21 @@ public void onReceive(final Context context, Intent intent) { final CharSequence responseText = remoteInput.getCharSequence(EXTRA_REMOTE_REPLY); if (responseText != null) { - Util.runOnAnyBackgroundThread(() -> { - DcContext dcContext = DcHelper.getAccounts(context).getAccount(accountId); - dcContext.marknoticedChat(chatId); - dcContext.markseenMsgs(new int[]{msgId}); - if (dcContext.getChat(chatId).isContactRequest()) { - dcContext.acceptChat(chatId); - } + Util.runOnAnyBackgroundThread( + () -> { + DcContext dcContext = DcHelper.getAccounts(context).getAccount(accountId); + dcContext.marknoticedChat(chatId); + dcContext.markseenMsgs(new int[] {msgId}); + if (dcContext.getChat(chatId).isContactRequest()) { + dcContext.acceptChat(chatId); + } - DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); - msg.setText(responseText.toString()); - dcContext.sendMsg(chatId, msg); + DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT); + msg.setText(responseText.toString()); + dcContext.sendMsg(chatId, msg); - DcHelper.getNotificationCenter(context).removeNotifications(accountId, chatId); - }); + DcHelper.getNotificationCenter(context).removeNotifications(accountId, chatId); + }); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index 42becce2b..f647bad13 100644 --- a/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.permissions; - import android.Manifest; import android.app.Activity; import android.content.Context; @@ -13,36 +12,36 @@ import android.view.Display; import android.view.ViewGroup; import android.view.WindowManager; - import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.LRUCache; -import org.thoughtcrime.securesms.util.ServiceUtil; - import java.lang.ref.WeakReference; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.LRUCache; +import org.thoughtcrime.securesms.util.ServiceUtil; public class Permissions { public static String[] galleryPermissions() { // on modern androids, the gallery picker works without permissions, // however, the "camera roll" still requires permissions. - // to get that dialog at a UX-wise good moment, we still always ask for permission when opening gallery. + // to get that dialog at a UX-wise good moment, we still always ask for permission when opening + // gallery. // just-always-asking this also avoids the mess with handling various paths for various apis. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return new String[]{Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO}; + return new String[] { + Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO + }; } else { - return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; + return new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}; } } @@ -68,8 +67,8 @@ public static class PermissionsBuilder { private Runnable anyPermanentlyDeniedListener; private Runnable anyResultListener; - private @DrawableRes int[] rationalDialogHeader; - private String rationaleDialogMessage; + private @DrawableRes int[] rationalDialogHeader; + private String rationaleDialogMessage; private boolean ifNecesary; @@ -93,7 +92,7 @@ public PermissionsBuilder ifNecessary() { public PermissionsBuilder ifNecessary(boolean condition) { this.ifNecesary = true; - this.condition = condition; + this.condition = condition; return this; } @@ -111,14 +110,16 @@ public PermissionsBuilder alwaysGrantOnSdk33() { return this; } - public PermissionsBuilder withRationaleDialog(@NonNull String message, @NonNull @DrawableRes int... headers) { - this.rationalDialogHeader = headers; + public PermissionsBuilder withRationaleDialog( + @NonNull String message, @NonNull @DrawableRes int... headers) { + this.rationalDialogHeader = headers; this.rationaleDialogMessage = message; return this; } public PermissionsBuilder withPermanentDenialDialog(@NonNull String message) { - return onAnyPermanentlyDenied(new SettingsDialogListener(permissionObject.getContext(), message)); + return onAnyPermanentlyDenied( + new SettingsDialogListener(permissionObject.getContext(), message)); } public PermissionsBuilder onAllGranted(Runnable allGrantedListener) { @@ -148,7 +149,12 @@ public void execute() { return; } - PermissionsRequest request = new PermissionsRequest(allGrantedListener, anyDeniedListener, anyPermanentlyDeniedListener, anyResultListener); + PermissionsRequest request = + new PermissionsRequest( + allGrantedListener, + anyDeniedListener, + anyPermanentlyDeniedListener, + anyResultListener); if (ifNecesary && (permissionObject.hasAll(requestedPermissions) || !condition)) { executePreGrantedPermissionsRequest(request); @@ -163,17 +169,22 @@ private void executePreGrantedPermissionsRequest(PermissionsRequest request) { int[] grantResults = new int[requestedPermissions.length]; Arrays.fill(grantResults, PackageManager.PERMISSION_GRANTED); - request.onResult(requestedPermissions, grantResults, new boolean[requestedPermissions.length]); + request.onResult( + requestedPermissions, grantResults, new boolean[requestedPermissions.length]); } @SuppressWarnings("ConstantConditions") private void executePermissionsRequestWithRationale(PermissionsRequest request) { - RationaleDialog.createFor(permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader) - .setPositiveButton(R.string.perm_continue, (dialog, which) -> executePermissionsRequest(request)) - .setNegativeButton(R.string.not_now, (dialog, which) -> executeNoPermissionsRequest(request)) - .show() - .getWindow() - .setLayout((int)(permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT); + RationaleDialog.createFor( + permissionObject.getContext(), rationaleDialogMessage, rationalDialogHeader) + .setPositiveButton( + R.string.perm_continue, (dialog, which) -> executePermissionsRequest(request)) + .setNegativeButton( + R.string.not_now, (dialog, which) -> executeNoPermissionsRequest(request)) + .show() + .getWindow() + .setLayout( + (int) (permissionObject.getWindowWidth() * .75), ViewGroup.LayoutParams.WRAP_CONTENT); } private void executePermissionsRequest(PermissionsRequest request) { @@ -184,7 +195,8 @@ private void executePermissionsRequest(PermissionsRequest request) { } for (String permission : requestedPermissions) { - request.addMapping(permission, permissionObject.shouldShouldPermissionRationale(permission)); + request.addMapping( + permission, permissionObject.shouldShouldPermissionRationale(permission)); } permissionObject.requestPermissions(requestCode, requestedPermissions); @@ -195,29 +207,32 @@ private void executeNoPermissionsRequest(PermissionsRequest request) { request.addMapping(permission, true); } - String[] permissions = filterNotGranted(permissionObject.getContext(), requestedPermissions); - int[] grantResults = new int[permissions.length]; + String[] permissions = filterNotGranted(permissionObject.getContext(), requestedPermissions); + int[] grantResults = new int[permissions.length]; Arrays.fill(grantResults, PackageManager.PERMISSION_DENIED); - boolean[] showDialog = new boolean[permissions.length]; + boolean[] showDialog = new boolean[permissions.length]; Arrays.fill(showDialog, true); request.onResult(permissions, grantResults, showDialog); } - } - private static void requestPermissions(@NonNull Activity activity, int requestCode, String... permissions) { - ActivityCompat.requestPermissions(activity, filterNotGranted(activity, permissions), requestCode); + private static void requestPermissions( + @NonNull Activity activity, int requestCode, String... permissions) { + ActivityCompat.requestPermissions( + activity, filterNotGranted(activity, permissions), requestCode); } - private static void requestPermissions(@NonNull Fragment fragment, int requestCode, String... permissions) { + private static void requestPermissions( + @NonNull Fragment fragment, int requestCode, String... permissions) { fragment.requestPermissions(filterNotGranted(fragment.getContext(), permissions), requestCode); } private static String[] filterNotGranted(@NonNull Context context, String... permissions) { List notGranted = new ArrayList<>(); for (String permission : permissions) { - if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission(context, permission) + != PackageManager.PERMISSION_GRANTED) { notGranted.add(permission); } } @@ -227,28 +242,44 @@ private static String[] filterNotGranted(@NonNull Context context, String... per public static boolean hasAny(@NonNull Context context, String... permissions) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true; for (String permission : permissions) { - if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) return true; + if (ContextCompat.checkSelfPermission(context, permission) + == PackageManager.PERMISSION_GRANTED) return true; } - return false; + return false; } public static boolean hasAll(@NonNull Context context, String... permissions) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true; for (String permission : permissions) { - if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) return false; + if (ContextCompat.checkSelfPermission(context, permission) + != PackageManager.PERMISSION_GRANTED) return false; } - return true; + return true; } - public static void onRequestPermissionsResult(Fragment fragment, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - onRequestPermissionsResult(new FragmentPermissionObject(fragment), requestCode, permissions, grantResults); + public static void onRequestPermissionsResult( + Fragment fragment, + int requestCode, + @NonNull String[] permissions, + @NonNull int[] grantResults) { + onRequestPermissionsResult( + new FragmentPermissionObject(fragment), requestCode, permissions, grantResults); } - public static void onRequestPermissionsResult(Activity activity, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - onRequestPermissionsResult(new ActivityPermissionObject(activity), requestCode, permissions, grantResults); + public static void onRequestPermissionsResult( + Activity activity, + int requestCode, + @NonNull String[] permissions, + @NonNull int[] grantResults) { + onRequestPermissionsResult( + new ActivityPermissionObject(activity), requestCode, permissions, grantResults); } - private static void onRequestPermissionsResult(@NonNull PermissionObject context, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + private static void onRequestPermissionsResult( + @NonNull PermissionObject context, + int requestCode, + @NonNull String[] permissions, + @NonNull int[] grantResults) { PermissionsRequest resultListener; synchronized (OUTSTANDING) { @@ -259,7 +290,7 @@ private static void onRequestPermissionsResult(@NonNull PermissionObject context boolean[] shouldShowRationaleDialog = new boolean[permissions.length]; - for (int i=0;i context; - private final String message; + private final String message; SettingsDialogListener(Context context, String message) { this.message = message; @@ -370,7 +404,9 @@ public void run() { new AlertDialog.Builder(context) .setTitle(R.string.perm_required_title) .setMessage(message) - .setPositiveButton(R.string.perm_continue, (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context))) + .setPositiveButton( + R.string.perm_continue, + (dialog, which) -> context.startActivity(getApplicationSettingsIntent(context))) .setNegativeButton(android.R.string.cancel, null) .show(); } diff --git a/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java b/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java index 1bc24d9aa..709f0ecf9 100644 --- a/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java +++ b/src/main/java/org/thoughtcrime/securesms/permissions/PermissionsRequest.java @@ -1,10 +1,7 @@ package org.thoughtcrime.securesms.permissions; - import android.content.pm.PackageManager; - import androidx.annotation.Nullable; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -20,21 +17,21 @@ class PermissionsRequest { private final @Nullable Runnable anyPermanentlyDeniedListener; private final @Nullable Runnable anyResultListener; - PermissionsRequest(@Nullable Runnable allGrantedListener, - @Nullable Runnable anyDeniedListener, - @Nullable Runnable anyPermanentlyDeniedListener, - @Nullable Runnable anyResultListener) - { - this.allGrantedListener = allGrantedListener; + PermissionsRequest( + @Nullable Runnable allGrantedListener, + @Nullable Runnable anyDeniedListener, + @Nullable Runnable anyPermanentlyDeniedListener, + @Nullable Runnable anyResultListener) { + this.allGrantedListener = allGrantedListener; - this.anyDeniedListener = anyDeniedListener; - this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener; - this.anyResultListener = anyResultListener; + this.anyDeniedListener = anyDeniedListener; + this.anyPermanentlyDeniedListener = anyPermanentlyDeniedListener; + this.anyResultListener = anyResultListener; } void onResult(String[] permissions, int[] grantResults, boolean[] shouldShowRationaleDialog) { - List granted = new ArrayList<>(permissions.length); - List denied = new ArrayList<>(permissions.length); + List granted = new ArrayList<>(permissions.length); + List denied = new ArrayList<>(permissions.length); List permanentlyDenied = new ArrayList<>(permissions.length); for (int i = 0; i < permissions.length; i++) { @@ -53,16 +50,18 @@ void onResult(String[] permissions, int[] grantResults, boolean[] shouldShowRati } } - if (allGrantedListener != null && !granted.isEmpty() && (denied.isEmpty() && permanentlyDenied.isEmpty())) { + if (allGrantedListener != null + && !granted.isEmpty() + && (denied.isEmpty() && permanentlyDenied.isEmpty())) { allGrantedListener.run(); } if (!denied.isEmpty()) { - if (anyDeniedListener != null) anyDeniedListener.run(); + if (anyDeniedListener != null) anyDeniedListener.run(); } if (!permanentlyDenied.isEmpty()) { - if (anyPermanentlyDeniedListener != null) anyPermanentlyDeniedListener.run(); + if (anyPermanentlyDeniedListener != null) anyPermanentlyDeniedListener.run(); } if (anyResultListener != null) { diff --git a/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java b/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java index 287b0aa11..f3ec33f9a 100644 --- a/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java +++ b/src/main/java/org/thoughtcrime/securesms/permissions/RationaleDialog.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.permissions; - import android.content.Context; import android.graphics.Color; import android.util.TypedValue; @@ -10,25 +9,25 @@ import android.widget.ImageView; import android.widget.LinearLayout.LayoutParams; import android.widget.TextView; - import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ViewUtil; public class RationaleDialog { - public static AlertDialog.Builder createFor(@NonNull Context context, @NonNull String message, @DrawableRes int... drawables) { - View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null); + public static AlertDialog.Builder createFor( + @NonNull Context context, @NonNull String message, @DrawableRes int... drawables) { + View view = LayoutInflater.from(context).inflate(R.layout.permissions_rationale_dialog, null); ViewGroup header = view.findViewById(R.id.header_container); - TextView text = view.findViewById(R.id.message); + TextView text = view.findViewById(R.id.message); - for (int i=0;i { - if (result.getResultCode() == RESULT_OK) { - openRelayListActivity(); - } - } - ); + screenLockLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + openRelayListActivity(); + } + }); showEmails = (ListPreference) this.findPreference("pref_show_emails"); if (showEmails != null) { - showEmails.setOnPreferenceChangeListener((preference, newValue) -> { - updateListSummary(preference, newValue); - dcContext.setConfigInt(CONFIG_SHOW_EMAILS, Util.objectToInt(newValue)); - return true; - }); + showEmails.setOnPreferenceChangeListener( + (preference, newValue) -> { + updateListSummary(preference, newValue); + dcContext.setConfigInt(CONFIG_SHOW_EMAILS, Util.objectToInt(newValue)); + return true; + }); } multiDeviceCheckbox = (CheckBoxPreference) this.findPreference("pref_bcc_self"); if (multiDeviceCheckbox != null) { - multiDeviceCheckbox.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (Boolean) newValue; - if (enabled) { - dcContext.setConfigInt(CONFIG_BCC_SELF, 1); - return true; - } else { - new AlertDialog.Builder(requireContext()) + multiDeviceCheckbox.setOnPreferenceChangeListener( + (preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + dcContext.setConfigInt(CONFIG_BCC_SELF, 1); + return true; + } else { + new AlertDialog.Builder(requireContext()) .setMessage(R.string.pref_multidevice_change_warn) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> { - dcContext.setConfigInt(CONFIG_BCC_SELF, 0); - ((CheckBoxPreference)preference).setChecked(false); - }) + .setPositiveButton( + R.string.ok, + (dialogInterface, i) -> { + dcContext.setConfigInt(CONFIG_BCC_SELF, 0); + ((CheckBoxPreference) preference).setChecked(false); + }) .setNegativeButton(R.string.cancel, null) .show(); - return false; - } - }); + return false; + } + }); } mvboxMoveCheckbox = (CheckBoxPreference) this.findPreference("pref_mvbox_move"); if (mvboxMoveCheckbox != null) { - mvboxMoveCheckbox.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (Boolean) newValue; - dcContext.setConfigInt(CONFIG_MVBOX_MOVE, enabled? 1 : 0); - return true; - }); + mvboxMoveCheckbox.setOnPreferenceChangeListener( + (preference, newValue) -> { + boolean enabled = (Boolean) newValue; + dcContext.setConfigInt(CONFIG_MVBOX_MOVE, enabled ? 1 : 0); + return true; + }); } onlyFetchMvboxCheckbox = this.findPreference("pref_only_fetch_mvbox"); if (onlyFetchMvboxCheckbox != null) { - onlyFetchMvboxCheckbox.setOnPreferenceChangeListener(((preference, newValue) -> { - final boolean enabled = (Boolean) newValue; - if (enabled) { - new AlertDialog.Builder(requireContext()) + onlyFetchMvboxCheckbox.setOnPreferenceChangeListener( + ((preference, newValue) -> { + final boolean enabled = (Boolean) newValue; + if (enabled) { + new AlertDialog.Builder(requireContext()) .setMessage(R.string.pref_imap_folder_warn_disable_defaults) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> { - dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 1); - ((CheckBoxPreference)preference).setChecked(true); - }) + .setPositiveButton( + R.string.ok, + (dialogInterface, i) -> { + dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 1); + ((CheckBoxPreference) preference).setChecked(true); + }) .setNegativeButton(R.string.cancel, null) .show(); - return false; - } else { - dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 0); - return true; - } - })); + return false; + } else { + dcContext.setConfigInt(CONFIG_ONLY_FETCH_MVBOX, 0); + return true; + } + })); } Preference submitDebugLog = this.findPreference("pref_view_log"); @@ -145,37 +147,47 @@ public void onCreate(Bundle paramBundle) { selfReportingCheckbox = this.findPreference("pref_stats_sending"); if (selfReportingCheckbox != null) { - selfReportingCheckbox.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (Boolean) newValue; - if (enabled) { - StatsSending.showStatsConfirmationDialog(requireActivity(), () -> { - ((CheckBoxPreference)preference).setChecked(true); + selfReportingCheckbox.setOnPreferenceChangeListener( + (preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + StatsSending.showStatsConfirmationDialog( + requireActivity(), + () -> { + ((CheckBoxPreference) preference).setChecked(true); + }); + return false; + } else { + dcContext.setConfigInt(CONFIG_STATS_SENDING, 0); + return true; + } }); - return false; - } else { - dcContext.setConfigInt(CONFIG_STATS_SENDING, 0); - return true; - } - }); } Preference proxySettings = this.findPreference("proxy_settings_button"); if (proxySettings != null) { - proxySettings.setOnPreferenceClickListener((preference) -> { - startActivity(new Intent(requireActivity(), ProxySettingsActivity.class)); - return true; - }); + proxySettings.setOnPreferenceClickListener( + (preference) -> { + startActivity(new Intent(requireActivity(), ProxySettingsActivity.class)); + return true; + }); } Preference relayListBtn = this.findPreference("pref_relay_list_button"); if (relayListBtn != null) { - relayListBtn.setOnPreferenceClickListener(((preference) -> { - boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.transports), getString(R.string.enter_system_secret_to_continue), screenLockLauncher); - if (!result) { - openRelayListActivity(); - } - return true; - })); + relayListBtn.setOnPreferenceClickListener( + ((preference) -> { + boolean result = + ScreenLockUtil.applyScreenLock( + requireActivity(), + getString(R.string.transports), + getString(R.string.enter_system_secret_to_continue), + screenLockLauncher); + if (!result) { + openRelayListActivity(); + } + return true; + })); } if (dcContext.isChatmail()) { @@ -191,16 +203,18 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, String root @Override public void onResume() { super.onResume(); - Objects.requireNonNull(((ApplicationPreferencesActivity) requireActivity()).getSupportActionBar()).setTitle(R.string.menu_advanced); + Objects.requireNonNull( + ((ApplicationPreferencesActivity) requireActivity()).getSupportActionBar()) + .setTitle(R.string.menu_advanced); String value = Integer.toString(dcContext.getConfigInt("show_emails")); showEmails.setValue(value); updateListSummary(showEmails, value); - selfReportingCheckbox.setChecked(0!=dcContext.getConfigInt(CONFIG_STATS_SENDING)); - multiDeviceCheckbox.setChecked(0!=dcContext.getConfigInt(CONFIG_BCC_SELF)); - mvboxMoveCheckbox.setChecked(0!=dcContext.getConfigInt(CONFIG_MVBOX_MOVE)); - onlyFetchMvboxCheckbox.setChecked(0!=dcContext.getConfigInt(CONFIG_ONLY_FETCH_MVBOX)); + selfReportingCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_STATS_SENDING)); + multiDeviceCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_BCC_SELF)); + mvboxMoveCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_MVBOX_MOVE)); + onlyFetchMvboxCheckbox.setChecked(0 != dcContext.getConfigInt(CONFIG_ONLY_FETCH_MVBOX)); } protected File copyToCacheDir(Uri uri) throws IOException { @@ -217,8 +231,9 @@ protected File copyToCacheDir(Uri uri) throws IOException { try { if (context == null) return ""; - String app = context.getString(R.string.app_name); - String version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + String app = context.getString(R.string.app_name); + String version = + context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; return String.format("%s %s", app, version); } catch (PackageManager.NameNotFoundException e) { @@ -246,15 +261,17 @@ public boolean onPreferenceClick(@NonNull Preference preference) { inputField.setSelection(inputField.getText().length()); inputField.setInputType(TYPE_TEXT_VARIATION_URI); new AlertDialog.Builder(requireActivity()) - .setTitle(R.string.webxdc_store_url) - .setMessage(R.string.webxdc_store_url_explain) - .setView(gl) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (dlg, btn) -> { + .setTitle(R.string.webxdc_store_url) + .setMessage(R.string.webxdc_store_url_explain) + .setView(gl) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton( + android.R.string.ok, + (dlg, btn) -> { Prefs.setWebxdcStoreUrl(requireActivity(), inputField.getText().toString()); updateWebxdcStoreSummary(); }) - .show(); + .show(); return true; } } @@ -262,7 +279,7 @@ public boolean onPreferenceClick(@NonNull Preference preference) { private void updateWebxdcStoreSummary() { Preference preference = this.findPreference(Prefs.WEBXDC_STORE_URL_PREF); if (preference != null) { - preference.setSummary(Prefs.getWebxdcStoreUrl(requireActivity())); + preference.setSummary(Prefs.getWebxdcStoreUrl(requireActivity())); } } @@ -270,5 +287,4 @@ private void openRelayListActivity() { Intent intent = new Intent(requireActivity(), RelayListActivity.class); startActivity(intent); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java index e5dc4939c..c10437a65 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/AppearancePreferenceFragment.java @@ -3,19 +3,16 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.ListPreference; import androidx.preference.Preference; - +import java.util.Arrays; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.Prefs; -import java.util.Arrays; - public class AppearancePreferenceFragment extends ListSummaryPreferenceFragment { @Override @@ -23,8 +20,9 @@ public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); this.findPreference(Prefs.THEME_PREF).setOnPreferenceChangeListener(new ListSummaryListener()); - initializeListSummary((ListPreference)findPreference(Prefs.THEME_PREF)); - this.findPreference(Prefs.BACKGROUND_PREF).setOnPreferenceClickListener(new BackgroundClickListener()); + initializeListSummary((ListPreference) findPreference(Prefs.THEME_PREF)); + this.findPreference(Prefs.BACKGROUND_PREF) + .setOnPreferenceClickListener(new BackgroundClickListener()); } @Override @@ -35,19 +33,22 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, String root @Override public void onStart() { super.onStart(); - getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener((ApplicationPreferencesActivity)getActivity()); + getPreferenceScreen() + .getSharedPreferences() + .registerOnSharedPreferenceChangeListener((ApplicationPreferencesActivity) getActivity()); } @Override public void onResume() { super.onResume(); - ((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.pref_appearance); + ((ApplicationPreferencesActivity) getActivity()) + .getSupportActionBar() + .setTitle(R.string.pref_appearance); String imagePath = Prefs.getBackgroundImagePath(getContext(), dcContext.getAccountId()); String backgroundString; - if(imagePath.isEmpty()){ + if (imagePath.isEmpty()) { backgroundString = this.getString(R.string.def); - } - else{ + } else { backgroundString = this.getString(R.string.custom); } this.findPreference(Prefs.BACKGROUND_PREF).setSummary(backgroundString); @@ -56,29 +57,34 @@ public void onResume() { @Override public void onStop() { super.onStop(); - getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener((ApplicationPreferencesActivity) getActivity()); + getPreferenceScreen() + .getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener((ApplicationPreferencesActivity) getActivity()); } public static CharSequence getSummary(Context context) { - String[] themeEntries = context.getResources().getStringArray(R.array.pref_theme_entries); - String[] themeEntryValues = context.getResources().getStringArray(R.array.pref_theme_values); + String[] themeEntries = context.getResources().getStringArray(R.array.pref_theme_entries); + String[] themeEntryValues = context.getResources().getStringArray(R.array.pref_theme_values); int themeIndex = Arrays.asList(themeEntryValues).indexOf(Prefs.getTheme(context)); if (themeIndex == -1) themeIndex = 0; - String imagePath = Prefs.getBackgroundImagePath(context, DcHelper.getContext(context).getAccountId()); + String imagePath = + Prefs.getBackgroundImagePath(context, DcHelper.getContext(context).getAccountId()); String backgroundString; - if(imagePath.isEmpty()){ + if (imagePath.isEmpty()) { backgroundString = context.getString(R.string.def); - } - else{ + } else { backgroundString = context.getString(R.string.custom); } // adding combined strings as "Read receipt: %1$s, Screen lock: %1$s, " // makes things inflexible on changes and/or adds lot of additional works to programmers. // however, if needed, we can refine this later. - return themeEntries[themeIndex] + ", " - + context.getString(R.string.pref_background) + " " + backgroundString; + return themeEntries[themeIndex] + + ", " + + context.getString(R.string.pref_background) + + " " + + backgroundString; } private class BackgroundClickListener implements Preference.OnPreferenceClickListener { diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/ChatBackgroundActivity.java b/src/main/java/org/thoughtcrime/securesms/preferences/ChatBackgroundActivity.java index 3723cfbad..16a71f2f9 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/ChatBackgroundActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/ChatBackgroundActivity.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.preferences; - import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -16,11 +15,11 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.Toast; - import androidx.appcompat.app.ActionBar; - import com.bumptech.glide.load.engine.DiskCacheStrategy; - +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.concurrent.ExecutionException; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.R; @@ -30,172 +29,176 @@ import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.ServiceUtil; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.util.concurrent.ExecutionException; - public class ChatBackgroundActivity extends PassphraseRequiredActionBarActivity { - Button galleryButton; - Button defaultButton; - ImageView preview; + Button galleryButton; + Button defaultButton; + ImageView preview; - String tempDestinationPath; - Uri imageUri; - Boolean imageUpdate = false; + String tempDestinationPath; + Uri imageUri; + Boolean imageUpdate = false; - private int accountId; + private int accountId; - @Override - protected void onCreate(Bundle savedInstanceState, boolean ready) { - setContentView(R.layout.activity_select_chat_background); - - defaultButton = findViewById(R.id.set_default_button); - galleryButton = findViewById(R.id.from_gallery_button); - preview = findViewById(R.id.preview); - accountId = DcHelper.getContext(getApplicationContext()).getAccountId(); - - defaultButton.setOnClickListener(new DefaultClickListener()); - galleryButton.setOnClickListener(new GalleryClickListener()); - - String backgroundImagePath = Prefs.getBackgroundImagePath(this, accountId); - if(backgroundImagePath.isEmpty()){ - setDefaultLayoutBackgroundImage(); - }else { - setLayoutBackgroundImage(backgroundImagePath); - } + @Override + protected void onCreate(Bundle savedInstanceState, boolean ready) { + setContentView(R.layout.activity_select_chat_background); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(R.string.pref_background); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); - } - } + defaultButton = findViewById(R.id.set_default_button); + galleryButton = findViewById(R.id.from_gallery_button); + preview = findViewById(R.id.preview); + accountId = DcHelper.getContext(getApplicationContext()).getAccountId(); - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuInflater inflater = this.getMenuInflater(); - menu.clear(); - inflater.inflate(R.menu.chat_background, menu); - super.onPrepareOptionsMenu(menu); - return true; + defaultButton.setOnClickListener(new DefaultClickListener()); + galleryButton.setOnClickListener(new GalleryClickListener()); + + String backgroundImagePath = Prefs.getBackgroundImagePath(this, accountId); + if (backgroundImagePath.isEmpty()) { + setDefaultLayoutBackgroundImage(); + } else { + setLayoutBackgroundImage(backgroundImagePath); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - if (id == R.id.apply_background) { - // handle confirmation button click here - Context context = getApplicationContext(); - if(imageUpdate) { - if (imageUri != null) { - Thread thread = new Thread() { - @Override - public void run() { - String destination = context.getFilesDir().getAbsolutePath() + "/background."+ accountId; - Prefs.setBackgroundImagePath(context, accountId, destination); - scaleAndSaveImage(context, destination); - } - }; - thread.start(); - } else { - Prefs.setBackgroundImagePath(context, accountId, ""); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.pref_background); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuInflater inflater = this.getMenuInflater(); + menu.clear(); + inflater.inflate(R.menu.chat_background, menu); + super.onPrepareOptionsMenu(menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.apply_background) { + // handle confirmation button click here + Context context = getApplicationContext(); + if (imageUpdate) { + if (imageUri != null) { + Thread thread = + new Thread() { + @Override + public void run() { + String destination = + context.getFilesDir().getAbsolutePath() + "/background." + accountId; + Prefs.setBackgroundImagePath(context, accountId, destination); + scaleAndSaveImage(context, destination); } - } - finish(); - return true; - } else if (id == android.R.id.home) { - // handle close button click here - finish(); - return true; + }; + thread.start(); + } else { + Prefs.setBackgroundImagePath(context, accountId, ""); } - - return super.onOptionsItemSelected(item); + } + finish(); + return true; + } else if (id == android.R.id.home) { + // handle close button click here + finish(); + return true; } - private void scaleAndSaveImage(Context context, String destinationPath) { - try{ - Display display = ServiceUtil.getWindowManager(context).getDefaultDisplay(); - Point size = new Point(); - display.getSize(size); - // resize so that the larger side fits the screen accurately - int largerSide = Math.max(size.x, size.y); - Bitmap scaledBitmap = GlideApp.with(context) - .asBitmap() - .load(imageUri) - .fitCenter() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .submit(largerSide, largerSide) - .get(); - FileOutputStream outStream = new FileOutputStream(destinationPath); - scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 85, outStream); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - Prefs.setBackgroundImagePath(context, accountId, ""); - showBackgroundSaveError(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - Prefs.setBackgroundImagePath(context, accountId, ""); - showBackgroundSaveError(); - } + return super.onOptionsItemSelected(item); + } + + private void scaleAndSaveImage(Context context, String destinationPath) { + try { + Display display = ServiceUtil.getWindowManager(context).getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + // resize so that the larger side fits the screen accurately + int largerSide = Math.max(size.x, size.y); + Bitmap scaledBitmap = + GlideApp.with(context) + .asBitmap() + .load(imageUri) + .fitCenter() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .submit(largerSide, largerSide) + .get(); + FileOutputStream outStream = new FileOutputStream(destinationPath); + scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 85, outStream); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + Prefs.setBackgroundImagePath(context, accountId, ""); + showBackgroundSaveError(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + Prefs.setBackgroundImagePath(context, accountId, ""); + showBackgroundSaveError(); } - - private void setLayoutBackgroundImage(String backgroundImagePath) { - Drawable image = Drawable.createFromPath(backgroundImagePath); - preview.setImageDrawable(image); + } + + private void setLayoutBackgroundImage(String backgroundImagePath) { + Drawable image = Drawable.createFromPath(backgroundImagePath); + preview.setImageDrawable(image); + } + + private void setDefaultLayoutBackgroundImage() { + Drawable image = getResources().getDrawable(R.drawable.background_hd); + preview.setImageDrawable(image); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + final Context context = getApplicationContext(); + if (data != null + && context != null + && resultCode == RESULT_OK + && requestCode == ApplicationPreferencesActivity.REQUEST_CODE_SET_BACKGROUND) { + imageUri = data.getData(); + if (imageUri != null) { + Thread thread = + new Thread() { + @Override + public void run() { + tempDestinationPath = context.getFilesDir().getAbsolutePath() + "/background-temp"; + scaleAndSaveImage(context, tempDestinationPath); + runOnUiThread( + () -> { + // Stuff that updates the UI + setLayoutBackgroundImage(tempDestinationPath); + }); + } + }; + thread.start(); + } + imageUpdate = true; } + } - private void setDefaultLayoutBackgroundImage() { - Drawable image = getResources().getDrawable(R.drawable.background_hd); - preview.setImageDrawable(image); - } + private void showBackgroundSaveError() { + Toast.makeText(this, R.string.error, Toast.LENGTH_LONG).show(); + } + private class DefaultClickListener implements View.OnClickListener { @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - final Context context = getApplicationContext(); - if (data != null && context != null && resultCode == RESULT_OK && requestCode == ApplicationPreferencesActivity.REQUEST_CODE_SET_BACKGROUND) { - imageUri = data.getData(); - if (imageUri != null) { - Thread thread = new Thread(){ - @Override - public void run() { - tempDestinationPath = context.getFilesDir().getAbsolutePath() + "/background-temp"; - scaleAndSaveImage(context, tempDestinationPath); - runOnUiThread(() -> { - // Stuff that updates the UI - setLayoutBackgroundImage(tempDestinationPath); - }); - } - }; - thread.start(); - } - imageUpdate=true; - } + public void onClick(View view) { + imageUri = null; + tempDestinationPath = ""; + setDefaultLayoutBackgroundImage(); + imageUpdate = true; } + } - private void showBackgroundSaveError() { - Toast.makeText(this, R.string.error, Toast.LENGTH_LONG).show(); - } - - private class DefaultClickListener implements View.OnClickListener { - @Override - public void onClick(View view) { - imageUri = null; - tempDestinationPath = ""; - setDefaultLayoutBackgroundImage(); - imageUpdate=true; - } - - } - - private class GalleryClickListener implements View.OnClickListener { - @Override - public void onClick(View view) { - AttachmentManager.selectImage(ChatBackgroundActivity.this, ApplicationPreferencesActivity.REQUEST_CODE_SET_BACKGROUND); - } + private class GalleryClickListener implements View.OnClickListener { + @Override + public void onClick(View view) { + AttachmentManager.selectImage( + ChatBackgroundActivity.this, ApplicationPreferencesActivity.REQUEST_CODE_SET_BACKGROUND); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java index ab7aff9f0..7b0fe16c1 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/ChatsPreferenceFragment.java @@ -6,21 +6,14 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.preference.CheckBoxPreference; import androidx.preference.ListPreference; import androidx.preference.Preference; - import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; @@ -39,32 +32,33 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment { public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); - screenLockLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK) { - performBackup(); - } - } - ); + screenLockLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + performBackup(); + } + }); mediaQuality = (ListPreference) this.findPreference("pref_compression"); if (mediaQuality != null) { - mediaQuality.setOnPreferenceChangeListener((preference, newValue) -> { - updateListSummary(preference, newValue); - dcContext.setConfigInt(DcHelper.CONFIG_MEDIA_QUALITY, Util.objectToInt(newValue)); - return true; - }); + mediaQuality.setOnPreferenceChangeListener( + (preference, newValue) -> { + updateListSummary(preference, newValue); + dcContext.setConfigInt(DcHelper.CONFIG_MEDIA_QUALITY, Util.objectToInt(newValue)); + return true; + }); } - autoDownload = findPreference("auto_download"); if (autoDownload != null) { - autoDownload.setOnPreferenceChangeListener((preference, newValue) -> { - updateListSummary(preference, newValue); - dcContext.setConfigInt("download_limit", Util.objectToInt(newValue)); - return true; - }); + autoDownload.setOnPreferenceChangeListener( + (preference, newValue) -> { + updateListSummary(preference, newValue); + dcContext.setConfigInt("download_limit", Util.objectToInt(newValue)); + return true; + }); } nicerAutoDownloadNames(); @@ -82,7 +76,9 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, String root @Override public void onResume() { super.onResume(); - ((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.pref_chats); + ((ApplicationPreferencesActivity) getActivity()) + .getSupportActionBar() + .setTitle(R.string.pref_chats); String value = Integer.toString(dcContext.getConfigInt(DcHelper.CONFIG_MEDIA_QUALITY)); mediaQuality.setValue(value); @@ -111,17 +107,21 @@ private void nicerAutoDownloadNames() { // Assumes `entryValues` are sorted smallest (index 0) to largest (last index) // and returns the an item close to `selectedValue`. - private String alignToMaxEntry(@NonNull String selectedValue, @NonNull CharSequence[] entryValues) { + private String alignToMaxEntry( + @NonNull String selectedValue, @NonNull CharSequence[] entryValues) { try { int selectedValueInt = Integer.parseInt(selectedValue); for (int i = entryValues.length - 1; i >= 1 /*first is returned below*/; i--) { - int entryValueMin = i == 1 ? (Integer.parseInt(entryValues[i - 1].toString()) + 1) : Integer.parseInt(entryValues[i].toString()); + int entryValueMin = + i == 1 + ? (Integer.parseInt(entryValues[i - 1].toString()) + 1) + : Integer.parseInt(entryValues[i].toString()); if (selectedValueInt >= entryValueMin) { return entryValues[i].toString(); } } return entryValues[0].toString(); - } catch(Exception e) { + } catch (Exception e) { return selectedValue; } } @@ -143,7 +143,12 @@ public static CharSequence getSummary(Context context) { private class BackupListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(@NonNull Preference preference) { - boolean result = ScreenLockUtil.applyScreenLock(requireActivity(), getString(R.string.pref_backup), getString(R.string.enter_system_secret_to_continue), screenLockLauncher); + boolean result = + ScreenLockUtil.applyScreenLock( + requireActivity(), + getString(R.string.pref_backup), + getString(R.string.enter_system_secret_to_continue), + screenLockLauncher); if (!result) { performBackup(); } @@ -153,23 +158,35 @@ public boolean onPreferenceClick(@NonNull Preference preference) { private void performBackup() { Permissions.with(requireActivity()) - .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) // READ_EXTERNAL_STORAGE required to read folder contents and to generate backup names - .alwaysGrantOnSdk30() - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) - .onAllGranted(() -> { - AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()) + .request( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission + .READ_EXTERNAL_STORAGE) // READ_EXTERNAL_STORAGE required to read folder contents + // and to generate backup names + .alwaysGrantOnSdk30() + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_storage_denied)) + .onAllGranted( + () -> { + AlertDialog.Builder builder = + new AlertDialog.Builder(requireActivity()) .setTitle(R.string.pref_backup) .setMessage(R.string.pref_backup_export_explain) .setNeutralButton(android.R.string.cancel, null) - .setPositiveButton(R.string.pref_backup_export_this, (dialogInterface, i) -> startImexOne(DcContext.DC_IMEX_EXPORT_BACKUP)); + .setPositiveButton( + R.string.pref_backup_export_this, + (dialogInterface, i) -> startImexOne(DcContext.DC_IMEX_EXPORT_BACKUP)); int[] allAccounts = DcHelper.getAccounts(requireActivity()).getAll(); if (allAccounts.length > 1) { - String exportAllString = requireActivity().getString(R.string.pref_backup_export_all, allAccounts.length); - builder.setNegativeButton(exportAllString, (dialogInterface, i) -> startImexAll(DcContext.DC_IMEX_EXPORT_BACKUP)); + String exportAllString = + requireActivity() + .getString(R.string.pref_backup_export_all, allAccounts.length); + builder.setNegativeButton( + exportAllString, + (dialogInterface, i) -> startImexAll(DcContext.DC_IMEX_EXPORT_BACKUP)); } builder.show(); }) - .execute(); + .execute(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java index f6bbe11f0..2f5c40fb7 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/CorrectedPreferenceFragment.java @@ -2,7 +2,6 @@ import android.os.Bundle; import android.view.View; - import androidx.preference.PreferenceFragmentCompat; public abstract class CorrectedPreferenceFragment extends PreferenceFragmentCompat { diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java index 7ad5ec0a0..bc9bb6bb0 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/ListSummaryPreferenceFragment.java @@ -1,19 +1,18 @@ package org.thoughtcrime.securesms.preferences; - import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.preference.ListPreference; import androidx.preference.Preference; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -21,11 +20,8 @@ import org.thoughtcrime.securesms.service.NotificationController; import org.thoughtcrime.securesms.util.views.ProgressDialog; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -public abstract class ListSummaryPreferenceFragment extends CorrectedPreferenceFragment implements DcEventCenter.DcEventDelegate { +public abstract class ListSummaryPreferenceFragment extends CorrectedPreferenceFragment + implements DcEventCenter.DcEventDelegate { protected DcContext dcContext; private NotificationController notificationController; @@ -78,8 +74,8 @@ protected String getSelectedSummary(Preference preference, Object value) { ListPreference listPref = (ListPreference) preference; int entryIndex = Arrays.asList(listPref.getEntryValues()).indexOf(value); return entryIndex >= 0 && entryIndex < listPref.getEntries().length - ? listPref.getEntries()[entryIndex].toString() - : getString(R.string.unknown); + ? listPref.getEntries()[entryIndex].toString() + : getString(R.string.unknown); } protected void updateListSummary(Preference preference, Object value) { @@ -114,14 +110,13 @@ protected void startImexAll(int what) { } } - protected void startImexOne(int what) - { + protected void startImexOne(int what) { String path = DcHelper.getImexDir().getAbsolutePath(); startImexOne(what, path, path); } protected void startImexOne(int what, String imexPath, String pathAsDisplayedToUser) { - imexAccounts = new int[]{ dcContext.getAccountId() }; + imexAccounts = new int[] {dcContext.getAccountId()}; imexProgress = new HashMap<>(); accountsDone = 0; showProgressDialog(); @@ -129,10 +124,11 @@ protected void startImexOne(int what, String imexPath, String pathAsDisplayedToU } protected ProgressDialog progressDialog = null; - protected int progressWhat = 0; - protected String pathAsDisplayedToUser = ""; - protected void startImexInner(int accountId, int what, String imexPath, String pathAsDisplayedToUser) - { + protected int progressWhat = 0; + protected String pathAsDisplayedToUser = ""; + + protected void startImexInner( + int accountId, int what, String imexPath, String pathAsDisplayedToUser) { DcContext dcContext = DcHelper.getAccounts(getActivity()).getAccount(accountId); this.pathAsDisplayedToUser = pathAsDisplayedToUser; progressWhat = what; @@ -146,8 +142,10 @@ private void stopOngoingProcess() { } private void showProgressDialog() { - notificationController = GenericForegroundService.startForegroundTask(getContext(), getString(R.string.export_backup_desktop)); - if( progressDialog!=null ) { + notificationController = + GenericForegroundService.startForegroundTask( + getContext(), getString(R.string.export_backup_desktop)); + if (progressDialog != null) { progressDialog.dismiss(); progressDialog = null; } @@ -155,11 +153,14 @@ private void showProgressDialog() { progressDialog.setMessage(getActivity().getString(R.string.one_moment)); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setCancelable(false); - progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getActivity().getString(android.R.string.cancel), (dialog, which) -> { - notificationController.close(); - notificationController = null; - stopOngoingProcess(); - }); + progressDialog.setButton( + DialogInterface.BUTTON_NEGATIVE, + getActivity().getString(android.R.string.cancel), + (dialog, which) -> { + notificationController.close(); + notificationController = null; + stopOngoingProcess(); + }); progressDialog.show(); } @@ -173,13 +174,13 @@ private int getTotalProgress() { @Override public void handleEvent(@NonNull DcEvent event) { - if (event.getId()== DcContext.DC_EVENT_IMEX_PROGRESS) { + if (event.getId() == DcContext.DC_EVENT_IMEX_PROGRESS) { NotificationController notifController = notificationController; if (notifController == null) return; long progress = event.getData1Int(); Context context = getActivity(); - if (progress==0/*error/aborted*/) { + if (progress == 0 /*error/aborted*/) { notifController.close(); notificationController = null; stopOngoingProcess(); @@ -187,19 +188,17 @@ public void handleEvent(@NonNull DcEvent event) { progressDialog = null; DcContext dcContext = DcHelper.getAccounts(context).getAccount(event.getAccountId()); new AlertDialog.Builder(context) - .setMessage(dcContext.getLastError()) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - else if (progress<1000/*progress in permille*/) { + .setMessage(dcContext.getLastError()) + .setPositiveButton(android.R.string.ok, null) + .show(); + } else if (progress < 1000 /*progress in permille*/) { imexProgress.put(event.getAccountId(), (int) progress); int totalProgress = getTotalProgress(); int percent = totalProgress / (10 * imexAccounts.length); String formattedPercent = percent > 0 ? String.format(" %d%%", percent) : ""; progressDialog.setMessage(getResources().getString(R.string.one_moment) + formattedPercent); notifController.setProgress(1000L * imexAccounts.length, totalProgress, formattedPercent); - } - else if (progress==1000/*done*/) { + } else if (progress == 1000 /*done*/) { accountsDone++; if (accountsDone == imexAccounts.length) { notifController.close(); @@ -207,12 +206,12 @@ else if (progress==1000/*done*/) { progressDialog.dismiss(); progressDialog = null; new AlertDialog.Builder(context) - .setMessage(context.getString(R.string.pref_backup_written_to_x, pathAsDisplayedToUser)) - .setPositiveButton(android.R.string.ok, null) - .show(); + .setMessage( + context.getString(R.string.pref_backup_written_to_x, pathAsDisplayedToUser)) + .setPositiveButton(android.R.string.ok, null) + .show(); } } } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index 256ebb010..87cbe5bac 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -15,7 +15,6 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.Log; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -25,7 +24,6 @@ import androidx.preference.CheckBoxPreference; import androidx.preference.ListPreference; import androidx.preference.Preference; - import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; @@ -33,7 +31,8 @@ import org.thoughtcrime.securesms.notifications.FcmReceiveService; import org.thoughtcrime.securesms.util.Prefs; -public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment implements Preference.OnPreferenceChangeListener { +public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragment + implements Preference.OnPreferenceChangeListener { private static final String TAG = NotificationsPreferenceFragment.class.getSimpleName(); @@ -47,22 +46,23 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme public void onCreate(Bundle paramBundle) { super.onCreate(paramBundle); - ringtonePickerLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null) { - Uri uri = result.getData().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + ringtonePickerLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + Uri uri = + result.getData().getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); - if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) { - Prefs.removeNotificationRingtone(getContext()); - } else { - Prefs.setNotificationRingtone(getContext(), uri != null ? uri : Uri.EMPTY); - } + if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)) { + Prefs.removeNotificationRingtone(getContext()); + } else { + Prefs.setNotificationRingtone(getContext(), uri != null ? uri : Uri.EMPTY); + } - initializeRingtoneSummary(findPreference(Prefs.RINGTONE_PREF)); - } - } - ); + initializeRingtoneSummary(findPreference(Prefs.RINGTONE_PREF)); + } + }); this.findPreference(Prefs.LED_COLOR_PREF) .setOnPreferenceChangeListener(new ListSummaryListener()); @@ -74,20 +74,24 @@ public void onCreate(Bundle paramBundle) { .setOnPreferenceChangeListener(new ListSummaryListener()); this.findPreference(Prefs.RINGTONE_PREF) - .setOnPreferenceClickListener(preference -> { - Uri current = Prefs.getNotificationRingtone(getContext()); - if (current.toString().isEmpty()) current = null; // silent - - Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current); - ringtonePickerLauncher.launch(intent); - - return true; - }); + .setOnPreferenceClickListener( + preference -> { + Uri current = Prefs.getNotificationRingtone(getContext()); + if (current.toString().isEmpty()) current = null; // silent + + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra( + RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + Settings.System.DEFAULT_NOTIFICATION_URI); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current); + ringtonePickerLauncher.launch(intent); + + return true; + }); initializeListSummary((ListPreference) findPreference(Prefs.LED_COLOR_PREF)); initializeListSummary((ListPreference) findPreference(Prefs.NOTIFICATION_PRIVACY_PREF)); @@ -98,38 +102,40 @@ public void onCreate(Bundle paramBundle) { ignoreBattery = this.findPreference("pref_ignore_battery_optimizations"); if (ignoreBattery != null) { ignoreBattery.setVisible(needsIgnoreBatteryOptimizations()); - ignoreBattery.setOnPreferenceChangeListener((preference, newValue) -> { - requestToggleIgnoreBatteryOptimizations(); - return true; - }); + ignoreBattery.setOnPreferenceChangeListener( + (preference, newValue) -> { + requestToggleIgnoreBatteryOptimizations(); + return true; + }); } - // reliableService is just used for displaying the actual value // of the reliable service preference that is managed via // Prefs.setReliableService() and Prefs.reliableService() - reliableService = this.findPreference("pref_reliable_service2"); + reliableService = this.findPreference("pref_reliable_service2"); if (reliableService != null) { reliableService.setOnPreferenceChangeListener(this); } notificationsEnabled = this.findPreference("pref_enable_notifications"); if (notificationsEnabled != null) { - notificationsEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (Boolean) newValue; - dcContext.setMuted(!enabled); - notificationsEnabled.setSummary(getSummary(getContext(), false)); - return true; - }); + notificationsEnabled.setOnPreferenceChangeListener( + (preference, newValue) -> { + boolean enabled = (Boolean) newValue; + dcContext.setMuted(!enabled); + notificationsEnabled.setSummary(getSummary(getContext(), false)); + return true; + }); } mentionNotifEnabled = this.findPreference("pref_enable_mention_notifications"); if (mentionNotifEnabled != null) { - mentionNotifEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (Boolean) newValue; - dcContext.setMentionsEnabled(enabled); - return true; - }); + mentionNotifEnabled.setOnPreferenceChangeListener( + (preference, newValue) -> { + boolean enabled = (Boolean) newValue; + dcContext.setMentionsEnabled(enabled); + return true; + }); } } @@ -141,7 +147,9 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, String root @Override public void onResume() { super.onResume(); - ((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.pref_notifications); + ((ApplicationPreferencesActivity) getActivity()) + .getSupportActionBar() + .setTitle(R.string.pref_notifications); // update ignoreBattery in onResume() to reflects changes done in the system settings ignoreBattery.setChecked(isIgnoringBatteryOptimizations()); @@ -206,8 +214,8 @@ private boolean isIgnoringBatteryOptimizations() { if (!needsIgnoreBatteryOptimizations()) { return true; } - PowerManager pm = (PowerManager)getActivity().getSystemService(Context.POWER_SERVICE); - if(pm.isIgnoringBatteryOptimizations(getActivity().getPackageName())) { + PowerManager pm = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE); + if (pm.isIgnoringBatteryOptimizations(getActivity().getPackageName())) { return true; } return false; @@ -219,9 +227,14 @@ private void requestToggleIgnoreBatteryOptimizations() { try { if (needsIgnoreBatteryOptimizations() - && !isIgnoringBatteryOptimizations() - && ContextCompat.checkSelfPermission(context, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_GRANTED) { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:" + context.getPackageName())); + && !isIgnoringBatteryOptimizations() + && ContextCompat.checkSelfPermission( + context, Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + == PackageManager.PERMISSION_GRANTED) { + Intent intent = + new Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + context.getPackageName())); context.startActivity(intent); openManualSettings = false; } @@ -230,16 +243,19 @@ private void requestToggleIgnoreBatteryOptimizations() { } if (openManualSettings && needsIgnoreBatteryOptimizations()) { - // fire ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS if ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS fails - // or if isIgnoringBatteryOptimizations() is already true (there is no intent to re-enable battery optimizations) + // fire ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS if + // ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS fails + // or if isIgnoringBatteryOptimizations() is already true (there is no intent to re-enable + // battery optimizations) Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); context.startActivity(intent); } } private void initializeRingtoneSummary(Preference pref) { - RingtoneSummaryListener listener = (RingtoneSummaryListener) pref.getOnPreferenceChangeListener(); - Uri uri = Prefs.getNotificationRingtone(getContext()); + RingtoneSummaryListener listener = + (RingtoneSummaryListener) pref.getOnPreferenceChangeListener(); + Uri uri = Prefs.getNotificationRingtone(getContext()); listener.onPreferenceChange(pref, uri); } @@ -250,14 +266,15 @@ public static CharSequence getSummary(Context context) { public static CharSequence getSummary(Context context, boolean detailed) { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || notificationManager.areNotificationsEnabled()) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + || notificationManager.areNotificationsEnabled()) { if (DcHelper.getContext(context).isMuted()) { - return detailed? context.getString(R.string.off) : ""; + return detailed ? context.getString(R.string.off) : ""; } if (FcmReceiveService.getToken() == null && !Prefs.reliableService(context)) { return "⚠️ " + context.getString(R.string.unreliable_bg_notifications); } - return detailed? context.getString(R.string.on) : ""; + return detailed ? context.getString(R.string.on) : ""; } else { return "⚠️ " + context.getString(R.string.disabled_in_system_settings); } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/PrivacyPreferenceFragment.java b/src/main/java/org/thoughtcrime/securesms/preferences/PrivacyPreferenceFragment.java index 023e88278..ad7116ecf 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/PrivacyPreferenceFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/PrivacyPreferenceFragment.java @@ -1,8 +1,5 @@ package org.thoughtcrime.securesms.preferences; -import static android.app.Activity.RESULT_OK; -import static org.thoughtcrime.securesms.connect.DcHelper.CONFIG_SHOW_EMAILS; - import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -10,15 +7,12 @@ import android.widget.CheckBox; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.preference.CheckBoxPreference; import androidx.preference.ListPreference; import androidx.preference.Preference; - import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.BlockedContactsActivity; import org.thoughtcrime.securesms.R; @@ -41,7 +35,8 @@ public void onCreate(Bundle paramBundle) { readReceiptsCheckbox = (CheckBoxPreference) this.findPreference("pref_read_receipts"); readReceiptsCheckbox.setOnPreferenceChangeListener(new ReadReceiptToggleListener()); - this.findPreference("preference_category_blocked").setOnPreferenceClickListener(new BlockedContactsClickListener()); + this.findPreference("preference_category_blocked") + .setOnPreferenceClickListener(new BlockedContactsClickListener()); autoDelDevice = findPreference("autodel_device"); autoDelDevice.setOnPreferenceChangeListener(new AutodelChangeListener("delete_device_after")); @@ -64,7 +59,9 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, String root @Override public void onResume() { super.onResume(); - ((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.pref_privacy); + ((ApplicationPreferencesActivity) getActivity()) + .getSupportActionBar() + .setTitle(R.string.pref_privacy); readReceiptsCheckbox.setChecked(0 != dcContext.getConfigInt("mdns_enabled")); initAutodelFromCore(); @@ -73,7 +70,12 @@ public void onResume() { private void initAutodelFromCore() { String value = Integer.toString(dcContext.getConfigInt("delete_server_after")); autoDelServer.setValue(value); - updateListSummary(autoDelServer, value, (value.equals("0") || dcContext.isChatmail())? null : getString(R.string.autodel_server_enabled_hint)); + updateListSummary( + autoDelServer, + value, + (value.equals("0") || dcContext.isChatmail()) + ? null + : getString(R.string.autodel_server_enabled_hint)); value = Integer.toString(dcContext.getConfigInt("delete_device_after")); autoDelDevice.setValue(value); @@ -84,7 +86,7 @@ public static CharSequence getSummary(Context context) { DcContext dcContext = DcHelper.getContext(context); final String onRes = context.getString(R.string.on); final String offRes = context.getString(R.string.off); - String readReceiptState = dcContext.getConfigInt("mdns_enabled")!=0? onRes : offRes; + String readReceiptState = dcContext.getConfigInt("mdns_enabled") != 0 ? onRes : offRes; return context.getString(R.string.pref_read_receipts) + " " + readReceiptState; } @@ -118,24 +120,30 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { int timeout = Util.objectToInt(newValue); Context context = preference.getContext(); boolean fromServer = coreKey.equals("delete_server_after"); - if (timeout>0 && !(fromServer && dcContext.isChatmail())) { + if (timeout > 0 && !(fromServer && dcContext.isChatmail())) { int delCount = DcHelper.getContext(context).estimateDeletionCount(fromServer, timeout); View gl = View.inflate(getActivity(), R.layout.dialog_with_checkbox, null); CheckBox confirmCheckbox = gl.findViewById(R.id.dialog_checkbox); TextView msg = gl.findViewById(R.id.dialog_message); - // If we'd use both `setMessage()` and `setView()` on the same AlertDialog, on small screens the + // If we'd use both `setMessage()` and `setView()` on the same AlertDialog, on small screens + // the // "OK" and "Cancel" buttons would not be show. So, put the message into our custom view: - msg.setText(String.format(context.getString(fromServer? - R.string.autodel_server_ask : R.string.autodel_device_ask), - delCount, getSelectedSummary(preference, newValue))); + msg.setText( + String.format( + context.getString( + fromServer ? R.string.autodel_server_ask : R.string.autodel_device_ask), + delCount, + getSelectedSummary(preference, newValue))); confirmCheckbox.setText(R.string.autodel_confirm); new AlertDialog.Builder(context) - .setTitle(preference.getTitle()) - .setView(gl) - .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { + .setTitle(preference.getTitle()) + .setView(gl) + .setPositiveButton( + android.R.string.ok, + (dialog, whichButton) -> { if (confirmCheckbox.isChecked()) { dcContext.setConfigInt(coreKey, timeout); initAutodelFromCore(); @@ -143,22 +151,30 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { onPreferenceChange(preference, newValue); } }) - .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> initAutodelFromCore()) - .setCancelable(true) // Enable the user to quickly cancel if they are intimidated by the warnings :) - .setOnCancelListener(dialog -> initAutodelFromCore()) - .show(); - } else if (fromServer && timeout == 1 /*at once, using a constant that cannot be used in .xml would weaken grep ability*/) { + .setNegativeButton( + android.R.string.cancel, (dialog, whichButton) -> initAutodelFromCore()) + .setCancelable( + true) // Enable the user to quickly cancel if they are intimidated by the warnings + // :) + .setOnCancelListener(dialog -> initAutodelFromCore()) + .show(); + } else if (fromServer + && timeout + == 1 /*at once, using a constant that cannot be used in .xml would weaken grep ability*/) { new AlertDialog.Builder(context) - .setTitle(R.string.autodel_server_warn_multi_device_title) - .setMessage(R.string.autodel_server_warn_multi_device) - .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { + .setTitle(R.string.autodel_server_warn_multi_device_title) + .setMessage(R.string.autodel_server_warn_multi_device) + .setPositiveButton( + android.R.string.ok, + (dialog, whichButton) -> { dcContext.setConfigInt(coreKey, timeout); initAutodelFromCore(); }) - .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> initAutodelFromCore()) - .setCancelable(true) - .setOnCancelListener(dialog -> initAutodelFromCore()) - .show(); + .setNegativeButton( + android.R.string.cancel, (dialog, whichButton) -> initAutodelFromCore()) + .setCancelable(true) + .setOnCancelListener(dialog -> initAutodelFromCore()) + .show(); } else { updateListSummary(preference, newValue); dcContext.setConfigInt(coreKey, timeout); @@ -172,7 +188,9 @@ private class ScreenShotSecurityListener implements Preference.OnPreferenceChang public boolean onPreferenceChange(Preference preference, Object newValue) { boolean enabled = (Boolean) newValue; Prefs.setScreenSecurityEnabled(getContext(), enabled); - Toast.makeText(getContext(), R.string.pref_screen_security_please_restart_hint, Toast.LENGTH_LONG).show(); + Toast.makeText( + getContext(), R.string.pref_screen_security_please_restart_hint, Toast.LENGTH_LONG) + .show(); return true; } } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java b/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java index 251af9b93..76d53e58f 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/widgets/NotificationPrivacyPreference.java @@ -15,5 +15,4 @@ public boolean isDisplayContact() { public boolean isDisplayMessage() { return "all".equals(preference); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java b/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java index 307c79a76..02325f619 100644 --- a/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java +++ b/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java @@ -1,18 +1,14 @@ package org.thoughtcrime.securesms.preferences.widgets; - import android.content.Context; import android.text.TextUtils; import android.util.AttributeSet; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; - import com.bumptech.glide.load.engine.DiskCacheStrategy; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.contacts.avatars.MyProfileContactPhoto; @@ -23,8 +19,8 @@ public class ProfilePreference extends Preference { private ImageView avatarView; - private TextView profileNameView; - private TextView profileStatusView; + private TextView profileNameView; + private TextView profileStatusView; public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); @@ -53,9 +49,9 @@ private void initialize() { @Override public void onBindViewHolder(@NonNull PreferenceViewHolder viewHolder) { super.onBindViewHolder(viewHolder); - avatarView = (ImageView)viewHolder.findViewById(R.id.avatar); - profileNameView = (TextView)viewHolder.findViewById(R.id.profile_name); - profileStatusView = (TextView)viewHolder.findViewById(R.id.profile_status); + avatarView = (ImageView) viewHolder.findViewById(R.id.avatar); + profileNameView = (TextView) viewHolder.findViewById(R.id.profile_name); + profileStatusView = (TextView) viewHolder.findViewById(R.id.profile_status); refresh(); } @@ -64,16 +60,19 @@ public void refresh() { if (profileNameView == null) return; final String address = DcHelper.get(getContext(), DcHelper.CONFIG_CONFIGURED_ADDRESS); - final MyProfileContactPhoto profileImage = new MyProfileContactPhoto(address, String.valueOf(Prefs.getProfileAvatarId(getContext()))); + final MyProfileContactPhoto profileImage = + new MyProfileContactPhoto(address, String.valueOf(Prefs.getProfileAvatarId(getContext()))); GlideApp.with(getContext().getApplicationContext()) - .load(profileImage) - .error(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(getContext(), getContext().getResources().getColor(R.color.grey_400))) - .circleCrop() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(avatarView); - - final String profileName = DcHelper.get(getContext(), DcHelper.CONFIG_DISPLAY_NAME); + .load(profileImage) + .error( + new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp) + .asDrawable(getContext(), getContext().getResources().getColor(R.color.grey_400))) + .circleCrop() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(avatarView); + + final String profileName = DcHelper.get(getContext(), DcHelper.CONFIG_DISPLAY_NAME); if (!TextUtils.isEmpty(profileName)) { profileNameView.setText(profileName); } else { diff --git a/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index e56c5f0c4..2f250a1b4 100644 --- a/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -1,72 +1,71 @@ package org.thoughtcrime.securesms.profiles; - import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcContext; - -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.scribbles.ScribbleActivity; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.scribbles.ScribbleActivity; public class AvatarHelper { - /* the maximum width/height an avatar should have */ - public static final int AVATAR_SIZE = 512; + /* the maximum width/height an avatar should have */ + public static final int AVATAR_SIZE = 512; - public static void setGroupAvatar(Context context, int chatId, Bitmap bitmap) { - DcContext dcContext = DcHelper.getContext(context); + public static void setGroupAvatar(Context context, int chatId, Bitmap bitmap) { + DcContext dcContext = DcHelper.getContext(context); - if (bitmap == null) { - dcContext.setChatProfileImage(chatId, null); - } else { - try { - File avatar = File.createTempFile("groupavatar", ".jpg", context.getCacheDir()); - FileOutputStream out = new FileOutputStream(avatar); - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); - out.close(); - dcContext.setChatProfileImage(chatId, avatar.getPath()); // The avatar is copied to the blobs directory here... - //noinspection ResultOfMethodCallIgnored - avatar.delete(); // ..., now we can delete it. - } catch (IOException e) { - e.printStackTrace(); - } - } + if (bitmap == null) { + dcContext.setChatProfileImage(chatId, null); + } else { + try { + File avatar = File.createTempFile("groupavatar", ".jpg", context.getCacheDir()); + FileOutputStream out = new FileOutputStream(avatar); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); + out.close(); + dcContext.setChatProfileImage( + chatId, avatar.getPath()); // The avatar is copied to the blobs directory here... + //noinspection ResultOfMethodCallIgnored + avatar.delete(); // ..., now we can delete it. + } catch (IOException e) { + e.printStackTrace(); + } } + } - public static File getSelfAvatarFile(@NonNull Context context) { - String dirString = DcHelper.getContext(context).getConfig(DcHelper.CONFIG_SELF_AVATAR); - return new File(dirString); - } - - public static void setSelfAvatar(@NonNull Context context, @Nullable Bitmap bitmap) throws IOException { - if (bitmap == null) { - DcHelper.set(context, DcHelper.CONFIG_SELF_AVATAR, null); - } else { - File avatar = File.createTempFile("selfavatar", ".jpg", context.getCacheDir()); - FileOutputStream out = new FileOutputStream(avatar); - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); - out.close(); - DcHelper.set(context, DcHelper.CONFIG_SELF_AVATAR, avatar.getPath()); // The avatar is copied to the blobs directory here... - //noinspection ResultOfMethodCallIgnored - avatar.delete(); // ..., now we can delete it. - } - } + public static File getSelfAvatarFile(@NonNull Context context) { + String dirString = DcHelper.getContext(context).getConfig(DcHelper.CONFIG_SELF_AVATAR); + return new File(dirString); + } - public static void cropAvatar(Activity context, Uri imageUri) { - Intent intent = new Intent(context, ScribbleActivity.class); - intent.setData(imageUri); - intent.putExtra(ScribbleActivity.CROP_AVATAR, true); - context.startActivityForResult(intent, ScribbleActivity.SCRIBBLE_REQUEST_CODE); + public static void setSelfAvatar(@NonNull Context context, @Nullable Bitmap bitmap) + throws IOException { + if (bitmap == null) { + DcHelper.set(context, DcHelper.CONFIG_SELF_AVATAR, null); + } else { + File avatar = File.createTempFile("selfavatar", ".jpg", context.getCacheDir()); + FileOutputStream out = new FileOutputStream(avatar); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); + out.close(); + DcHelper.set( + context, + DcHelper.CONFIG_SELF_AVATAR, + avatar.getPath()); // The avatar is copied to the blobs directory here... + //noinspection ResultOfMethodCallIgnored + avatar.delete(); // ..., now we can delete it. } + } + public static void cropAvatar(Activity context, Uri imageUri) { + Intent intent = new Intent(context, ScribbleActivity.class); + intent.setData(imageUri); + intent.putExtra(ScribbleActivity.CROP_AVATAR, true); + context.startActivityForResult(intent, ScribbleActivity.SCRIBBLE_REQUEST_CODE); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java b/src/main/java/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java index 0a85bc73e..f15250270 100644 --- a/src/main/java/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java +++ b/src/main/java/org/thoughtcrime/securesms/providers/PersistentBlobProvider.java @@ -7,14 +7,8 @@ import android.net.Uri; import android.util.Log; import android.webkit.MimeTypeMap; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.util.FileProviderUtil; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.Util; - import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -24,29 +18,35 @@ import java.io.OutputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.thoughtcrime.securesms.util.FileProviderUtil; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.Util; public class PersistentBlobProvider { private static final String TAG = PersistentBlobProvider.class.getSimpleName(); - private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture-new"; - public static final Uri CONTENT_URI = Uri.parse(URI_STRING); - public static final String AUTHORITY = "org.thoughtcrime.securesms"; - public static final String EXPECTED_PATH_OLD = "capture/*/*/#"; - public static final String EXPECTED_PATH_NEW = "capture-new/*/*/*/*/#"; - - private static final int MIMETYPE_PATH_SEGMENT = 1; - public static final int FILENAME_PATH_SEGMENT = 2; - private static final int FILESIZE_PATH_SEGMENT = 3; - - private static final String BLOB_EXTENSION = "blob"; - private static final int MATCH_OLD = 1; - private static final int MATCH_NEW = 2; - - private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{ - addURI(AUTHORITY, EXPECTED_PATH_OLD, MATCH_OLD); - addURI(AUTHORITY, EXPECTED_PATH_NEW, MATCH_NEW); - }}; + private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture-new"; + public static final Uri CONTENT_URI = Uri.parse(URI_STRING); + public static final String AUTHORITY = "org.thoughtcrime.securesms"; + public static final String EXPECTED_PATH_OLD = "capture/*/*/#"; + public static final String EXPECTED_PATH_NEW = "capture-new/*/*/*/*/#"; + + private static final int MIMETYPE_PATH_SEGMENT = 1; + public static final int FILENAME_PATH_SEGMENT = 2; + private static final int FILESIZE_PATH_SEGMENT = 3; + + private static final String BLOB_EXTENSION = "blob"; + private static final int MATCH_OLD = 1; + private static final int MATCH_NEW = 2; + + private static final UriMatcher MATCHER = + new UriMatcher(UriMatcher.NO_MATCH) { + { + addURI(AUTHORITY, EXPECTED_PATH_OLD, MATCH_OLD); + addURI(AUTHORITY, EXPECTED_PATH_NEW, MATCH_NEW); + } + }; private static volatile PersistentBlobProvider instance; @@ -62,75 +62,85 @@ public static PersistentBlobProvider getInstance() { } @SuppressLint("UseSparseArrays") - private final ExecutorService executor = Executors.newCachedThreadPool(); + private final ExecutorService executor = Executors.newCachedThreadPool(); - private PersistentBlobProvider() { - } + private PersistentBlobProvider() {} - public Uri create(@NonNull Context context, - @NonNull byte[] blobBytes, - @NonNull String mimeType, - @Nullable String fileName) - { + public Uri create( + @NonNull Context context, + @NonNull byte[] blobBytes, + @NonNull String mimeType, + @Nullable String fileName) { final long id = System.currentTimeMillis(); if (fileName == null) { fileName = "file." + MediaUtil.getExtensionFromMimeType(mimeType); } - return create(context, new ByteArrayInputStream(blobBytes), id, mimeType, fileName, (long) blobBytes.length); - } - - public Uri create(@NonNull Context context, - @NonNull InputStream input, - @NonNull String mimeType, - @Nullable String fileName, - @Nullable Long fileSize) - { + return create( + context, + new ByteArrayInputStream(blobBytes), + id, + mimeType, + fileName, + (long) blobBytes.length); + } + + public Uri create( + @NonNull Context context, + @NonNull InputStream input, + @NonNull String mimeType, + @Nullable String fileName, + @Nullable Long fileSize) { if (fileName == null) { fileName = "file." + MediaUtil.getExtensionFromMimeType(mimeType); } return create(context, input, System.currentTimeMillis(), mimeType, fileName, fileSize); } - private Uri create(@NonNull Context context, - @NonNull InputStream input, - long id, - @NonNull String mimeType, - @Nullable String fileName, - @Nullable Long fileSize) - { + private Uri create( + @NonNull Context context, + @NonNull InputStream input, + long id, + @NonNull String mimeType, + @Nullable String fileName, + @Nullable Long fileSize) { persistToDisk(context, id, input); - final Uri uniqueUri = CONTENT_URI.buildUpon() - .appendPath(mimeType) - .appendPath(fileName) - .appendEncodedPath(String.valueOf(fileSize)) - .appendEncodedPath(String.valueOf(System.currentTimeMillis())) - .build(); + final Uri uniqueUri = + CONTENT_URI + .buildUpon() + .appendPath(mimeType) + .appendPath(fileName) + .appendEncodedPath(String.valueOf(fileSize)) + .appendEncodedPath(String.valueOf(System.currentTimeMillis())) + .build(); return ContentUris.withAppendedId(uniqueUri, id); } - private void persistToDisk(@NonNull Context context, - final long id, final InputStream input) - { - executor.submit(() -> { - try { - OutputStream output = new FileOutputStream(getFile(context, id)); - Util.copy(input, output); - } catch (IOException e) { - Log.w(TAG, e); - } - }); - } - - public Uri createForExternal(@NonNull Context context, @NonNull String mimeType) throws IOException, IllegalStateException, NullPointerException { - File target = new File(getExternalDir(context), System.currentTimeMillis() + "." + getExtensionFromMimeType(mimeType)); + private void persistToDisk(@NonNull Context context, final long id, final InputStream input) { + executor.submit( + () -> { + try { + OutputStream output = new FileOutputStream(getFile(context, id)); + Util.copy(input, output); + } catch (IOException e) { + Log.w(TAG, e); + } + }); + } + + public Uri createForExternal(@NonNull Context context, @NonNull String mimeType) + throws IOException, IllegalStateException, NullPointerException { + File target = + new File( + getExternalDir(context), + System.currentTimeMillis() + "." + getExtensionFromMimeType(mimeType)); return FileProviderUtil.getUriFor(context, target); } public boolean delete(@NonNull Context context, @NonNull Uri uri) { switch (MATCHER.match(uri)) { - case MATCH_OLD: - case MATCH_NEW: - return getFile(context, ContentUris.parseId(uri)).delete(); + case MATCH_OLD: + case MATCH_NEW: + return getFile(context, ContentUris.parseId(uri)).delete(); } //noinspection SimplifiableIfStatement @@ -147,13 +157,13 @@ public boolean delete(@NonNull Context context, @NonNull Uri uri) { } private File getFile(@NonNull Context context, long id) { - File legacy = getLegacyFile(context, id); - File cache = getCacheFile(context, id); + File legacy = getLegacyFile(context, id); + File cache = getCacheFile(context, id); File modernCache = getModernCacheFile(context, id); - if (legacy.exists()) return legacy; - else if (cache.exists()) return cache; - else return modernCache; + if (legacy.exists()) return legacy; + else if (cache.exists()) return cache; + else return modernCache; } private File getLegacyFile(@NonNull Context context, long id) { @@ -168,15 +178,17 @@ private File getModernCacheFile(@NonNull Context context, long id) { return new File(context.getCacheDir(), "capture-m-" + id + "." + BLOB_EXTENSION); } - public static @Nullable String getMimeType(@NonNull Context context, @NonNull Uri persistentBlobUri) { + public static @Nullable String getMimeType( + @NonNull Context context, @NonNull Uri persistentBlobUri) { if (!isAuthority(context, persistentBlobUri)) return null; return isExternalBlobUri(context, persistentBlobUri) ? getMimeTypeFromExtension(persistentBlobUri) : persistentBlobUri.getPathSegments().get(MIMETYPE_PATH_SEGMENT); } - public static @Nullable String getFileName(@NonNull Context context, @NonNull Uri persistentBlobUri) { - if (!isAuthority(context, persistentBlobUri)) return null; + public static @Nullable String getFileName( + @NonNull Context context, @NonNull Uri persistentBlobUri) { + if (!isAuthority(context, persistentBlobUri)) return null; if (isExternalBlobUri(context, persistentBlobUri)) return null; if (MATCHER.match(persistentBlobUri) == MATCH_OLD) return null; @@ -184,7 +196,7 @@ private File getModernCacheFile(@NonNull Context context, long id) { } public static @Nullable Long getFileSize(@NonNull Context context, Uri persistentBlobUri) { - if (!isAuthority(context, persistentBlobUri)) return null; + if (!isAuthority(context, persistentBlobUri)) return null; if (isExternalBlobUri(context, persistentBlobUri)) return null; if (MATCHER.match(persistentBlobUri) == MATCH_OLD) return null; @@ -202,14 +214,15 @@ private File getModernCacheFile(@NonNull Context context, long id) { } private static @NonNull String getMimeTypeFromExtension(@NonNull Uri uri) { - final String mimeType = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(MediaUtil.getFileExtensionFromUrl(uri.toString())); + final String mimeType = + MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(MediaUtil.getFileExtensionFromUrl(uri.toString())); return mimeType != null ? mimeType : "application/octet-stream"; } private static @NonNull File getExternalDir(Context context) throws IOException { File externalDir = context.getExternalCacheDir(); - if (externalDir==null) { + if (externalDir == null) { externalDir = context.getCacheDir(); } if (externalDir == null) { diff --git a/src/main/java/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java b/src/main/java/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java index 3d395c398..fb89f23d4 100644 --- a/src/main/java/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java +++ b/src/main/java/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.providers; import androidx.annotation.NonNull; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -10,8 +9,8 @@ public class SingleUseBlobProvider { - public static final String AUTHORITY = "org.thoughtcrime.securesms"; - public static final String PATH = "memory/*/#"; + public static final String AUTHORITY = "org.thoughtcrime.securesms"; + public static final String PATH = "memory/*/#"; private final Map cache = new HashMap<>(); @@ -28,8 +27,6 @@ private SingleUseBlobProvider() {} cache.remove(id); if (cached != null) return new ByteArrayInputStream(cached); - else throw new IOException("ID not found: " + id); - + else throw new IOException("ID not found: " + id); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/proxy/ProxyListAdapter.java b/src/main/java/org/thoughtcrime/securesms/proxy/ProxyListAdapter.java index 63217586b..1e6a21ba2 100644 --- a/src/main/java/org/thoughtcrime/securesms/proxy/ProxyListAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/proxy/ProxyListAdapter.java @@ -10,19 +10,15 @@ import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcLot; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.connect.DcHelper; - import java.util.Collections; import java.util.LinkedList; import java.util.List; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.connect.DcHelper; public class ProxyListAdapter extends BaseAdapter { private enum ProxyState { @@ -31,15 +27,14 @@ private enum ProxyState { NOT_CONNECTED, } - @NonNull private final Context context; - @NonNull private final DcContext dcContext; - @NonNull private final List proxies = new LinkedList<>(); + @NonNull private final Context context; + @NonNull private final DcContext dcContext; + @NonNull private final List proxies = new LinkedList<>(); @Nullable private ItemClickListener itemClickListener; @Nullable private ProxyState proxyState; @Nullable private String selectedProxy; - public ProxyListAdapter(@NonNull Context context) - { + public ProxyListAdapter(@NonNull Context context) { this.context = context; this.dcContext = DcHelper.getContext(context); } @@ -70,7 +65,7 @@ public View getView(final int position, View v, final ViewGroup parent) { ImageView checkmark = v.findViewById(R.id.checkmark); TextView status = v.findViewById(R.id.status); - final String proxyUrl = (String)getItem(position); + final String proxyUrl = (String) getItem(position); final DcLot qrParsed = dcContext.checkQr(proxyUrl); if (qrParsed.getState() == DcContext.DC_QR_PROXY) { host.setText(qrParsed.getText1()); @@ -81,7 +76,7 @@ public View getView(final int position, View v, final ViewGroup parent) { } if (proxyUrl.equals(selectedProxy)) { checkmark.setVisibility(View.VISIBLE); - if(dcContext.isConfigured() == 1 && dcContext.getConfigInt(CONFIG_PROXY_ENABLED) == 1) { + if (dcContext.isConfigured() == 1 && dcContext.getConfigInt(CONFIG_PROXY_ENABLED) == 1) { status.setVisibility(View.VISIBLE); status.setText(getConnectivityString()); } else { @@ -92,21 +87,26 @@ public View getView(final int position, View v, final ViewGroup parent) { status.setVisibility(View.GONE); } - v.setOnClickListener(view -> { - if (itemClickListener != null) { - itemClickListener.onItemClick(proxyUrl); - } - }); - v.findViewById(R.id.share).setOnClickListener(view -> { - if (itemClickListener != null) { - itemClickListener.onItemShare(proxyUrl); - } - }); - v.findViewById(R.id.delete).setOnClickListener(view -> { - if (itemClickListener != null) { - itemClickListener.onItemDelete(proxyUrl); - } - }); + v.setOnClickListener( + view -> { + if (itemClickListener != null) { + itemClickListener.onItemClick(proxyUrl); + } + }); + v.findViewById(R.id.share) + .setOnClickListener( + view -> { + if (itemClickListener != null) { + itemClickListener.onItemShare(proxyUrl); + } + }); + v.findViewById(R.id.delete) + .setOnClickListener( + view -> { + if (itemClickListener != null) { + itemClickListener.onItemDelete(proxyUrl); + } + }); return v; } @@ -116,7 +116,7 @@ public void changeData(String newProxies) { if (!TextUtils.isEmpty(newProxies)) { Collections.addAll(proxies, newProxies.split("\n")); } - selectedProxy = proxies.isEmpty()? null : proxies.get(0); + selectedProxy = proxies.isEmpty() ? null : proxies.get(0); proxyState = null; // to force notifyDataSetChanged() in refreshConnectivity() refreshConnectivity(); } @@ -168,8 +168,9 @@ public void setItemClickListener(@Nullable ItemClickListener listener) { public interface ItemClickListener { void onItemClick(String proxyUrl); + void onItemShare(String proxyUrl); + void onItemDelete(String proxyUrl); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/proxy/ProxySettingsActivity.java b/src/main/java/org/thoughtcrime/securesms/proxy/ProxySettingsActivity.java index 7b917e94a..80d08f852 100644 --- a/src/main/java/org/thoughtcrime/securesms/proxy/ProxySettingsActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/proxy/ProxySettingsActivity.java @@ -11,19 +11,17 @@ import android.widget.EditText; import android.widget.ListView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SwitchCompat; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcLot; import com.caverock.androidsvg.SVG; import com.caverock.androidsvg.SVGImageView; import com.caverock.androidsvg.SVGParseException; - +import java.util.LinkedList; import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcEventCenter; @@ -31,10 +29,8 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.LinkedList; - public class ProxySettingsActivity extends BaseActionBarActivity - implements ProxyListAdapter.ItemClickListener, DcEventCenter.DcEventDelegate { + implements ProxyListAdapter.ItemClickListener, DcEventCenter.DcEventDelegate { private SwitchCompat proxySwitch; private ProxyListAdapter adapter; @@ -60,14 +56,15 @@ public void onCreate(Bundle bundle) { adapter.setItemClickListener(this); proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1); - proxySwitch.setOnClickListener(l -> { - if (proxySwitch.isChecked() && adapter.getCount() == 0) { - showAddProxyDialog(); - } else { - DcHelper.set(this, CONFIG_PROXY_ENABLED, proxySwitch.isChecked()? "1" : "0"); - DcHelper.getContext(this).restartIo(); - } - }); + proxySwitch.setOnClickListener( + l -> { + if (proxySwitch.isChecked() && adapter.getCount() == 0) { + showAddProxyDialog(); + } else { + DcHelper.set(this, CONFIG_PROXY_ENABLED, proxySwitch.isChecked() ? "1" : "0"); + DcHelper.getContext(this).restartIo(); + } + }); proxyList.setAdapter(adapter); proxyList.addHeaderView(View.inflate(this, R.layout.proxy_list_header, null), null, false); @@ -123,33 +120,38 @@ public void onItemShare(String proxyUrl) { e.printStackTrace(); } - AlertDialog dialog = new AlertDialog.Builder(this) - .setView(view) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.proxy_share_link, (dlg, btn) -> { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, proxyUrl); - startActivity(Intent.createChooser(intent, getString(R.string.chat_share_with_title))); - }) - .show(); + AlertDialog dialog = + new AlertDialog.Builder(this) + .setView(view) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton( + R.string.proxy_share_link, + (dlg, btn) -> { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, proxyUrl); + startActivity( + Intent.createChooser(intent, getString(R.string.chat_share_with_title))); + }) + .show(); } @Override public void onItemDelete(String proxyUrl) { String host = DcHelper.getContext(this).checkQr(proxyUrl).getText1(); - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle(R.string.proxy_delete) - .setMessage(getString(R.string.proxy_delete_explain, host)) - .setPositiveButton(R.string.delete, (dlg, btn) -> deleteProxy(proxyUrl)) - .setNegativeButton(android.R.string.cancel, null) - .show(); + AlertDialog dialog = + new AlertDialog.Builder(this) + .setTitle(R.string.proxy_delete) + .setMessage(getString(R.string.proxy_delete_explain, host)) + .setPositiveButton(R.string.delete, (dlg, btn) -> deleteProxy(proxyUrl)) + .setNegativeButton(android.R.string.cancel, null) + .show(); Util.redPositiveButton(dialog); } private void deleteProxy(String proxyUrl) { final LinkedList proxies = new LinkedList<>(); - for (String proxy: DcHelper.get(this, CONFIG_PROXY_URL).split("\n")) { + for (String proxy : DcHelper.get(this, CONFIG_PROXY_URL).split("\n")) { if (!proxy.equals(proxyUrl)) { proxies.add(proxy); } @@ -170,30 +172,34 @@ private void showAddProxyDialog() { inputField.setHint(R.string.proxy_add_url_hint); new AlertDialog.Builder(this) - .setTitle(R.string.proxy_add) - .setMessage(R.string.proxy_add_explain) - .setView(view) - .setPositiveButton(R.string.proxy_use_proxy, (dialog, whichButton) -> { - String newProxy = inputField.getText().toString().trim(); - DcContext dcContext = DcHelper.getContext(this); - final DcLot qrParsed = dcContext.checkQr(newProxy); - if (qrParsed.getState() == DcContext.DC_QR_PROXY) { - dcContext.setConfigFromQr(newProxy); - DcHelper.getContext(this).restartIo(); - adapter.changeData(DcHelper.get(this, CONFIG_PROXY_URL)); - } else { - Toast.makeText(this, R.string.proxy_invalid, Toast.LENGTH_LONG).show(); - } - proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1); - }) - .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> { - if (proxySwitch.isChecked() && adapter.getCount() == 0) { - // user enabled switch without having proxies yet, revert - proxySwitch.setChecked(false); - } - }) - .setCancelable(false) - .show(); + .setTitle(R.string.proxy_add) + .setMessage(R.string.proxy_add_explain) + .setView(view) + .setPositiveButton( + R.string.proxy_use_proxy, + (dialog, whichButton) -> { + String newProxy = inputField.getText().toString().trim(); + DcContext dcContext = DcHelper.getContext(this); + final DcLot qrParsed = dcContext.checkQr(newProxy); + if (qrParsed.getState() == DcContext.DC_QR_PROXY) { + dcContext.setConfigFromQr(newProxy); + DcHelper.getContext(this).restartIo(); + adapter.changeData(DcHelper.get(this, CONFIG_PROXY_URL)); + } else { + Toast.makeText(this, R.string.proxy_invalid, Toast.LENGTH_LONG).show(); + } + proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1); + }) + .setNegativeButton( + android.R.string.cancel, + (dialog, whichButton) -> { + if (proxySwitch.isChecked() && adapter.getCount() == 0) { + // user enabled switch without having proxies yet, revert + proxySwitch.setChecked(false); + } + }) + .setCancelable(false) + .show(); } private void handleOpenProxyUrl() { @@ -207,17 +213,19 @@ private void handleOpenProxyUrl() { final DcLot qrParsed = dcContext.checkQr(uri.toString()); if (qrParsed.getState() == DcContext.DC_QR_PROXY) { new AlertDialog.Builder(this) - .setTitle(R.string.proxy_use_proxy) - .setMessage(getString(R.string.proxy_use_proxy_confirm, qrParsed.getText1())) - .setPositiveButton(R.string.proxy_use_proxy, (dlg, btn) -> { - dcContext.setConfigFromQr(uri.toString()); - dcContext.restartIo(); - adapter.changeData(DcHelper.get(this, CONFIG_PROXY_URL)); - proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1); - }) - .setNegativeButton(R.string.cancel, null) - .setCancelable(false) - .show(); + .setTitle(R.string.proxy_use_proxy) + .setMessage(getString(R.string.proxy_use_proxy_confirm, qrParsed.getText1())) + .setPositiveButton( + R.string.proxy_use_proxy, + (dlg, btn) -> { + dcContext.setConfigFromQr(uri.toString()); + dcContext.restartIo(); + adapter.changeData(DcHelper.get(this, CONFIG_PROXY_URL)); + proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1); + }) + .setNegativeButton(R.string.cancel, null) + .setCancelable(false) + .show(); } else { Toast.makeText(this, R.string.proxy_invalid, Toast.LENGTH_LONG).show(); } @@ -230,5 +238,4 @@ public void handleEvent(@NonNull DcEvent event) { adapter.refreshConnectivity(); } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/BackupProviderFragment.java b/src/main/java/org/thoughtcrime/securesms/qr/BackupProviderFragment.java index 3fc78a98e..6cffc3212 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/BackupProviderFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/BackupProviderFragment.java @@ -11,17 +11,14 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; - import com.b44t.messenger.DcBackupProvider; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.caverock.androidsvg.SVG; import com.caverock.androidsvg.SVGImageView; import com.caverock.androidsvg.SVGParseException; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; @@ -29,177 +26,188 @@ public class BackupProviderFragment extends Fragment implements DcEventCenter.DcEventDelegate { - private final static String TAG = BackupProviderFragment.class.getSimpleName(); - - private DcContext dcContext; - private DcBackupProvider dcBackupProvider; - - private TextView statusLine; - private ProgressBar progressBar; - private View topText; - private SVGImageView qrImageView; - private boolean isFinishing; - private Thread prepareThread; - private Thread waitThread; - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } + private static final String TAG = BackupProviderFragment.class.getSimpleName(); + + private DcContext dcContext; + private DcBackupProvider dcBackupProvider; + + private TextView statusLine; + private ProgressBar progressBar; + private View topText; + private SVGImageView qrImageView; + private boolean isFinishing; + private Thread prepareThread; + private Thread waitThread; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.backup_provider_fragment, container, false); + statusLine = view.findViewById(R.id.status_line); + progressBar = view.findViewById(R.id.progress_bar); + topText = view.findViewById(R.id.top_text); + qrImageView = view.findViewById(R.id.qrImage); + setHasOptionsMenu(true); + + statusLine.setText(R.string.preparing_account); + progressBar.setIndeterminate(true); + + dcContext = DcHelper.getContext(getActivity()); + DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_IMEX_PROGRESS, this); + + prepareThread = + new Thread( + () -> { + Log.i(TAG, "##### newBackupProvider()"); + dcBackupProvider = dcContext.newBackupProvider(); + Log.i(TAG, "##### newBackupProvider() returned"); + Util.runOnMain( + () -> { + BackupTransferActivity activity = getTransferActivity(); + if (activity == null || activity.isFinishing() || isFinishing) { + return; + } + progressBar.setVisibility(View.GONE); + if (!dcBackupProvider.isOk()) { + activity.setTransferError("Cannot create backup provider"); + return; + } + statusLine.setVisibility(View.GONE); + topText.setVisibility(View.VISIBLE); + try { + SVG svg = + SVG.getFromString(QrShowFragment.fixSVG(dcBackupProvider.getQrSvg())); + qrImageView.setSVG(svg); + qrImageView.setVisibility(View.VISIBLE); + } catch (SVGParseException e) { + e.printStackTrace(); + } + waitThread = + new Thread( + () -> { + Log.i( + TAG, + "##### waitForReceiver() with qr: " + dcBackupProvider.getQr()); + dcBackupProvider.waitForReceiver(); + Log.i(TAG, "##### done waiting"); + }); + waitThread.start(); + }); + }); + prepareThread.start(); - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.backup_provider_fragment, container, false); - statusLine = view.findViewById(R.id.status_line); - progressBar = view.findViewById(R.id.progress_bar); - topText = view.findViewById(R.id.top_text); - qrImageView = view.findViewById(R.id.qrImage); - setHasOptionsMenu(true); + BackupTransferActivity.appendSSID(getActivity(), view.findViewById(R.id.same_network_hint)); - statusLine.setText(R.string.preparing_account); - progressBar.setIndeterminate(true); - - dcContext = DcHelper.getContext(getActivity()); - DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_IMEX_PROGRESS, this); - - prepareThread = new Thread(() -> { - Log.i(TAG, "##### newBackupProvider()"); - dcBackupProvider = dcContext.newBackupProvider(); - Log.i(TAG, "##### newBackupProvider() returned"); - Util.runOnMain(() -> { - BackupTransferActivity activity = getTransferActivity(); - if (activity == null || activity.isFinishing() || isFinishing) { - return; - } - progressBar.setVisibility(View.GONE); - if (!dcBackupProvider.isOk()) { - activity.setTransferError("Cannot create backup provider"); - return; - } - statusLine.setVisibility(View.GONE); - topText.setVisibility(View.VISIBLE); - try { - SVG svg = SVG.getFromString(QrShowFragment.fixSVG(dcBackupProvider.getQrSvg())); - qrImageView.setSVG(svg); - qrImageView.setVisibility(View.VISIBLE); - } catch (SVGParseException e) { - e.printStackTrace(); - } - waitThread = new Thread(() -> { - Log.i(TAG, "##### waitForReceiver() with qr: "+dcBackupProvider.getQr()); - dcBackupProvider.waitForReceiver(); - Log.i(TAG, "##### done waiting"); - }); - waitThread.start(); - }); - }); - prepareThread.start(); + return view; + } - BackupTransferActivity.appendSSID(getActivity(), view.findViewById(R.id.same_network_hint)); + @Override + public void onDestroyView() { + isFinishing = true; + DcHelper.getEventCenter(getActivity()).removeObservers(this); + dcContext.stopOngoingProcess(); - return view; + try { + prepareThread.join(); + } catch (Exception e) { + e.printStackTrace(); } - @Override - public void onDestroyView() { - isFinishing = true; - DcHelper.getEventCenter(getActivity()).removeObservers(this); - dcContext.stopOngoingProcess(); - - try { - prepareThread.join(); - } catch (Exception e) { - e.printStackTrace(); - } - - // prepareThread has failed to create waitThread, as we already did wait for prepareThread right above. - // Order of waiting is important here. - if (waitThread!=null) { - try { - waitThread.join(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - if (dcBackupProvider != null) { - dcBackupProvider.unref(); - } - super.onDestroyView(); + // prepareThread has failed to create waitThread, as we already did wait for prepareThread right + // above. + // Order of waiting is important here. + if (waitThread != null) { + try { + waitThread.join(); + } catch (Exception e) { + e.printStackTrace(); + } } - @Override - public void onPrepareOptionsMenu(Menu menu) { - menu.findItem(R.id.copy).setVisible(qrImageView.getVisibility() == View.VISIBLE); - super.onPrepareOptionsMenu(menu); + if (dcBackupProvider != null) { + dcBackupProvider.unref(); + } + super.onDestroyView(); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.copy).setVisible(qrImageView.getVisibility() == View.VISIBLE); + super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + super.onOptionsItemSelected(item); + + if (item.getItemId() == R.id.copy) { + if (dcBackupProvider != null) { + Util.writeTextToClipboard(getActivity(), dcBackupProvider.getQr()); + Toast.makeText(getActivity(), getString(R.string.done), Toast.LENGTH_SHORT).show(); + getTransferActivity().warnAboutCopiedQrCodeOnAbort = true; + } + return true; } - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - super.onOptionsItemSelected(item); - - if (item.getItemId() == R.id.copy) { - if (dcBackupProvider != null) { - Util.writeTextToClipboard(getActivity(), dcBackupProvider.getQr()); - Toast.makeText(getActivity(), getString(R.string.done), Toast.LENGTH_SHORT).show(); - getTransferActivity().warnAboutCopiedQrCodeOnAbort = true; - } - return true; + return false; + } + + @Override + public void handleEvent(@NonNull DcEvent event) { + if (event.getId() == DcContext.DC_EVENT_IMEX_PROGRESS) { + if (isFinishing) { + return; } - return false; - } + int permille = event.getData1Int(); + int percent = 0; + int percentMax = 0; + boolean hideQrCode = false; + String statusLineText = ""; + + Log.i(TAG, "DC_EVENT_IMEX_PROGRESS, " + permille); + if (permille == 0) { + getTransferActivity().setTransferError("Sending Error"); + hideQrCode = true; + } else if (permille < 1000) { + percent = permille / 10; + percentMax = 100; + statusLineText = getString(R.string.transferring); + hideQrCode = true; + } else if (permille == 1000) { + statusLineText = getString(R.string.done) + " \uD83D\uDE00"; + getTransferActivity() + .setTransferState(BackupTransferActivity.TransferState.TRANSFER_SUCCESS); + progressBar.setVisibility(View.GONE); + hideQrCode = true; + } - @Override - public void handleEvent(@NonNull DcEvent event) { - if (event.getId() == DcContext.DC_EVENT_IMEX_PROGRESS) { - if (isFinishing) { - return; - } - - int permille = event.getData1Int(); - int percent = 0; - int percentMax = 0; - boolean hideQrCode = false; - String statusLineText = ""; - - Log.i(TAG,"DC_EVENT_IMEX_PROGRESS, " + permille); - if (permille == 0) { - getTransferActivity().setTransferError("Sending Error"); - hideQrCode = true; - } else if (permille < 1000) { - percent = permille/10; - percentMax = 100; - statusLineText = getString(R.string.transferring); - hideQrCode = true; - } else if (permille == 1000) { - statusLineText = getString(R.string.done) + " \uD83D\uDE00"; - getTransferActivity().setTransferState(BackupTransferActivity.TransferState.TRANSFER_SUCCESS); - progressBar.setVisibility(View.GONE); - hideQrCode = true; - } - - statusLine.setText(statusLineText); - getTransferActivity().notificationController.setProgress(percentMax, percent, statusLineText); - if (percentMax == 0) { - progressBar.setIndeterminate(true); - } else { - progressBar.setIndeterminate(false); - progressBar.setMax(percentMax); - progressBar.setProgress(percent); - } - - if (hideQrCode && qrImageView.getVisibility() != View.GONE) { - qrImageView.setVisibility(View.GONE); - topText.setVisibility(View.GONE); - statusLine.setVisibility(View.VISIBLE); - progressBar.setVisibility(permille == 1000 ? View.GONE : View.VISIBLE); - } - } - } + statusLine.setText(statusLineText); + getTransferActivity().notificationController.setProgress(percentMax, percent, statusLineText); + if (percentMax == 0) { + progressBar.setIndeterminate(true); + } else { + progressBar.setIndeterminate(false); + progressBar.setMax(percentMax); + progressBar.setProgress(percent); + } - private BackupTransferActivity getTransferActivity() { - return (BackupTransferActivity) getActivity(); + if (hideQrCode && qrImageView.getVisibility() != View.GONE) { + qrImageView.setVisibility(View.GONE); + topText.setVisibility(View.GONE); + statusLine.setVisibility(View.VISIBLE); + progressBar.setVisibility(permille == 1000 ? View.GONE : View.VISIBLE); + } } + } + + private BackupTransferActivity getTransferActivity() { + return (BackupTransferActivity) getActivity(); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/BackupReceiverFragment.java b/src/main/java/org/thoughtcrime/securesms/qr/BackupReceiverFragment.java index 9dc659633..651e87ceb 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/BackupReceiverFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/BackupReceiverFragment.java @@ -8,109 +8,110 @@ import android.view.WindowManager; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.util.Util; - public class BackupReceiverFragment extends Fragment implements DcEventCenter.DcEventDelegate { - private final static String TAG = BackupProviderFragment.class.getSimpleName(); - - private DcContext dcContext; - private TextView statusLine; - private ProgressBar progressBar; - private TextView sameNetworkHint; - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.backup_receiver_fragment, container, false); - statusLine = view.findViewById(R.id.status_line); - progressBar = view.findViewById(R.id.progress_bar); - sameNetworkHint = view.findViewById(R.id.same_network_hint); - - statusLine.setText(R.string.connectivity_connecting); + private static final String TAG = BackupProviderFragment.class.getSimpleName(); + + private DcContext dcContext; + private TextView statusLine; + private ProgressBar progressBar; + private TextView sameNetworkHint; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.backup_receiver_fragment, container, false); + statusLine = view.findViewById(R.id.status_line); + progressBar = view.findViewById(R.id.progress_bar); + sameNetworkHint = view.findViewById(R.id.same_network_hint); + + statusLine.setText(R.string.connectivity_connecting); + progressBar.setIndeterminate(true); + + dcContext = DcHelper.getContext(getActivity()); + DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_IMEX_PROGRESS, this); + + String qrCode = getActivity().getIntent().getStringExtra(BackupTransferActivity.QR_CODE); + + new Thread( + () -> { + Log.i(TAG, "##### receiveBackup() with qr: " + qrCode); + boolean res = dcContext.receiveBackup(qrCode); + Log.i(TAG, "##### receiveBackup() done with result: " + res); + }) + .start(); + + BackupTransferActivity.appendSSID(getActivity(), sameNetworkHint); + + return view; + } + + @Override + public void onDestroyView() { + dcContext.stopOngoingProcess(); + super.onDestroyView(); + DcHelper.getEventCenter(getActivity()).removeObservers(this); + } + + @Override + public void handleEvent(@NonNull DcEvent event) { + if (event.getId() == DcContext.DC_EVENT_IMEX_PROGRESS) { + int permille = event.getData1Int(); + int percent = 0; + int percentMax = 0; + boolean hideSameNetworkHint = false; + String statusLineText = ""; + + Log.i(TAG, "DC_EVENT_IMEX_PROGRESS, " + permille); + if (permille == 0) { + DcHelper.maybeShowMigrationError(getTransferActivity()); + getTransferActivity().setTransferError("Receiving Error"); + } else if (permille < 1000) { + percent = permille / 10; + percentMax = 100; + String formattedPercent = + percent > 0 ? String.format(Util.getLocale(), " %d%%", percent) : ""; + statusLineText = getString(R.string.transferring) + formattedPercent; + hideSameNetworkHint = true; + } else if (permille == 1000) { + getTransferActivity() + .setTransferState(BackupTransferActivity.TransferState.TRANSFER_SUCCESS); + getTransferActivity().doFinish(); + return; + } + + statusLine.setText(statusLineText); + getTransferActivity().notificationController.setProgress(percentMax, percent, statusLineText); + if (percentMax == 0) { progressBar.setIndeterminate(true); - - dcContext = DcHelper.getContext(getActivity()); - DcHelper.getEventCenter(getActivity()).addObserver(DcContext.DC_EVENT_IMEX_PROGRESS, this); - - String qrCode = getActivity().getIntent().getStringExtra(BackupTransferActivity.QR_CODE); - - new Thread(() -> { - Log.i(TAG, "##### receiveBackup() with qr: "+qrCode); - boolean res = dcContext.receiveBackup(qrCode); - Log.i(TAG, "##### receiveBackup() done with result: "+res); - }).start(); - - BackupTransferActivity.appendSSID(getActivity(), sameNetworkHint); - - return view; + } else { + progressBar.setIndeterminate(false); + progressBar.setMax(percentMax); + progressBar.setProgress(percent); + } + + if (hideSameNetworkHint && sameNetworkHint.getVisibility() != View.GONE) { + sameNetworkHint.setVisibility(View.GONE); + } } + } - @Override - public void onDestroyView() { - dcContext.stopOngoingProcess(); - super.onDestroyView(); - DcHelper.getEventCenter(getActivity()).removeObservers(this); - } - - @Override - public void handleEvent(@NonNull DcEvent event) { - if (event.getId() == DcContext.DC_EVENT_IMEX_PROGRESS) { - int permille = event.getData1Int(); - int percent = 0; - int percentMax = 0; - boolean hideSameNetworkHint = false; - String statusLineText = ""; - - Log.i(TAG,"DC_EVENT_IMEX_PROGRESS, " + permille); - if (permille == 0) { - DcHelper.maybeShowMigrationError(getTransferActivity()); - getTransferActivity().setTransferError("Receiving Error"); - } else if (permille < 1000) { - percent = permille/10; - percentMax = 100; - String formattedPercent = percent > 0 ? String.format(Util.getLocale(), " %d%%", percent) : ""; - statusLineText = getString(R.string.transferring) + formattedPercent; - hideSameNetworkHint = true; - } else if (permille == 1000) { - getTransferActivity().setTransferState(BackupTransferActivity.TransferState.TRANSFER_SUCCESS); - getTransferActivity().doFinish(); - return; - } - - statusLine.setText(statusLineText); - getTransferActivity().notificationController.setProgress(percentMax, percent, statusLineText); - if (percentMax == 0) { - progressBar.setIndeterminate(true); - } else { - progressBar.setIndeterminate(false); - progressBar.setMax(percentMax); - progressBar.setProgress(percent); - } - - if (hideSameNetworkHint && sameNetworkHint.getVisibility() != View.GONE) { - sameNetworkHint.setVisibility(View.GONE); - } - } - } - - private BackupTransferActivity getTransferActivity() { - return (BackupTransferActivity) getActivity(); - } + private BackupTransferActivity getTransferActivity() { + return (BackupTransferActivity) getActivity(); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/BackupTransferActivity.java b/src/main/java/org/thoughtcrime/securesms/qr/BackupTransferActivity.java index 92ddb1bfa..303b3a2a0 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/BackupTransferActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/BackupTransferActivity.java @@ -10,12 +10,10 @@ import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; - import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; - import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.ConversationListActivity; @@ -29,208 +27,235 @@ public class BackupTransferActivity extends BaseActionBarActivity { - private final static String TAG = BackupTransferActivity.class.getSimpleName(); + private static final String TAG = BackupTransferActivity.class.getSimpleName(); - public enum TransferMode { - INVALID(0), - SENDER_SHOW_QR(1), - RECEIVER_SCAN_QR(2); - private final int i; - TransferMode(int i) { this.i = i; } - public int getInt() { return i; } - public static TransferMode fromInt(int i) { return values()[i]; } - }; + public enum TransferMode { + INVALID(0), + SENDER_SHOW_QR(1), + RECEIVER_SCAN_QR(2); + private final int i; - public enum TransferState { - TRANSFER_UNKNOWN, - TRANSFER_ERROR, - TRANSFER_SUCCESS; - }; + TransferMode(int i) { + this.i = i; + } - public static final String TRANSFER_MODE = "transfer_mode"; - public static final String QR_CODE = "qr_code"; + public int getInt() { + return i; + } - private TransferMode transferMode = TransferMode.RECEIVER_SCAN_QR; - private TransferState transferState = TransferState.TRANSFER_UNKNOWN; + public static TransferMode fromInt(int i) { + return values()[i]; + } + }; - NotificationController notificationController; - private boolean notificationControllerClosed = false; - public boolean warnAboutCopiedQrCodeOnAbort = false; + public enum TransferState { + TRANSFER_UNKNOWN, + TRANSFER_ERROR, + TRANSFER_SUCCESS; + }; - private OnBackPressedCallback backPressedCallback; + public static final String TRANSFER_MODE = "transfer_mode"; + public static final String QR_CODE = "qr_code"; - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); + private TransferMode transferMode = TransferMode.RECEIVER_SCAN_QR; + private TransferState transferState = TransferState.TRANSFER_UNKNOWN; - transferMode = TransferMode.fromInt(getIntent().getIntExtra(TRANSFER_MODE, TransferMode.INVALID.getInt())); - if (transferMode == TransferMode.INVALID) { - throw new RuntimeException("bad transfer mode"); - } + NotificationController notificationController; + private boolean notificationControllerClosed = false; + public boolean warnAboutCopiedQrCodeOnAbort = false; + + private OnBackPressedCallback backPressedCallback; - DcHelper.getAccounts(this).stopIo(); + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); - String title = getString(transferMode == TransferMode.RECEIVER_SCAN_QR ? R.string.multidevice_receiver_title : R.string.multidevice_title); - notificationController = GenericForegroundService.startForegroundTask(this, title); + transferMode = + TransferMode.fromInt(getIntent().getIntExtra(TRANSFER_MODE, TransferMode.INVALID.getInt())); + if (transferMode == TransferMode.INVALID) { + throw new RuntimeException("bad transfer mode"); + } - setContentView(R.layout.backup_provider_activity); + DcHelper.getAccounts(this).stopIo(); - switch(transferMode) { - case SENDER_SHOW_QR: - initFragment(R.id.backup_provider_fragment, new BackupProviderFragment(), icicle); - break; + String title = + getString( + transferMode == TransferMode.RECEIVER_SCAN_QR + ? R.string.multidevice_receiver_title + : R.string.multidevice_title); + notificationController = GenericForegroundService.startForegroundTask(this, title); - case RECEIVER_SCAN_QR: - initFragment(R.id.backup_provider_fragment, new BackupReceiverFragment(), icicle); - break; - } + setContentView(R.layout.backup_provider_activity); - ActionBar supportActionBar = getSupportActionBar(); - supportActionBar.setDisplayHomeAsUpEnabled(true); - supportActionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); - supportActionBar.setTitle(title); + switch (transferMode) { + case SENDER_SHOW_QR: + initFragment(R.id.backup_provider_fragment, new BackupProviderFragment(), icicle); + break; + + case RECEIVER_SCAN_QR: + initFragment(R.id.backup_provider_fragment, new BackupReceiverFragment(), icicle); + break; + } - // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(findViewById(R.id.backup_provider_fragment)); + ActionBar supportActionBar = getSupportActionBar(); + supportActionBar.setDisplayHomeAsUpEnabled(true); + supportActionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + supportActionBar.setTitle(title); - backPressedCallback = new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - finishOrAskToFinish(); - } + // add padding to avoid content hidden behind system bars + ViewUtil.applyWindowInsets(findViewById(R.id.backup_provider_fragment)); + + backPressedCallback = + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + finishOrAskToFinish(); + } }; - getOnBackPressedDispatcher().addCallback(this, backPressedCallback); - } + getOnBackPressedDispatcher().addCallback(this, backPressedCallback); + } - @Override - protected void onDestroy() { - super.onDestroy(); - if (!notificationControllerClosed) { - notificationController.close(); - } - DcHelper.getAccounts(this).startIo(); + @Override + protected void onDestroy() { + super.onDestroy(); + if (!notificationControllerClosed) { + notificationController.close(); } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.clear(); - getMenuInflater().inflate(R.menu.backup_transfer_menu, menu); - return super.onPrepareOptionsMenu(menu); + DcHelper.getAccounts(this).startIo(); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + getMenuInflater().inflate(R.menu.backup_transfer_menu, menu); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + super.onOptionsItemSelected(item); + + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + getOnBackPressedDispatcher().onBackPressed(); + return true; + } else if (itemId == R.id.troubleshooting) { + DcHelper.openHelp(this, "#multiclient"); + return true; + } else if (itemId == R.id.view_log_button) { + startActivity(new Intent(this, LogViewActivity.class)); + return true; } - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - super.onOptionsItemSelected(item); - - int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - getOnBackPressedDispatcher().onBackPressed(); - return true; - } else if (itemId == R.id.troubleshooting) { - DcHelper.openHelp(this, "#multiclient"); - return true; - } else if (itemId == R.id.view_log_button) { - startActivity(new Intent(this, LogViewActivity.class)); - return true; - } + return false; + } - return false; - } + public void setTransferState(TransferState transferState) { + this.transferState = transferState; + } - public void setTransferState(TransferState transferState) { - this.transferState = transferState; - } + public void setTransferError(@NonNull String errorContext) { + if (this.transferState != TransferState.TRANSFER_ERROR) { + this.transferState = TransferState.TRANSFER_ERROR; - public void setTransferError(@NonNull String errorContext) { - if (this.transferState != TransferState.TRANSFER_ERROR) { - this.transferState = TransferState.TRANSFER_ERROR; - - String lastError = DcHelper.getContext(this).getLastError(); - if (lastError.isEmpty()) { - lastError = ""; - } - - String error = errorContext; - if (!error.isEmpty()) { - error += ": "; - } - error += lastError; - - new AlertDialog.Builder(this) - .setMessage(error) - .setPositiveButton(android.R.string.ok, null) - .setCancelable(false) - .show(); - } - } + String lastError = DcHelper.getContext(this).getLastError(); + if (lastError.isEmpty()) { + lastError = ""; + } - private void finishOrAskToFinish() { - switch (transferState) { - case TRANSFER_ERROR: - case TRANSFER_SUCCESS: - doFinish(); - break; - - default: - String msg = getString(R.string.multidevice_abort); - if (warnAboutCopiedQrCodeOnAbort) { - msg += "\n\n" + getString(R.string.multidevice_abort_will_invalidate_copied_qr); - } - new AlertDialog.Builder(this) - .setMessage(msg) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> doFinish()) - .setNegativeButton(R.string.cancel, null) - .show(); - break; - } - } + String error = errorContext; + if (!error.isEmpty()) { + error += ": "; + } + error += lastError; - public void doFinish() { - // the permanent notification will prevent other activities to be started and kill BackupTransferActivity; - // close it before starting other activities - notificationController.close(); - notificationControllerClosed = true; - - if (transferMode == TransferMode.RECEIVER_SCAN_QR && transferState == TransferState.TRANSFER_SUCCESS) { - startActivity(new Intent(getApplicationContext(), ConversationListActivity.class)); - } else if (transferMode == TransferMode.SENDER_SHOW_QR) { - // restart the activities that were removed when BackupTransferActivity was started at (**2) - // (we removed the activity backstack as otherwise a tap on the ArcaneChat icon on the home screen would - // call onNewIntent() which cannot be aborted and will kill BackupTransferActivity. - // if all activities are removed, onCreate() will be called and that can be aborted, so that - // a tap in the home icon just opens BackupTransferActivity. - // (the user can leave ArcaneChat during backup transfer :) - // a proper fix would maybe to not rely onNewIntent() at all - but that would require more refactorings - // and needs lots if testing in complicated areas (share ...)) - startActivity(new Intent(getApplicationContext(), ConversationListActivity.class)); - startActivity(new Intent(this, ApplicationPreferencesActivity.class)); - overridePendingTransition(0, 0); + new AlertDialog.Builder(this) + .setMessage(error) + .setPositiveButton(android.R.string.ok, null) + .setCancelable(false) + .show(); + } + } + + private void finishOrAskToFinish() { + switch (transferState) { + case TRANSFER_ERROR: + case TRANSFER_SUCCESS: + doFinish(); + break; + + default: + String msg = getString(R.string.multidevice_abort); + if (warnAboutCopiedQrCodeOnAbort) { + msg += "\n\n" + getString(R.string.multidevice_abort_will_invalidate_copied_qr); } - finish(); + new AlertDialog.Builder(this) + .setMessage(msg) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> doFinish()) + .setNegativeButton(R.string.cancel, null) + .show(); + break; + } + } + + public void doFinish() { + // the permanent notification will prevent other activities to be started and kill + // BackupTransferActivity; + // close it before starting other activities + notificationController.close(); + notificationControllerClosed = true; + + if (transferMode == TransferMode.RECEIVER_SCAN_QR + && transferState == TransferState.TRANSFER_SUCCESS) { + startActivity(new Intent(getApplicationContext(), ConversationListActivity.class)); + } else if (transferMode == TransferMode.SENDER_SHOW_QR) { + // restart the activities that were removed when BackupTransferActivity was started at (**2) + // (we removed the activity backstack as otherwise a tap on the ArcaneChat icon on the home + // screen would + // call onNewIntent() which cannot be aborted and will kill BackupTransferActivity. + // if all activities are removed, onCreate() will be called and that can be aborted, so that + // a tap in the home icon just opens BackupTransferActivity. + // (the user can leave ArcaneChat during backup transfer :) + // a proper fix would maybe to not rely onNewIntent() at all - but that would require more + // refactorings + // and needs lots if testing in complicated areas (share ...)) + startActivity(new Intent(getApplicationContext(), ConversationListActivity.class)); + startActivity(new Intent(this, ApplicationPreferencesActivity.class)); + overridePendingTransition(0, 0); } + finish(); + } - public static void appendSSID(Activity activity, final TextView textView) { - if (textView != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - new Thread(() -> { + public static void appendSSID(Activity activity, final TextView textView) { + if (textView != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + new Thread( + () -> { try { - // depending on the android version, getting the SSID requires none, all or one of - // ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION, NEARBY_WIFI_DEVICES, ACCESS_WIFI_STATE, ACCESS_NETWORK_STATE and maybe even more. - final WifiManager wifiManager = (WifiManager)activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - if (wifiManager.isWifiEnabled()) { - final WifiInfo info = wifiManager.getConnectionInfo(); - final String ssid = info.getSSID(); - Log.i(TAG, "wifi ssid: "+ssid); - if (!ssid.equals("")) { // "" may be returned on insufficient rights - Util.runOnMain(() -> { - textView.setText(textView.getText() + " (" + ssid + ")"); - }); - } + // depending on the android version, getting the SSID requires none, all or one of + // ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION, NEARBY_WIFI_DEVICES, + // ACCESS_WIFI_STATE, ACCESS_NETWORK_STATE and maybe even more. + final WifiManager wifiManager = + (WifiManager) + activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifiManager.isWifiEnabled()) { + final WifiInfo info = wifiManager.getConnectionInfo(); + final String ssid = info.getSSID(); + Log.i(TAG, "wifi ssid: " + ssid); + if (!ssid.equals( + "")) { // "" may be returned on insufficient + // rights + Util.runOnMain( + () -> { + textView.setText(textView.getText() + " (" + ssid + ")"); + }); } + } } catch (Exception e) { - e.printStackTrace(); + e.printStackTrace(); } - }).start(); - } + }) + .start(); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/CustomCaptureManager.java b/src/main/java/org/thoughtcrime/securesms/qr/CustomCaptureManager.java index 22ceed6f2..86958eb63 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/CustomCaptureManager.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/CustomCaptureManager.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.qr; import android.app.Activity; - import com.journeyapps.barcodescanner.BarcodeResult; import com.journeyapps.barcodescanner.CaptureManager; import com.journeyapps.barcodescanner.DecoratedBarcodeView; @@ -21,9 +20,11 @@ public void setResultInterceptor(OnResultInterceptor interceptor) { @Override protected void returnResult(BarcodeResult rawResult) { if (interceptor != null) { - interceptor.onResult(rawResult, () -> { - super.returnResult(rawResult); - }); + interceptor.onResult( + rawResult, + () -> { + super.returnResult(rawResult); + }); } else { super.returnResult(rawResult); } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java index a9f08134c..eecb2fdf9 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrActivity.java @@ -13,13 +13,11 @@ import android.view.MenuItem; import android.view.View; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.viewpager.widget.ViewPager; - import com.google.android.material.tabs.TabLayout; import com.google.zxing.BinaryBitmap; import com.google.zxing.MultiFormatReader; @@ -28,7 +26,8 @@ import com.google.zxing.Result; import com.google.zxing.client.android.Intents; import com.google.zxing.common.HybridBinarizer; - +import java.io.FileNotFoundException; +import java.io.InputStream; import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; @@ -39,233 +38,237 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.io.FileNotFoundException; -import java.io.InputStream; - public class QrActivity extends BaseActionBarActivity implements View.OnClickListener { - private final static String TAG = QrActivity.class.getSimpleName(); - public final static String EXTRA_SCAN_RELAY = "scan_relay"; - - private final static int REQUEST_CODE_IMAGE = 46243; - private final static int TAB_SHOW = 0; - private final static int TAB_SCAN = 1; - - private TabLayout tabLayout; - private ViewPager viewPager; - private QrShowFragment qrShowFragment; - private boolean scanRelay; - - @Override - protected void onPreCreate() { - dynamicTheme = new DynamicNoActionBarTheme(); - super.onPreCreate(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_qr); - - scanRelay = getIntent().getBooleanExtra(EXTRA_SCAN_RELAY, false); - - qrShowFragment = new QrShowFragment(this); - tabLayout = ViewUtil.findById(this, R.id.tab_layout); - viewPager = ViewUtil.findById(this, R.id.pager); - ProfilePagerAdapter adapter = new ProfilePagerAdapter(this, getSupportFragmentManager()); - viewPager.setAdapter(adapter); - - setSupportActionBar(ViewUtil.findById(this, R.id.toolbar)); - assert getSupportActionBar() != null; - getSupportActionBar().setTitle(scanRelay? R.string.add_transport : R.string.menu_new_contact); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - viewPager.setCurrentItem(scanRelay? TAB_SCAN : TAB_SHOW); - if (scanRelay) tabLayout.setVisibility(View.GONE); - - viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - } - - @Override - public void onPageSelected(int position) { - QrActivity.this.invalidateOptionsMenu(); - checkPermissions(position, adapter, viewPager); - } - - @Override - public void onPageScrollStateChanged(int state) { - } + private static final String TAG = QrActivity.class.getSimpleName(); + public static final String EXTRA_SCAN_RELAY = "scan_relay"; + + private static final int REQUEST_CODE_IMAGE = 46243; + private static final int TAB_SHOW = 0; + private static final int TAB_SCAN = 1; + + private TabLayout tabLayout; + private ViewPager viewPager; + private QrShowFragment qrShowFragment; + private boolean scanRelay; + + @Override + protected void onPreCreate() { + dynamicTheme = new DynamicNoActionBarTheme(); + super.onPreCreate(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_qr); + + scanRelay = getIntent().getBooleanExtra(EXTRA_SCAN_RELAY, false); + + qrShowFragment = new QrShowFragment(this); + tabLayout = ViewUtil.findById(this, R.id.tab_layout); + viewPager = ViewUtil.findById(this, R.id.pager); + ProfilePagerAdapter adapter = new ProfilePagerAdapter(this, getSupportFragmentManager()); + viewPager.setAdapter(adapter); + + setSupportActionBar(ViewUtil.findById(this, R.id.toolbar)); + assert getSupportActionBar() != null; + getSupportActionBar().setTitle(scanRelay ? R.string.add_transport : R.string.menu_new_contact); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + viewPager.setCurrentItem(scanRelay ? TAB_SCAN : TAB_SHOW); + if (scanRelay) tabLayout.setVisibility(View.GONE); + + viewPager.addOnPageChangeListener( + new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled( + int position, float positionOffset, int positionOffsetPixels) {} + + @Override + public void onPageSelected(int position) { + QrActivity.this.invalidateOptionsMenu(); + checkPermissions(position, adapter, viewPager); + } + + @Override + public void onPageScrollStateChanged(int state) {} }); - tabLayout.setupWithViewPager(viewPager); + tabLayout.setupWithViewPager(viewPager); + } + + private void checkPermissions(int position, ProfilePagerAdapter adapter, ViewPager viewPager) { + if (position == TAB_SCAN) { + Permissions.with(QrActivity.this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied)) + .onAllGranted( + () -> + ((QrScanFragment) adapter.getItem(TAB_SCAN)) + .handleQrScanWithPermissions(QrActivity.this)) + .onAnyDenied( + () -> { + if (scanRelay) { + Toast.makeText( + this, getString(R.string.chat_camera_unavailable), Toast.LENGTH_LONG) + .show(); + } else { + viewPager.setCurrentItem(TAB_SHOW); + } + }) + .execute(); } - - private void checkPermissions(int position, ProfilePagerAdapter adapter, ViewPager viewPager) { - if (position == TAB_SCAN) { - Permissions.with(QrActivity.this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied)) - .onAllGranted(() -> ((QrScanFragment) adapter.getItem(TAB_SCAN)).handleQrScanWithPermissions(QrActivity.this)) - .onAnyDenied(() -> { - if (scanRelay) { - Toast.makeText(this, getString(R.string.chat_camera_unavailable), Toast.LENGTH_LONG).show(); - } else { - viewPager.setCurrentItem(TAB_SHOW); - } - }) - .execute(); - } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + getMenuInflater().inflate(R.menu.qr_show, menu); + menu.findItem(R.id.new_classic_contact) + .setVisible(!scanRelay && !DcHelper.getContext(this).isChatmail()); + + Util.redMenuItem(menu, R.id.withdraw); + if (tabLayout.getSelectedTabPosition() == TAB_SCAN) { + menu.findItem(R.id.withdraw).setVisible(false); } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.clear(); - getMenuInflater().inflate(R.menu.qr_show, menu); - menu.findItem(R.id.new_classic_contact).setVisible(!scanRelay && !DcHelper.getContext(this).isChatmail()); - - Util.redMenuItem(menu, R.id.withdraw); - if(tabLayout.getSelectedTabPosition() == TAB_SCAN) { - menu.findItem(R.id.withdraw).setVisible(false); - } - - return super.onPrepareOptionsMenu(menu); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + return true; + } else if (itemId == R.id.new_classic_contact) { + this.startActivity(new Intent(this, NewContactActivity.class)); + } else if (itemId == R.id.withdraw) { + qrShowFragment.withdrawQr(); + } else if (itemId == R.id.load_from_image) { + AttachmentManager.selectImage(this, REQUEST_CODE_IMAGE); + } else if (itemId == R.id.paste) { + setQrResult(Util.getTextFromClipboard(this)); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - finish(); - return true; - } else if (itemId == R.id.new_classic_contact) { - this.startActivity(new Intent(this, NewContactActivity.class)); - } else if (itemId == R.id.withdraw) { - qrShowFragment.withdrawQr(); - } else if (itemId == R.id.load_from_image) { - AttachmentManager.selectImage(this, REQUEST_CODE_IMAGE); - } else if (itemId == R.id.paste) { - setQrResult(Util.getTextFromClipboard(this)); + return false; + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + if (permissions.length > 0 + && Manifest.permission.CAMERA.equals(permissions[0]) + && grantResults[0] == PackageManager.PERMISSION_DENIED) { + if (scanRelay) { + Toast.makeText(this, getString(R.string.chat_camera_unavailable), Toast.LENGTH_LONG).show(); + } else { + viewPager.setCurrentItem(TAB_SHOW); } - - return false; + // Workaround because sometimes something else requested the permissions before this class + // (probably the CameraView) and then this class didn't notice when it was denied } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - if (permissions.length > 0 - && Manifest.permission.CAMERA.equals(permissions[0]) - && grantResults[0] == PackageManager.PERMISSION_DENIED) { - if (scanRelay) { - Toast.makeText(this, getString(R.string.chat_camera_unavailable), Toast.LENGTH_LONG).show(); - } else { - viewPager.setCurrentItem(TAB_SHOW); + } + + @Override + public void onActivityResult(int reqCode, int resultCode, final Intent data) { + super.onActivityResult(reqCode, resultCode, data); + + if (resultCode != Activity.RESULT_OK) return; + + switch (reqCode) { + case REQUEST_CODE_IMAGE: + Uri uri = (data != null ? data.getData() : null); + if (uri != null) { + try { + InputStream inputStream = getContentResolver().openInputStream(uri); + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + if (bitmap == null) { + Log.e(TAG, "uri is not a bitmap: " + uri.toString()); + return; } - // Workaround because sometimes something else requested the permissions before this class - // (probably the CameraView) and then this class didn't notice when it was denied - } - } - - @Override - public void onActivityResult(int reqCode, int resultCode, final Intent data) { - super.onActivityResult(reqCode, resultCode, data); - - if (resultCode != Activity.RESULT_OK) - return; - - switch (reqCode) { - case REQUEST_CODE_IMAGE: - Uri uri = (data != null ? data.getData() : null); - if (uri != null) { - try { - InputStream inputStream = getContentResolver().openInputStream(uri); - Bitmap bitmap = BitmapFactory.decodeStream(inputStream); - if (bitmap == null) { - Log.e(TAG, "uri is not a bitmap: " + uri.toString()); - return; - } - int width = bitmap.getWidth(), height = bitmap.getHeight(); - int[] pixels = new int[width * height]; - bitmap.getPixels(pixels, 0, width, 0, 0, width, height); - bitmap.recycle(); - bitmap = null; - RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels); - BinaryBitmap bBitmap = new BinaryBitmap(new HybridBinarizer(source)); - MultiFormatReader reader = new MultiFormatReader(); - - try { - Result result = reader.decode(bBitmap); - setQrResult(result.getText()); - } catch (NotFoundException e) { - Log.e(TAG, "decode exception", e); - Toast.makeText(this, getString(R.string.qrscan_failed), Toast.LENGTH_LONG).show(); - } - } catch (FileNotFoundException e) { - Log.e(TAG, "can not open file: " + uri.toString(), e); - } - } - break; + int width = bitmap.getWidth(), height = bitmap.getHeight(); + int[] pixels = new int[width * height]; + bitmap.getPixels(pixels, 0, width, 0, 0, width, height); + bitmap.recycle(); + bitmap = null; + RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels); + BinaryBitmap bBitmap = new BinaryBitmap(new HybridBinarizer(source)); + MultiFormatReader reader = new MultiFormatReader(); + + try { + Result result = reader.decode(bBitmap); + setQrResult(result.getText()); + } catch (NotFoundException e) { + Log.e(TAG, "decode exception", e); + Toast.makeText(this, getString(R.string.qrscan_failed), Toast.LENGTH_LONG).show(); + } + } catch (FileNotFoundException e) { + Log.e(TAG, "can not open file: " + uri.toString(), e); + } } + break; } + } - private void setQrResult(String qrData) { - Intent intent = new Intent(); - intent.putExtra(Intents.Scan.RESULT, qrData); - setResult(RESULT_OK, intent); - finish(); - } + private void setQrResult(String qrData) { + Intent intent = new Intent(); + intent.putExtra(Intents.Scan.RESULT, qrData); + setResult(RESULT_OK, intent); + finish(); + } - @Override - public void onClick(View v) { - viewPager.setCurrentItem(TAB_SCAN); - } + @Override + public void onClick(View v) { + viewPager.setCurrentItem(TAB_SCAN); + } - private class ProfilePagerAdapter extends FragmentStatePagerAdapter { + private class ProfilePagerAdapter extends FragmentStatePagerAdapter { - private final QrActivity activity; + private final QrActivity activity; - ProfilePagerAdapter(QrActivity activity, FragmentManager fragmentManager) { - super(fragmentManager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - this.activity = activity; - } + ProfilePagerAdapter(QrActivity activity, FragmentManager fragmentManager) { + super(fragmentManager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + this.activity = activity; + } - @NonNull - @Override - public Fragment getItem(int position) { - Fragment fragment; + @NonNull + @Override + public Fragment getItem(int position) { + Fragment fragment; - switch (position) { - case TAB_SHOW: - fragment = activity.qrShowFragment; - break; + switch (position) { + case TAB_SHOW: + fragment = activity.qrShowFragment; + break; - default: - fragment = new QrScanFragment(); - break; - } + default: + fragment = new QrScanFragment(); + break; + } - return fragment; - } + return fragment; + } - @Override - public int getCount() { - return 2; - } + @Override + public int getCount() { + return 2; + } - @Override - public CharSequence getPageTitle(int position) { - switch (position) { - case TAB_SHOW: - return getString(R.string.qrshow_title); + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case TAB_SHOW: + return getString(R.string.qrshow_title); - default: - return getString(R.string.qrscan_title); - } - } + default: + return getString(R.string.qrscan_title); + } } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java b/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java index 55d925af5..2b8af6bbf 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrCodeHandler.java @@ -5,16 +5,16 @@ import android.content.Intent; import android.util.Log; import android.widget.Toast; - import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.SecurejoinSource; +import chat.delta.rpc.types.SecurejoinUiPath; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcLot; -import com.google.zxing.integration.android.IntentResult; - import org.thoughtcrime.securesms.ConversationActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.AccountManager; @@ -25,39 +25,33 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.views.ProgressDialog; -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; - -import chat.delta.rpc.types.SecurejoinSource; -import chat.delta.rpc.types.SecurejoinUiPath; - public class QrCodeHandler { - private static final String TAG = QrCodeHandler.class.getSimpleName(); - - public static int SECUREJOIN_SOURCE_EXTERNAL_LINK = 1; - public static int SECUREJOIN_SOURCE_INTERNAL_LINK = 2; - public static int SECUREJOIN_SOURCE_CLIPBOARD = 3; - public static int SECUREJOIN_SOURCE_IMAGE_LOADED = 4; - public static int SECUREJOIN_SOURCE_SCAN = 5; - - public static int SECUREJOIN_UIPATH_QR_ICON = 1; - public static int SECUREJOIN_UIPATH_NEW_CONTACT = 2; - - - private final Activity activity; - private final DcContext dcContext; - private final Rpc rpc; - private final int accId; - - public QrCodeHandler(Activity activity) { - this.activity = activity; - dcContext = DcHelper.getContext(activity); - rpc = DcHelper.getRpc(activity); - accId = dcContext.getAccountId(); - } + private static final String TAG = QrCodeHandler.class.getSimpleName(); + + public static int SECUREJOIN_SOURCE_EXTERNAL_LINK = 1; + public static int SECUREJOIN_SOURCE_INTERNAL_LINK = 2; + public static int SECUREJOIN_SOURCE_CLIPBOARD = 3; + public static int SECUREJOIN_SOURCE_IMAGE_LOADED = 4; + public static int SECUREJOIN_SOURCE_SCAN = 5; + + public static int SECUREJOIN_UIPATH_QR_ICON = 1; + public static int SECUREJOIN_UIPATH_NEW_CONTACT = 2; + + private final Activity activity; + private final DcContext dcContext; + private final Rpc rpc; + private final int accId; + + public QrCodeHandler(Activity activity) { + this.activity = activity; + dcContext = DcHelper.getContext(activity); + rpc = DcHelper.getRpc(activity); + accId = dcContext.getAccountId(); + } /** Process only QR about getting in contact or joining chats */ - public void handleOnlySecureJoinQr(String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { + public void handleOnlySecureJoinQr( + String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { final DcLot qrParsed = dcContext.checkQr(rawString); if (!handleSecureJoinQr(qrParsed, rawString, source, uiPath)) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); @@ -66,7 +60,8 @@ public void handleOnlySecureJoinQr(String rawString, SecurejoinSource source, Se } } - private boolean handleSecureJoinQr(DcLot qrParsed, String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { + private boolean handleSecureJoinQr( + DcLot qrParsed, String rawString, SecurejoinSource source, SecurejoinUiPath uiPath) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); String name = dcContext.getContact(qrParsed.getId()).getDisplayName(); switch (qrParsed.getState()) { @@ -79,14 +74,20 @@ private boolean handleSecureJoinQr(DcLot qrParsed, String rawString, SecurejoinS case DcContext.DC_QR_WITHDRAW_VERIFYCONTACT: case DcContext.DC_QR_WITHDRAW_VERIFYGROUP: case DcContext.DC_QR_WITHDRAW_JOINBROADCAST: - String message = qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT ? activity.getString(R.string.withdraw_verifycontact_explain) - : qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT ? activity.getString(R.string.withdraw_verifygroup_explain, qrParsed.getText1()) - : activity.getString(R.string.withdraw_joinbroadcast_explain, qrParsed.getText1()); + String message = + qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT + ? activity.getString(R.string.withdraw_verifycontact_explain) + : qrParsed.getState() == DcContext.DC_QR_WITHDRAW_VERIFYCONTACT + ? activity.getString(R.string.withdraw_verifygroup_explain, qrParsed.getText1()) + : activity.getString( + R.string.withdraw_joinbroadcast_explain, qrParsed.getText1()); builder.setTitle(R.string.qrshow_title); builder.setMessage(message); - builder.setNeutralButton(R.string.reset, (dialog, which) -> { - dcContext.setConfigFromQr(rawString); - }); + builder.setNeutralButton( + R.string.reset, + (dialog, which) -> { + dcContext.setConfigFromQr(rawString); + }); builder.setPositiveButton(R.string.ok, null); Util.redButton(builder.show(), AlertDialog.BUTTON_NEUTRAL); return true; @@ -96,9 +97,11 @@ private boolean handleSecureJoinQr(DcLot qrParsed, String rawString, SecurejoinS case DcContext.DC_QR_REVIVE_JOINBROADCAST: builder.setTitle(R.string.qrshow_title); builder.setMessage(activity.getString(R.string.revive_verifycontact_explain)); - builder.setNeutralButton(R.string.revive_qr_code, (dialog, which) -> { - dcContext.setConfigFromQr(rawString); - }); + builder.setNeutralButton( + R.string.revive_qr_code, + (dialog, which) -> { + dcContext.setConfigFromQr(rawString); + }); builder.setPositiveButton(R.string.ok, null); break; @@ -123,7 +126,8 @@ private boolean handleSecureJoinQr(DcLot qrParsed, String rawString, SecurejoinS } /** Process only QR about adding relays/profiles (DCACCOUNT: / DCLOGIN:) */ - public void handleOnlyAddRelayQr(String rawString, @Nullable ActivityResultLauncher screenLockLauncher) { + public void handleOnlyAddRelayQr( + String rawString, @Nullable ActivityResultLauncher screenLockLauncher) { final DcLot qrParsed = dcContext.checkQr(rawString); if (!handleAddRelayQr(qrParsed, rawString, screenLockLauncher)) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); @@ -132,23 +136,33 @@ public void handleOnlyAddRelayQr(String rawString, @Nullable ActivityResultLaunc } } - private boolean handleAddRelayQr(DcLot qrParsed, String rawString, @Nullable ActivityResultLauncher screenLockLauncher) { + private boolean handleAddRelayQr( + DcLot qrParsed, + String rawString, + @Nullable ActivityResultLauncher screenLockLauncher) { switch (qrParsed.getState()) { case DcContext.DC_QR_ACCOUNT: case DcContext.DC_QR_LOGIN: AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.confirm_add_transport); builder.setMessage(qrParsed.getText1()); - builder.setPositiveButton(R.string.ok, (d, w) -> { - if (screenLockLauncher != null) { - boolean result = ScreenLockUtil.applyScreenLock(activity, activity.getString(R.string.add_transport), activity.getString(R.string.enter_system_secret_to_continue), screenLockLauncher); - if (!result) { + builder.setPositiveButton( + R.string.ok, + (d, w) -> { + if (screenLockLauncher != null) { + boolean result = + ScreenLockUtil.applyScreenLock( + activity, + activity.getString(R.string.add_transport), + activity.getString(R.string.enter_system_secret_to_continue), + screenLockLauncher); + if (!result) { + addRelay(rawString); + } + } else { // Screen lock not needed addRelay(rawString); } - } else { // Screen lock not needed - addRelay(rawString); - } - }); + }); builder.setNegativeButton(R.string.cancel, null); builder.setCancelable(false); builder.create().show(); @@ -168,13 +182,16 @@ private boolean handleProxyQr(DcLot qrParsed, String rawString) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.proxy_use_proxy); builder.setMessage(activity.getString(R.string.proxy_use_proxy_confirm, qrParsed.getText1())); - builder.setPositiveButton(R.string.proxy_use_proxy, (dlg, btn) -> { - dcContext.setConfigFromQr(rawString); - dcContext.restartIo(); - showDoneToast(); - }); + builder.setPositiveButton( + R.string.proxy_use_proxy, + (dlg, btn) -> { + dcContext.setConfigFromQr(rawString); + dcContext.restartIo(); + showDoneToast(); + }); if (rawString.toLowerCase().startsWith("http")) { - builder.setNeutralButton(R.string.open, (d, b) -> IntentUtils.showInBrowser(activity, rawString)); + builder.setNeutralButton( + R.string.open, (d, b) -> IntentUtils.showInBrowser(activity, rawString)); } builder.setNegativeButton(R.string.cancel, null); builder.setCancelable(false); @@ -194,10 +211,15 @@ private boolean handleBackupQr(DcLot qrParsed, String rawString) { case DcContext.DC_QR_BACKUP2: AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.multidevice_receiver_title); - builder.setMessage(activity.getString(R.string.multidevice_receiver_scanning_ask) + "\n\n" + activity.getString(R.string.multidevice_same_network_hint)); - builder.setPositiveButton(R.string.perm_continue, (dialog, which) -> { - AccountManager.getInstance().addAccountFromSecondDevice(activity, rawString); - }); + builder.setMessage( + activity.getString(R.string.multidevice_receiver_scanning_ask) + + "\n\n" + + activity.getString(R.string.multidevice_same_network_hint)); + builder.setPositiveButton( + R.string.perm_continue, + (dialog, which) -> { + AccountManager.getInstance().addAccountFromSecondDevice(activity, rawString); + }); builder.setNegativeButton(R.string.cancel, null); builder.setCancelable(false); @@ -208,10 +230,11 @@ private boolean handleBackupQr(DcLot qrParsed, String rawString) { case DcContext.DC_QR_BACKUP_TOO_NEW: new AlertDialog.Builder(activity) - .setTitle(R.string.multidevice_receiver_title) - .setMessage(activity.getString(R.string.multidevice_receiver_needs_update)) - .setNegativeButton(R.string.ok, null) - .create().show(); + .setTitle(R.string.multidevice_receiver_title) + .setMessage(activity.getString(R.string.multidevice_receiver_needs_update)) + .setNegativeButton(R.string.ok, null) + .create() + .show(); return true; default: @@ -220,7 +243,11 @@ private boolean handleBackupQr(DcLot qrParsed, String rawString) { } /** Handle any kind of QR showing an AlertDialog adapted to the QR type. */ - public void handleQrData(String rawString, SecurejoinSource source, SecurejoinUiPath uiPath, ActivityResultLauncher relayLockLauncher) { + public void handleQrData( + String rawString, + SecurejoinSource source, + SecurejoinUiPath uiPath, + ActivityResultLauncher relayLockLauncher) { final DcLot qrParsed = dcContext.checkQr(rawString); if (handleSecureJoinQr(qrParsed, rawString, source, uiPath) || handleAddRelayQr(qrParsed, rawString, relayLockLauncher) @@ -236,159 +263,191 @@ public void handleQrData(String rawString, SecurejoinSource source, SecurejoinUi builder.create().show(); } - private void handleDefault(AlertDialog.Builder builder, String qrRawString, DcLot qrParsed) { - String msg; - final String scannedText; - switch (qrParsed.getState()) { - case DcContext.DC_QR_ERROR: - scannedText = qrRawString; - msg = qrParsed.getText1() + "\n\n" + activity.getString(R.string.qrscan_contains_text, scannedText); - break; - case DcContext.DC_QR_TEXT: - scannedText = qrParsed.getText1(); - msg = activity.getString(R.string.qrscan_contains_text, scannedText); - break; - default: - scannedText = qrRawString; - msg = activity.getString(R.string.qrscan_contains_text, scannedText); - break; - } - builder.setMessage(msg); - builder.setPositiveButton(android.R.string.ok, null); - builder.setNeutralButton(R.string.menu_copy_to_clipboard, (dialog, which) -> { - Util.writeTextToClipboard(activity, scannedText); - showDoneToast(); - }); + private void handleDefault(AlertDialog.Builder builder, String qrRawString, DcLot qrParsed) { + String msg; + final String scannedText; + switch (qrParsed.getState()) { + case DcContext.DC_QR_ERROR: + scannedText = qrRawString; + msg = + qrParsed.getText1() + + "\n\n" + + activity.getString(R.string.qrscan_contains_text, scannedText); + break; + case DcContext.DC_QR_TEXT: + scannedText = qrParsed.getText1(); + msg = activity.getString(R.string.qrscan_contains_text, scannedText); + break; + default: + scannedText = qrRawString; + msg = activity.getString(R.string.qrscan_contains_text, scannedText); + break; } + builder.setMessage(msg); + builder.setPositiveButton(android.R.string.ok, null); + builder.setNeutralButton( + R.string.menu_copy_to_clipboard, + (dialog, which) -> { + Util.writeTextToClipboard(activity, scannedText); + showDoneToast(); + }); + } - private void showQrUrl(AlertDialog.Builder builder, DcLot qrParsed) { - final String url = qrParsed.getText1(); - String msg = String.format(activity.getString(R.string.qrscan_contains_url), url); - builder.setMessage(msg); - builder.setPositiveButton(R.string.open, (dialog, which) -> IntentUtils.showInBrowser(activity, url)); - builder.setNegativeButton(android.R.string.cancel, null); - builder.setNeutralButton(R.string.menu_copy_to_clipboard, (dialog, which) -> { - Util.writeTextToClipboard(activity, url); - showDoneToast(); + private void showQrUrl(AlertDialog.Builder builder, DcLot qrParsed) { + final String url = qrParsed.getText1(); + String msg = String.format(activity.getString(R.string.qrscan_contains_url), url); + builder.setMessage(msg); + builder.setPositiveButton( + R.string.open, (dialog, which) -> IntentUtils.showInBrowser(activity, url)); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setNeutralButton( + R.string.menu_copy_to_clipboard, + (dialog, which) -> { + Util.writeTextToClipboard(activity, url); + showDoneToast(); }); - } + } - private void showDoneToast() { - Toast.makeText(activity, activity.getString(R.string.done), Toast.LENGTH_SHORT).show(); - } + private void showDoneToast() { + Toast.makeText(activity, activity.getString(R.string.done), Toast.LENGTH_SHORT).show(); + } - private void showFingerprintOrQrSuccess(AlertDialog.Builder builder, DcLot qrParsed, String name) { - @StringRes int resId = qrParsed.getState() == DcContext.DC_QR_ADDR ? R.string.ask_start_chat_with : R.string.qrshow_x_verified; - builder.setMessage(activity.getString(resId, name)); - builder.setPositiveButton(R.string.start_chat, (dialogInterface, i) -> { - int chatId = dcContext.createChatByContactId(qrParsed.getId()); - Intent intent = new Intent(activity, ConversationActivity.class); - intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId); - if (qrParsed.getText1Meaning() == DcLot.DC_TEXT1_DRAFT) { - intent.putExtra(ConversationActivity.TEXT_EXTRA, qrParsed.getText1()); - } - activity.startActivity(intent); + private void showFingerprintOrQrSuccess( + AlertDialog.Builder builder, DcLot qrParsed, String name) { + @StringRes + int resId = + qrParsed.getState() == DcContext.DC_QR_ADDR + ? R.string.ask_start_chat_with + : R.string.qrshow_x_verified; + builder.setMessage(activity.getString(resId, name)); + builder.setPositiveButton( + R.string.start_chat, + (dialogInterface, i) -> { + int chatId = dcContext.createChatByContactId(qrParsed.getId()); + Intent intent = new Intent(activity, ConversationActivity.class); + intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId); + if (qrParsed.getText1Meaning() == DcLot.DC_TEXT1_DRAFT) { + intent.putExtra(ConversationActivity.TEXT_EXTRA, qrParsed.getText1()); + } + activity.startActivity(intent); }); - builder.setNegativeButton(android.R.string.cancel, null); - } + builder.setNegativeButton(android.R.string.cancel, null); + } - private void showFingerPrintError(AlertDialog.Builder builder, String name) { - builder.setMessage(activity.getString(R.string.qrscan_fingerprint_mismatch, name)); - builder.setPositiveButton(android.R.string.ok, null); - } + private void showFingerPrintError(AlertDialog.Builder builder, String name) { + builder.setMessage(activity.getString(R.string.qrscan_fingerprint_mismatch, name)); + builder.setPositiveButton(android.R.string.ok, null); + } - private void showVerifyFingerprintWithoutAddress(AlertDialog.Builder builder, DcLot qrParsed) { - builder.setMessage(activity.getString(R.string.qrscan_no_addr_found) + "\n\n" + activity.getString(R.string.qrscan_fingerprint_label) + ":\n" + qrParsed.getText1()); - builder.setPositiveButton(android.R.string.ok, null); - builder.setNeutralButton(R.string.menu_copy_to_clipboard, (dialog, which) -> { - Util.writeTextToClipboard(activity, qrParsed.getText1()); - showDoneToast(); + private void showVerifyFingerprintWithoutAddress(AlertDialog.Builder builder, DcLot qrParsed) { + builder.setMessage( + activity.getString(R.string.qrscan_no_addr_found) + + "\n\n" + + activity.getString(R.string.qrscan_fingerprint_label) + + ":\n" + + qrParsed.getText1()); + builder.setPositiveButton(android.R.string.ok, null); + builder.setNeutralButton( + R.string.menu_copy_to_clipboard, + (dialog, which) -> { + Util.writeTextToClipboard(activity, qrParsed.getText1()); + showDoneToast(); }); - } + } - private void showVerifyContactOrGroup(AlertDialog.Builder builder, - String qrRawString, - DcLot qrParsed, - String name, - SecurejoinSource source, - SecurejoinUiPath uipath) { - String msg; - switch (qrParsed.getState()) { - case DcContext.DC_QR_ASK_VERIFYGROUP: - msg = activity.getString(R.string.qrscan_ask_join_group, qrParsed.getText1()); - break; - case DcContext.DC_QR_ASK_JOIN_BROADCAST: - msg = activity.getString(R.string.qrscan_ask_join_channel, qrParsed.getText1()); - break; - default: - msg = activity.getString(R.string.ask_start_chat_with, name); - break; - } - builder.setMessage(msg); - builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + private void showVerifyContactOrGroup( + AlertDialog.Builder builder, + String qrRawString, + DcLot qrParsed, + String name, + SecurejoinSource source, + SecurejoinUiPath uipath) { + String msg; + switch (qrParsed.getState()) { + case DcContext.DC_QR_ASK_VERIFYGROUP: + msg = activity.getString(R.string.qrscan_ask_join_group, qrParsed.getText1()); + break; + case DcContext.DC_QR_ASK_JOIN_BROADCAST: + msg = activity.getString(R.string.qrscan_ask_join_channel, qrParsed.getText1()); + break; + default: + msg = activity.getString(R.string.ask_start_chat_with, name); + break; + } + builder.setMessage(msg); + builder.setPositiveButton( + android.R.string.ok, + (dialogInterface, i) -> { secureJoinByQr(qrRawString, source, uipath); }); - builder.setNegativeButton(android.R.string.cancel, null); - } + builder.setNegativeButton(android.R.string.cancel, null); + } public void secureJoinByQr(String qrRawString, SecurejoinSource source, SecurejoinUiPath uipath) { - try { - int newChatId = DcHelper.getRpc(activity).secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath); - if (newChatId == 0) throw new Exception("Securejoin failed to create a chat"); - - Intent intent = new Intent(activity, ConversationActivity.class); - intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId); - activity.startActivity(intent); - } catch (Exception e) { - e.printStackTrace(); - AlertDialog.Builder builder1 = new AlertDialog.Builder(activity); - builder1.setMessage(e.getMessage()); - builder1.setPositiveButton(android.R.string.ok, null); - builder1.create().show(); - } + try { + int newChatId = + DcHelper.getRpc(activity) + .secureJoinWithUxInfo(dcContext.getAccountId(), qrRawString, source, uipath); + if (newChatId == 0) throw new Exception("Securejoin failed to create a chat"); + + Intent intent = new Intent(activity, ConversationActivity.class); + intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, newChatId); + activity.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + AlertDialog.Builder builder1 = new AlertDialog.Builder(activity); + builder1.setMessage(e.getMessage()); + builder1.setPositiveButton(android.R.string.ok, null); + builder1.create().show(); + } } public void addRelay(String qrData) { - ProgressDialog progressDialog = new ProgressDialog(activity); - progressDialog.setMessage(activity.getResources().getString(R.string.one_moment)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); - String cancel = activity.getResources().getString(android.R.string.cancel); - progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, cancel, (d, w) -> { - dcContext.stopOngoingProcess(); - }); - progressDialog.show(); - - Util.runOnAnyBackgroundThread(() -> { - String error = null; - try { - rpc.addTransportFromQr(accId, qrData); - } catch (RpcException e) { - Log.w(TAG, e); - error = e.getMessage(); - } - final String finalError = error; - Util.runOnMain(() -> { - if (!progressDialog.isShowing()) return; // canceled dialog, nothing to do - if (finalError != null) { - new AlertDialog.Builder(activity) - .setTitle(R.string.error) - .setMessage(finalError) - .setPositiveButton(R.string.ok, null) - .show(); - } else { - showDoneToast(); - if (!(activity instanceof RelayListActivity)) { - activity.startActivity(new Intent(activity, RelayListActivity.class)); - } - } + ProgressDialog progressDialog = new ProgressDialog(activity); + progressDialog.setMessage(activity.getResources().getString(R.string.one_moment)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setCancelable(false); + String cancel = activity.getResources().getString(android.R.string.cancel); + progressDialog.setButton( + DialogInterface.BUTTON_NEGATIVE, + cancel, + (d, w) -> { + dcContext.stopOngoingProcess(); + }); + progressDialog.show(); + + Util.runOnAnyBackgroundThread( + () -> { + String error = null; try { - progressDialog.dismiss(); - } catch (IllegalArgumentException e) { - // see https://stackoverflow.com/a/5102572/4557005 + rpc.addTransportFromQr(accId, qrData); + } catch (RpcException e) { Log.w(TAG, e); + error = e.getMessage(); } + final String finalError = error; + Util.runOnMain( + () -> { + if (!progressDialog.isShowing()) return; // canceled dialog, nothing to do + if (finalError != null) { + new AlertDialog.Builder(activity) + .setTitle(R.string.error) + .setMessage(finalError) + .setPositiveButton(R.string.ok, null) + .show(); + } else { + showDoneToast(); + if (!(activity instanceof RelayListActivity)) { + activity.startActivity(new Intent(activity, RelayListActivity.class)); + } + } + try { + progressDialog.dismiss(); + } catch (IllegalArgumentException e) { + // see https://stackoverflow.com/a/5102572/4557005 + Log.w(TAG, e); + } + }); }); - }); - } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java b/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java index b86056aec..a9209e14c 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrScanFragment.java @@ -8,115 +8,111 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - import com.journeyapps.barcodescanner.CaptureManager; import com.journeyapps.barcodescanner.CompoundBarcodeView; import com.journeyapps.barcodescanner.DecoratedBarcodeView; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ViewUtil; public class QrScanFragment extends Fragment { - private static final String TAG = QrScanFragment.class.getSimpleName(); + private static final String TAG = QrScanFragment.class.getSimpleName(); - private CompoundBarcodeView barcodeScannerView; - private MyCaptureManager capture; + private CompoundBarcodeView barcodeScannerView; + private MyCaptureManager capture; - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - } + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + } - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.qr_scan_fragment, container, false); + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.qr_scan_fragment, container, false); - barcodeScannerView = view.findViewById(R.id.zxing_barcode_scanner); - barcodeScannerView.setStatusText(getString(R.string.qrscan_hint) + "\n "); + barcodeScannerView = view.findViewById(R.id.zxing_barcode_scanner); + barcodeScannerView.setStatusText(getString(R.string.qrscan_hint) + "\n "); - // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(barcodeScannerView.getStatusView()); + // add padding to avoid content hidden behind system bars + ViewUtil.applyWindowInsets(barcodeScannerView.getStatusView()); - return view; - } + return view; + } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - init(barcodeScannerView, requireActivity(), savedInstanceState); - } + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + init(barcodeScannerView, requireActivity(), savedInstanceState); + } - @Override - public void onResume() { - super.onResume(); - if (capture != null) { - capture.onResume(); - } + @Override + public void onResume() { + super.onResume(); + if (capture != null) { + capture.onResume(); } + } - @Override - public void onPause() { - super.onPause(); - if (capture != null) { - capture.onPause(); - } + @Override + public void onPause() { + super.onPause(); + if (capture != null) { + capture.onPause(); } + } - @Override - public void onDestroy() { - super.onDestroy(); - if (capture != null) { - capture.onDestroy(); - } + @Override + public void onDestroy() { + super.onDestroy(); + if (capture != null) { + capture.onDestroy(); } + } - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (capture != null) { - capture.onSaveInstanceState(outState); - } + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (capture != null) { + capture.onSaveInstanceState(outState); } - - void handleQrScanWithPermissions(Activity activity) { - if (barcodeScannerView != null) - init(barcodeScannerView, activity, null); + } + + void handleQrScanWithPermissions(Activity activity) { + if (barcodeScannerView != null) init(barcodeScannerView, activity, null); + } + + private void init( + CompoundBarcodeView barcodeScannerView, Activity activity, Bundle savedInstanceState) { + try { + capture = new MyCaptureManager(activity, barcodeScannerView); + capture.initializeFromIntent(activity.getIntent(), savedInstanceState); + capture.decode(); + } catch (Exception e) { + Log.w(TAG, e); } + } - private void init(CompoundBarcodeView barcodeScannerView, Activity activity, Bundle savedInstanceState) { - try { - capture = new MyCaptureManager(activity, barcodeScannerView); - capture.initializeFromIntent(activity.getIntent(), savedInstanceState); - capture.decode(); - } - catch(Exception e) { - Log.w(TAG, e); - } - } + public class MyCaptureManager extends CaptureManager { + private final Activity myActivity; - public class MyCaptureManager extends CaptureManager { - private final Activity myActivity; - - public MyCaptureManager(Activity activity, DecoratedBarcodeView barcodeView) { - super(activity, barcodeView); - myActivity = activity; - } - - // the original implementation of displayFrameworkBugMessageAndExit() calls Activity::finish() - // which makes _showing_ the QR-code impossible if scanning goes wrong. - // therefore, we only show a non-disturbing error here. - @Override - protected void displayFrameworkBugMessageAndExit(String message) { - if (TextUtils.isEmpty(message)) { - message = myActivity.getString(R.string.zxing_msg_camera_framework_bug); - } - Toast.makeText(myActivity, message, Toast.LENGTH_SHORT).show(); - } + public MyCaptureManager(Activity activity, DecoratedBarcodeView barcodeView) { + super(activity, barcodeView); + myActivity = activity; } + // the original implementation of displayFrameworkBugMessageAndExit() calls Activity::finish() + // which makes _showing_ the QR-code impossible if scanning goes wrong. + // therefore, we only show a non-disturbing error here. + @Override + protected void displayFrameworkBugMessageAndExit(String message) { + if (TextUtils.isEmpty(message)) { + message = myActivity.getString(R.string.zxing_msg_camera_framework_bug); + } + Toast.makeText(myActivity, message, Toast.LENGTH_SHORT).show(); + } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrShowActivity.java b/src/main/java/org/thoughtcrime/securesms/qr/QrShowActivity.java index 6e8d8db6c..efdff8758 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrShowActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrShowActivity.java @@ -3,11 +3,8 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; - import androidx.appcompat.app.ActionBar; - import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcEventCenter; @@ -17,75 +14,81 @@ public class QrShowActivity extends BaseActionBarActivity { - public final static String CHAT_ID = "chat_id"; - - DcEventCenter dcEventCenter; - - DcContext dcContext; - QrShowFragment fragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_qr_show); - fragment = (QrShowFragment)getSupportFragmentManager().findFragmentById(R.id.qrScannerFragment); - - // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(findViewById(R.id.qrScannerFragment), false, true, false, false); - - dcContext = DcHelper.getContext(this); - dcEventCenter = DcHelper.getEventCenter(this); - - Bundle extras = getIntent().getExtras(); - int chatId = 0; - if (extras != null) { - chatId = extras.getInt(CHAT_ID); - } - - ActionBar supportActionBar = getSupportActionBar(); - assert supportActionBar != null; - supportActionBar.setDisplayHomeAsUpEnabled(true); - supportActionBar.setElevation(0); // edge-to-edge: avoid top shadow - - if (chatId != 0) { - // verified-group - String groupName = dcContext.getChat(chatId).getName(); - supportActionBar.setTitle(groupName); - supportActionBar.setSubtitle(R.string.qrshow_join_group_title); - } else { - // verify-contact - String selfName = DcHelper.get(this, DcHelper.CONFIG_DISPLAY_NAME); // we cannot use MrContact.getDisplayName() as this would result in "Me" instead of - if (selfName.isEmpty()) { - selfName = DcHelper.get(this, DcHelper.CONFIG_CONFIGURED_ADDRESS, "unknown"); - } - supportActionBar.setTitle(selfName); - supportActionBar.setSubtitle(R.string.qrshow_join_contact_title); - } - } + public static final String CHAT_ID = "chat_id"; - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.qr_show, menu); - menu.findItem(R.id.new_classic_contact).setVisible(false); - menu.findItem(R.id.paste).setVisible(false); - menu.findItem(R.id.load_from_image).setVisible(false); - Util.redMenuItem(menu, R.id.withdraw); - return super.onCreateOptionsMenu(menu); - } + DcEventCenter dcEventCenter; - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); + DcContext dcContext; + QrShowFragment fragment; - int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - finish(); - return true; - } else if (itemId == R.id.withdraw) { - fragment.withdrawQr(); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_qr_show); + fragment = + (QrShowFragment) getSupportFragmentManager().findFragmentById(R.id.qrScannerFragment); - return false; + // add padding to avoid content hidden behind system bars + ViewUtil.applyWindowInsets(findViewById(R.id.qrScannerFragment), false, true, false, false); + + dcContext = DcHelper.getContext(this); + dcEventCenter = DcHelper.getEventCenter(this); + + Bundle extras = getIntent().getExtras(); + int chatId = 0; + if (extras != null) { + chatId = extras.getInt(CHAT_ID); } + + ActionBar supportActionBar = getSupportActionBar(); + assert supportActionBar != null; + supportActionBar.setDisplayHomeAsUpEnabled(true); + supportActionBar.setElevation(0); // edge-to-edge: avoid top shadow + + if (chatId != 0) { + // verified-group + String groupName = dcContext.getChat(chatId).getName(); + supportActionBar.setTitle(groupName); + supportActionBar.setSubtitle(R.string.qrshow_join_group_title); + } else { + // verify-contact + String selfName = + DcHelper.get( + this, + DcHelper + .CONFIG_DISPLAY_NAME); // we cannot use MrContact.getDisplayName() as this would + // result in "Me" instead of + if (selfName.isEmpty()) { + selfName = DcHelper.get(this, DcHelper.CONFIG_CONFIGURED_ADDRESS, "unknown"); + } + supportActionBar.setTitle(selfName); + supportActionBar.setSubtitle(R.string.qrshow_join_contact_title); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.qr_show, menu); + menu.findItem(R.id.new_classic_contact).setVisible(false); + menu.findItem(R.id.paste).setVisible(false); + menu.findItem(R.id.load_from_image).setVisible(false); + Util.redMenuItem(menu, R.id.withdraw); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + return true; + } else if (itemId == R.id.withdraw) { + fragment.withdrawQr(); + } + + return false; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/QrShowFragment.java b/src/main/java/org/thoughtcrime/securesms/qr/QrShowFragment.java index 8b1704d64..31317a96e 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/QrShowFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/QrShowFragment.java @@ -2,10 +2,7 @@ import android.app.Activity; import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; @@ -15,216 +12,226 @@ import android.widget.Button; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.caverock.androidsvg.SVG; import com.caverock.androidsvg.SVGImageView; import com.caverock.androidsvg.SVGParseException; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.ScaleStableImageView; import org.thoughtcrime.securesms.connect.DcEventCenter; import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.util.FileProviderUtil; import org.thoughtcrime.securesms.util.Util; -import java.io.File; -import java.io.FileOutputStream; - public class QrShowFragment extends Fragment implements DcEventCenter.DcEventDelegate { - private final static String TAG = QrShowFragment.class.getSimpleName(); - public final static int WHITE = 0xFFFFFFFF; - private final static int BLACK = 0xFF000000; - private final static int WIDTH = 400; - private final static int HEIGHT = 400; - private final static String CHAT_ID = "chat_id"; + private static final String TAG = QrShowFragment.class.getSimpleName(); + public static final int WHITE = 0xFFFFFFFF; + private static final int BLACK = 0xFF000000; + private static final int WIDTH = 400; + private static final int HEIGHT = 400; + private static final String CHAT_ID = "chat_id"; - private int chatId = 0; + private int chatId = 0; - private int numJoiners; + private int numJoiners; - private DcEventCenter dcEventCenter; - - private DcContext dcContext; - - private View.OnClickListener scanClicklistener; - - public QrShowFragment() { - this(null); - } + private DcEventCenter dcEventCenter; - public QrShowFragment(View.OnClickListener scanClicklistener) { - super(); - this.scanClicklistener = scanClicklistener; - } + private DcContext dcContext; - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); + private View.OnClickListener scanClicklistener; - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // keeping the screen on also avoids falling back from IDLE to POLL - } + public QrShowFragment() { + this(null); + } - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.qr_show_fragment, container, false); + public QrShowFragment(View.OnClickListener scanClicklistener) { + super(); + this.scanClicklistener = scanClicklistener; + } - dcContext = DcHelper.getContext(getActivity()); - dcEventCenter = DcHelper.getEventCenter(getActivity()); + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); - Bundle extras = getActivity().getIntent().getExtras(); - if (extras != null) { - chatId = extras.getInt(CHAT_ID); - } + getActivity() + .getWindow() + .addFlags( + WindowManager.LayoutParams + .FLAG_KEEP_SCREEN_ON); // keeping the screen on also avoids falling back from IDLE + // to POLL + } - dcEventCenter.addObserver(DcContext.DC_EVENT_SECUREJOIN_INVITER_PROGRESS, this); - - numJoiners = 0; - - ScaleStableImageView backgroundView = view.findViewById(R.id.background); - Drawable drawable = getActivity().getResources().getDrawable(R.drawable.background_hd); - backgroundView.setImageDrawable(drawable); - - SVGImageView imageView = view.findViewById(R.id.qrImage); - try { - SVG svg = SVG.getFromString(fixSVG(dcContext.getSecurejoinQrSvg(chatId))); - imageView.setSVG(svg); - } catch (SVGParseException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - Activity activity = getActivity(); - if (activity != null) { - activity.finish(); - } - } + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.qr_show_fragment, container, false); - view.findViewById(R.id.share_link_button).setOnClickListener((v) -> showInviteLinkDialog()); - Button scanBtn = view.findViewById(R.id.scan_qr_button); - if (scanClicklistener != null) { - scanBtn.setVisibility(View.VISIBLE); - scanBtn.setOnClickListener(scanClicklistener); - } else { - scanBtn.setVisibility(View.GONE); - } + dcContext = DcHelper.getContext(getActivity()); + dcEventCenter = DcHelper.getEventCenter(getActivity()); - return view; + Bundle extras = getActivity().getIntent().getExtras(); + if (extras != null) { + chatId = extras.getInt(CHAT_ID); } - public static String fixSVG(String svg) { - // HACK: move avatar-letter down, baseline alignment not working, - // see https://github.com/deltachat/deltachat-core-rust/pull/2815#issuecomment-978067378 , - // suggestions welcome :) - return svg.replace("y=\"281.136\"", "y=\"296\""); + dcEventCenter.addObserver(DcContext.DC_EVENT_SECUREJOIN_INVITER_PROGRESS, this); + + numJoiners = 0; + + ScaleStableImageView backgroundView = view.findViewById(R.id.background); + Drawable drawable = getActivity().getResources().getDrawable(R.drawable.background_hd); + backgroundView.setImageDrawable(drawable); + + SVGImageView imageView = view.findViewById(R.id.qrImage); + try { + SVG svg = SVG.getFromString(fixSVG(dcContext.getSecurejoinQrSvg(chatId))); + imageView.setSVG(svg); + } catch (SVGParseException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + Activity activity = getActivity(); + if (activity != null) { + activity.finish(); + } } - public void shareInviteURL() { - try { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - String inviteURL = dcContext.getSecurejoinQr(chatId); - intent.putExtra(Intent.EXTRA_TEXT, inviteURL); - startActivity(Intent.createChooser(intent, getString(R.string.chat_share_with_title))); - } catch (Exception e) { - Log.e(TAG, "failed to share invite URL", e); - } + view.findViewById(R.id.share_link_button).setOnClickListener((v) -> showInviteLinkDialog()); + Button scanBtn = view.findViewById(R.id.scan_qr_button); + if (scanClicklistener != null) { + scanBtn.setVisibility(View.VISIBLE); + scanBtn.setOnClickListener(scanClicklistener); + } else { + scanBtn.setVisibility(View.GONE); } - public void copyQrData() { - String inviteURL = dcContext.getSecurejoinQr(chatId); - Util.writeTextToClipboard(getActivity(), inviteURL); - Toast.makeText(getActivity(), getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show(); - } + return view; + } - public void withdrawQr() { - Activity activity = getActivity(); - String message; - if (chatId == 0) { - message = activity.getString(R.string.withdraw_verifycontact_explain); - } else { - DcChat chat = dcContext.getChat(chatId); - if (chat.getType() == DcChat.DC_CHAT_TYPE_GROUP) { - message = activity.getString(R.string.withdraw_verifygroup_explain, chat.getName()); - } else { - message = activity.getString(R.string.withdraw_joinbroadcast_explain, chat.getName()); - } - } - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.withdraw_qr_code); - builder.setMessage(message); - builder.setPositiveButton(R.string.reset, (dialog, which) -> { - DcContext dcContext = DcHelper.getContext(activity); - dcContext.setConfigFromQr(dcContext.getSecurejoinQr(chatId)); - activity.finish(); - }); - builder.setNegativeButton(R.string.cancel, null); - AlertDialog dialog = builder.show(); - Util.redPositiveButton(dialog); - } + public static String fixSVG(String svg) { + // HACK: move avatar-letter down, baseline alignment not working, + // see https://github.com/deltachat/deltachat-core-rust/pull/2815#issuecomment-978067378 , + // suggestions welcome :) + return svg.replace("y=\"281.136\"", "y=\"296\""); + } - public void showInviteLinkDialog() { - View view = View.inflate(getActivity(), R.layout.dialog_share_invite_link, null); + public void shareInviteURL() { + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); String inviteURL = dcContext.getSecurejoinQr(chatId); - ((TextView)view.findViewById(R.id.invite_link)).setText(inviteURL); - new AlertDialog.Builder(getActivity()) + intent.putExtra(Intent.EXTRA_TEXT, inviteURL); + startActivity(Intent.createChooser(intent, getString(R.string.chat_share_with_title))); + } catch (Exception e) { + Log.e(TAG, "failed to share invite URL", e); + } + } + + public void copyQrData() { + String inviteURL = dcContext.getSecurejoinQr(chatId); + Util.writeTextToClipboard(getActivity(), inviteURL); + Toast.makeText(getActivity(), getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) + .show(); + } + + public void withdrawQr() { + Activity activity = getActivity(); + String message; + if (chatId == 0) { + message = activity.getString(R.string.withdraw_verifycontact_explain); + } else { + DcChat chat = dcContext.getChat(chatId); + if (chat.getType() == DcChat.DC_CHAT_TYPE_GROUP) { + message = activity.getString(R.string.withdraw_verifygroup_explain, chat.getName()); + } else { + message = activity.getString(R.string.withdraw_joinbroadcast_explain, chat.getName()); + } + } + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.withdraw_qr_code); + builder.setMessage(message); + builder.setPositiveButton( + R.string.reset, + (dialog, which) -> { + DcContext dcContext = DcHelper.getContext(activity); + dcContext.setConfigFromQr(dcContext.getSecurejoinQr(chatId)); + activity.finish(); + }); + builder.setNegativeButton(R.string.cancel, null); + AlertDialog dialog = builder.show(); + Util.redPositiveButton(dialog); + } + + public void showInviteLinkDialog() { + View view = View.inflate(getActivity(), R.layout.dialog_share_invite_link, null); + String inviteURL = dcContext.getSecurejoinQr(chatId); + ((TextView) view.findViewById(R.id.invite_link)).setText(inviteURL); + new AlertDialog.Builder(getActivity()) .setView(view) .setNegativeButton(R.string.cancel, null) .setNeutralButton(R.string.menu_copy_to_clipboard, (d, b) -> copyQrData()) .setPositiveButton(R.string.menu_share, (d, b) -> shareInviteURL()) .create() .show(); + } + + @Override + public void onResume() { + super.onResume(); + if (!DcHelper.isNetworkConnected(getContext())) { + Toast.makeText( + getActivity(), R.string.qrshow_join_contact_no_connection_toast, Toast.LENGTH_LONG) + .show(); } - - @Override - public void onResume() { - super.onResume(); - if (!DcHelper.isNetworkConnected(getContext())) { - Toast.makeText(getActivity(), R.string.qrshow_join_contact_no_connection_toast, Toast.LENGTH_LONG).show(); - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - dcEventCenter.removeObservers(this); - } - - @Override - public void handleEvent(@NonNull DcEvent event) { - if (event.getId() == DcContext.DC_EVENT_SECUREJOIN_INVITER_PROGRESS) { - DcContext dcContext = DcHelper.getContext(getActivity()); - int contact_id = event.getData1Int(); - long progress = event.getData2Int(); - String msg = null; - if (progress == 300) { - msg = String.format(getString(R.string.qrshow_x_joining), dcContext.getContact(contact_id).getDisplayName()); - numJoiners++; - } else if (progress == 600) { - msg = String.format(getString(R.string.qrshow_x_verified), dcContext.getContact(contact_id).getDisplayName()); - } else if (progress == 800) { - msg = String.format(getString(R.string.qrshow_x_has_joined_group), dcContext.getContact(contact_id).getDisplayName()); - } - - if (msg != null) { - Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show(); - } - - if (progress == 1000) { - numJoiners--; - if (numJoiners <= 0) { - if (getActivity() != null) getActivity().finish(); - } - } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + dcEventCenter.removeObservers(this); + } + + @Override + public void handleEvent(@NonNull DcEvent event) { + if (event.getId() == DcContext.DC_EVENT_SECUREJOIN_INVITER_PROGRESS) { + DcContext dcContext = DcHelper.getContext(getActivity()); + int contact_id = event.getData1Int(); + long progress = event.getData2Int(); + String msg = null; + if (progress == 300) { + msg = + String.format( + getString(R.string.qrshow_x_joining), + dcContext.getContact(contact_id).getDisplayName()); + numJoiners++; + } else if (progress == 600) { + msg = + String.format( + getString(R.string.qrshow_x_verified), + dcContext.getContact(contact_id).getDisplayName()); + } else if (progress == 800) { + msg = + String.format( + getString(R.string.qrshow_x_has_joined_group), + dcContext.getContact(contact_id).getDisplayName()); + } + + if (msg != null) { + Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show(); + } + + if (progress == 1000) { + numJoiners--; + if (numJoiners <= 0) { + if (getActivity() != null) getActivity().finish(); } - + } } - - + } } diff --git a/src/main/java/org/thoughtcrime/securesms/qr/RegistrationQrActivity.java b/src/main/java/org/thoughtcrime/securesms/qr/RegistrationQrActivity.java index 931f88c70..437f4b9e9 100644 --- a/src/main/java/org/thoughtcrime/securesms/qr/RegistrationQrActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/qr/RegistrationQrActivity.java @@ -8,16 +8,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; - import com.b44t.messenger.DcContext; import com.b44t.messenger.DcLot; import com.journeyapps.barcodescanner.CompoundBarcodeView; - import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; @@ -27,187 +23,200 @@ public class RegistrationQrActivity extends BaseActionBarActivity { - public static final String ADD_AS_SECOND_DEVICE_EXTRA = "add_as_second_device"; - public static final String QRDATA_EXTRA = "qrdata"; + public static final String ADD_AS_SECOND_DEVICE_EXTRA = "add_as_second_device"; + public static final String QRDATA_EXTRA = "qrdata"; - private CustomCaptureManager capture; + private CustomCaptureManager capture; - private CompoundBarcodeView barcodeScannerView; + private CompoundBarcodeView barcodeScannerView; - private DcContext dcContext; + private DcContext dcContext; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + boolean addAsAnotherDevice = getIntent().getBooleanExtra(ADD_AS_SECOND_DEVICE_EXTRA, false); + if (addAsAnotherDevice) { + setContentView(R.layout.activity_registration_2nd_device_qr); + getSupportActionBar().setTitle(R.string.multidevice_receiver_title); + } else { + setContentView(R.layout.activity_registration_qr); + getSupportActionBar().setTitle(R.string.scan_invitation_code); + } + getSupportActionBar().setDisplayHomeAsUpEnabled(true); - boolean addAsAnotherDevice = getIntent().getBooleanExtra(ADD_AS_SECOND_DEVICE_EXTRA, false); - if (addAsAnotherDevice) { - setContentView(R.layout.activity_registration_2nd_device_qr); - getSupportActionBar().setTitle(R.string.multidevice_receiver_title); - } else { - setContentView(R.layout.activity_registration_qr); - getSupportActionBar().setTitle(R.string.scan_invitation_code); - } - getSupportActionBar().setDisplayHomeAsUpEnabled(true); + // add padding to avoid content hidden behind system bars + ViewUtil.applyWindowInsets(findViewById(R.id.layout_container), true, false, true, true); - // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(findViewById(R.id.layout_container), true, false, true, true); + barcodeScannerView = findViewById(R.id.zxing_barcode_scanner); + barcodeScannerView.setStatusText(getString(R.string.qrscan_hint) + "\n "); - barcodeScannerView = findViewById(R.id.zxing_barcode_scanner); - barcodeScannerView.setStatusText(getString(R.string.qrscan_hint) + "\n "); + View sameNetworkHint = findViewById(R.id.same_network_hint); + if (sameNetworkHint != null) { + BackupTransferActivity.appendSSID(this, findViewById(R.id.same_network_hint)); + } - View sameNetworkHint = findViewById(R.id.same_network_hint); - if (sameNetworkHint != null) { - BackupTransferActivity.appendSSID(this, findViewById(R.id.same_network_hint)); - } + if (savedInstanceState != null) { + init(barcodeScannerView, getIntent(), savedInstanceState); + } - if (savedInstanceState != null) { - init(barcodeScannerView, getIntent(), savedInstanceState); - } + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied)) + .onAnyResult(this::handleQrScanWithPermissions) + .onAnyDenied(this::handleQrScanWithDeniedPermission) + .execute(); - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied)) - .onAnyResult(this::handleQrScanWithPermissions) - .onAnyDenied(this::handleQrScanWithDeniedPermission) - .execute(); + dcContext = DcHelper.getContext(this); + } - dcContext = DcHelper.getContext(this); - } + private void handleQrScanWithPermissions() { + init(barcodeScannerView, getIntent(), null); + } - private void handleQrScanWithPermissions() { - init(barcodeScannerView, getIntent(), null); - } + private void handleQrScanWithDeniedPermission() { + setResult(Activity.RESULT_CANCELED); + finish(); + } - private void handleQrScanWithDeniedPermission() { - setResult(Activity.RESULT_CANCELED); - finish(); + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + int itemId = item.getItemId(); + if (itemId == android.R.id.home) { + finish(); + return true; + } else if (itemId == R.id.troubleshooting) { + DcHelper.openHelp(this, "#multiclient"); + return true; + } else if (itemId == R.id.menu_paste) { + String rawQr = Util.getTextFromClipboard(this); + + Runnable okCallback = + () -> { + Intent intent = new Intent(); + intent.putExtra(QRDATA_EXTRA, rawQr); + setResult(Activity.RESULT_OK, intent); + finish(); + }; + + showConfirmDialog(rawQr, okCallback, null); + + return true; } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - - int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - finish(); - return true; - } else if (itemId == R.id.troubleshooting) { - DcHelper.openHelp(this, "#multiclient"); - return true; - } else if (itemId == R.id.menu_paste) { - String rawQr = Util.getTextFromClipboard(this); - - Runnable okCallback = () -> { - Intent intent = new Intent(); - intent.putExtra(QRDATA_EXTRA, rawQr); - setResult(Activity.RESULT_OK, intent); - finish(); - }; - - showConfirmDialog(rawQr, okCallback, null); - - return true; - } - - return false; - } + return false; + } - private void showConfirmDialog(String rawQr, @NonNull Runnable okCallback, @Nullable Runnable cancelCallback) { - DcLot qrParsed = dcContext.checkQr(rawQr); + private void showConfirmDialog( + String rawQr, @NonNull Runnable okCallback, @Nullable Runnable cancelCallback) { + DcLot qrParsed = dcContext.checkQr(rawQr); - String dialogMsg = ""; - if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT) { - String name = dcContext.getContact(qrParsed.getId()).getDisplayName(); - dialogMsg = getString(R.string.instant_onboarding_confirm_contact, name); - } else if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) { - String groupName = qrParsed.getText1(); - dialogMsg = getString(R.string.instant_onboarding_confirm_group, groupName); - } + String dialogMsg = ""; + if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT) { + String name = dcContext.getContact(qrParsed.getId()).getDisplayName(); + dialogMsg = getString(R.string.instant_onboarding_confirm_contact, name); + } else if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) { + String groupName = qrParsed.getText1(); + dialogMsg = getString(R.string.instant_onboarding_confirm_group, groupName); + } - if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT + if (qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYCONTACT || qrParsed.getState() == DcContext.DC_QR_ASK_VERIFYGROUP) { - AlertDialog confirmDialog = new AlertDialog.Builder(this) - .setMessage(dialogMsg) - .setPositiveButton("OK", (dialog, which) -> { - okCallback.run(); - }) - .setNegativeButton("Cancel", (dialog, which) -> { - if (cancelCallback != null) { - cancelCallback.run(); - } - }) - .show(); - } else { - okCallback.run(); - } + AlertDialog confirmDialog = + new AlertDialog.Builder(this) + .setMessage(dialogMsg) + .setPositiveButton( + "OK", + (dialog, which) -> { + okCallback.run(); + }) + .setNegativeButton( + "Cancel", + (dialog, which) -> { + if (cancelCallback != null) { + cancelCallback.run(); + } + }) + .show(); + } else { + okCallback.run(); } + } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - } + public void onRequestPermissionsResult( + int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + } - private void init(CompoundBarcodeView barcodeScannerView, Intent intent, Bundle savedInstanceState) { - capture = new CustomCaptureManager(this, barcodeScannerView); + private void init( + CompoundBarcodeView barcodeScannerView, Intent intent, Bundle savedInstanceState) { + capture = new CustomCaptureManager(this, barcodeScannerView); - capture.setResultInterceptor((result, finishCallback) -> { + capture.setResultInterceptor( + (result, finishCallback) -> { String rawQr = result.getText(); - showConfirmDialog(rawQr, finishCallback, () -> { - barcodeScannerView.resume(); - capture.decode(); - }); + showConfirmDialog( + rawQr, + finishCallback, + () -> { + barcodeScannerView.resume(); + capture.decode(); + }); }); - capture.initializeFromIntent(intent, savedInstanceState); - capture.decode(); - } + capture.initializeFromIntent(intent, savedInstanceState); + capture.decode(); + } - @Override - protected void onResume() { - super.onResume(); - if (capture != null) { - capture.onResume(); - } + @Override + protected void onResume() { + super.onResume(); + if (capture != null) { + capture.onResume(); } + } - @Override - protected void onPause() { - super.onPause(); - if (capture != null) { - capture.onPause(); - } + @Override + protected void onPause() { + super.onPause(); + if (capture != null) { + capture.onPause(); } + } - @Override - protected void onDestroy() { - super.onDestroy(); - if (capture != null) { - capture.onDestroy(); - } + @Override + protected void onDestroy() { + super.onDestroy(); + if (capture != null) { + capture.onDestroy(); } + } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.clear(); - getMenuInflater().inflate(R.menu.registration_qr_activity, menu); - boolean addAsAnotherDevice = getIntent().getBooleanExtra(ADD_AS_SECOND_DEVICE_EXTRA, false); - menu.findItem(R.id.troubleshooting).setVisible(addAsAnotherDevice); - return super.onPrepareOptionsMenu(menu); - } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + getMenuInflater().inflate(R.menu.registration_qr_activity, menu); + boolean addAsAnotherDevice = getIntent().getBooleanExtra(ADD_AS_SECOND_DEVICE_EXTRA, false); + menu.findItem(R.id.troubleshooting).setVisible(addAsAnotherDevice); + return super.onPrepareOptionsMenu(menu); + } - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (capture != null) { - capture.onSaveInstanceState(outState); - } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (capture != null) { + capture.onSaveInstanceState(outState); } + } - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); - } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/reactions/AddReactionView.java b/src/main/java/org/thoughtcrime/securesms/reactions/AddReactionView.java index 0662fc29f..5188b93e9 100644 --- a/src/main/java/org/thoughtcrime/securesms/reactions/AddReactionView.java +++ b/src/main/java/org/thoughtcrime/securesms/reactions/AddReactionView.java @@ -4,189 +4,192 @@ import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; - import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; import androidx.emoji2.emojipicker.EmojiPickerView; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.Reactions; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.util.ViewUtil; - import java.util.Collections; import java.util.List; import java.util.Map; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; -import chat.delta.rpc.types.Reactions; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.util.ViewUtil; public class AddReactionView extends LinearLayout { - private AppCompatTextView[] defaultReactionViews; - private AppCompatTextView anyReactionView; - private boolean anyReactionClearsReaction; - private Context context; - private DcContext dcContext; - private Rpc rpc; - private DcMsg msgToReactTo; - private AddReactionListener listener; - - public AddReactionView(Context context) { - super(context); - } - - public AddReactionView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - private void init() { - if (context == null) { - context = getContext(); - dcContext = DcHelper.getContext(context); - rpc = DcHelper.getRpc(getContext()); - defaultReactionViews = new AppCompatTextView[]{ - findViewById(R.id.reaction_0), - findViewById(R.id.reaction_1), - findViewById(R.id.reaction_2), - findViewById(R.id.reaction_3), - findViewById(R.id.reaction_4), + private AppCompatTextView[] defaultReactionViews; + private AppCompatTextView anyReactionView; + private boolean anyReactionClearsReaction; + private Context context; + private DcContext dcContext; + private Rpc rpc; + private DcMsg msgToReactTo; + private AddReactionListener listener; + + public AddReactionView(Context context) { + super(context); + } + + public AddReactionView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private void init() { + if (context == null) { + context = getContext(); + dcContext = DcHelper.getContext(context); + rpc = DcHelper.getRpc(getContext()); + defaultReactionViews = + new AppCompatTextView[] { + findViewById(R.id.reaction_0), + findViewById(R.id.reaction_1), + findViewById(R.id.reaction_2), + findViewById(R.id.reaction_3), + findViewById(R.id.reaction_4), }; - for (int i = 0; i < defaultReactionViews.length; i++) { - final int ii = i; - defaultReactionViews[i].setOnClickListener(v -> defaultReactionClicked(ii)); - } - anyReactionView = findViewById(R.id.reaction_any); - anyReactionView.setOnClickListener(v -> anyReactionClicked()); + for (int i = 0; i < defaultReactionViews.length; i++) { + final int ii = i; + defaultReactionViews[i].setOnClickListener(v -> defaultReactionClicked(ii)); } + anyReactionView = findViewById(R.id.reaction_any); + anyReactionView.setOnClickListener(v -> anyReactionClicked()); } + } - public void show(DcMsg msgToReactTo, View parentView, AddReactionListener listener) { - init(); // init delayed as needed - - if ( msgToReactTo.isInfo() - || !dcContext.getChat(msgToReactTo.getChatId()).canSend()) { - return; - } - - this.msgToReactTo = msgToReactTo; - this.listener = listener; - - final String existingReaction = getSelfReaction(); - boolean existingHilited = false; - for (AppCompatTextView defaultReactionView : defaultReactionViews) { - if (defaultReactionView.getText().toString().equals(existingReaction)) { - defaultReactionView.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); - existingHilited = true; - } else { - defaultReactionView.setBackground(null); - } - } + public void show(DcMsg msgToReactTo, View parentView, AddReactionListener listener) { + init(); // init delayed as needed - if (existingReaction != null && !existingHilited) { - anyReactionView.setText(existingReaction); - anyReactionView.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); - anyReactionClearsReaction = true; - } else { - anyReactionView.setText("⋯"); - anyReactionView.setBackground(null); - anyReactionClearsReaction = false; - } - - final int offset = (int)(this.getHeight() * 0.666); - int x = (int)parentView.getX(); - if (msgToReactTo.isOutgoing()) { - x += parentView.getWidth() - offset - this.getWidth(); - } else { - x += offset; - } - ViewUtil.setLeftMargin(this, Math.max(x, 0)); - - int y = Math.max((int)parentView.getY() - offset, offset/2); - ViewUtil.setTopMargin(this, y); - - setVisibility(View.VISIBLE); + if (msgToReactTo.isInfo() || !dcContext.getChat(msgToReactTo.getChatId()).canSend()) { + return; } - public void hide() { - setVisibility(View.GONE); + this.msgToReactTo = msgToReactTo; + this.listener = listener; + + final String existingReaction = getSelfReaction(); + boolean existingHilited = false; + for (AppCompatTextView defaultReactionView : defaultReactionViews) { + if (defaultReactionView.getText().toString().equals(existingReaction)) { + defaultReactionView.setBackground( + ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); + existingHilited = true; + } else { + defaultReactionView.setBackground(null); + } } - public void move(int dy) { - if (msgToReactTo != null && getVisibility() == View.VISIBLE) { - ViewUtil.setTopMargin(this, (int) this.getY() - dy); - } + if (existingReaction != null && !existingHilited) { + anyReactionView.setText(existingReaction); + anyReactionView.setBackground( + ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); + anyReactionClearsReaction = true; + } else { + anyReactionView.setText("⋯"); + anyReactionView.setBackground(null); + anyReactionClearsReaction = false; } - private String getSelfReaction() { - String result = null; - try { - final Reactions reactions = rpc.getMessageReactions(dcContext.getAccountId(), msgToReactTo.getId()); - if (reactions != null) { - final Map> reactionsByContact = reactions.reactionsByContact; - final List selfReactions = reactionsByContact.get(String.valueOf(DcContact.DC_CONTACT_ID_SELF)); - if (selfReactions != null && !selfReactions.isEmpty()) { - result = selfReactions.get(0); - } - } - } catch(RpcException e) { - e.printStackTrace(); - } - return result; + final int offset = (int) (this.getHeight() * 0.666); + int x = (int) parentView.getX(); + if (msgToReactTo.isOutgoing()) { + x += parentView.getWidth() - offset - this.getWidth(); + } else { + x += offset; } + ViewUtil.setLeftMargin(this, Math.max(x, 0)); - private void defaultReactionClicked(int i) { - final String reaction = defaultReactionViews[i].getText().toString(); - sendReaction(reaction); + int y = Math.max((int) parentView.getY() - offset, offset / 2); + ViewUtil.setTopMargin(this, y); - if (listener != null) { - listener.onShallHide(); - } - } + setVisibility(View.VISIBLE); + } - private void anyReactionClicked() { - if (anyReactionClearsReaction) { - sendReaction(null); - } else { - View pickerLayout = View.inflate(context, R.layout.reaction_picker, null); - - final AlertDialog alertDialog = new AlertDialog.Builder(context) - .setView(pickerLayout) - .setTitle(R.string.react) - .setPositiveButton(R.string.cancel, null) - .create(); - - EmojiPickerView pickerView = ViewUtil.findById(pickerLayout, R.id.emoji_picker); - pickerView.setOnEmojiPickedListener((it) -> { - sendReaction(it.getEmoji()); - alertDialog.dismiss(); - }); - - alertDialog.show(); - } + public void hide() { + setVisibility(View.GONE); + } - if (listener != null) { - listener.onShallHide(); + public void move(int dy) { + if (msgToReactTo != null && getVisibility() == View.VISIBLE) { + ViewUtil.setTopMargin(this, (int) this.getY() - dy); + } + } + + private String getSelfReaction() { + String result = null; + try { + final Reactions reactions = + rpc.getMessageReactions(dcContext.getAccountId(), msgToReactTo.getId()); + if (reactions != null) { + final Map> reactionsByContact = reactions.reactionsByContact; + final List selfReactions = + reactionsByContact.get(String.valueOf(DcContact.DC_CONTACT_ID_SELF)); + if (selfReactions != null && !selfReactions.isEmpty()) { + result = selfReactions.get(0); } + } + } catch (RpcException e) { + e.printStackTrace(); } + return result; + } - private void sendReaction(final String reaction) { - try { - if (reaction == null || reaction.equals(getSelfReaction())) { - rpc.sendReaction(dcContext.getAccountId(), msgToReactTo.getId(), Collections.singletonList("")); - } else { - rpc.sendReaction(dcContext.getAccountId(), msgToReactTo.getId(), Collections.singletonList(reaction)); - } - } catch(Exception e) { - e.printStackTrace(); - } + private void defaultReactionClicked(int i) { + final String reaction = defaultReactionViews[i].getText().toString(); + sendReaction(reaction); + + if (listener != null) { + listener.onShallHide(); + } + } + + private void anyReactionClicked() { + if (anyReactionClearsReaction) { + sendReaction(null); + } else { + View pickerLayout = View.inflate(context, R.layout.reaction_picker, null); + + final AlertDialog alertDialog = + new AlertDialog.Builder(context) + .setView(pickerLayout) + .setTitle(R.string.react) + .setPositiveButton(R.string.cancel, null) + .create(); + + EmojiPickerView pickerView = ViewUtil.findById(pickerLayout, R.id.emoji_picker); + pickerView.setOnEmojiPickedListener( + (it) -> { + sendReaction(it.getEmoji()); + alertDialog.dismiss(); + }); + + alertDialog.show(); } - public interface AddReactionListener { - void onShallHide(); + if (listener != null) { + listener.onShallHide(); } + } + + private void sendReaction(final String reaction) { + try { + if (reaction == null || reaction.equals(getSelfReaction())) { + rpc.sendReaction( + dcContext.getAccountId(), msgToReactTo.getId(), Collections.singletonList("")); + } else { + rpc.sendReaction( + dcContext.getAccountId(), msgToReactTo.getId(), Collections.singletonList(reaction)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public interface AddReactionListener { + void onShallHide(); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientItem.java b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientItem.java index 872c73681..92631f0c6 100644 --- a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientItem.java +++ b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientItem.java @@ -5,11 +5,8 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; - import com.b44t.messenger.DcContact; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.connect.DcHelper; @@ -20,11 +17,11 @@ public class ReactionRecipientItem extends LinearLayout { private AvatarImageView contactPhotoImage; - private TextView nameView; - private TextView reactionView; + private TextView nameView; + private TextView reactionView; - private int contactId; - private String reaction; + private int contactId; + private String reaction; public ReactionRecipientItem(Context context) { super(context); @@ -38,15 +35,15 @@ public ReactionRecipientItem(Context context, AttributeSet attrs) { protected void onFinishInflate() { super.onFinishInflate(); this.contactPhotoImage = findViewById(R.id.contact_photo_image); - this.nameView = findViewById(R.id.name); - this.reactionView = findViewById(R.id.reaction); + this.nameView = findViewById(R.id.name); + this.reactionView = findViewById(R.id.reaction); ViewUtil.setTextViewGravityStart(this.nameView, getContext()); } public void bind(@NonNull GlideRequests glideRequests, int contactId, String reaction) { - this.contactId = contactId; - this.reaction = reaction; + this.contactId = contactId; + this.reaction = reaction; DcContact dcContact = DcHelper.getContext(getContext()).getContact(contactId); Recipient recipient = new Recipient(getContext(), dcContact); this.contactPhotoImage.setAvatar(glideRequests, recipient, false); diff --git a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java index 4c6c6de5f..e50198bf9 100644 --- a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java @@ -4,23 +4,19 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - +import java.util.ArrayList; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.Pair; -import java.util.ArrayList; - -public class ReactionRecipientsAdapter extends RecyclerView.Adapter -{ +public class ReactionRecipientsAdapter extends RecyclerView.Adapter { private @NonNull ArrayList> contactsReactions = new ArrayList<>(); - private final LayoutInflater layoutInflater; - private final ItemClickListener clickListener; - private final GlideRequests glideRequests; + private final LayoutInflater layoutInflater; + private final ItemClickListener clickListener; + private final GlideRequests glideRequests; @Override public int getItemCount() { @@ -34,24 +30,29 @@ public ViewHolder(View itemView) { } public abstract void bind(@NonNull GlideRequests glideRequests, int contactId, String reaction); + public abstract void unbind(@NonNull GlideRequests glideRequests); } public static class ReactionViewHolder extends ViewHolder { - ReactionViewHolder(@NonNull final View itemView, - @Nullable final ItemClickListener clickListener) { + ReactionViewHolder( + @NonNull final View itemView, @Nullable final ItemClickListener clickListener) { super(itemView); - itemView.setOnClickListener(view -> { - if (clickListener != null) { - clickListener.onItemClick(getView()); - } - }); - ((ReactionRecipientItem) itemView).getReactionView().setOnClickListener(view -> { - if (clickListener != null) { - clickListener.onReactionClick(getView()); - } - }); + itemView.setOnClickListener( + view -> { + if (clickListener != null) { + clickListener.onItemClick(getView()); + } + }); + ((ReactionRecipientItem) itemView) + .getReactionView() + .setOnClickListener( + view -> { + if (clickListener != null) { + clickListener.onReactionClick(getView()); + } + }); } public ReactionRecipientItem getView() { @@ -68,10 +69,10 @@ public void unbind(@NonNull GlideRequests glideRequests) { } } - public ReactionRecipientsAdapter(@NonNull Context context, - @NonNull GlideRequests glideRequests, - @Nullable ItemClickListener clickListener) - { + public ReactionRecipientsAdapter( + @NonNull Context context, + @NonNull GlideRequests glideRequests, + @Nullable ItemClickListener clickListener) { super(); this.layoutInflater = LayoutInflater.from(context); this.glideRequests = glideRequests; @@ -80,8 +81,10 @@ public ReactionRecipientsAdapter(@NonNull Context context, @NonNull @Override - public ReactionRecipientsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ReactionViewHolder(layoutInflater.inflate(R.layout.reaction_recipient_item, parent, false), clickListener); + public ReactionRecipientsAdapter.ViewHolder onCreateViewHolder( + @NonNull ViewGroup parent, int viewType) { + return new ReactionViewHolder( + layoutInflater.inflate(R.layout.reaction_recipient_item, parent, false), clickListener); } @Override @@ -98,11 +101,12 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) public interface ItemClickListener { void onItemClick(ReactionRecipientItem item); + void onReactionClick(ReactionRecipientItem item); } public void changeData(ArrayList> contactsReactions) { - this.contactsReactions = contactsReactions==null? new ArrayList<>() : contactsReactions; + this.contactsReactions = contactsReactions == null ? new ArrayList<>() : contactsReactions; notifyDataSetChanged(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsConversationView.java b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsConversationView.java index 4c23426ea..5707aca76 100644 --- a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsConversationView.java +++ b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsConversationView.java @@ -8,19 +8,15 @@ import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.ViewUtil; - +import chat.delta.rpc.types.Reaction; import java.util.ArrayList; import java.util.List; - -import chat.delta.rpc.types.Reaction; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.ViewUtil; public class ReactionsConversationView extends LinearLayout { @@ -42,7 +38,10 @@ public ReactionsConversationView(Context context, @Nullable AttributeSet attrs) private void init(@Nullable AttributeSet attrs) { if (attrs != null) { - TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ReactionsConversationView, 0, 0); + TypedArray typedArray = + getContext() + .getTheme() + .obtainStyledAttributes(attrs, R.styleable.ReactionsConversationView, 0, 0); isIncoming = typedArray.getInt(R.styleable.ReactionsConversationView_reaction_type, 0) == 2; } } @@ -72,7 +71,8 @@ public void setReactions(List reactions) { } } - private static @NonNull List buildShortenedReactionsList(@NonNull List reactions) { + private static @NonNull List buildShortenedReactionsList( + @NonNull List reactions) { if (reactions.size() > 3) { List shortened = new ArrayList<>(3); shortened.add(reactions.get(0)); @@ -80,8 +80,8 @@ public void setReactions(List reactions) { int count = 0; boolean isFromSelf = false; for (int index = 2; index < reactions.size(); index++) { - count += reactions.get(index).count; - isFromSelf = isFromSelf || reactions.get(index).isFromSelf; + count += reactions.get(index).count; + isFromSelf = isFromSelf || reactions.get(index).isFromSelf; } Reaction reaction = new Reaction(); reaction.emoji = null; @@ -95,11 +95,12 @@ public void setReactions(List reactions) { } } - private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction) { - View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false); + private static View buildPill( + @NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction) { + View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false); AppCompatTextView emojiView = root.findViewById(R.id.reactions_pill_emoji); - TextView countView = root.findViewById(R.id.reactions_pill_count); - View spacer = root.findViewById(R.id.reactions_pill_spacer); + TextView countView = root.findViewById(R.id.reactions_pill_count); + View spacer = root.findViewById(R.id.reactions_pill_spacer); if (reaction.emoji != null) { emojiView.setText(reaction.emoji); @@ -117,8 +118,10 @@ private static View buildPill(@NonNull Context context, @NonNull ViewGroup paren } if (reaction.isFromSelf) { - root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); - countView.setTextColor(ContextCompat.getColor(context, R.color.reaction_pill_text_color_selected)); + root.setBackground( + ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); + countView.setTextColor( + ContextCompat.getColor(context, R.color.reaction_pill_text_color_selected)); } else { root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)); } diff --git a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java index 29225a944..bb2ed0763 100644 --- a/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDetailsFragment.java @@ -6,17 +6,21 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.Reactions; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; - +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import org.thoughtcrime.securesms.ProfileActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcEventCenter; @@ -25,16 +29,8 @@ import org.thoughtcrime.securesms.util.Pair; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; -import chat.delta.rpc.types.Reactions; - -public class ReactionsDetailsFragment extends DialogFragment implements DcEventCenter.DcEventDelegate { +public class ReactionsDetailsFragment extends DialogFragment + implements DcEventCenter.DcEventDelegate { private static final String TAG = ReactionsDetailsFragment.class.getSimpleName(); private static final String ARG_MSG_ID = "msg_id"; @@ -53,8 +49,10 @@ public static ReactionsDetailsFragment newInstance(int msgId) { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - msgId = getArguments() != null? getArguments().getInt(ARG_MSG_ID, 0) : 0; - adapter = new ReactionRecipientsAdapter(requireActivity(), GlideApp.with(requireActivity()), new ListClickListener()); + msgId = getArguments() != null ? getArguments().getInt(ARG_MSG_ID, 0) : 0; + adapter = + new ReactionRecipientsAdapter( + requireActivity(), GlideApp.with(requireActivity()), new ListClickListener()); LayoutInflater inflater = requireActivity().getLayoutInflater(); View view = inflater.inflate(R.layout.reactions_details_fragment, null); @@ -68,7 +66,8 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { DcEventCenter eventCenter = DcHelper.getEventCenter(requireContext()); eventCenter.addObserver(DcContext.DC_EVENT_REACTIONS_CHANGED, this); - AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()) + AlertDialog.Builder builder = + new AlertDialog.Builder(requireActivity()) .setTitle(R.string.reactions) .setNegativeButton(R.string.ok, null); return builder.setView(view).create(); @@ -95,18 +94,20 @@ private void refreshData() { int accId = DcHelper.getContext(requireActivity()).getAccountId(); try { - final Reactions reactions = DcHelper.getRpc(requireActivity()).getMessageReactions(accId, msgId); + final Reactions reactions = + DcHelper.getRpc(requireActivity()).getMessageReactions(accId, msgId); ArrayList> contactsReactions = new ArrayList<>(); if (reactions != null) { Map> reactionsByContact = reactions.reactionsByContact; - List selfReactions = reactionsByContact.remove(String.valueOf(DcContact.DC_CONTACT_ID_SELF)); - for (String contact: reactionsByContact.keySet()) { - for (String reaction: reactionsByContact.get(contact)) { + List selfReactions = + reactionsByContact.remove(String.valueOf(DcContact.DC_CONTACT_ID_SELF)); + for (String contact : reactionsByContact.keySet()) { + for (String reaction : reactionsByContact.get(contact)) { contactsReactions.add(new Pair<>(Integer.parseInt(contact), reaction)); } } if (selfReactions != null) { - for (String reaction: selfReactions) { + for (String reaction : selfReactions) { contactsReactions.add(new Pair<>(DcContact.DC_CONTACT_ID_SELF, reaction)); } } @@ -129,12 +130,13 @@ private String getSelfReaction(Rpc rpc, int accId) { final Reactions reactions = rpc.getMessageReactions(accId, msgId); if (reactions != null) { final Map> reactionsByContact = reactions.reactionsByContact; - final List selfReactions = reactionsByContact.get(String.valueOf(DcContact.DC_CONTACT_ID_SELF)); + final List selfReactions = + reactionsByContact.get(String.valueOf(DcContact.DC_CONTACT_ID_SELF)); if (selfReactions != null && !selfReactions.isEmpty()) { result = selfReactions.get(0); } } - } catch(RpcException e) { + } catch (RpcException e) { e.printStackTrace(); } return result; @@ -151,7 +153,7 @@ private void sendReaction(final String reaction) { } else { rpc.sendReaction(accId, msgId, Collections.singletonList(reaction)); } - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -160,11 +162,11 @@ private class ListClickListener implements ReactionRecipientsAdapter.ItemClickLi @Override public void onItemClick(ReactionRecipientItem item) { - int contactId = item.getContactId(); - if (contactId != DcContact.DC_CONTACT_ID_SELF) { - ReactionsDetailsFragment.this.dismiss(); - openConversation(contactId); - } + int contactId = item.getContactId(); + if (contactId != DcContact.DC_CONTACT_ID_SELF) { + ReactionsDetailsFragment.this.dismiss(); + openConversation(contactId); + } } @Override @@ -173,5 +175,4 @@ public void onReactionClick(ReactionRecipientItem item) { ReactionsDetailsFragment.this.dismiss(); } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 82aa6b29b..447487ddc 100644 --- a/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -22,17 +22,18 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.TextUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import chat.delta.rpc.types.VcardContact; import com.b44t.messenger.DcChat; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; - +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.WeakHashMap; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; -import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.LocalFileContactPhoto; @@ -44,26 +45,20 @@ import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.Util; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.WeakHashMap; - -import chat.delta.rpc.types.VcardContact; - public class Recipient { - private final Set listeners = Collections.newSetFromMap(new WeakHashMap()); + private final Set listeners = + Collections.newSetFromMap(new WeakHashMap()); private final @NonNull Address address; - private final @Nullable String customLabel; + private final @Nullable String customLabel; - private @Nullable Uri systemContactPhoto; - private final Uri contactUri; + private @Nullable Uri systemContactPhoto; + private final Uri contactUri; - private final @Nullable String profileName; - private @Nullable String profileAvatar; + private final @Nullable String profileName; + private @Nullable String profileAvatar; private final @Nullable DcChat dcChat; private @Nullable DcContact dcContact; @@ -78,14 +73,13 @@ public class Recipient { public static @NonNull Recipient from(@NonNull Context context, @NonNull Address address) { if (address == null) throw new AssertionError(address); DcContext dcContext = DcHelper.getContext(context); - if(address.isDcContact()) { + if (address.isDcContact()) { return new Recipient(context, dcContext.getContact(address.getDcContactId())); } else if (address.isDcChat()) { return new Recipient(context, dcContext.getChat(address.getDcChatId())); - } - else if(DcHelper.getContext(context).mayBeValidAddr(address.toString())) { + } else if (DcHelper.getContext(context).mayBeValidAddr(address.toString())) { int contactId = dcContext.lookupContactIdByAddr(address.toString()); - if(contactId!=0) { + if (contactId != 0) { return new Recipient(context, dcContext.getContact(contactId)); } } @@ -104,52 +98,54 @@ public Recipient(@NonNull Context context, @NonNull DcContact dcContact) { this(context, null, dcContact, null, null); } - public Recipient(@NonNull Context context, @NonNull DcContact dcContact, @NonNull String profileName) { + public Recipient( + @NonNull Context context, @NonNull DcContact dcContact, @NonNull String profileName) { this(context, null, dcContact, profileName, null); } - private Recipient(@NonNull Context context, @Nullable DcChat dcChat, @Nullable DcContact dcContact, @Nullable String profileName, @Nullable VcardContact vContact) { - this.dcChat = dcChat; - this.dcContact = dcContact; - this.profileName = profileName; - this.vContact = vContact; - this.contactUri = null; - this.systemContactPhoto = null; - this.customLabel = null; - this.profileAvatar = null; - - if(dcContact!=null) { + private Recipient( + @NonNull Context context, + @Nullable DcChat dcChat, + @Nullable DcContact dcContact, + @Nullable String profileName, + @Nullable VcardContact vContact) { + this.dcChat = dcChat; + this.dcContact = dcContact; + this.profileName = profileName; + this.vContact = vContact; + this.contactUri = null; + this.systemContactPhoto = null; + this.customLabel = null; + this.profileAvatar = null; + + if (dcContact != null) { this.address = Address.fromContact(dcContact.getId()); maybeSetSystemContactPhoto(context, dcContact); if (dcContact.getId() == DcContact.DC_CONTACT_ID_SELF) { setProfileAvatar("SELF"); } - } - else if(dcChat!=null) { + } else if (dcChat != null) { int chatId = dcChat.getId(); this.address = Address.fromChat(chatId); if (!dcChat.isMultiUser()) { DcContext dcContext = DcHelper.getAccounts(context).getAccount(dcChat.getAccountId()); int[] contacts = dcContext.getChatContacts(chatId); - if( contacts.length>=1 ) { + if (contacts.length >= 1) { this.dcContact = dcContext.getContact(contacts[0]); maybeSetSystemContactPhoto(context, this.dcContact); } } - } - else { + } else { this.address = Address.UNKNOWN; } } public @Nullable String getName() { - if(dcChat!=null) { + if (dcChat != null) { return dcChat.getName(); - } - else if(dcContact!=null) { + } else if (dcContact != null) { return dcContact.getDisplayName(); - } - else if(vContact!=null) { + } else if (vContact != null) { return vContact.displayName; } return ""; @@ -176,7 +172,7 @@ public void setProfileAvatar(@Nullable String profileAvatar) { } public boolean isMultiUserRecipient() { - return dcChat!=null && dcChat.isMultiUser(); + return dcChat != null && dcChat.isMultiUser(); } public synchronized void addListener(RecipientModifiedListener listener) { @@ -193,13 +189,11 @@ public synchronized String toShortString() { public int getFallbackAvatarColor() { int rgb = 0x00808080; - if(dcChat!=null) { + if (dcChat != null) { rgb = dcChat.getColor(); - } - else if(dcContact!=null) { + } else if (dcContact != null) { rgb = dcContact.getColor(); - } - else if(vContact!=null) { + } else if (vContact != null) { rgb = Color.parseColor(vContact.color); } return Color.argb(0xFF, Color.red(rgb), Color.green(rgb), Color.blue(rgb)); @@ -209,34 +203,34 @@ else if(vContact!=null) { return getFallbackAvatarDrawable(context, true); } - public synchronized @NonNull Drawable getFallbackAvatarDrawable(Context context, boolean roundShape) { + public synchronized @NonNull Drawable getFallbackAvatarDrawable( + Context context, boolean roundShape) { return getFallbackContactPhoto().asDrawable(context, getFallbackAvatarColor(), roundShape); } public synchronized @NonNull GeneratedContactPhoto getFallbackContactPhoto() { String name = getName(); if (!TextUtils.isEmpty(profileName)) return new GeneratedContactPhoto(profileName); - else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name); - else return new GeneratedContactPhoto("#"); + else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name); + else return new GeneratedContactPhoto("#"); } public synchronized @Nullable ContactPhoto getContactPhoto(Context context) { LocalFileContactPhoto contactPhoto = null; - if (dcChat!=null) { + if (dcChat != null) { contactPhoto = new GroupRecordContactPhoto(context, address, dcChat); - } - else if (dcContact!=null) { - contactPhoto = new ProfileContactPhoto(context, address, dcContact); + } else if (dcContact != null) { + contactPhoto = new ProfileContactPhoto(context, address, dcContact); } - if (contactPhoto!=null) { + if (contactPhoto != null) { String path = contactPhoto.getPath(context); if (path != null && !path.isEmpty()) { return contactPhoto; } } - if (vContact!=null && vContact.profileImage != null) { + if (vContact != null && vContact.profileImage != null) { return new VcardContactPhoto(vContact); } @@ -290,26 +284,34 @@ private void notifyListeners() { localListeners = new HashSet<>(listeners); } - for (RecipientModifiedListener listener : localListeners) - listener.onModified(this); + for (RecipientModifiedListener listener : localListeners) listener.onModified(this); } - public DcChat getChat() - { - return dcChat!=null? dcChat : new DcChat(0, 0); + public DcChat getChat() { + return dcChat != null ? dcChat : new DcChat(0, 0); } @NonNull @Override public String toString() { - return "Recipient{" + - "listeners=" + listeners + - ", address=" + address + - ", customLabel='" + customLabel + '\'' + - ", systemContactPhoto=" + systemContactPhoto + - ", contactUri=" + contactUri + - ", profileName='" + profileName + '\'' + - ", profileAvatar='" + profileAvatar + '\'' + - '}'; + return "Recipient{" + + "listeners=" + + listeners + + ", address=" + + address + + ", customLabel='" + + customLabel + + '\'' + + ", systemContactPhoto=" + + systemContactPhoto + + ", contactUri=" + + contactUri + + ", profileName='" + + profileName + + '\'' + + ", profileAvatar='" + + profileAvatar + + '\'' + + '}'; } } diff --git a/src/main/java/org/thoughtcrime/securesms/recipients/RecipientModifiedListener.java b/src/main/java/org/thoughtcrime/securesms/recipients/RecipientModifiedListener.java index d6e0955c3..adda87df3 100644 --- a/src/main/java/org/thoughtcrime/securesms/recipients/RecipientModifiedListener.java +++ b/src/main/java/org/thoughtcrime/securesms/recipients/RecipientModifiedListener.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.recipients; - public interface RecipientModifiedListener { public void onModified(Recipient recipient); } diff --git a/src/main/java/org/thoughtcrime/securesms/relay/EditRelayActivity.java b/src/main/java/org/thoughtcrime/securesms/relay/EditRelayActivity.java index 62c5e905e..e14de1d2f 100644 --- a/src/main/java/org/thoughtcrime/securesms/relay/EditRelayActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/relay/EditRelayActivity.java @@ -20,19 +20,22 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.SwitchCompat; import androidx.constraintlayout.widget.Group; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.EnteredCertificateChecks; +import chat.delta.rpc.types.EnteredLoginParam; +import chat.delta.rpc.types.Socket; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcProvider; import com.google.android.material.textfield.TextInputEditText; - +import java.util.List; import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.LogViewActivity; @@ -47,495 +50,513 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.ProgressDialog; -import java.util.List; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; -import chat.delta.rpc.types.EnteredCertificateChecks; -import chat.delta.rpc.types.EnteredLoginParam; -import chat.delta.rpc.types.Socket; - -public class EditRelayActivity extends BaseActionBarActivity implements DcEventCenter.DcEventDelegate { - - private enum VerificationType { - EMAIL, - SERVER, - PORT, - } - - private static final String TAG = EditRelayActivity.class.getSimpleName(); - public final static String EXTRA_ADDR = "extra_addr"; - - private TextInputEditText emailInput; - private TextInputEditText passwordInput; - - private View providerLayout; - private TextView providerHint; - private TextView providerLink; - private @Nullable DcProvider provider; - - private Group advancedGroup; - private ImageView advancedIcon; - private ProgressDialog progressDialog; - private boolean cancelled = false; - - Spinner imapSecurity; - Spinner smtpSecurity; - Spinner certCheck; - - private SwitchCompat proxySwitch; - - Rpc rpc; - int accId; - - @Override - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - rpc = DcHelper.getRpc(this); - accId = DcHelper.getContext(this).getAccountId(); - - setContentView(R.layout.activity_edittransport); - - // add padding to avoid content hidden behind system bars - ViewUtil.applyWindowInsets(findViewById(R.id.content_container)); - - - emailInput = findViewById(R.id.email_text); - passwordInput = findViewById(R.id.password_text); - - providerLayout = findViewById(R.id.provider_layout); - providerHint = findViewById(R.id.provider_hint); - providerLink = findViewById(R.id.provider_link); - providerLink.setOnClickListener(l -> onProviderLink()); - - advancedGroup = findViewById(R.id.advanced_group); - advancedIcon = findViewById(R.id.advanced_icon); - TextView advancedTextView = findViewById(R.id.advanced_text); - TextInputEditText imapServerInput = findViewById(R.id.imap_server_text); - TextInputEditText imapPortInput = findViewById(R.id.imap_port_text); - TextInputEditText smtpServerInput = findViewById(R.id.smtp_server_text); - TextInputEditText smtpPortInput = findViewById(R.id.smtp_port_text); - TextView viewLogText = findViewById(R.id.view_log_button); - - imapSecurity = findViewById(R.id.imap_security); - smtpSecurity = findViewById(R.id.smtp_security); - certCheck = findViewById(R.id.cert_check); - - proxySwitch = findViewById(R.id.proxy_settings); - proxySwitch.setOnClickListener(l -> { - proxySwitch.setChecked(!proxySwitch.isChecked()); // revert toggle - startActivity(new Intent(this, ProxySettingsActivity.class)); +public class EditRelayActivity extends BaseActionBarActivity + implements DcEventCenter.DcEventDelegate { + + private enum VerificationType { + EMAIL, + SERVER, + PORT, + } + + private static final String TAG = EditRelayActivity.class.getSimpleName(); + public static final String EXTRA_ADDR = "extra_addr"; + + private TextInputEditText emailInput; + private TextInputEditText passwordInput; + + private View providerLayout; + private TextView providerHint; + private TextView providerLink; + private @Nullable DcProvider provider; + + private Group advancedGroup; + private ImageView advancedIcon; + private ProgressDialog progressDialog; + private boolean cancelled = false; + + Spinner imapSecurity; + Spinner smtpSecurity; + Spinner certCheck; + + private SwitchCompat proxySwitch; + + Rpc rpc; + int accId; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + rpc = DcHelper.getRpc(this); + accId = DcHelper.getContext(this).getAccountId(); + + setContentView(R.layout.activity_edittransport); + + // add padding to avoid content hidden behind system bars + ViewUtil.applyWindowInsets(findViewById(R.id.content_container)); + + emailInput = findViewById(R.id.email_text); + passwordInput = findViewById(R.id.password_text); + + providerLayout = findViewById(R.id.provider_layout); + providerHint = findViewById(R.id.provider_hint); + providerLink = findViewById(R.id.provider_link); + providerLink.setOnClickListener(l -> onProviderLink()); + + advancedGroup = findViewById(R.id.advanced_group); + advancedIcon = findViewById(R.id.advanced_icon); + TextView advancedTextView = findViewById(R.id.advanced_text); + TextInputEditText imapServerInput = findViewById(R.id.imap_server_text); + TextInputEditText imapPortInput = findViewById(R.id.imap_port_text); + TextInputEditText smtpServerInput = findViewById(R.id.smtp_server_text); + TextInputEditText smtpPortInput = findViewById(R.id.smtp_port_text); + TextView viewLogText = findViewById(R.id.view_log_button); + + imapSecurity = findViewById(R.id.imap_security); + smtpSecurity = findViewById(R.id.smtp_security); + certCheck = findViewById(R.id.cert_check); + + proxySwitch = findViewById(R.id.proxy_settings); + proxySwitch.setOnClickListener( + l -> { + proxySwitch.setChecked(!proxySwitch.isChecked()); // revert toggle + startActivity(new Intent(this, ProxySettingsActivity.class)); }); - String addr = getIntent().getStringExtra(EXTRA_ADDR); - EnteredLoginParam config = null; - try { - List relays = rpc.listTransports(accId); - for (EnteredLoginParam relay : relays) { - if (addr != null && addr.equals(relay.addr)) { - config = relay; - break; - } - } - if (config == null && !relays.isEmpty()) { - Log.e(TAG, "Error got unknown address: " + addr); - finish(); - return; - }; - } catch (RpcException e) { - Log.e(TAG, "Error calling Rpc.listTransports()", e); - finish(); - return; - } - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle( - config != null? R.string.edit_transport : R.string.manual_account_setup_option - ); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); + String addr = getIntent().getStringExtra(EXTRA_ADDR); + EnteredLoginParam config = null; + try { + List relays = rpc.listTransports(accId); + for (EnteredLoginParam relay : relays) { + if (addr != null && addr.equals(relay.addr)) { + config = relay; + break; } - - if (config != null) emailInput.setEnabled(false); - emailInput.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { } - @Override - public void afterTextChanged(Editable s) { maybeCleanProviderInfo(); } - }); - emailInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.EMAIL)); - imapServerInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.SERVER)); - imapPortInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.PORT)); - smtpServerInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.SERVER)); - smtpPortInput.setOnFocusChangeListener((view, focused) -> focusListener(view, focused, VerificationType.PORT)); - advancedTextView.setOnClickListener(l -> onAdvancedSettings()); - advancedIcon.setOnClickListener(l -> onAdvancedSettings()); - advancedIcon.setRotation(45); - viewLogText.setOnClickListener((view) -> showLog()); - - boolean expandAdvanced = false; - int intVal; - - intVal = DcHelper.getInt(this, CONFIG_PROXY_ENABLED); - proxySwitch.setChecked(intVal == 1); - expandAdvanced = expandAdvanced || intVal == 1; - - if (config != null) { // configured - emailInput.setText(config.addr); - if(!TextUtils.isEmpty(config.addr)) { - emailInput.setSelection(config.addr.length(), config.addr.length()); - } - passwordInput.setText(config.password); - - TextInputEditText imapLoginInput = findViewById(R.id.imap_login_text); - imapLoginInput.setText(config.imapUser); - expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.imapUser); - - imapServerInput.setText(config.imapServer); - expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.imapServer); - - if (config.imapPort != null) imapPortInput.setText(config.imapPort.toString()); - expandAdvanced = expandAdvanced || config.imapPort != null; - - intVal = socketSecurityToInt(config.imapSecurity); - imapSecurity.setSelection(ViewUtil.checkBounds(intVal, imapSecurity)); - expandAdvanced = expandAdvanced || intVal != 0; - - TextInputEditText smtpLoginInput = findViewById(R.id.smtp_login_text); - smtpLoginInput.setText(config.smtpUser); - expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.smtpUser); - - TextInputEditText smtpPasswordInput = findViewById(R.id.smtp_password_text); - smtpPasswordInput.setText(config.smtpPassword); - expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.smtpPassword); - - smtpServerInput.setText(config.smtpServer); - expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.smtpServer); - - if (config.smtpPort != null) smtpPortInput.setText(config.smtpPort.toString()); - expandAdvanced = expandAdvanced || config.smtpPort != null; - - intVal = socketSecurityToInt(config.smtpSecurity); - smtpSecurity.setSelection(ViewUtil.checkBounds(intVal, smtpSecurity)); - expandAdvanced = expandAdvanced || intVal != 0; - - intVal = certificateChecksToInt(config.certificateChecks); - certCheck.setSelection(ViewUtil.checkBounds(intVal, certCheck)); - expandAdvanced = expandAdvanced || intVal != 0; - } - - if (expandAdvanced) { onAdvancedSettings(); } - registerForEvents(); + } + if (config == null && !relays.isEmpty()) { + Log.e(TAG, "Error got unknown address: " + addr); + finish(); + return; + } + ; + } catch (RpcException e) { + Log.e(TAG, "Error calling Rpc.listTransports()", e); + finish(); + return; } - private void registerForEvents() { - DcHelper.getEventCenter(this).addObserver(DcContext.DC_EVENT_CONFIGURE_PROGRESS, this); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle( + config != null ? R.string.edit_transport : R.string.manual_account_setup_option); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); } - @Override - public void onResume() { - super.onResume(); - proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1); - } + if (config != null) emailInput.setEnabled(false); + emailInput.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - private void showLog() { - Intent intent = new Intent(getApplicationContext(), LogViewActivity.class); - startActivity(intent); - } + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuInflater inflater = this.getMenuInflater(); - menu.clear(); - inflater.inflate(R.menu.registration, menu); - super.onPrepareOptionsMenu(menu); - return true; + @Override + public void afterTextChanged(Editable s) { + maybeCleanProviderInfo(); + } + }); + emailInput.setOnFocusChangeListener( + (view, focused) -> focusListener(view, focused, VerificationType.EMAIL)); + imapServerInput.setOnFocusChangeListener( + (view, focused) -> focusListener(view, focused, VerificationType.SERVER)); + imapPortInput.setOnFocusChangeListener( + (view, focused) -> focusListener(view, focused, VerificationType.PORT)); + smtpServerInput.setOnFocusChangeListener( + (view, focused) -> focusListener(view, focused, VerificationType.SERVER)); + smtpPortInput.setOnFocusChangeListener( + (view, focused) -> focusListener(view, focused, VerificationType.PORT)); + advancedTextView.setOnClickListener(l -> onAdvancedSettings()); + advancedIcon.setOnClickListener(l -> onAdvancedSettings()); + advancedIcon.setRotation(45); + viewLogText.setOnClickListener((view) -> showLog()); + + boolean expandAdvanced = false; + int intVal; + + intVal = DcHelper.getInt(this, CONFIG_PROXY_ENABLED); + proxySwitch.setChecked(intVal == 1); + expandAdvanced = expandAdvanced || intVal == 1; + + if (config != null) { // configured + emailInput.setText(config.addr); + if (!TextUtils.isEmpty(config.addr)) { + emailInput.setSelection(config.addr.length(), config.addr.length()); + } + passwordInput.setText(config.password); + + TextInputEditText imapLoginInput = findViewById(R.id.imap_login_text); + imapLoginInput.setText(config.imapUser); + expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.imapUser); + + imapServerInput.setText(config.imapServer); + expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.imapServer); + + if (config.imapPort != null) imapPortInput.setText(config.imapPort.toString()); + expandAdvanced = expandAdvanced || config.imapPort != null; + + intVal = socketSecurityToInt(config.imapSecurity); + imapSecurity.setSelection(ViewUtil.checkBounds(intVal, imapSecurity)); + expandAdvanced = expandAdvanced || intVal != 0; + + TextInputEditText smtpLoginInput = findViewById(R.id.smtp_login_text); + smtpLoginInput.setText(config.smtpUser); + expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.smtpUser); + + TextInputEditText smtpPasswordInput = findViewById(R.id.smtp_password_text); + smtpPasswordInput.setText(config.smtpPassword); + expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.smtpPassword); + + smtpServerInput.setText(config.smtpServer); + expandAdvanced = expandAdvanced || !TextUtils.isEmpty(config.smtpServer); + + if (config.smtpPort != null) smtpPortInput.setText(config.smtpPort.toString()); + expandAdvanced = expandAdvanced || config.smtpPort != null; + + intVal = socketSecurityToInt(config.smtpSecurity); + smtpSecurity.setSelection(ViewUtil.checkBounds(intVal, smtpSecurity)); + expandAdvanced = expandAdvanced || intVal != 0; + + intVal = certificateChecksToInt(config.certificateChecks); + certCheck.setSelection(ViewUtil.checkBounds(intVal, certCheck)); + expandAdvanced = expandAdvanced || intVal != 0; } - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - if (id == R.id.do_register) { - updateProviderInfo(); - onLogin(); - return true; - } else if (id == android.R.id.home) { - // handle close button click here - finish(); - return true; - } - return super.onOptionsItemSelected(item); + if (expandAdvanced) { + onAdvancedSettings(); } - - @Override - public void onDestroy() { - super.onDestroy(); - DcHelper.getEventCenter(this).removeObservers(this); + registerForEvents(); + } + + private void registerForEvents() { + DcHelper.getEventCenter(this).addObserver(DcContext.DC_EVENT_CONFIGURE_PROGRESS, this); + } + + @Override + public void onResume() { + super.onResume(); + proxySwitch.setChecked(DcHelper.getInt(this, CONFIG_PROXY_ENABLED) == 1); + } + + private void showLog() { + Intent intent = new Intent(getApplicationContext(), LogViewActivity.class); + startActivity(intent); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuInflater inflater = this.getMenuInflater(); + menu.clear(); + inflater.inflate(R.menu.registration, menu); + super.onPrepareOptionsMenu(menu); + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.do_register) { + updateProviderInfo(); + onLogin(); + return true; + } else if (id == android.R.id.home) { + // handle close button click here + finish(); + return true; } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + return super.onOptionsItemSelected(item); + } + + @Override + public void onDestroy() { + super.onDestroy(); + DcHelper.getEventCenter(this).removeObservers(this); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + } + + private void focusListener(View view, boolean focused, VerificationType type) { + + if (!focused) { + TextInputEditText inputEditText = (TextInputEditText) view; + switch (type) { + case EMAIL: + verifyEmail(inputEditText); + updateProviderInfo(); + break; + case SERVER: + verifyServer(inputEditText); + break; + case PORT: + verifyPort(inputEditText); + break; + } } - - private void focusListener(View view, boolean focused, VerificationType type) { - - if (!focused) { - TextInputEditText inputEditText = (TextInputEditText) view; - switch (type) { - case EMAIL: - verifyEmail(inputEditText); - updateProviderInfo(); - break; - case SERVER: - verifyServer(inputEditText); - break; - case PORT: - verifyPort(inputEditText); - break; - } - } - } - - private void updateProviderInfo() { - Util.runOnBackground(() -> { - provider = getContext(this).getProviderFromEmailWithDns(emailInput.getText().toString()); - Util.runOnMain(() -> { - if (provider!=null) { - Resources res = getResources(); - providerHint.setText(provider.getBeforeLoginHint()); - switch (provider.getStatus()) { - case DcProvider.DC_PROVIDER_STATUS_PREPARATION: - providerHint.setTextColor(res.getColor(R.color.provider_prep_fg)); - providerLink.setTextColor(res.getColor(R.color.provider_prep_fg)); - providerLayout.setBackgroundColor(res.getColor(R.color.provider_prep_bg)); - providerLayout.setVisibility(View.VISIBLE); - break; - - case DcProvider.DC_PROVIDER_STATUS_BROKEN: - providerHint.setTextColor(res.getColor(R.color.provider_broken_fg)); - providerLink.setTextColor(res.getColor(R.color.provider_broken_fg)); - providerLayout.setBackgroundColor(getResources().getColor(R.color.provider_broken_bg)); - providerLayout.setVisibility(View.VISIBLE); - break; - - default: - providerLayout.setVisibility(View.GONE); - break; + } + + private void updateProviderInfo() { + Util.runOnBackground( + () -> { + provider = getContext(this).getProviderFromEmailWithDns(emailInput.getText().toString()); + Util.runOnMain( + () -> { + if (provider != null) { + Resources res = getResources(); + providerHint.setText(provider.getBeforeLoginHint()); + switch (provider.getStatus()) { + case DcProvider.DC_PROVIDER_STATUS_PREPARATION: + providerHint.setTextColor(res.getColor(R.color.provider_prep_fg)); + providerLink.setTextColor(res.getColor(R.color.provider_prep_fg)); + providerLayout.setBackgroundColor(res.getColor(R.color.provider_prep_bg)); + providerLayout.setVisibility(View.VISIBLE); + break; + + case DcProvider.DC_PROVIDER_STATUS_BROKEN: + providerHint.setTextColor(res.getColor(R.color.provider_broken_fg)); + providerLink.setTextColor(res.getColor(R.color.provider_broken_fg)); + providerLayout.setBackgroundColor( + getResources().getColor(R.color.provider_broken_bg)); + providerLayout.setVisibility(View.VISIBLE); + break; + + default: + providerLayout.setVisibility(View.GONE); + break; + } + } else { + providerLayout.setVisibility(View.GONE); } - } else { - providerLayout.setVisibility(View.GONE); - } - }); + }); }); - } + } - private void maybeCleanProviderInfo() { - if (provider!=null && providerLayout.getVisibility()==View.VISIBLE) { - provider = null; - providerLayout.setVisibility(View.GONE); - } + private void maybeCleanProviderInfo() { + if (provider != null && providerLayout.getVisibility() == View.VISIBLE) { + provider = null; + providerLayout.setVisibility(View.GONE); } - - private void onProviderLink() { - if (provider!=null) { - String url = provider.getOverviewPage(); - if(!url.isEmpty()) { - IntentUtils.showInBrowser(this, url); - } else { - // this should normally not happen - Toast.makeText(this, "ErrProviderWithoutUrl", Toast.LENGTH_LONG).show(); - } - } + } + + private void onProviderLink() { + if (provider != null) { + String url = provider.getOverviewPage(); + if (!url.isEmpty()) { + IntentUtils.showInBrowser(this, url); + } else { + // this should normally not happen + Toast.makeText(this, "ErrProviderWithoutUrl", Toast.LENGTH_LONG).show(); + } } + } - private void verifyEmail(TextInputEditText view) { - String error = getString(R.string.login_error_mail); - String email = view.getText().toString(); - if (!DcHelper.getContext(this).mayBeValidAddr(email)) { - view.setError(error); - } + private void verifyEmail(TextInputEditText view) { + String error = getString(R.string.login_error_mail); + String email = view.getText().toString(); + if (!DcHelper.getContext(this).mayBeValidAddr(email)) { + view.setError(error); } - - private void verifyServer(TextInputEditText view) { - String error = getString(R.string.login_error_server); - String server = view.getText().toString(); - if (!TextUtils.isEmpty(server) && !Patterns.DOMAIN_NAME.matcher(server).matches() - && !Patterns.IP_ADDRESS.matcher(server).matches() - && !Patterns.WEB_URL.matcher(server).matches() - && !"localhost".equals(server)) { - view.setError(error); - } + } + + private void verifyServer(TextInputEditText view) { + String error = getString(R.string.login_error_server); + String server = view.getText().toString(); + if (!TextUtils.isEmpty(server) + && !Patterns.DOMAIN_NAME.matcher(server).matches() + && !Patterns.IP_ADDRESS.matcher(server).matches() + && !Patterns.WEB_URL.matcher(server).matches() + && !"localhost".equals(server)) { + view.setError(error); } - - private void verifyPort(TextInputEditText view) { - String error = getString(R.string.login_error_port); - String portString = view.getText().toString(); - if (!portString.isEmpty()) { - try { - int port = Integer.valueOf(portString); - if (port < 1 || port > 65535) { - view.setError(error); - } - } catch (NumberFormatException exception) { - view.setError(error); - } + } + + private void verifyPort(TextInputEditText view) { + String error = getString(R.string.login_error_port); + String portString = view.getText().toString(); + if (!portString.isEmpty()) { + try { + int port = Integer.valueOf(portString); + if (port < 1 || port > 65535) { + view.setError(error); } + } catch (NumberFormatException exception) { + view.setError(error); + } } - - private void onAdvancedSettings() { - boolean advancedViewVisible = advancedGroup.getVisibility() == View.VISIBLE; - if (advancedViewVisible) { - advancedGroup.setVisibility(View.GONE); - advancedIcon.setRotation(45); - } else { - advancedGroup.setVisibility(View.VISIBLE); - advancedIcon.setRotation(0); - } + } + + private void onAdvancedSettings() { + boolean advancedViewVisible = advancedGroup.getVisibility() == View.VISIBLE; + if (advancedViewVisible) { + advancedGroup.setVisibility(View.GONE); + advancedIcon.setRotation(45); + } else { + advancedGroup.setVisibility(View.VISIBLE); + advancedIcon.setRotation(0); } + } - private void onLogin() { - if (!verifyRequiredFields()) { - Toast.makeText(this, R.string.login_error_required_fields, Toast.LENGTH_LONG).show(); - return; - } + private void onLogin() { + if (!verifyRequiredFields()) { + Toast.makeText(this, R.string.login_error_required_fields, Toast.LENGTH_LONG).show(); + return; + } - cancelled = false; - setupConfig(); + cancelled = false; + setupConfig(); - if (progressDialog != null) { - progressDialog.dismiss(); - progressDialog = null; - } + if (progressDialog != null) { + progressDialog.dismiss(); + progressDialog = null; + } - progressDialog = new ProgressDialog(this); - progressDialog.setMessage(getString(R.string.one_moment)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.setCancelable(false); - progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(android.R.string.cancel), (dialog, which) -> { + progressDialog = new ProgressDialog(this); + progressDialog.setMessage(getString(R.string.one_moment)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setCancelable(false); + progressDialog.setButton( + DialogInterface.BUTTON_NEGATIVE, + getString(android.R.string.cancel), + (dialog, which) -> { cancelled = true; DcHelper.getContext(this).stopOngoingProcess(); }); - progressDialog.show(); + progressDialog.show(); + } + + private boolean verifyRequiredFields() { + String email = emailInput.getText().toString(); + return DcHelper.getContext(this).mayBeValidAddr(email) + && !passwordInput.getText().toString().isEmpty(); + } + + private EnteredCertificateChecks certificateChecksFromInt(int position) { + switch (position) { + case 0: + return EnteredCertificateChecks.automatic; + case 1: + return EnteredCertificateChecks.strict; + case 2: + return EnteredCertificateChecks.acceptInvalidCertificates; } - - private boolean verifyRequiredFields() { - String email = emailInput.getText().toString(); - return DcHelper.getContext(this).mayBeValidAddr(email) - && !passwordInput.getText().toString().isEmpty(); + throw new IllegalArgumentException("Invalid certificate position: " + position); + } + + private int certificateChecksToInt(EnteredCertificateChecks check) { + if (check == null) return 0; + + switch (check) { + case strict: + return 1; + case acceptInvalidCertificates: + return 2; + case automatic: + default: + return 0; } - - private EnteredCertificateChecks certificateChecksFromInt(int position) { - switch (position) { - case 0: - return EnteredCertificateChecks.automatic; - case 1: - return EnteredCertificateChecks.strict; - case 2: - return EnteredCertificateChecks.acceptInvalidCertificates; - } - throw new IllegalArgumentException("Invalid certificate position: " + position); + } + + public static Socket socketSecurityFromInt(int position) { + switch (position) { + case 0: + return Socket.automatic; + case 1: + return Socket.ssl; + case 2: + return Socket.starttls; + case 3: + return Socket.plain; } - - private int certificateChecksToInt(EnteredCertificateChecks check) { - if (check == null) return 0; - - switch (check) { - case strict: - return 1; - case acceptInvalidCertificates: - return 2; - case automatic: - default: - return 0; - } - } - - public static Socket socketSecurityFromInt(int position) { - switch (position) { - case 0: - return Socket.automatic; - case 1: - return Socket.ssl; - case 2: - return Socket.starttls; - case 3: - return Socket.plain; - } - throw new IllegalArgumentException("Invalid socketSecurity position: " + position); + throw new IllegalArgumentException("Invalid socketSecurity position: " + position); + } + + public static int socketSecurityToInt(Socket security) { + if (security == null) return 0; + + switch (security) { + case ssl: + return 1; + case starttls: + return 2; + case plain: + return 3; + case automatic: + default: + return 0; } - - public static int socketSecurityToInt(Socket security) { - if (security == null) return 0; - - switch (security) { - case ssl: - return 1; - case starttls: - return 2; - case plain: - return 3; - case automatic: - default: - return 0; - } - } - - private void setupConfig() { - DcHelper.getEventCenter(this).captureNextError(); - - EnteredLoginParam param = new EnteredLoginParam(); - param.addr = getParam(R.id.email_text, true); - param.password = getParam(R.id.password_text, false); - param.imapServer = getParam(R.id.imap_server_text, true); - param.imapPort = Util.objectToInt(getParam(R.id.imap_port_text, true)); - param.imapSecurity = socketSecurityFromInt(imapSecurity.getSelectedItemPosition()); - param.imapUser = getParam(R.id.imap_login_text, false); - param.smtpServer = getParam(R.id.smtp_server_text, true); - param.smtpPort = Util.objectToInt(getParam(R.id.smtp_port_text, true)); - param.smtpSecurity = socketSecurityFromInt(smtpSecurity.getSelectedItemPosition()); - param.smtpUser = getParam(R.id.smtp_login_text, false); - param.smtpPassword = getParam(R.id.smtp_password_text, false); - param.certificateChecks = certificateChecksFromInt(certCheck.getSelectedItemPosition()); - - new Thread(() -> { - try { + } + + private void setupConfig() { + DcHelper.getEventCenter(this).captureNextError(); + + EnteredLoginParam param = new EnteredLoginParam(); + param.addr = getParam(R.id.email_text, true); + param.password = getParam(R.id.password_text, false); + param.imapServer = getParam(R.id.imap_server_text, true); + param.imapPort = Util.objectToInt(getParam(R.id.imap_port_text, true)); + param.imapSecurity = socketSecurityFromInt(imapSecurity.getSelectedItemPosition()); + param.imapUser = getParam(R.id.imap_login_text, false); + param.smtpServer = getParam(R.id.smtp_server_text, true); + param.smtpPort = Util.objectToInt(getParam(R.id.smtp_port_text, true)); + param.smtpSecurity = socketSecurityFromInt(smtpSecurity.getSelectedItemPosition()); + param.smtpUser = getParam(R.id.smtp_login_text, false); + param.smtpPassword = getParam(R.id.smtp_password_text, false); + param.certificateChecks = certificateChecksFromInt(certCheck.getSelectedItemPosition()); + + new Thread( + () -> { + try { rpc.addOrUpdateTransport(accId, param); DcHelper.getEventCenter(this).endCaptureNextError(); progressDialog.dismiss(); - Intent conversationList = new Intent(getApplicationContext(), ConversationListActivity.class); + Intent conversationList = + new Intent(getApplicationContext(), ConversationListActivity.class); startActivity(conversationList); finish(); - } catch (RpcException e) { + } catch (RpcException e) { DcHelper.getEventCenter(this).endCaptureNextError(); if (!cancelled) { - Util.runOnMain(() -> { - progressDialog.dismiss(); - WelcomeActivity.maybeShowConfigurationError(this, e.getMessage()); - }); + Util.runOnMain( + () -> { + progressDialog.dismiss(); + WelcomeActivity.maybeShowConfigurationError(this, e.getMessage()); + }); } - } - }).start(); - } - - private String getParam(@IdRes int viewId, boolean doTrim) { - TextInputEditText view = findViewById(viewId); - String value = view.getText().toString(); - if(doTrim) { - value = value.trim(); - } - return value.isEmpty()? null : value; + } + }) + .start(); + } + + private String getParam(@IdRes int viewId, boolean doTrim) { + TextInputEditText view = findViewById(viewId); + String value = view.getText().toString(); + if (doTrim) { + value = value.trim(); } - - @Override - public void handleEvent(@NonNull DcEvent event) { - if (event.getId()==DcContext.DC_EVENT_CONFIGURE_PROGRESS) { - long progress = event.getData1Int(); // progress in permille - int percent = (int)progress / 10; - progressDialog.setMessage(getResources().getString(R.string.one_moment)+String.format(" %d%%", percent)); - } + return value.isEmpty() ? null : value; + } + + @Override + public void handleEvent(@NonNull DcEvent event) { + if (event.getId() == DcContext.DC_EVENT_CONFIGURE_PROGRESS) { + long progress = event.getData1Int(); // progress in permille + int percent = (int) progress / 10; + progressDialog.setMessage( + getResources().getString(R.string.one_moment) + String.format(" %d%%", percent)); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java b/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java index 35e8b0210..f8876b0ed 100644 --- a/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/relay/RelayListActivity.java @@ -6,7 +6,6 @@ import android.view.ContextMenu; import android.view.MenuItem; import android.view.View; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -15,12 +14,14 @@ import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; +import chat.delta.rpc.types.EnteredLoginParam; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; - +import java.util.List; import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton; @@ -32,14 +33,8 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; -import java.util.List; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; -import chat.delta.rpc.types.EnteredLoginParam; - public class RelayListActivity extends BaseActionBarActivity - implements RelayListAdapter.OnRelayClickListener, DcEventCenter.DcEventDelegate { + implements RelayListAdapter.OnRelayClickListener, DcEventCenter.DcEventDelegate { private static final String TAG = RelayListActivity.class.getSimpleName(); public static final String EXTRA_QR_DATA = "qr_data"; @@ -48,8 +43,12 @@ public class RelayListActivity extends BaseActionBarActivity private Rpc rpc; private int accId; - /** QR provided via Intent extras needs to be saved to pass it to QrCodeHandler when authorization finishes */ + /** + * QR provided via Intent extras needs to be saved to pass it to QrCodeHandler when authorization + * finishes + */ private String qrData = null; + private ActivityResultLauncher screenLockLauncher; private ActivityResultLauncher qrScannerLauncher; @@ -61,30 +60,31 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_relay_list); - qrScannerLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(result.getResultCode(), result.getData()); - new QrCodeHandler(this).handleOnlyAddRelayQr(scanResult.getContents(), null); - } - } - ); - screenLockLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() != RESULT_OK) { - // if user canceled unlocking, then finish - finish(); - return; - } - // user authorized, then proceed to handle the QR data - if (qrData != null) { - new QrCodeHandler(this).handleOnlyAddRelayQr(qrData, null); - qrData = null; - } - } - ); + qrScannerLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + IntentResult scanResult = + IntentIntegrator.parseActivityResult(result.getResultCode(), result.getData()); + new QrCodeHandler(this).handleOnlyAddRelayQr(scanResult.getContents(), null); + } + }); + screenLockLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() != RESULT_OK) { + // if user canceled unlocking, then finish + finish(); + return; + } + // user authorized, then proceed to handle the QR data + if (qrData != null) { + new QrCodeHandler(this).handleOnlyAddRelayQr(qrData, null); + qrData = null; + } + }); rpc = DcHelper.getRpc(this); accId = DcHelper.getContext(this).getAccountId(); @@ -106,25 +106,31 @@ protected void onCreate(Bundle savedInstanceState) { qrData = getIntent().getStringExtra(EXTRA_QR_DATA); if (qrData != null) { // when the activity is opened with a QR data, we need to ask for authorization first - boolean result = ScreenLockUtil.applyScreenLock(this, getString(R.string.add_transport), getString(R.string.enter_system_secret_to_continue), screenLockLauncher); + boolean result = + ScreenLockUtil.applyScreenLock( + this, + getString(R.string.add_transport), + getString(R.string.enter_system_secret_to_continue), + screenLockLauncher); if (!result) { new QrCodeHandler(this).handleOnlyAddRelayQr(qrData, null); } } - fabAdd.setOnClickListener(v -> { - Intent intent = new IntentIntegrator(this) - .setCaptureActivity(QrActivity.class) - .addExtra(QrActivity.EXTRA_SCAN_RELAY, true) - .createScanIntent(); - qrScannerLauncher.launch(intent); - }); + fabAdd.setOnClickListener( + v -> { + Intent intent = + new IntentIntegrator(this) + .setCaptureActivity(QrActivity.class) + .addExtra(QrActivity.EXTRA_SCAN_RELAY, true) + .createScanIntent(); + qrScannerLauncher.launch(intent); + }); LinearLayoutManager layoutManager = new LinearLayoutManager(this); // Add the default divider (uses the theme’s `android.R.attr.listDivider`) - DividerItemDecoration divider = new DividerItemDecoration( - recyclerView.getContext(), - layoutManager.getOrientation()); + DividerItemDecoration divider = + new DividerItemDecoration(recyclerView.getContext(), layoutManager.getOrientation()); recyclerView.addItemDecoration(divider); recyclerView.setLayoutManager(layoutManager); @@ -145,38 +151,40 @@ public void onDestroy() { } private void loadRelays() { - Util.runOnAnyBackgroundThread(() -> { - String mainRelayAddr = ""; - try { - mainRelayAddr = rpc.getConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS); - } catch (RpcException e) { - Log.e(TAG, "RPC.getConfig() failed", e); - } - String finalMainRelayAddr = mainRelayAddr; - - try { - List relays = rpc.listTransports(accId); - - Util.runOnMain(() -> adapter.setRelays(relays, finalMainRelayAddr)); - } catch (RpcException e) { - Log.e(TAG, "RPC.listTransports() failed", e); - Util.runOnMain(() -> adapter.setRelays(null, finalMainRelayAddr)); - } - }); + Util.runOnAnyBackgroundThread( + () -> { + String mainRelayAddr = ""; + try { + mainRelayAddr = rpc.getConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS); + } catch (RpcException e) { + Log.e(TAG, "RPC.getConfig() failed", e); + } + String finalMainRelayAddr = mainRelayAddr; + + try { + List relays = rpc.listTransports(accId); + + Util.runOnMain(() -> adapter.setRelays(relays, finalMainRelayAddr)); + } catch (RpcException e) { + Log.e(TAG, "RPC.listTransports() failed", e); + Util.runOnMain(() -> adapter.setRelays(null, finalMainRelayAddr)); + } + }); } @Override public void onRelayClick(EnteredLoginParam relay) { if (relay.addr != null && !relay.addr.equals(adapter.getMainRelay())) { - Util.runOnAnyBackgroundThread(() -> { - try { - rpc.setConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS, relay.addr); - } catch (RpcException e) { - Log.e(TAG, "RPC.setConfig() failed", e); - } - - loadRelays(); - }); + Util.runOnAnyBackgroundThread( + () -> { + try { + rpc.setConfig(accId, DcHelper.CONFIG_CONFIGURED_ADDRESS, relay.addr); + } catch (RpcException e) { + Log.e(TAG, "RPC.setConfig() failed", e); + } + + loadRelays(); + }); } } @@ -230,18 +238,20 @@ private void onRelayEdit(EnteredLoginParam relay) { private void onRelayDelete(EnteredLoginParam relay) { new AlertDialog.Builder(this) - .setTitle(R.string.remove_transport) - .setMessage(getString(R.string.confirm_remove_transport, relay.addr)) - .setPositiveButton(R.string.ok, (dialog, which) -> { - try { - rpc.deleteTransport(accId, relay.addr); - loadRelays(); - } catch (RpcException e) { - Log.e(TAG, "RPC.deleteTransport() failed", e); - } - }) - .setNegativeButton(R.string.cancel, null) - .show(); + .setTitle(R.string.remove_transport) + .setMessage(getString(R.string.confirm_remove_transport, relay.addr)) + .setPositiveButton( + R.string.ok, + (dialog, which) -> { + try { + rpc.deleteTransport(accId, relay.addr); + loadRelays(); + } catch (RpcException e) { + Log.e(TAG, "RPC.deleteTransport() failed", e); + } + }) + .setNegativeButton(R.string.cancel, null) + .show(); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/relay/RelayListAdapter.java b/src/main/java/org/thoughtcrime/securesms/relay/RelayListAdapter.java index d97354f30..7f7d4d0a8 100644 --- a/src/main/java/org/thoughtcrime/securesms/relay/RelayListAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/relay/RelayListAdapter.java @@ -5,17 +5,13 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - -import org.thoughtcrime.securesms.R; - +import chat.delta.rpc.types.EnteredLoginParam; import java.util.ArrayList; import java.util.List; - -import chat.delta.rpc.types.EnteredLoginParam; +import org.thoughtcrime.securesms.R; public class RelayListAdapter extends RecyclerView.Adapter { @@ -25,6 +21,7 @@ public class RelayListAdapter extends RecyclerView.Adapter relays, String mainRelay @NonNull @Override public RelayViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.relay_list_item, parent, false); + View view = + LayoutInflater.from(parent.getContext()).inflate(R.layout.relay_list_item, parent, false); return new RelayViewHolder(view); } @@ -76,22 +73,24 @@ public RelayViewHolder(@NonNull View itemView) { public void bind(EnteredLoginParam relay, boolean isMain, OnRelayClickListener listener) { String[] parts = relay.addr.split("@"); - titleText.setText(parts.length == 2? parts[1] : parts[0]); - subtitleText.setText(parts.length == 2? parts[0] : ""); + titleText.setText(parts.length == 2 ? parts[1] : parts[0]); + subtitleText.setText(parts.length == 2 ? parts[0] : ""); mainIndicator.setVisibility(isMain ? View.VISIBLE : View.INVISIBLE); - itemView.setOnClickListener(v -> { - if (listener != null) { - listener.onRelayClick(relay); - } - }); - - itemView.setOnLongClickListener(v -> { - if (listener != null) { - listener.onRelayLongClick(v, relay); - } - return true; - }); + itemView.setOnClickListener( + v -> { + if (listener != null) { + listener.onRelayClick(relay); + } + }); + + itemView.setOnLongClickListener( + v -> { + if (listener != null) { + listener.onRelayLongClick(v, relay); + } + return true; + }); } } } diff --git a/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 3e299cfd7..df0c8af3d 100644 --- a/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.scribbles; +import static android.app.Activity.RESULT_OK; + import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; @@ -9,11 +11,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.ImageEditorMediaConstraints; @@ -30,15 +34,8 @@ import org.thoughtcrime.securesms.util.Prefs; import org.thoughtcrime.securesms.util.Util; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; - -import static android.app.Activity.RESULT_OK; - -public final class ImageEditorFragment extends Fragment implements ImageEditorHud.EventListener, - VerticalSlideColorPicker.OnColorChangeListener{ +public final class ImageEditorFragment extends Fragment + implements ImageEditorHud.EventListener, VerticalSlideColorPicker.OnColorChangeListener { private static final String TAG = ImageEditorFragment.class.getSimpleName(); @@ -48,10 +45,9 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu private EditorModel restoredModel; - @Nullable - private EditorElement currentSelection; - private int imageMaxHeight; - private int imageMaxWidth; + @Nullable private EditorElement currentSelection; + private int imageMaxHeight; + private int imageMaxWidth; public static class Data { private final Bundle bundle; @@ -79,10 +75,10 @@ public EditorModel readModel() { } } - private Uri imageUri; - private ImageEditorHud imageEditorHud; + private Uri imageUri; + private ImageEditorHud imageEditorHud; private ImageEditorView imageEditorView; - private boolean cropAvatar; + private boolean cropAvatar; public static ImageEditorFragment newInstance(@NonNull Uri imageUri, boolean cropAvatar) { Bundle args = new Bundle(); @@ -105,13 +101,16 @@ public void onCreate(@Nullable Bundle savedInstanceState) { MediaConstraints mediaConstraints = new ImageEditorMediaConstraints(); - imageMaxWidth = mediaConstraints.getImageMaxWidth(requireContext()); + imageMaxWidth = mediaConstraints.getImageMaxWidth(requireContext()); imageMaxHeight = mediaConstraints.getImageMaxHeight(requireContext()); } @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.image_editor_fragment, container, false); } @@ -119,7 +118,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - imageEditorHud = view.findViewById(R.id.scribble_hud); + imageEditorHud = view.findViewById(R.id.scribble_hud); imageEditorView = view.findViewById(R.id.image_editor_view); imageEditorHud.setEventListener(this); @@ -138,8 +137,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat } if (editorModel == null) { - editorModel = cropAvatar? EditorModel.createForCircleEditing() : new EditorModel(); - EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight)); + editorModel = cropAvatar ? EditorModel.createForCircleEditing() : new EditorModel(); + EditorElement image = + new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight)); image.getFlags().setSelectable(false).persist(); editorModel.addElement(image); } @@ -178,14 +178,15 @@ private void changeEntityColor(int selectedColor) { } private void startTextEntityEditing(@NonNull EditorElement textElement, boolean selectAll) { - imageEditorView.startTextEditing(textElement, Prefs.isIncognitoKeyboardEnabled(getContext()), selectAll); + imageEditorView.startTextEditing( + textElement, Prefs.isIncognitoKeyboardEnabled(getContext()), selectAll); } protected void addText() { - String initialText = ""; - int color = imageEditorHud.getActiveColor(); - MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color); - EditorElement element = new EditorElement(renderer); + String initialText = ""; + int color = imageEditorHud.getActiveColor(); + MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color); + EditorElement element = new EditorElement(renderer); imageEditorView.getModel().addElementCentered(element, 1); imageEditorView.invalidate(); @@ -209,7 +210,7 @@ public void onModeStarted(@NonNull ImageEditorHud.Mode mode) { switch (mode) { case CROP: imageEditorView.getModel().startCrop(); - break; + break; case DRAW: imageEditorView.startDrawing(0.01f, Paint.Cap.ROUND, false); @@ -219,10 +220,11 @@ public void onModeStarted(@NonNull ImageEditorHud.Mode mode) { imageEditorView.startDrawing(0.03f, Paint.Cap.SQUARE, false); break; - case BLUR: { - imageEditorView.startDrawing(0.075f, Paint.Cap.ROUND, true); - break; - } + case BLUR: + { + imageEditorView.startDrawing(0.075f, Paint.Cap.ROUND, true); + break; + } case TEXT: addText(); @@ -237,42 +239,43 @@ public void onModeStarted(@NonNull ImageEditorHud.Mode mode) { @Override public void onSave() { - Util.runOnBackground(() -> { - Activity activity = ImageEditorFragment.this.getActivity(); - if (activity == null) { - return; - } - Bitmap bitmap = imageEditorView.getModel().render(activity); - PersistentBlobProvider provider = PersistentBlobProvider.getInstance(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); - - byte[] data = baos.toByteArray(); - baos = null; - bitmap = null; - - Uri uri = null; - if (cropAvatar) { - File file = new File(activity.getCacheDir(), "cropped"); - try { - FileOutputStream stream = new FileOutputStream(file); - stream.write(data); - stream.flush(); - stream.close(); - uri = Uri.fromFile(file); - } catch (IOException e) { - e.printStackTrace(); + Util.runOnBackground( + () -> { + Activity activity = ImageEditorFragment.this.getActivity(); + if (activity == null) { return; - } - } else { - uri = provider.create(activity, data, MediaUtil.IMAGE_JPEG, null); - } - - Intent intent = new Intent(); - intent.setData(uri); - activity.setResult(RESULT_OK, intent); - activity.finish(); - }); + } + Bitmap bitmap = imageEditorView.getModel().render(activity); + PersistentBlobProvider provider = PersistentBlobProvider.getInstance(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); + + byte[] data = baos.toByteArray(); + baos = null; + bitmap = null; + + Uri uri = null; + if (cropAvatar) { + File file = new File(activity.getCacheDir(), "cropped"); + try { + FileOutputStream stream = new FileOutputStream(file); + stream.write(data); + stream.flush(); + stream.close(); + uri = Uri.fromFile(file); + } catch (IOException e) { + e.printStackTrace(); + return; + } + } else { + uri = provider.create(activity, data, MediaUtil.IMAGE_JPEG, null); + } + + Intent intent = new Intent(); + intent.setData(uri); + activity.setResult(RESULT_OK, intent); + activity.finish(); + }); } @Override @@ -304,9 +307,7 @@ public void onRotate90AntiClockwise() { } @Override - public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) { - - } + public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {} private void refreshUniqueColors() { imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha()); @@ -316,47 +317,51 @@ private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAv imageEditorHud.setUndoAvailability(undoAvailable); } - private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() { - - @Override - public void onEntityDown(@Nullable EditorElement editorElement) { - if (editorElement == null) { - currentSelection = null; - imageEditorHud.enterMode(ImageEditorHud.Mode.NONE); - imageEditorView.doneTextEditing(); - } - } - - @Override - public void onEntitySingleTap(@Nullable EditorElement editorElement) { - currentSelection = editorElement; - if (currentSelection != null) { - if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { - setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), imageEditorView.isTextEditing()); - } else { - imageEditorHud.enterMode(ImageEditorHud.Mode.MOVE_DELETE); - } - } - } - - @Override - public void onEntityDoubleTap(@NonNull EditorElement editorElement) { - currentSelection = editorElement; - if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { - setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), true); + private final ImageEditorView.TapListener selectionListener = + new ImageEditorView.TapListener() { + + @Override + public void onEntityDown(@Nullable EditorElement editorElement) { + if (editorElement == null) { + currentSelection = null; + imageEditorHud.enterMode(ImageEditorHud.Mode.NONE); + imageEditorView.doneTextEditing(); + } + } + + @Override + public void onEntitySingleTap(@Nullable EditorElement editorElement) { + currentSelection = editorElement; + if (currentSelection != null) { + if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { + setTextElement( + editorElement, + (ColorableRenderer) editorElement.getRenderer(), + imageEditorView.isTextEditing()); + } else { + imageEditorHud.enterMode(ImageEditorHud.Mode.MOVE_DELETE); + } + } + } + + @Override + public void onEntityDoubleTap(@NonNull EditorElement editorElement) { + currentSelection = editorElement; + if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { + setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), true); + } } - } - private void setTextElement(@NonNull EditorElement editorElement, - @NonNull ColorableRenderer colorableRenderer, - boolean startEditing) - { - int color = colorableRenderer.getColor(); - imageEditorHud.enterMode(ImageEditorHud.Mode.TEXT); - imageEditorHud.setActiveColor(color); - if (startEditing) { - startTextEntityEditing(editorElement, false); - } - } - }; + private void setTextElement( + @NonNull EditorElement editorElement, + @NonNull ColorableRenderer colorableRenderer, + boolean startEditing) { + int color = colorableRenderer.getColor(); + imageEditorHud.enterMode(ImageEditorHud.Mode.TEXT); + imageEditorHud.setActiveColor(color); + if (startEditing) { + startTextEntityEditing(editorElement, false); + } + } + }; } diff --git a/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java b/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java index 4a987b67a..668c37f34 100644 --- a/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java +++ b/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java @@ -2,56 +2,50 @@ import android.content.Context; import android.graphics.Color; -import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter; -import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker; - import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter; +import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker; /** - * The HUD (heads-up display) that contains all of the tools for interacting with - * {@link org.thoughtcrime.securesms.imageeditor.ImageEditorView} + * The HUD (heads-up display) that contains all of the tools for interacting with {@link + * org.thoughtcrime.securesms.imageeditor.ImageEditorView} */ public final class ImageEditorHud extends LinearLayout { - private View cropButton; - private View cropFlipButton; - private View cropRotateButton; - private View drawButton; - private View highlightButton; - private View blurButton; - private View textButton; - private View undoButton; - private View saveButton; - private View deleteButton; - private View confirmButton; + private View cropButton; + private View cropFlipButton; + private View cropRotateButton; + private View drawButton; + private View highlightButton; + private View blurButton; + private View textButton; + private View undoButton; + private View saveButton; + private View deleteButton; + private View confirmButton; private VerticalSlideColorPicker colorPicker; - private RecyclerView colorPalette; - private View scribbleBlurHelpText; + private RecyclerView colorPalette; + private View scribbleBlurHelpText; - @NonNull - private EventListener eventListener = NULL_EVENT_LISTENER; - @Nullable - private ColorPaletteAdapter colorPaletteAdapter; + @NonNull private EventListener eventListener = NULL_EVENT_LISTENER; + @Nullable private ColorPaletteAdapter colorPaletteAdapter; private final Map> visibilityModeMap = new HashMap<>(); - private final Set allViews = new HashSet<>(); + private final Set allViews = new HashSet<>(); - private Mode currentMode; + private Mode currentMode; private boolean undoAvailable; public ImageEditorHud(@NonNull Context context) { @@ -73,20 +67,20 @@ private void initialize() { inflate(getContext(), R.layout.image_editor_hud, this); setOrientation(VERTICAL); - cropButton = findViewById(R.id.scribble_crop_button); - cropFlipButton = findViewById(R.id.scribble_crop_flip); + cropButton = findViewById(R.id.scribble_crop_button); + cropFlipButton = findViewById(R.id.scribble_crop_flip); cropRotateButton = findViewById(R.id.scribble_crop_rotate); - colorPalette = findViewById(R.id.scribble_color_palette); - drawButton = findViewById(R.id.scribble_draw_button); - highlightButton = findViewById(R.id.scribble_highlight_button); - blurButton = findViewById(R.id.scribble_blur_button); - textButton = findViewById(R.id.scribble_text_button); - undoButton = findViewById(R.id.scribble_undo_button); - saveButton = findViewById(R.id.scribble_save_button); - deleteButton = findViewById(R.id.scribble_delete_button); - confirmButton = findViewById(R.id.scribble_confirm_button); - colorPicker = findViewById(R.id.scribble_color_picker); - scribbleBlurHelpText = findViewById(R.id.scribble_blur_help_text); + colorPalette = findViewById(R.id.scribble_color_palette); + drawButton = findViewById(R.id.scribble_draw_button); + highlightButton = findViewById(R.id.scribble_highlight_button); + blurButton = findViewById(R.id.scribble_blur_button); + textButton = findViewById(R.id.scribble_text_button); + undoButton = findViewById(R.id.scribble_undo_button); + saveButton = findViewById(R.id.scribble_save_button); + deleteButton = findViewById(R.id.scribble_delete_button); + confirmButton = findViewById(R.id.scribble_confirm_button); + colorPicker = findViewById(R.id.scribble_color_picker); + scribbleBlurHelpText = findViewById(R.id.scribble_blur_help_text); initializeViews(); initializeVisibilityMap(); @@ -94,7 +88,15 @@ private void initialize() { } private void initializeVisibilityMap() { - setVisibleViewsWhenInMode(Mode.NONE, drawButton, highlightButton, blurButton, textButton, cropButton, undoButton, saveButton); + setVisibleViewsWhenInMode( + Mode.NONE, + drawButton, + highlightButton, + blurButton, + textButton, + cropButton, + undoButton, + saveButton); setVisibleViewsWhenInMode(Mode.DRAW, confirmButton, undoButton, colorPicker, colorPalette); @@ -106,7 +108,8 @@ private void initializeVisibilityMap() { setVisibleViewsWhenInMode(Mode.MOVE_DELETE, confirmButton, deleteButton); - setVisibleViewsWhenInMode(Mode.CROP, confirmButton, cropFlipButton, cropRotateButton, undoButton); + setVisibleViewsWhenInMode( + Mode.CROP, confirmButton, cropFlipButton, cropRotateButton, undoButton); for (Set views : visibilityModeMap.values()) { allViews.addAll(views); @@ -120,10 +123,11 @@ private void setVisibleViewsWhenInMode(Mode mode, View... views) { private void initializeViews() { undoButton.setOnClickListener(v -> eventListener.onUndo()); - deleteButton.setOnClickListener(v -> { - eventListener.onDelete(); - setMode(Mode.NONE); - }); + deleteButton.setOnClickListener( + v -> { + eventListener.onDelete(); + setMode(Mode.NONE); + }); cropButton.setOnClickListener(v -> setMode(Mode.CROP)); cropFlipButton.setOnClickListener(v -> eventListener.onFlipHorizontal()); @@ -175,10 +179,18 @@ private void setMode(@NonNull Mode mode, boolean notify) { updateButtonVisibility(mode); switch (mode) { - case DRAW: presentModeDraw(); break; - case HIGHLIGHT: presentModeHighlight(); break; - case TEXT: presentModeText(); break; - case BLUR: presentModeBlur(); break; + case DRAW: + presentModeDraw(); + break; + case HIGHLIGHT: + presentModeHighlight(); + break; + case TEXT: + presentModeText(); + break; + case BLUR: + presentModeBlur(); + break; } if (notify) { @@ -195,9 +207,9 @@ private void updateButtonVisibility(@NonNull Mode mode) { } private boolean buttonIsVisible(@Nullable Set visibleButtons, @NonNull View button) { - return visibleButtons != null && - visibleButtons.contains(button) && - (button != undoButton || undoAvailable); + return visibleButtons != null + && visibleButtons.contains(button) + && (button != undoButton || undoAvailable); } private void presentModeBlur() { @@ -220,9 +232,11 @@ private void presentModeText() { colorPicker.setActiveColor(Color.WHITE); } - private final VerticalSlideColorPicker.OnColorChangeListener standardOnColorChangeListener = selectedColor -> eventListener.onColorChange(selectedColor); + private final VerticalSlideColorPicker.OnColorChangeListener standardOnColorChangeListener = + selectedColor -> eventListener.onColorChange(selectedColor); - private final VerticalSlideColorPicker.OnColorChangeListener highlightOnColorChangeListener = selectedColor -> eventListener.onColorChange(replaceAlphaWith128(selectedColor)); + private final VerticalSlideColorPicker.OnColorChangeListener highlightOnColorChangeListener = + selectedColor -> eventListener.onColorChange(replaceAlphaWith128(selectedColor)); private static int replaceAlphaWith128(int color) { return color & ~0xff000000 | 0x80000000; @@ -231,7 +245,8 @@ private static int replaceAlphaWith128(int color) { public void setUndoAvailability(boolean undoAvailable) { this.undoAvailable = undoAvailable; - undoButton.setVisibility(buttonIsVisible(visibilityModeMap.get(currentMode), undoButton) ? VISIBLE : GONE); + undoButton.setVisibility( + buttonIsVisible(visibilityModeMap.get(currentMode), undoButton) ? VISIBLE : GONE); } public enum Mode { @@ -246,47 +261,47 @@ public enum Mode { public interface EventListener { void onModeStarted(@NonNull Mode mode); + void onColorChange(int color); + void onUndo(); + void onDelete(); + void onSave(); + void onFlipHorizontal(); + void onRotate90AntiClockwise(); + void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard); } - private static final EventListener NULL_EVENT_LISTENER = new EventListener() { + private static final EventListener NULL_EVENT_LISTENER = + new EventListener() { - @Override - public void onModeStarted(@NonNull Mode mode) { - } + @Override + public void onModeStarted(@NonNull Mode mode) {} - @Override - public void onColorChange(int color) { - } + @Override + public void onColorChange(int color) {} - @Override - public void onUndo() { - } + @Override + public void onUndo() {} - @Override - public void onDelete() { - } + @Override + public void onDelete() {} - @Override - public void onSave() { - } + @Override + public void onSave() {} - @Override - public void onFlipHorizontal() { - } + @Override + public void onFlipHorizontal() {} - @Override - public void onRotate90AntiClockwise() { - } + @Override + public void onRotate90AntiClockwise() {} - @Override - public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) { - } - }; + @Override + public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {} + }; } diff --git a/src/main/java/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java b/src/main/java/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java index d9fd5d86a..8dba90012 100644 --- a/src/main/java/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java @@ -1,14 +1,13 @@ package org.thoughtcrime.securesms.scribbles; import android.os.Bundle; - import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; public class ScribbleActivity extends PassphraseRequiredActionBarActivity { - public static final int SCRIBBLE_REQUEST_CODE = 31424; - public static final String CROP_AVATAR = "crop_avatar"; + public static final int SCRIBBLE_REQUEST_CODE = 31424; + public static final String CROP_AVATAR = "crop_avatar"; ImageEditorFragment imageEditorFragment; protected boolean allowInLockedMode() { @@ -25,6 +24,9 @@ protected void onPreCreate() { protected void onCreate(Bundle savedInstanceState, boolean ready) { setContentView(R.layout.scribble_activity); boolean cropAvatar = getIntent().getBooleanExtra(CROP_AVATAR, false); - imageEditorFragment = initFragment(R.id.scribble_container, ImageEditorFragment.newInstance(getIntent().getData(), cropAvatar)); + imageEditorFragment = + initFragment( + R.id.scribble_container, + ImageEditorFragment.newInstance(getIntent().getData(), cropAvatar)); } } diff --git a/src/main/java/org/thoughtcrime/securesms/scribbles/StickerLoader.java b/src/main/java/org/thoughtcrime/securesms/scribbles/StickerLoader.java index 8e0ec7e4c..1ba5fc012 100644 --- a/src/main/java/org/thoughtcrime/securesms/scribbles/StickerLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/scribbles/StickerLoader.java @@ -1,29 +1,24 @@ /** * Copyright (C) 2016 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.scribbles; - import android.content.Context; -import androidx.annotation.NonNull; import android.util.Log; - -import org.thoughtcrime.securesms.util.AsyncLoader; - +import androidx.annotation.NonNull; import java.io.IOException; +import org.thoughtcrime.securesms.util.AsyncLoader; class StickerLoader extends AsyncLoader { @@ -37,12 +32,11 @@ class StickerLoader extends AsyncLoader { } @Override - public @NonNull - String[] loadInBackground() { + public @NonNull String[] loadInBackground() { try { String[] files = getContext().getAssets().list(assetDirectory); - for (int i=0;iThis 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.scribbles; import android.content.Intent; import android.os.Bundle; +import android.view.MenuItem; import androidx.annotation.Nullable; - -import com.google.android.material.tabs.TabLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.viewpager.widget.ViewPager; -import android.view.MenuItem; - +import com.google.android.material.tabs.TabLayout; import org.thoughtcrime.securesms.R; -public class StickerSelectActivity extends FragmentActivity implements StickerSelectFragment.StickerSelectionListener { +public class StickerSelectActivity extends FragmentActivity + implements StickerSelectFragment.StickerSelectionListener { private static final String TAG = StickerSelectActivity.class.getSimpleName(); public static final String EXTRA_STICKER_FILE = "extra_sticker_file"; - private static final int[] TAB_TITLES = new int[] { - R.drawable.ic_tag_faces_white_24dp, - R.drawable.ic_work_white_24dp, - R.drawable.ic_pets_white_24dp, - R.drawable.ic_local_dining_white_24dp, - R.drawable.ic_wb_sunny_white_24dp - }; + private static final int[] TAB_TITLES = + new int[] { + R.drawable.ic_tag_faces_white_24dp, + R.drawable.ic_work_white_24dp, + R.drawable.ic_pets_white_24dp, + R.drawable.ic_local_dining_white_24dp, + R.drawable.ic_wb_sunny_white_24dp + }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -55,7 +53,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { TabLayout tabLayout = findViewById(R.id.camera_sticker_tabs); tabLayout.setupWithViewPager(viewPager); - for (int i=0;i { +public class StickerSelectFragment extends Fragment + implements LoaderManager.LoaderCallbacks { - private RecyclerView recyclerView; - private GlideRequests glideRequests; - private String assetDirectory; + private RecyclerView recyclerView; + private GlideRequests glideRequests; + private String assetDirectory; private StickerSelectionListener listener; public static StickerSelectFragment newInstance(String assetDirectory) { @@ -54,10 +53,10 @@ public static StickerSelectFragment newInstance(String assetDirectory) { return fragment; } - public @Nullable View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) - { + public @Nullable View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.scribble_select_sticker_fragment, container, false); this.recyclerView = view.findViewById(R.id.stickers_recycler_view); @@ -68,7 +67,7 @@ public static StickerSelectFragment newInstance(String assetDirectory) { public void onActivityCreated(Bundle bundle) { super.onActivityCreated(bundle); - this.glideRequests = GlideApp.with(this); + this.glideRequests = GlideApp.with(this); this.assetDirectory = getArguments().getString("assetDirectory"); getLoaderManager().initLoader(0, null, this); @@ -96,28 +95,33 @@ public void setListener(StickerSelectionListener listener) { class StickersAdapter extends RecyclerView.Adapter { - private final GlideRequests glideRequests; - private final String[] stickerFiles; + private final GlideRequests glideRequests; + private final String[] stickerFiles; private final LayoutInflater layoutInflater; - StickersAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @NonNull String[] stickerFiles) { - this.glideRequests = glideRequests; - this.stickerFiles = stickerFiles; + StickersAdapter( + @NonNull Context context, + @NonNull GlideRequests glideRequests, + @NonNull String[] stickerFiles) { + this.glideRequests = glideRequests; + this.stickerFiles = stickerFiles; this.layoutInflater = LayoutInflater.from(context); } @Override public @NonNull StickerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new StickerViewHolder(layoutInflater.inflate(R.layout.scribble_sticker_item, parent, false)); + return new StickerViewHolder( + layoutInflater.inflate(R.layout.scribble_sticker_item, parent, false)); } @Override public void onBindViewHolder(@NonNull StickerViewHolder holder, int position) { holder.fileName = stickerFiles[position]; - glideRequests.load(Uri.parse("file:///android_asset/" + holder.fileName)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(holder.image); + glideRequests + .load(Uri.parse("file:///android_asset/" + holder.fileName)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(holder.image); } @Override @@ -143,12 +147,13 @@ class StickerViewHolder extends RecyclerView.ViewHolder { StickerViewHolder(View itemView) { super(itemView); image = itemView.findViewById(R.id.sticker_image); - itemView.setOnClickListener(view -> { - int pos = getAdapterPosition(); - if (pos >= 0) { - onStickerSelected(fileName); - } - }); + itemView.setOnClickListener( + view -> { + int pos = getAdapterPosition(); + if (pos >= 0) { + onStickerSelected(fileName); + } + }); } } } @@ -156,6 +161,4 @@ class StickerViewHolder extends RecyclerView.ViewHolder { interface StickerSelectionListener { void onStickerSelected(String name); } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java b/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java index b822122df..15423d46f 100644 --- a/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java @@ -10,22 +10,19 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Parcel; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RenderScript; import android.renderscript.ScriptIntrinsicBlur; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; - import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; - +import java.util.concurrent.ExecutionException; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; @@ -36,38 +33,36 @@ import org.thoughtcrime.securesms.mms.GlideRequest; import org.thoughtcrime.securesms.util.BitmapUtil; -import java.util.concurrent.ExecutionException; - /** * Uses Glide to load an image and implements a {@link Renderer}. * - * The image can be encrypted. + *

The image can be encrypted. */ final class UriGlideRenderer implements Renderer { private static final String TAG = UriGlideRenderer.class.getSimpleName(); private static final int PREVIEW_DIMENSION_LIMIT = 2048; - private static final int MAX_BLUR_DIMENSION = 300; + private static final int MAX_BLUR_DIMENSION = 300; - private final Uri imageUri; - private final Paint paint = new Paint(); - private final Matrix imageProjectionMatrix = new Matrix(); - private final Matrix temp = new Matrix(); - private final Matrix blurScaleMatrix = new Matrix(); + private final Uri imageUri; + private final Paint paint = new Paint(); + private final Matrix imageProjectionMatrix = new Matrix(); + private final Matrix temp = new Matrix(); + private final Matrix blurScaleMatrix = new Matrix(); private final boolean decryptable; - private final int maxWidth; - private final int maxHeight; + private final int maxWidth; + private final int maxHeight; - @Nullable private Bitmap bitmap; - @Nullable private Bitmap blurredBitmap; - @Nullable private Paint blurPaint; + @Nullable private Bitmap bitmap; + @Nullable private Bitmap blurredBitmap; + @Nullable private Paint blurPaint; UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) { - this.imageUri = imageUri; + this.imageUri = imageUri; this.decryptable = decryptable; - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; paint.setAntiAlias(true); paint.setFilterBitmap(true); paint.setDither(true); @@ -84,19 +79,22 @@ public void render(@NonNull RendererContext rendererContext) { throw new RuntimeException(e); } } else { - getBitmapGlideRequest(rendererContext.context, true).into(new CustomTarget() { - @Override - public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { - setBitmap(rendererContext, resource); - - rendererContext.invalidate.onInvalidate(UriGlideRenderer.this); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - bitmap = null; - } - }); + getBitmapGlideRequest(rendererContext.context, true) + .into( + new CustomTarget() { + @Override + public void onResourceReady( + @NonNull Bitmap resource, @Nullable Transition transition) { + setBitmap(rendererContext, resource); + + rendererContext.invalidate.onInvalidate(UriGlideRenderer.this); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + bitmap = null; + } + }); } } @@ -111,7 +109,11 @@ public void onLoadCleared(@Nullable Drawable placeholder) { int alpha = paint.getAlpha(); paint.setAlpha(rendererContext.getAlpha(alpha)); - rendererContext.canvas.drawBitmap(bitmap, 0, 0, rendererContext.getMaskPaint() != null ? rendererContext.getMaskPaint() : paint); + rendererContext.canvas.drawBitmap( + bitmap, + 0, + 0, + rendererContext.getMaskPaint() != null ? rendererContext.getMaskPaint() : paint); paint.setAlpha(alpha); @@ -119,28 +121,29 @@ public void onLoadCleared(@Nullable Drawable placeholder) { renderBlurOverlay(rendererContext); } else if (rendererContext.isBlockingLoad()) { - // If failed to load, we draw a black out, in case image was sticker positioned to cover private info. + // If failed to load, we draw a black out, in case image was sticker positioned to cover + // private info. rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint); } } private void renderBlurOverlay(RendererContext rendererContext) { - boolean renderMask = false; - - for (EditorElement child : rendererContext.getChildren()) { - if (child.getZOrder() == EditorModel.Z_MASK) { - renderMask = true; - if (blurPaint == null) { - blurPaint = new Paint(); - blurPaint.setAntiAlias(true); - blurPaint.setFilterBitmap(true); - blurPaint.setDither(true); - } - blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); - rendererContext.setMaskPaint(blurPaint); - child.draw(rendererContext); + boolean renderMask = false; + + for (EditorElement child : rendererContext.getChildren()) { + if (child.getZOrder() == EditorModel.Z_MASK) { + renderMask = true; + if (blurPaint == null) { + blurPaint = new Paint(); + blurPaint.setAntiAlias(true); + blurPaint.setFilterBitmap(true); + blurPaint.setDither(true); } + blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + rendererContext.setMaskPaint(blurPaint); + child.draw(rendererContext); } + } if (renderMask) { rendererContext.save(); @@ -152,9 +155,10 @@ private void renderBlurOverlay(RendererContext rendererContext) { if (blurredBitmap == null) { blurredBitmap = blur(bitmap, rendererContext.context); - blurScaleMatrix.setRectToRect(new RectF(0, 0, blurredBitmap.getWidth(), blurredBitmap.getHeight()), - new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()), - Matrix.ScaleToFit.FILL); + blurScaleMatrix.setRectToRect( + new RectF(0, 0, blurredBitmap.getWidth(), blurredBitmap.getHeight()), + new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()), + Matrix.ScaleToFit.FILL); } rendererContext.canvas.concat(blurScaleMatrix); @@ -166,20 +170,23 @@ private void renderBlurOverlay(RendererContext rendererContext) { } private GlideRequest getBitmapGlideRequest(@NonNull Context context, boolean preview) { - int width = this.maxWidth; + int width = this.maxWidth; int height = this.maxHeight; if (preview) { - width = Math.min(width, PREVIEW_DIMENSION_LIMIT); + width = Math.min(width, PREVIEW_DIMENSION_LIMIT); height = Math.min(height, PREVIEW_DIMENSION_LIMIT); } return GlideApp.with(context) - .asBitmap() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .override(width, height) - .centerInside() - .load(decryptable && imageUri!=null ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) : imageUri); + .asBitmap() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .override(width, height) + .centerInside() + .load( + decryptable && imageUri != null + ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) + : imageUri); } @Override @@ -195,7 +202,7 @@ private boolean pixelAlphaNotZero(float x, float y) { imageProjectionMatrix.invert(temp); float[] onBmp = new float[2]; - temp.mapPoints(onBmp, new float[]{ x, y }); + temp.mapPoints(onBmp, new float[] {x, y}); int xInt = (int) onBmp[0]; int yInt = (int) onBmp[1]; @@ -221,9 +228,12 @@ private boolean pixelAlphaNotZero(float x, float y) { private void setBitmap(@NonNull RendererContext rendererContext, @Nullable Bitmap bitmap) { this.bitmap = bitmap; if (bitmap != null) { - RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); imageProjectionMatrix.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); - rendererContext.rendererReady.onReady(UriGlideRenderer.this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight())); + rendererContext.rendererReady.onReady( + UriGlideRenderer.this, + cropMatrix(bitmap), + new Point(bitmap.getWidth(), bitmap.getHeight())); } } @@ -239,15 +249,28 @@ private static Matrix cropMatrix(Bitmap bitmap) { @RequiresApi(17) private static @NonNull Bitmap blur(Bitmap bitmap, Context context) { - Point previewSize = scaleKeepingAspectRatio(new Point(bitmap.getWidth(), bitmap.getHeight()), PREVIEW_DIMENSION_LIMIT); - Point blurSize = scaleKeepingAspectRatio(new Point(previewSize.x / 2, previewSize.y / 2 ), MAX_BLUR_DIMENSION); - Bitmap small = BitmapUtil.createScaledBitmap(bitmap, blurSize.x, blurSize.y); - - Log.d(TAG, "Bitmap: " + bitmap.getWidth() + "x" + bitmap.getHeight() + ", Blur: " + blurSize.x + "x" + blurSize.y); - - RenderScript rs = RenderScript.create(context); - Allocation input = Allocation.createFromBitmap(rs, small); - Allocation output = Allocation.createTyped(rs, input.getType()); + Point previewSize = + scaleKeepingAspectRatio( + new Point(bitmap.getWidth(), bitmap.getHeight()), PREVIEW_DIMENSION_LIMIT); + Point blurSize = + scaleKeepingAspectRatio( + new Point(previewSize.x / 2, previewSize.y / 2), MAX_BLUR_DIMENSION); + Bitmap small = BitmapUtil.createScaledBitmap(bitmap, blurSize.x, blurSize.y); + + Log.d( + TAG, + "Bitmap: " + + bitmap.getWidth() + + "x" + + bitmap.getHeight() + + ", Blur: " + + blurSize.x + + "x" + + blurSize.y); + + RenderScript rs = RenderScript.create(context); + Allocation input = Allocation.createFromBitmap(rs, small); + Allocation output = Allocation.createTyped(rs, input.getType()); ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); script.setRadius(25f); @@ -267,7 +290,7 @@ private static Matrix cropMatrix(Bitmap bitmap) { outX = maxDimen; outY = maxDimen; - float widthRatio = dimens.x / (float) maxDimen; + float widthRatio = dimens.x / (float) maxDimen; float heightRatio = dimens.y / (float) maxDimen; if (widthRatio > heightRatio) { @@ -280,21 +303,19 @@ private static Matrix cropMatrix(Bitmap bitmap) { return new Point(outX, outY); } - public static final Creator CREATOR = new Creator() { - @Override - public UriGlideRenderer createFromParcel(Parcel in) { - return new UriGlideRenderer(Uri.parse(in.readString()), - in.readInt() == 1, - in.readInt(), - in.readInt() - ); - } + public static final Creator CREATOR = + new Creator() { + @Override + public UriGlideRenderer createFromParcel(Parcel in) { + return new UriGlideRenderer( + Uri.parse(in.readString()), in.readInt() == 1, in.readInt(), in.readInt()); + } - @Override - public UriGlideRenderer[] newArray(int size) { - return new UriGlideRenderer[size]; - } - }; + @Override + public UriGlideRenderer[] newArray(int size) { + return new UriGlideRenderer[size]; + } + }; @Override public int describeContents() { @@ -303,7 +324,7 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(imageUri!=null? imageUri.toString() : ""); + dest.writeString(imageUri != null ? imageUri.toString() : ""); dest.writeInt(decryptable ? 1 : 0); dest.writeInt(maxWidth); dest.writeInt(maxHeight); diff --git a/src/main/java/org/thoughtcrime/securesms/scribbles/widget/ColorPaletteAdapter.java b/src/main/java/org/thoughtcrime/securesms/scribbles/widget/ColorPaletteAdapter.java index 996b4d3f2..548665d8b 100644 --- a/src/main/java/org/thoughtcrime/securesms/scribbles/widget/ColorPaletteAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/scribbles/widget/ColorPaletteAdapter.java @@ -5,16 +5,13 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - -import org.thoughtcrime.securesms.R; - import java.util.ArrayList; import java.util.Collection; import java.util.List; +import org.thoughtcrime.securesms.R; public class ColorPaletteAdapter extends RecyclerView.Adapter { @@ -24,7 +21,8 @@ public class ColorPaletteAdapter extends RecyclerView.AdapterPermission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. + *

The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. + *

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package org.thoughtcrime.securesms.scribbles.widget; - -import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -34,40 +28,38 @@ import android.graphics.Path; import android.graphics.RectF; import android.graphics.Shader; -import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; - import org.thoughtcrime.securesms.R; public class VerticalSlideColorPicker extends View { private static final float INDICATOR_TO_BAR_WIDTH_RATIO = 0.5f; - private Paint paint; - private Paint strokePaint; - private Paint indicatorStrokePaint; - private Paint indicatorFillPaint; - private Path path; + private Paint paint; + private Paint strokePaint; + private Paint indicatorStrokePaint; + private Paint indicatorFillPaint; + private Path path; private Bitmap bitmap; private Canvas bitmapCanvas; - private int viewWidth; - private int viewHeight; - private int centerX; - private float colorPickerRadius; - private RectF colorPickerBody; + private int viewWidth; + private int viewHeight; + private int centerX; + private float colorPickerRadius; + private RectF colorPickerBody; private OnColorChangeListener onColorChangeListener; - private int borderColor; - private float borderWidth; - private float indicatorRadius; - private int[] colors; + private int borderColor; + private float borderWidth; + private float indicatorRadius; + private int[] colors; - private int touchY; - private int activeColor; + private int touchY; + private int activeColor; public VerticalSlideColorPicker(Context context) { super(context); @@ -77,14 +69,19 @@ public VerticalSlideColorPicker(Context context) { public VerticalSlideColorPicker(Context context, AttributeSet attrs) { super(context, attrs); - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.VerticalSlideColorPicker, 0, 0); + TypedArray a = + context + .getTheme() + .obtainStyledAttributes(attrs, R.styleable.VerticalSlideColorPicker, 0, 0); try { - int colorsResourceId = a.getResourceId(R.styleable.VerticalSlideColorPicker_pickerColors, R.array.scribble_colors); + int colorsResourceId = + a.getResourceId( + R.styleable.VerticalSlideColorPicker_pickerColors, R.array.scribble_colors); - colors = a.getResources().getIntArray(colorsResourceId); - borderColor = a.getColor(R.styleable.VerticalSlideColorPicker_pickerBorderColor, Color.WHITE); - borderWidth = a.getDimension(R.styleable.VerticalSlideColorPicker_pickerBorderWidth, 10f); + colors = a.getResources().getIntArray(colorsResourceId); + borderColor = a.getColor(R.styleable.VerticalSlideColorPicker_pickerBorderColor, Color.WHITE); + borderWidth = a.getDimension(R.styleable.VerticalSlideColorPicker_pickerBorderWidth, 10f); } finally { a.recycle(); @@ -98,7 +95,8 @@ public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyl init(); } - public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public VerticalSlideColorPicker( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(); } @@ -130,9 +128,17 @@ private void init() { protected void onDraw(Canvas canvas) { super.onDraw(canvas); - path.addCircle(centerX, borderWidth + colorPickerRadius + indicatorRadius, colorPickerRadius, Path.Direction.CW); + path.addCircle( + centerX, + borderWidth + colorPickerRadius + indicatorRadius, + colorPickerRadius, + Path.Direction.CW); path.addRect(colorPickerBody, Path.Direction.CW); - path.addCircle(centerX, viewHeight - (borderWidth + colorPickerRadius + indicatorRadius), colorPickerRadius, Path.Direction.CW); + path.addCircle( + centerX, + viewHeight - (borderWidth + colorPickerRadius + indicatorRadius), + colorPickerRadius, + Path.Direction.CW); bitmapCanvas.drawColor(Color.TRANSPARENT); @@ -153,7 +159,7 @@ public boolean onTouchEvent(MotionEvent event) { touchY = (int) Math.min(event.getY(), colorPickerBody.bottom); touchY = (int) Math.max(colorPickerBody.top, touchY); - activeColor = bitmap.getPixel(viewWidth/2, touchY); + activeColor = bitmap.getPixel(viewWidth / 2, touchY); if (onColorChangeListener != null) { onColorChangeListener.onColorChange(activeColor); @@ -168,30 +174,34 @@ public boolean onTouchEvent(MotionEvent event) { protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - viewWidth = w; + viewWidth = w; viewHeight = h; if (viewWidth <= 0 || viewHeight <= 0) return; int barWidth = (int) (viewWidth * INDICATOR_TO_BAR_WIDTH_RATIO); - centerX = viewWidth / 2; - indicatorRadius = (viewWidth / 2) - borderWidth; + centerX = viewWidth / 2; + indicatorRadius = (viewWidth / 2) - borderWidth; colorPickerRadius = (barWidth / 2) - borderWidth; - colorPickerBody = new RectF(centerX - colorPickerRadius, - borderWidth + colorPickerRadius + indicatorRadius, - centerX + colorPickerRadius, - viewHeight - (borderWidth + colorPickerRadius + indicatorRadius)); + colorPickerBody = + new RectF( + centerX - colorPickerRadius, + borderWidth + colorPickerRadius + indicatorRadius, + centerX + colorPickerRadius, + viewHeight - (borderWidth + colorPickerRadius + indicatorRadius)); - LinearGradient gradient = new LinearGradient(0, colorPickerBody.top, 0, colorPickerBody.bottom, colors, null, Shader.TileMode.CLAMP); + LinearGradient gradient = + new LinearGradient( + 0, colorPickerBody.top, 0, colorPickerBody.bottom, colors, null, Shader.TileMode.CLAMP); paint.setShader(gradient); if (bitmap != null) { bitmap.recycle(); } - bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888); + bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888); bitmapCanvas = new Canvas(bitmap); } @@ -235,4 +245,4 @@ public void setOnColorChangeListener(OnColorChangeListener onColorChangeListener public interface OnColorChangeListener { void onColorChange(int selectedColor); } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/search/SearchFragment.java b/src/main/java/org/thoughtcrime/securesms/search/SearchFragment.java index b6a2b0c5a..0854e3110 100644 --- a/src/main/java/org/thoughtcrime/securesms/search/SearchFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/search/SearchFragment.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.search; - import static org.thoughtcrime.securesms.util.ShareUtil.isRelayingMessageContent; import android.content.res.Configuration; @@ -10,7 +9,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -18,14 +16,13 @@ import androidx.lifecycle.ViewModelProviders; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcChat; import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcEvent; import com.b44t.messenger.DcMsg; - +import java.util.Set; import org.thoughtcrime.securesms.BaseConversationListAdapter; import org.thoughtcrime.securesms.BaseConversationListFragment; import org.thoughtcrime.securesms.ConversationListActivity; @@ -36,22 +33,18 @@ import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; -import java.util.Set; - -/** - * A fragment that is displayed to do full-text search of messages, groups, and contacts. - */ +/** A fragment that is displayed to do full-text search of messages, groups, and contacts. */ public class SearchFragment extends BaseConversationListFragment - implements SearchListAdapter.EventListener, DcEventCenter.DcEventDelegate { + implements SearchListAdapter.EventListener, DcEventCenter.DcEventDelegate { - public static final String TAG = "SearchFragment"; + public static final String TAG = "SearchFragment"; - private TextView noResultsView; + private TextView noResultsView; private StickyHeaderDecoration listDecoration; - private SearchViewModel viewModel; + private SearchViewModel viewModel; private SearchListAdapter listAdapter; - private String pendingQuery; + private String pendingQuery; public static SearchFragment newInstance() { Bundle args = new Bundle(); @@ -66,7 +59,10 @@ public static SearchFragment newInstance() { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - viewModel = ViewModelProviders.of(this, (ViewModelProvider.Factory) new SearchViewModel.Factory(requireContext())).get(SearchViewModel.class); + viewModel = + ViewModelProviders.of( + this, (ViewModelProvider.Factory) new SearchViewModel.Factory(requireContext())) + .get(SearchViewModel.class); DcEventCenter eventCenter = DcHelper.getEventCenter(requireContext()); eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this); eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this); @@ -85,7 +81,10 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_search, container, false); } @@ -93,9 +92,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { noResultsView = view.findViewById(R.id.search_no_results); RecyclerView listView = view.findViewById(R.id.search_list); - fab = view.findViewById(R.id.fab); + fab = view.findViewById(R.id.fab); - listAdapter = new SearchListAdapter(getContext(), GlideApp.with(this), this); + listAdapter = new SearchListAdapter(getContext(), GlideApp.with(this), this); listDecoration = new StickyHeaderDecoration(listAdapter, false, true); fab.setVisibility(View.GONE); @@ -108,24 +107,29 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onStart() { super.onStart(); viewModel.setForwardingMode(isRelayingMessageContent(getActivity())); - viewModel.getSearchResult().observe(this, result -> { - result = result != null ? result : SearchResult.EMPTY; - - listAdapter.updateResults(result); - listDecoration.invalidateLayouts(); - - if (result.isEmpty()) { - if (TextUtils.isEmpty(viewModel.getLastQuery().trim())) { - noResultsView.setVisibility(View.GONE); - } else { - noResultsView.setVisibility(View.VISIBLE); - noResultsView.setText(getString(R.string.search_no_result_for_x, viewModel.getLastQuery())); - } - } else { - noResultsView.setVisibility(View.VISIBLE); - noResultsView.setText(""); - } - }); + viewModel + .getSearchResult() + .observe( + this, + result -> { + result = result != null ? result : SearchResult.EMPTY; + + listAdapter.updateResults(result); + listDecoration.invalidateLayouts(); + + if (result.isEmpty()) { + if (TextUtils.isEmpty(viewModel.getLastQuery().trim())) { + noResultsView.setVisibility(View.GONE); + } else { + noResultsView.setVisibility(View.VISIBLE); + noResultsView.setText( + getString(R.string.search_no_result_for_x, viewModel.getLastQuery())); + } + } else { + noResultsView.setVisibility(View.VISIBLE); + noResultsView.setText(""); + } + }); } @Override @@ -163,17 +167,19 @@ public void onContactClicked(@NonNull DcContact contact) { if (conversationList != null) { DcContext dcContext = DcHelper.getContext(requireContext()); int chatId = dcContext.getChatIdByContactId(contact.getId()); - if(chatId==0) { + if (chatId == 0) { new AlertDialog.Builder(requireContext()) .setMessage(getString(R.string.ask_start_chat_with, contact.getDisplayName())) .setCancelable(true) .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - int chatId1 = dcContext.createChatByContactId(contact.getId()); - conversationList.onCreateConversation(chatId1); - }).show(); - } - else { + .setPositiveButton( + android.R.string.ok, + (dialog, which) -> { + int chatId1 = dcContext.createChatByContactId(contact.getId()); + conversationList.onCreateConversation(chatId1); + }) + .show(); + } else { conversationList.onCreateConversation(chatId); } } @@ -214,7 +220,7 @@ protected boolean offerToArchive() { DcContext dcContext = DcHelper.getContext(requireActivity()); final Set selectedChats = listAdapter.getBatchSelections(); for (long chatId : selectedChats) { - DcChat dcChat = dcContext.getChat((int)chatId); + DcChat dcChat = dcContext.getChat((int) chatId); if (dcChat.getVisibility() != DcChat.DC_CHAT_VISIBILITY_ARCHIVED) { return true; } diff --git a/src/main/java/org/thoughtcrime/securesms/search/SearchListAdapter.java b/src/main/java/org/thoughtcrime/securesms/search/SearchListAdapter.java index db334b7cc..24dbd0faf 100644 --- a/src/main/java/org/thoughtcrime/securesms/search/SearchListAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/search/SearchListAdapter.java @@ -5,16 +5,14 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - +import java.util.Set; import org.thoughtcrime.securesms.BaseConversationListAdapter; import org.thoughtcrime.securesms.ConversationListItem; import org.thoughtcrime.securesms.R; @@ -24,39 +22,37 @@ import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; -import java.util.Set; - -class SearchListAdapter extends BaseConversationListAdapter - implements StickyHeaderDecoration.StickyHeaderAdapter -{ - private static final int TYPE_CHATS = 1; - private static final int TYPE_CONTACTS = 2; - private static final int TYPE_MESSAGES = 3; +class SearchListAdapter + extends BaseConversationListAdapter + implements StickyHeaderDecoration.StickyHeaderAdapter { + private static final int TYPE_CHATS = 1; + private static final int TYPE_CONTACTS = 2; + private static final int TYPE_MESSAGES = 3; private final GlideRequests glideRequests; private final EventListener eventListener; - @NonNull - private SearchResult searchResult = SearchResult.EMPTY; + @NonNull private SearchResult searchResult = SearchResult.EMPTY; + + final Context context; + final DcContext dcContext; // reset on account switching is not needed because SearchFragment and - final Context context; - final DcContext dcContext; // reset on account switching is not needed because SearchFragment and SearchListAdapter are recreated in every search start + // SearchListAdapter are recreated in every search start - SearchListAdapter(Context context, - @NonNull GlideRequests glideRequests, - @NonNull EventListener eventListener) - { + SearchListAdapter( + Context context, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) { this.glideRequests = glideRequests; this.eventListener = eventListener; - this.context = context; - this.dcContext = DcHelper.getContext(context); + this.context = context; + this.dcContext = DcHelper.getContext(context); } @NonNull @Override public SearchResultViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new SearchResultViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.conversation_list_item_view, parent, false)); + return new SearchResultViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.conversation_list_item_view, parent, false)); } @Override @@ -64,7 +60,14 @@ public void onBindViewHolder(@NonNull SearchResultViewHolder holder, int positio DcChatlist.Item conversationResult = getConversationResult(position); if (conversationResult != null) { - holder.bind(context, conversationResult, glideRequests, eventListener, batchSet, batchMode, searchResult.getQuery()); + holder.bind( + context, + conversationResult, + glideRequests, + eventListener, + batchSet, + batchMode, + searchResult.getQuery()); return; } @@ -105,13 +108,14 @@ public long getHeaderId(int position) { @Override public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { - return new HeaderViewHolder(LayoutInflater.from(parent.getContext()) - .inflate(R.layout.contact_selection_list_divider, parent, false)); + return new HeaderViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.contact_selection_list_divider, parent, false)); } @Override public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { - int headerType = (int)getHeaderId(position); + int headerType = (int) getHeaderId(position); int textId = R.plurals.n_messages; int count = 1; boolean maybeLimitedTo1000 = false; @@ -128,13 +132,17 @@ public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { case TYPE_MESSAGES: textId = R.plurals.n_messages; count = searchResult.getMessages().length; - maybeLimitedTo1000 = count==1000; // a count of 1000 results may be limited, see documentation of dc_search_msgs() + maybeLimitedTo1000 = + count == 1000; // a count of 1000 results may be limited, see documentation of + // dc_search_msgs() break; } String title = context.getResources().getQuantityString(textId, count, count); if (maybeLimitedTo1000) { - title = title.replace("000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000" + title = + title.replace( + "000", "000+"); // skipping the first digit allows formattings as "1.000" or "1,000" } viewHolder.bind(title); } @@ -147,7 +155,7 @@ void updateResults(@NonNull SearchResult result) { @Override public void selectAllThreads() { for (int i = 0; i < searchResult.getChats().getCnt(); i++) { - batchSet.add((long)searchResult.getChats().getItem(i).chatId); + batchSet.add((long) searchResult.getChats().getItem(i).chatId); } notifyDataSetChanged(); } @@ -186,8 +194,11 @@ private int getFirstMessageIndex() { public interface EventListener { void onConversationClicked(@NonNull DcChatlist.Item chatlistItem); + void onConversationLongClicked(@NonNull DcChatlist.Item chatlistItem); + void onContactClicked(@NonNull DcContact contact); + void onMessageClicked(@NonNull DcMsg message); } @@ -200,38 +211,48 @@ static class SearchResultViewHolder extends RecyclerView.ViewHolder { root = (ConversationListItem) itemView; } - void bind(Context context, - @NonNull DcChatlist.Item chatlistItem, - @NonNull GlideRequests glideRequests, - @NonNull EventListener eventListener, - @NonNull Set selectedThreads, - boolean batchMode, - @Nullable String query) - { + void bind( + Context context, + @NonNull DcChatlist.Item chatlistItem, + @NonNull GlideRequests glideRequests, + @NonNull EventListener eventListener, + @NonNull Set selectedThreads, + boolean batchMode, + @Nullable String query) { DcContext dcContext = DcHelper.getContext(context); - ThreadRecord threadRecord = DcHelper.getThreadRecord(context, chatlistItem.summary, dcContext.getChat(chatlistItem.chatId)); - root.bind(threadRecord, chatlistItem.msgId, chatlistItem.summary, glideRequests, selectedThreads, batchMode, query); + ThreadRecord threadRecord = + DcHelper.getThreadRecord( + context, chatlistItem.summary, dcContext.getChat(chatlistItem.chatId)); + root.bind( + threadRecord, + chatlistItem.msgId, + chatlistItem.summary, + glideRequests, + selectedThreads, + batchMode, + query); root.setOnClickListener(view -> eventListener.onConversationClicked(chatlistItem)); - root.setOnLongClickListener(view -> { - eventListener.onConversationLongClicked(chatlistItem); - return true; - }); + root.setOnLongClickListener( + view -> { + eventListener.onConversationLongClicked(chatlistItem); + return true; + }); } - void bind(@NonNull DcContact contactResult, - @NonNull GlideRequests glideRequests, - @NonNull EventListener eventListener, - @Nullable String query) - { + void bind( + @NonNull DcContact contactResult, + @NonNull GlideRequests glideRequests, + @NonNull EventListener eventListener, + @Nullable String query) { root.bind(contactResult, glideRequests, query); root.setOnClickListener(view -> eventListener.onContactClicked(contactResult)); } - void bind(@NonNull DcMsg messageResult, - @NonNull GlideRequests glideRequests, - @NonNull EventListener eventListener, - @Nullable String query) - { + void bind( + @NonNull DcMsg messageResult, + @NonNull GlideRequests glideRequests, + @NonNull EventListener eventListener, + @Nullable String query) { root.bind(messageResult, glideRequests, query); root.setOnClickListener(view -> eventListener.onMessageClicked(messageResult)); } diff --git a/src/main/java/org/thoughtcrime/securesms/search/SearchViewModel.java b/src/main/java/org/thoughtcrime/securesms/search/SearchViewModel.java index 413a60c2d..1276ec981 100644 --- a/src/main/java/org/thoughtcrime/securesms/search/SearchViewModel.java +++ b/src/main/java/org/thoughtcrime/securesms/search/SearchViewModel.java @@ -3,32 +3,29 @@ import android.content.Context; import android.text.TextUtils; import android.util.Log; - import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; - import com.b44t.messenger.DcChatlist; import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.search.model.SearchResult; import org.thoughtcrime.securesms.util.Util; class SearchViewModel extends ViewModel { - private static final String TAG = SearchViewModel.class.getSimpleName(); - private final ObservingLiveData searchResult; - private String lastQuery; - private final DcContext dcContext; - private boolean forwarding = false; - private boolean inBgSearch; - private boolean needsAnotherBgSearch; + private static final String TAG = SearchViewModel.class.getSimpleName(); + private final ObservingLiveData searchResult; + private String lastQuery; + private final DcContext dcContext; + private boolean forwarding = false; + private boolean inBgSearch; + private boolean needsAnotherBgSearch; SearchViewModel(@NonNull Context context) { - this.dcContext = DcHelper.getContext(context.getApplicationContext()); - this.searchResult = new ObservingLiveData(); + this.dcContext = DcHelper.getContext(context.getApplicationContext()); + this.searchResult = new ObservingLiveData(); } LiveData getSearchResult() { @@ -39,7 +36,6 @@ public void setForwardingMode(boolean forwarding) { this.forwarding = forwarding; } - void updateQuery(String query) { lastQuery = query; updateQuery(); @@ -51,21 +47,21 @@ public void updateQuery() { Log.i(TAG, "... search call debounced"); } else { inBgSearch = true; - Util.runOnBackground(() -> { - - Util.sleep(100); - needsAnotherBgSearch = false; - queryAndCallback(lastQuery, searchResult::postValue); - - while (needsAnotherBgSearch) { - Util.sleep(100); - needsAnotherBgSearch = false; - Log.i(TAG, "... executing debounced search call"); - queryAndCallback(lastQuery, searchResult::postValue); - } - - inBgSearch = false; - }); + Util.runOnBackground( + () -> { + Util.sleep(100); + needsAnotherBgSearch = false; + queryAndCallback(lastQuery, searchResult::postValue); + + while (needsAnotherBgSearch) { + Util.sleep(100); + needsAnotherBgSearch = false; + Log.i(TAG, "... executing debounced search call"); + queryAndCallback(lastQuery, searchResult::postValue); + } + + inBgSearch = false; + }); } } @@ -79,7 +75,8 @@ private void queryAndCallback(@NonNull String query, @NonNull SearchViewModel.Ca // #1 search for chats long startMs = System.currentTimeMillis(); - DcChatlist conversations = dcContext.getChatlist(forwarding? DcContext.DC_GCL_FOR_FORWARDING : 0, query, 0); + DcChatlist conversations = + dcContext.getChatlist(forwarding ? DcContext.DC_GCL_FOR_FORWARDING : 0, query, 0); overallCnt += conversations.getCnt(); Log.i(TAG, "⏰ getChatlist(" + query + "): " + (System.currentTimeMillis() - startMs) + "ms"); @@ -127,11 +124,9 @@ String getLastQuery() { } @Override - protected void onCleared() { - } + protected void onCleared() {} - private static class ObservingLiveData extends MutableLiveData { - } + private static class ObservingLiveData extends MutableLiveData {} public static class Factory extends ViewModelProvider.NewInstanceFactory { diff --git a/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java b/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java index 5711179e4..edd21447d 100644 --- a/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java +++ b/src/main/java/org/thoughtcrime/securesms/search/model/SearchResult.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.search.model; import androidx.annotation.NonNull; - import com.b44t.messenger.DcChatlist; /** @@ -10,22 +9,23 @@ */ public class SearchResult { - public static final SearchResult EMPTY = new SearchResult("", new int[]{}, new DcChatlist(0, 0), new int[]{}); + public static final SearchResult EMPTY = + new SearchResult("", new int[] {}, new DcChatlist(0, 0), new int[] {}); - private final String query; - private final int[] contacts; + private final String query; + private final int[] contacts; private final DcChatlist conversations; - private final int[] messages; - - public SearchResult(@NonNull String query, - @NonNull int[] contacts, - @NonNull DcChatlist conversations, - @NonNull int[] messages) - { - this.query = query; - this.contacts = contacts; + private final int[] messages; + + public SearchResult( + @NonNull String query, + @NonNull int[] contacts, + @NonNull DcChatlist conversations, + @NonNull int[] messages) { + this.query = query; + this.contacts = contacts; this.conversations = conversations; - this.messages = messages; + this.messages = messages; } public int[] getContacts() { diff --git a/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java b/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java index 8cbe1de63..315fb10fb 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java +++ b/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java @@ -4,7 +4,6 @@ import android.content.Intent; import android.os.Bundle; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; @@ -17,10 +16,8 @@ import androidx.media3.session.SessionCommand; import androidx.media3.session.SessionCommands; import androidx.media3.session.SessionResult; - import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; - import org.thoughtcrime.securesms.ConversationListActivity; public class AudioPlaybackService extends MediaSessionService { @@ -34,60 +31,60 @@ public class AudioPlaybackService extends MediaSessionService { public void onCreate() { super.onCreate(); - AudioAttributes audioAttributes = new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) // USAGE_VOICE_COMMUNICATION is for VoIP calls - .setContentType(C.AUDIO_CONTENT_TYPE_SPEECH) - .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) // USAGE_VOICE_COMMUNICATION is for VoIP calls + .setContentType(C.AUDIO_CONTENT_TYPE_SPEECH) + .build(); - player = new ExoPlayer.Builder(this) - .setAudioAttributes(audioAttributes, true) - .setHandleAudioBecomingNoisy(true) - .build(); + player = + new ExoPlayer.Builder(this) + .setAudioAttributes(audioAttributes, true) + .setHandleAudioBecomingNoisy(true) + .build(); // This is for click on the notification to go back to app Intent intent = new Intent(this, ConversationListActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - PendingIntent initialIntent = PendingIntent.getActivity( - this, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE - ); - - session = new MediaSession.Builder(this, player) - .setSessionActivity(initialIntent) - .setCallback(new MediaSession.Callback() { - - @OptIn(markerClass = UnstableApi.class) - @Override - public MediaSession.ConnectionResult onConnect( - MediaSession session, - MediaSession.ControllerInfo controller - ) { - SessionCommands sessionCommands = MediaSession - .ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() - .add(new SessionCommand("UPDATE_ACTIVITY_CONTEXT", new Bundle())) + PendingIntent initialIntent = + PendingIntent.getActivity( + this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + session = + new MediaSession.Builder(this, player) + .setSessionActivity(initialIntent) + .setCallback( + new MediaSession.Callback() { + + @OptIn(markerClass = UnstableApi.class) + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + SessionCommands sessionCommands = + MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS + .buildUpon() + .add(new SessionCommand("UPDATE_ACTIVITY_CONTEXT", new Bundle())) + .build(); + + return new MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands(sessionCommands) + .build(); + } + + @NonNull + @Override + public ListenableFuture onCustomCommand( + MediaSession session, + MediaSession.ControllerInfo controller, + SessionCommand customCommand, + Bundle args) { + if ("UPDATE_ACTIVITY_CONTEXT".equals(customCommand.customAction)) { + updateSessionActivity(args); + } + return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); + } + }) .build(); - - return new MediaSession.ConnectionResult.AcceptedResultBuilder(session) - .setAvailableSessionCommands(sessionCommands) - .build(); - } - - @NonNull - @Override - public ListenableFuture onCustomCommand( - MediaSession session, - MediaSession.ControllerInfo controller, - SessionCommand customCommand, - Bundle args - ) { - if ("UPDATE_ACTIVITY_CONTEXT".equals(customCommand.customAction)) { - updateSessionActivity(args); - } - return Futures.immediateFuture( - new SessionResult(SessionResult.RESULT_SUCCESS)); - } - }) - .build(); } @OptIn(markerClass = UnstableApi.class) @@ -104,12 +101,12 @@ private void updateSessionActivity(Bundle args) { intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtras(args); - PendingIntent pendingIntent = PendingIntent.getActivity( - this, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE - ); + PendingIntent pendingIntent = + PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); session.setSessionActivity(pendingIntent); } diff --git a/src/main/java/org/thoughtcrime/securesms/service/BootReceiver.java b/src/main/java/org/thoughtcrime/securesms/service/BootReceiver.java index 3fcc8be61..dbba46948 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/BootReceiver.java +++ b/src/main/java/org/thoughtcrime/securesms/service/BootReceiver.java @@ -13,5 +13,4 @@ public void onReceive(Context context, Intent intent) { // there's nothing more to do here as all initialisation stuff is already done in // on program startup which is done before this broadcast is sent. } - } diff --git a/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java b/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java index 82b86c07f..263290af1 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java +++ b/src/main/java/org/thoughtcrime/securesms/service/FetchForegroundService.java @@ -6,11 +6,9 @@ import android.content.Intent; import android.os.IBinder; import android.util.Log; - import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; - import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.ForegroundDetector; @@ -65,27 +63,31 @@ public void onCreate() { Log.i(TAG, "Creating fetch service"); super.onCreate(); - Notification notification = new NotificationCompat.Builder(this, NotificationCenter.CH_GENERIC) - .setContentTitle(getString(R.string.connectivity_updating)) - .setSmallIcon(R.drawable.notification_permanent) - .build(); + Notification notification = + new NotificationCompat.Builder(this, NotificationCenter.CH_GENERIC) + .setContentTitle(getString(R.string.connectivity_updating)) + .setSmallIcon(R.drawable.notification_permanent) + .build(); try { startForeground(NotificationCenter.ID_FETCH, notification); - Util.runOnAnyBackgroundThread(() -> { - Log.i(TAG, "Starting fetch"); - if (!ApplicationContext.getDcAccounts().backgroundFetch(300)) { // as startForeground() was called, there is time - FetchForegroundService.stop(this); - } // else we stop FetchForegroundService on DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE - }); + Util.runOnAnyBackgroundThread( + () -> { + Log.i(TAG, "Starting fetch"); + if (!ApplicationContext.getDcAccounts() + .backgroundFetch(300)) { // as startForeground() was called, there is time + FetchForegroundService.stop(this); + } // else we stop FetchForegroundService on DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE + }); } catch (Exception e) { Log.e(TAG, "Error calling startForeground()", e); } } public static void fetchSynchronously() { - // According to the documentation https://firebase.google.com/docs/cloud-messaging/android/receive, + // According to the documentation + // https://firebase.google.com/docs/cloud-messaging/android/receive, // we need to handle the message within 20s, and the time window may be even shorter than 20s, // so, use 10s to be safe. fetchingSynchronously = true; @@ -97,9 +99,11 @@ public static void fetchSynchronously() { while (fetchingSynchronously) { try { // The `wait()` needs to be enclosed in a while loop because there may be - // "spurious wake-ups", i.e. `wait()` may return even though `notifyAll()` wasn't called. + // "spurious wake-ups", i.e. `wait()` may return even though `notifyAll()` wasn't + // called. STOP_NOTIFIER.wait(); - } catch (InterruptedException ex) {} + } catch (InterruptedException ex) { + } } } } @@ -121,5 +125,4 @@ public void onTimeout(int startId, int fgsType) { ApplicationContext.getDcAccounts().stopBackgroundFetch(); stopSelf(); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java b/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java index 569153394..62db53bb3 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/src/main/java/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -11,23 +11,20 @@ import android.os.Build; import android.os.IBinder; import android.util.Log; - import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat.Builder; import androidx.core.content.ContextCompat; - -import org.thoughtcrime.securesms.DummyActivity; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.notifications.NotificationCenter; -import org.thoughtcrime.securesms.util.IntentUtils; - import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import org.thoughtcrime.securesms.DummyActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.notifications.NotificationCenter; +import org.thoughtcrime.securesms.util.IntentUtils; public final class GenericForegroundService extends Service { @@ -35,17 +32,17 @@ public final class GenericForegroundService extends Service { private final IBinder binder = new LocalBinder(); - private static final String EXTRA_TITLE = "extra_title"; - private static final String EXTRA_CONTENT_TEXT = "extra_content_text"; - private static final String EXTRA_CHANNEL_ID = "extra_channel_id"; - private static final String EXTRA_ICON_RES = "extra_icon_res"; - private static final String EXTRA_ID = "extra_id"; - private static final String EXTRA_PROGRESS = "extra_progress"; - private static final String EXTRA_PROGRESS_MAX = "extra_progress_max"; + private static final String EXTRA_TITLE = "extra_title"; + private static final String EXTRA_CONTENT_TEXT = "extra_content_text"; + private static final String EXTRA_CHANNEL_ID = "extra_channel_id"; + private static final String EXTRA_ICON_RES = "extra_icon_res"; + private static final String EXTRA_ID = "extra_id"; + private static final String EXTRA_PROGRESS = "extra_progress"; + private static final String EXTRA_PROGRESS_MAX = "extra_progress_max"; private static final String EXTRA_PROGRESS_INDETERMINATE = "extra_progress_indeterminate"; private static final String ACTION_START = "start"; - private static final String ACTION_STOP = "stop"; + private static final String ACTION_STOP = "stop"; private static final AtomicInteger NEXT_ID = new AtomicInteger(); private static final AtomicBoolean CHANNEL_CREATED = new AtomicBoolean(false); @@ -54,7 +51,9 @@ public final class GenericForegroundService extends Service { private final LinkedHashMap allActiveMessages = new LinkedHashMap<>(); - private static final Entry DEFAULTS = new Entry("", "", NotificationCenter.CH_GENERIC, R.drawable.icon_notification, -1, 0, 0, false); + private static final Entry DEFAULTS = + new Entry( + "", "", NotificationCenter.CH_GENERIC, R.drawable.icon_notification, -1, 0, 0, false); private @Nullable Entry lastPosted; @@ -66,9 +65,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { synchronized (GenericForegroundService.class) { String action = intent.getAction(); - if (ACTION_START.equals(action)) handleStart(intent); - else if (ACTION_STOP .equals(action)) handleStop(intent); - else throw new IllegalStateException(String.format("Action needs to be %s or %s.", ACTION_START, ACTION_STOP)); + if (ACTION_START.equals(action)) handleStart(intent); + else if (ACTION_STOP.equals(action)) handleStop(intent); + else + throw new IllegalStateException( + String.format("Action needs to be %s or %s.", ACTION_START, ACTION_STOP)); updateNotification(); @@ -89,7 +90,6 @@ private synchronized void updateNotification() { } } - private synchronized void handleStart(@NonNull Intent intent) { Entry entry = Entry.fromIntent(intent); @@ -112,14 +112,18 @@ private synchronized void handleStop(@NonNull Intent intent) { private void postObligatoryForegroundNotification(@NonNull Entry active) { lastPosted = active; - startForeground(NotificationCenter.ID_GENERIC, new Builder(this, active.channelId) - .setSmallIcon(active.iconRes) - .setContentTitle(active.title) - .setTicker(active.contentText) - .setContentText(active.contentText) - .setProgress(active.progressMax, active.progress, active.indeterminate) - .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, DummyActivity.class), IntentUtils.FLAG_MUTABLE())) - .build()); + startForeground( + NotificationCenter.ID_GENERIC, + new Builder(this, active.channelId) + .setSmallIcon(active.iconRes) + .setContentTitle(active.title) + .setTicker(active.contentText) + .setContentText(active.contentText) + .setProgress(active.progressMax, active.progress, active.indeterminate) + .setContentIntent( + PendingIntent.getActivity( + this, 0, new Intent(this, DummyActivity.class), IntentUtils.FLAG_MUTABLE())) + .build()); } @Override @@ -127,8 +131,8 @@ public IBinder onBind(Intent intent) { return binder; } - - public static NotificationController startForegroundTask(@NonNull Context context, @NonNull String task) { + public static NotificationController startForegroundTask( + @NonNull Context context, @NonNull String task) { startedCounter++; final int id = NEXT_ID.getAndIncrement(); @@ -151,14 +155,15 @@ public static void stopForegroundTask(@NonNull Context context, int id) { intent.putExtra(EXTRA_ID, id); ContextCompat.startForegroundService(context, intent); - startedCounter = Math.max(startedCounter-1, 0); + startedCounter = Math.max(startedCounter - 1, 0); } public static boolean isForegroundTaskStarted() { return startedCounter > 0; } - synchronized void replaceProgress(int id, int progressMax, int progress, boolean indeterminate, String message) { + synchronized void replaceProgress( + int id, int progressMax, int progress, boolean indeterminate, String message) { Entry oldEntry = allActiveMessages.get(id); if (oldEntry == null) { @@ -170,7 +175,16 @@ synchronized void replaceProgress(int id, int progressMax, int progress, boolean message = oldEntry.contentText; } - Entry newEntry = new Entry(oldEntry.title, message, oldEntry.channelId, oldEntry.iconRes, oldEntry.id, progressMax, progress, indeterminate); + Entry newEntry = + new Entry( + oldEntry.title, + message, + oldEntry.channelId, + oldEntry.iconRes, + oldEntry.id, + progressMax, + progress, + indeterminate); if (oldEntry.equals(newEntry)) { Log.d(TAG, String.format("handleReplace() skip, no change %s", newEntry)); @@ -185,36 +199,47 @@ synchronized void replaceProgress(int id, int progressMax, int progress, boolean } @TargetApi(Build.VERSION_CODES.O) - static public void createFgNotificationChannel(Context context) { - if(!CHANNEL_CREATED.get() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + public static void createFgNotificationChannel(Context context) { + if (!CHANNEL_CREATED.get() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CHANNEL_CREATED.set(true); - NotificationChannel channel = new NotificationChannel(NotificationCenter.CH_GENERIC, - "Generic Background Service", NotificationManager.IMPORTANCE_MIN); - channel.setDescription("Ensure app will not be killed while long ongoing background tasks are running."); + NotificationChannel channel = + new NotificationChannel( + NotificationCenter.CH_GENERIC, + "Generic Background Service", + NotificationManager.IMPORTANCE_MIN); + channel.setDescription( + "Ensure app will not be killed while long ongoing background tasks are running."); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } - private static class Entry { - final @NonNull String title; - final @NonNull String contentText; - final @NonNull String channelId; - final int id; - final @DrawableRes int iconRes; - final int progress; - final int progressMax; - final boolean indeterminate; - - private Entry(@NonNull String title, @NonNull String contentText, @NonNull String channelId, @DrawableRes int iconRes, int id, int progressMax, int progress, boolean indeterminate) { - this.title = title; - this.contentText = contentText; - this.channelId = channelId; - this.iconRes = iconRes; - this.id = id; - this.progress = progress; - this.progressMax = progressMax; + final @NonNull String title; + final @NonNull String contentText; + final @NonNull String channelId; + final int id; + final @DrawableRes int iconRes; + final int progress; + final int progressMax; + final boolean indeterminate; + + private Entry( + @NonNull String title, + @NonNull String contentText, + @NonNull String channelId, + @DrawableRes int iconRes, + int id, + int progressMax, + int progress, + boolean indeterminate) { + this.title = title; + this.contentText = contentText; + this.channelId = channelId; + this.iconRes = iconRes; + this.id = id; + this.progress = progress; + this.progressMax = progressMax; this.indeterminate = indeterminate; } @@ -230,17 +255,26 @@ private static Entry fromIntent(@NonNull Intent intent) { String channelId = intent.getStringExtra(EXTRA_CHANNEL_ID); if (channelId == null) channelId = DEFAULTS.channelId; - int iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULTS.iconRes); - int progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULTS.progress); - int progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULTS.progressMax); - boolean indeterminate = intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULTS.indeterminate); + int iconRes = intent.getIntExtra(EXTRA_ICON_RES, DEFAULTS.iconRes); + int progress = intent.getIntExtra(EXTRA_PROGRESS, DEFAULTS.progress); + int progressMax = intent.getIntExtra(EXTRA_PROGRESS_MAX, DEFAULTS.progressMax); + boolean indeterminate = + intent.getBooleanExtra(EXTRA_PROGRESS_INDETERMINATE, DEFAULTS.indeterminate); - return new Entry(title, contentText, channelId, iconRes, id, progressMax, progress, indeterminate); + return new Entry( + title, contentText, channelId, iconRes, id, progressMax, progress, indeterminate); } @Override public @NonNull String toString() { - return String.format(Locale.ENGLISH, "ChannelId: %s Id: %d Progress: %d/%d %s", channelId, id, progress, progressMax, indeterminate ? "indeterminate" : "determinate"); + return String.format( + Locale.ENGLISH, + "ChannelId: %s Id: %d Progress: %d/%d %s", + channelId, + id, + progress, + progressMax, + indeterminate ? "indeterminate" : "determinate"); } @Override @@ -249,14 +283,14 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Entry entry = (Entry) o; - return id == entry.id && - iconRes == entry.iconRes && - progress == entry.progress && - progressMax == entry.progressMax && - indeterminate == entry.indeterminate && - title.equals(entry.title) && - contentText.equals(entry.contentText) && - channelId.equals(entry.channelId); + return id == entry.id + && iconRes == entry.iconRes + && progress == entry.progress + && progressMax == entry.progressMax + && indeterminate == entry.indeterminate + && title.equals(entry.title) + && contentText.equals(entry.contentText) + && channelId.equals(entry.channelId); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java b/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java index b45c8fc49..db259d8d7 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java +++ b/src/main/java/org/thoughtcrime/securesms/service/NotificationController.java @@ -5,25 +5,20 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; - import androidx.annotation.NonNull; - import java.util.concurrent.atomic.AtomicReference; -/** - * Class to control notifications triggered by GenericForeGroundService. - * - */ +/** Class to control notifications triggered by GenericForeGroundService. */ public final class NotificationController { private final @NonNull Context context; private final int id; - private int progress; - private int progressMax; + private int progress; + private int progressMax; private boolean indeterminate; - private String message = ""; - private long percent = -1; + private String message = ""; + private long percent = -1; private final ServiceConnection serviceConnection; @@ -31,26 +26,31 @@ public final class NotificationController { NotificationController(@NonNull Context context, int id) { this.context = context; - this.id = id; - - serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - GenericForegroundService.LocalBinder binder = (GenericForegroundService.LocalBinder) service; - GenericForegroundService genericForegroundService = binder.getService(); - - NotificationController.this.service.set(genericForegroundService); - - updateProgressOnService(); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - service.set(null); - } - }; - - context.bindService(new Intent(context, GenericForegroundService.class), serviceConnection, Context.BIND_AUTO_CREATE); + this.id = id; + + serviceConnection = + new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + GenericForegroundService.LocalBinder binder = + (GenericForegroundService.LocalBinder) service; + GenericForegroundService genericForegroundService = binder.getService(); + + NotificationController.this.service.set(genericForegroundService); + + updateProgressOnService(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + service.set(null); + } + }; + + context.bindService( + new Intent(context, GenericForegroundService.class), + serviceConnection, + Context.BIND_AUTO_CREATE); } public int getId() { @@ -61,7 +61,7 @@ public void close() { try { GenericForegroundService.stopForegroundTask(context, id); context.unbindService(serviceConnection); - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -74,16 +74,18 @@ public void setProgress(long newProgressMax, long newProgress, @NonNull String n setProgress((int) newProgressMax, (int) newProgress, false, newMessage); } - private synchronized void setProgress(int newProgressMax, int newProgress, boolean indeterminant, @NonNull String newMessage) { + private synchronized void setProgress( + int newProgressMax, int newProgress, boolean indeterminant, @NonNull String newMessage) { int newPercent = newProgressMax != 0 ? 100 * newProgress / newProgressMax : -1; - boolean same = newPercent == percent && indeterminate == indeterminant && newMessage.equals(message); + boolean same = + newPercent == percent && indeterminate == indeterminant && newMessage.equals(message); - percent = newPercent; - progress = newProgress; - progressMax = newProgressMax; + percent = newPercent; + progress = newProgress; + progressMax = newProgressMax; indeterminate = indeterminant; - message = newMessage; + message = newMessage; if (same) return; @@ -95,6 +97,6 @@ private synchronized void updateProgressOnService() { if (genericForegroundService == null) return; - genericForegroundService.replaceProgress(id, progressMax, progress, indeterminate, message); + genericForegroundService.replaceProgress(id, progressMax, progress, indeterminate, message); } } diff --git a/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.java b/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.java index fe7412a5f..ed56c5feb 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.java +++ b/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.java @@ -3,27 +3,25 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; - import org.thoughtcrime.securesms.util.Prefs; /** - * Respond to a PanicKit trigger Intent by locking the app. PanicKit provides a - * common framework for creating "panic button" apps that can trigger actions - * in "panic responder" apps. In this case, the response is to lock the app, - * if it has been configured to do so via the Signal lock preference. If the - * user has not set a passphrase, then the panic trigger intent does nothing. + * Respond to a PanicKit trigger Intent by locking the app. PanicKit provides a common framework for + * creating "panic button" apps that can trigger actions in "panic responder" apps. In this case, + * the response is to lock the app, if it has been configured to do so via the Signal lock + * preference. If the user has not set a passphrase, then the panic trigger intent does nothing. */ public class PanicResponderListener extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (intent != null && !Prefs.isPasswordDisabled(context) && - "info.guardianproject.panic.action.TRIGGER".equals(intent.getAction())) - { + if (intent != null + && !Prefs.isPasswordDisabled(context) + && "info.guardianproject.panic.action.TRIGGER".equals(intent.getAction())) { // as delta is protected with the system credentials, // the current suggestion on "panic" would probably just be to lock the device. // this would also lock delta chat. // however, we leave this class to allow easy changes on this. } } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/util/AccessibilityUtil.java b/src/main/java/org/thoughtcrime/securesms/util/AccessibilityUtil.java index 266b3cb87..e6dbd3b83 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/AccessibilityUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/AccessibilityUtil.java @@ -6,14 +6,15 @@ public final class AccessibilityUtil { - private AccessibilityUtil() { - } + private AccessibilityUtil() {} public static boolean areAnimationsDisabled(Context context) { if (context == null) { Log.e("AccessibilityUtil", "animationsDisabled: context was null"); return false; } - return Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1) == 0f; + return Settings.Global.getFloat( + context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1) + == 0f; } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/AndroidSignalProtocolLogger.java b/src/main/java/org/thoughtcrime/securesms/util/AndroidSignalProtocolLogger.java index 4e4c56f67..7bd1b7125 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/AndroidSignalProtocolLogger.java +++ b/src/main/java/org/thoughtcrime/securesms/util/AndroidSignalProtocolLogger.java @@ -1,7 +1,7 @@ /** * Copyright (C) 2014-2016 Open Whisper Systems * - * Licensed according to the LICENSE file in this repository. + *

Licensed according to the LICENSE file in this repository. */ package org.thoughtcrime.securesms.util; @@ -10,14 +10,16 @@ public class AndroidSignalProtocolLogger implements SignalProtocolLogger { - private static final SparseIntArray PRIORITY_MAP = new SparseIntArray(5) {{ - put(SignalProtocolLogger.INFO, Log.INFO); - put(SignalProtocolLogger.ASSERT, Log.ASSERT); - put(SignalProtocolLogger.DEBUG, Log.DEBUG); - put(SignalProtocolLogger.VERBOSE, Log.VERBOSE); - put(SignalProtocolLogger.WARN, Log.WARN); - - }}; + private static final SparseIntArray PRIORITY_MAP = + new SparseIntArray(5) { + { + put(SignalProtocolLogger.INFO, Log.INFO); + put(SignalProtocolLogger.ASSERT, Log.ASSERT); + put(SignalProtocolLogger.DEBUG, Log.DEBUG); + put(SignalProtocolLogger.VERBOSE, Log.VERBOSE); + put(SignalProtocolLogger.WARN, Log.WARN); + } + }; @Override public void log(int priority, String tag, String message) { diff --git a/src/main/java/org/thoughtcrime/securesms/util/AsyncLoader.java b/src/main/java/org/thoughtcrime/securesms/util/AsyncLoader.java index 1a0f0d2d3..6217c5e09 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/AsyncLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/util/AsyncLoader.java @@ -17,17 +17,15 @@ */ import android.content.Context; - import androidx.loader.content.AsyncTaskLoader; /** - * Loader which extends AsyncTaskLoaders and handles caveats - * as pointed out in http://code.google.com/p/android/issues/detail?id=14944. + * Loader which extends AsyncTaskLoaders and handles caveats as pointed out in + * http://code.google.com/p/android/issues/detail?id=14944. * - * Based on CursorLoader.java in the Fragment compatibility package + *

Based on CursorLoader.java in the Fragment compatibility package * * @author Alexander Blom (me@alexanderblom.se) - * * @param data type */ public abstract class AsyncLoader extends AsyncTaskLoader { @@ -49,7 +47,6 @@ public void deliverResult(D data) { super.deliverResult(data); } - @Override protected void onStartLoading() { if (data != null) { @@ -76,6 +73,4 @@ protected void onReset() { data = null; } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java b/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java index fd85c17d0..24a367187 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java @@ -5,7 +5,6 @@ import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.util.Base64; - import java.io.ByteArrayOutputStream; public class AvatarUtil { @@ -47,5 +46,4 @@ public static String asDataUri(Drawable drawable) { String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP); return "data:image/jpeg;base64," + base64; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java index 5eb057079..a4e53fd82 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -10,18 +10,15 @@ import android.graphics.drawable.Drawable; import android.util.Log; import android.util.Pair; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.exifinterface.media.ExifInterface; - import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicBoolean; - import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; @@ -41,10 +38,10 @@ public static Bitmap createScaledBitmap(Bitmap bitmap, int maxWidth, int maxHeig return bitmap; } - int newWidth = maxWidth; + int newWidth = maxWidth; int newHeight = maxHeight; - float widthRatio = bitmap.getWidth() / (float) maxWidth; + float widthRatio = bitmap.getWidth() / (float) maxWidth; float heightRatio = bitmap.getHeight() / (float) maxHeight; if (widthRatio > heightRatio) { @@ -57,11 +54,10 @@ public static Bitmap createScaledBitmap(Bitmap bitmap, int maxWidth, int maxHeig } private static BitmapFactory.Options getImageDimensions(InputStream inputStream) - throws BitmapDecodingException - { + throws BitmapDecodingException { BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BufferedInputStream fis = new BufferedInputStream(inputStream); + options.inJustDecodeBounds = true; + BufferedInputStream fis = new BufferedInputStream(inputStream); BitmapFactory.decodeStream(fis, null, options); try { fis.close(); @@ -70,50 +66,52 @@ private static BitmapFactory.Options getImageDimensions(InputStream inputStream) } if (options.outWidth == -1 || options.outHeight == -1) { - throw new BitmapDecodingException("Failed to decode image dimensions: " + options.outWidth + ", " + options.outHeight); + throw new BitmapDecodingException( + "Failed to decode image dimensions: " + options.outWidth + ", " + options.outHeight); } return options; } @Nullable - public static Pair getExifDimensions(InputStream inputStream) throws IOException { - ExifInterface exif = new ExifInterface(inputStream); - int width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0); - int height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0); + public static Pair getExifDimensions(InputStream inputStream) + throws IOException { + ExifInterface exif = new ExifInterface(inputStream); + int width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0); + int height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0); if (width == 0 && height == 0) { return null; } int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); - if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || - orientation == ExifInterface.ORIENTATION_ROTATE_270 || - orientation == ExifInterface.ORIENTATION_TRANSVERSE || - orientation == ExifInterface.ORIENTATION_TRANSPOSE) - { + if (orientation == ExifInterface.ORIENTATION_ROTATE_90 + || orientation == ExifInterface.ORIENTATION_ROTATE_270 + || orientation == ExifInterface.ORIENTATION_TRANSVERSE + || orientation == ExifInterface.ORIENTATION_TRANSPOSE) { return new Pair<>(height, width); } return new Pair<>(width, height); } - public static Pair getDimensions(InputStream inputStream) throws BitmapDecodingException { + public static Pair getDimensions(InputStream inputStream) + throws BitmapDecodingException { BitmapFactory.Options options = getImageDimensions(inputStream); return new Pair<>(options.outWidth, options.outHeight); } - public static byte[] createFromNV21(@NonNull final byte[] data, - final int width, - final int height, - int rotation, - final Rect croppingRect, - final boolean flipHorizontal) - throws IOException - { + public static byte[] createFromNV21( + @NonNull final byte[] data, + final int width, + final int height, + int rotation, + final Rect croppingRect, + final boolean flipHorizontal) + throws IOException { byte[] rotated = rotateNV21(data, width, height, rotation, flipHorizontal); - final int rotatedWidth = rotation % 180 > 0 ? height : width; - final int rotatedHeight = rotation % 180 > 0 ? width : height; - YuvImage previewImage = new YuvImage(rotated, ImageFormat.NV21, - rotatedWidth, rotatedHeight, null); + final int rotatedWidth = rotation % 180 > 0 ? height : width; + final int rotatedHeight = rotation % 180 > 0 ? width : height; + YuvImage previewImage = + new YuvImage(rotated, ImageFormat.NV21, rotatedWidth, rotatedHeight, null); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); previewImage.compressToJpeg(croppingRect, 80, outputStream); @@ -129,90 +127,98 @@ public static byte[] createFromNV21(@NonNull final byte[] data, * * http://www.fourcc.org/yuv.php#NV21 */ - public static byte[] rotateNV21(@NonNull final byte[] yuv, - final int width, - final int height, - final int rotation, - final boolean flipHorizontal) - throws IOException - { + public static byte[] rotateNV21( + @NonNull final byte[] yuv, + final int width, + final int height, + final int rotation, + final boolean flipHorizontal) + throws IOException { if (rotation == 0) return yuv; if (rotation % 90 != 0 || rotation < 0 || rotation > 270) { throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0"); } else if ((width * height * 3) / 2 != yuv.length) { - throw new IOException("provided width and height don't jive with the data length (" + - yuv.length + "). Width: " + width + " height: " + height + - " = data length: " + (width * height * 3) / 2); + throw new IOException( + "provided width and height don't jive with the data length (" + + yuv.length + + "). Width: " + + width + + " height: " + + height + + " = data length: " + + (width * height * 3) / 2); } - final byte[] output = new byte[yuv.length]; - final int frameSize = width * height; - final boolean swap = rotation % 180 != 0; - final boolean xflip = flipHorizontal ? rotation % 270 == 0 : rotation % 270 != 0; - final boolean yflip = rotation >= 180; + final byte[] output = new byte[yuv.length]; + final int frameSize = width * height; + final boolean swap = rotation % 180 != 0; + final boolean xflip = flipHorizontal ? rotation % 270 == 0 : rotation % 270 != 0; + final boolean yflip = rotation >= 180; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { final int yIn = j * width + i; final int uIn = frameSize + (j >> 1) * width + (i & ~1); - final int vIn = uIn + 1; + final int vIn = uIn + 1; - final int wOut = swap ? height : width; - final int hOut = swap ? width : height; - final int iSwapped = swap ? j : i; - final int jSwapped = swap ? i : j; - final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped; - final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped; + final int wOut = swap ? height : width; + final int hOut = swap ? width : height; + final int iSwapped = swap ? j : i; + final int jSwapped = swap ? i : j; + final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped; + final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped; final int yOut = jOut * wOut + iOut; final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1); final int vOut = uOut + 1; - output[yOut] = (byte)(0xff & yuv[yIn]); - output[uOut] = (byte)(0xff & yuv[uIn]); - output[vOut] = (byte)(0xff & yuv[vIn]); + output[yOut] = (byte) (0xff & yuv[yIn]); + output[uOut] = (byte) (0xff & yuv[uIn]); + output[vOut] = (byte) (0xff & yuv[vIn]); } } return output; } - public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) { + public static Bitmap createFromDrawable( + final Drawable drawable, final int width, final int height) { final AtomicBoolean created = new AtomicBoolean(false); - final Bitmap[] result = new Bitmap[1]; - - Runnable runnable = new Runnable() { - @Override - public void run() { - if (drawable instanceof BitmapDrawable) { - result[0] = ((BitmapDrawable) drawable).getBitmap(); - } else { - int canvasWidth = drawable.getIntrinsicWidth(); - if (canvasWidth <= 0) canvasWidth = width; - - int canvasHeight = drawable.getIntrinsicHeight(); - if (canvasHeight <= 0) canvasHeight = height; - - Bitmap bitmap; - - try { - bitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - } catch (Exception e) { - Log.w(TAG, e); - bitmap = null; + final Bitmap[] result = new Bitmap[1]; + + Runnable runnable = + new Runnable() { + @Override + public void run() { + if (drawable instanceof BitmapDrawable) { + result[0] = ((BitmapDrawable) drawable).getBitmap(); + } else { + int canvasWidth = drawable.getIntrinsicWidth(); + if (canvasWidth <= 0) canvasWidth = width; + + int canvasHeight = drawable.getIntrinsicHeight(); + if (canvasHeight <= 0) canvasHeight = height; + + Bitmap bitmap; + + try { + bitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + } catch (Exception e) { + Log.w(TAG, e); + bitmap = null; + } + + result[0] = bitmap; + } + + synchronized (result) { + created.set(true); + result.notifyAll(); + } } - - result[0] = bitmap; - } - - synchronized (result) { - created.set(true); - result.notifyAll(); - } - } - }; + }; Util.runOnMain(runnable); @@ -241,10 +247,10 @@ public static int getMaxTextureSize() { int maximumTextureSize = 0; for (int i = 0; i < totalConfigurations[0]; i++) { - egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize); + egl.eglGetConfigAttrib( + display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize); - if (maximumTextureSize < textureSize[0]) - maximumTextureSize = textureSize[0]; + if (maximumTextureSize < textureSize[0]) maximumTextureSize = textureSize[0]; } egl.eglTerminate(display); diff --git a/src/main/java/org/thoughtcrime/securesms/util/Conversions.java b/src/main/java/org/thoughtcrime/securesms/util/Conversions.java index 7fe341111..81b8416ca 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Conversions.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Conversions.java @@ -1,18 +1,16 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.util; @@ -25,14 +23,14 @@ public static byte[] longToByteArray(long l) { } public static int longToByteArray(byte[] bytes, int offset, long value) { - bytes[offset + 7] = (byte)value; - bytes[offset + 6] = (byte)(value >> 8); - bytes[offset + 5] = (byte)(value >> 16); - bytes[offset + 4] = (byte)(value >> 24); - bytes[offset + 3] = (byte)(value >> 32); - bytes[offset + 2] = (byte)(value >> 40); - bytes[offset + 1] = (byte)(value >> 48); - bytes[offset] = (byte)(value >> 56); + bytes[offset + 7] = (byte) value; + bytes[offset + 6] = (byte) (value >> 8); + bytes[offset + 5] = (byte) (value >> 16); + bytes[offset + 4] = (byte) (value >> 24); + bytes[offset + 3] = (byte) (value >> 32); + bytes[offset + 2] = (byte) (value >> 40); + bytes[offset + 1] = (byte) (value >> 48); + bytes[offset] = (byte) (value >> 56); return 8; } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index eb99f6367..4aca721c7 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -18,18 +18,13 @@ import android.content.Context; import android.text.format.DateFormat; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.R; - import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; +import org.thoughtcrime.securesms.R; -/** - * Utility methods to help display dates in a nice, easily readable way. - */ +/** Utility methods to help display dates in a nice, easily readable way. */ public class DateUtils extends android.text.format.DateUtils { private static boolean isWithin(final long millis, final long span, final TimeUnit unit) { @@ -75,13 +70,13 @@ public static String getBriefRelativeTimeSpanString(final Context c, final long public static String getExtendedTimeSpanString(final Context c, final long timestamp) { StringBuilder format = new StringBuilder(); - if (DateUtils.isToday(timestamp)) {} - else if (isWithin(timestamp, 6, TimeUnit.DAYS)) format.append("EEE "); + if (DateUtils.isToday(timestamp)) { + } else if (isWithin(timestamp, 6, TimeUnit.DAYS)) format.append("EEE "); else if (isWithin(timestamp, 365, TimeUnit.DAYS)) format.append("MMM d, "); - else format.append("MMM d, yyyy, "); + else format.append("MMM d, yyyy, "); if (DateFormat.is24HourFormat(c)) format.append("HH:mm"); - else format.append("hh:mm a"); + else format.append("hh:mm a"); return getFormattedDateTime(timestamp, format.toString()); } @@ -90,16 +85,17 @@ public static String getExtendedRelativeTimeSpanString(final Context c, final lo if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { return c.getString(R.string.now); } else if (isWithin(timestamp, 1, TimeUnit.HOURS)) { - int mins = (int)TimeUnit.MINUTES.convert(System.currentTimeMillis() - timestamp, TimeUnit.MILLISECONDS); + int mins = + (int) + TimeUnit.MINUTES.convert( + System.currentTimeMillis() - timestamp, TimeUnit.MILLISECONDS); return c.getResources().getQuantityString(R.plurals.n_minutes, mins, mins); } else { return getExtendedTimeSpanString(c, timestamp); } } - public static String getRelativeDate(@NonNull Context context, - long timestamp) - { + public static String getRelativeDate(@NonNull Context context, long timestamp) { if (isToday(timestamp)) { return context.getString(R.string.today); } else if (isYesterday(timestamp)) { @@ -114,9 +110,11 @@ private static String getLocalizedPattern(String template) { } public static String getFormatedDuration(long millis) { - return String.format("%02d:%02d", - TimeUnit.MILLISECONDS.toMinutes(millis), - TimeUnit.MILLISECONDS.toSeconds(millis-(TimeUnit.MILLISECONDS.toMinutes(millis)*60000))); + return String.format( + "%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(millis), + TimeUnit.MILLISECONDS.toSeconds( + millis - (TimeUnit.MILLISECONDS.toMinutes(millis) * 60000))); } public static String getFormattedCallDuration(Context c, int seconds) { diff --git a/src/main/java/org/thoughtcrime/securesms/util/Debouncer.java b/src/main/java/org/thoughtcrime/securesms/util/Debouncer.java index 9389728ff..e7eac834a 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Debouncer.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Debouncer.java @@ -6,22 +6,22 @@ * A class that will throttle the number of runnables executed to be at most once every specified * interval. * - * Useful for performing actions in response to rapid user input, such as inputting text, where you - * don't necessarily want to perform an action after every input. + *

Useful for performing actions in response to rapid user input, such as inputting text, where + * you don't necessarily want to perform an action after every input. * - * See http://rxmarbles.com/#debounce + *

See http://rxmarbles.com/#debounce */ public class Debouncer { private final Handler handler; - private final long threshold; + private final long threshold; /** - * @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every - * {@code threshold} milliseconds. + * @param threshold Only one runnable will be executed via {@link #publish(Runnable)} every {@code + * threshold} milliseconds. */ public Debouncer(long threshold) { - this.handler = new Handler(); + this.handler = new Handler(); this.threshold = threshold; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/DrawableUtil.java b/src/main/java/org/thoughtcrime/securesms/util/DrawableUtil.java index b1aaef80b..e16db515c 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/DrawableUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/DrawableUtil.java @@ -2,18 +2,22 @@ import android.graphics.Bitmap; import android.graphics.Canvas; - import androidx.annotation.NonNull; public final class DrawableUtil { - private static final int SHORTCUT_INFO_BITMAP_SIZE = ViewUtil.dpToPx(108); + private static final int SHORTCUT_INFO_BITMAP_SIZE = ViewUtil.dpToPx(108); private static final int SHORTCUT_INFO_WRAPPED_SIZE = ViewUtil.dpToPx(72); - private static final int SHORTCUT_INFO_PADDING = (SHORTCUT_INFO_BITMAP_SIZE - SHORTCUT_INFO_WRAPPED_SIZE) / 2; + private static final int SHORTCUT_INFO_PADDING = + (SHORTCUT_INFO_BITMAP_SIZE - SHORTCUT_INFO_WRAPPED_SIZE) / 2; public static @NonNull Bitmap wrapBitmapForShortcutInfo(@NonNull Bitmap toWrap) { - Bitmap bitmap = Bitmap.createBitmap(SHORTCUT_INFO_BITMAP_SIZE, SHORTCUT_INFO_BITMAP_SIZE, Bitmap.Config.ARGB_8888); - Bitmap scaled = Bitmap.createScaledBitmap(toWrap, SHORTCUT_INFO_WRAPPED_SIZE, SHORTCUT_INFO_WRAPPED_SIZE, true); + Bitmap bitmap = + Bitmap.createBitmap( + SHORTCUT_INFO_BITMAP_SIZE, SHORTCUT_INFO_BITMAP_SIZE, Bitmap.Config.ARGB_8888); + Bitmap scaled = + Bitmap.createScaledBitmap( + toWrap, SHORTCUT_INFO_WRAPPED_SIZE, SHORTCUT_INFO_WRAPPED_SIZE, true); Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(scaled, SHORTCUT_INFO_PADDING, SHORTCUT_INFO_PADDING, null); diff --git a/src/main/java/org/thoughtcrime/securesms/util/DynamicNoActionBarTheme.java b/src/main/java/org/thoughtcrime/securesms/util/DynamicNoActionBarTheme.java index f5760e91d..3a66994b7 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/DynamicNoActionBarTheme.java +++ b/src/main/java/org/thoughtcrime/securesms/util/DynamicNoActionBarTheme.java @@ -2,7 +2,6 @@ import androidx.annotation.NonNull; import androidx.annotation.StyleRes; - import org.thoughtcrime.securesms.R; public class DynamicNoActionBarTheme extends DynamicTheme { diff --git a/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java b/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java index 234631b33..420f84c4c 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java +++ b/src/main/java/org/thoughtcrime/securesms/util/DynamicTheme.java @@ -5,16 +5,14 @@ import android.content.Intent; import android.content.res.Configuration; import android.os.Build; - import androidx.annotation.NonNull; import androidx.annotation.StyleRes; import androidx.appcompat.app.AppCompatDelegate; - import org.thoughtcrime.securesms.R; public class DynamicTheme { - public static final String DARK = "dark"; + public static final String DARK = "dark"; public static final String LIGHT = "light"; public static final String SYSTEM = "system"; public static final String PURPLE = "purple"; @@ -27,18 +25,18 @@ public class DynamicTheme { private int currentTheme; public void onCreate(Activity activity) { - //boolean wasDarkTheme = isDarkTheme; + // boolean wasDarkTheme = isDarkTheme; currentTheme = getSelectedTheme(activity); - //isDarkTheme = isDarkTheme(activity); + // isDarkTheme = isDarkTheme(activity); activity.setTheme(currentTheme); // In case you introduce a CachedInflater and there are problems with the dark mode, uncomment // this line and the line in onResume(): - //if (isDarkTheme != wasDarkTheme) { - //CachedInflater.from(activity).clear(); - //} + // if (isDarkTheme != wasDarkTheme) { + // CachedInflater.from(activity).clear(); + // } } public void onResume(Activity activity) { @@ -48,7 +46,7 @@ public void onResume(Activity activity) { OverridePendingTransition.invoke(activity); activity.startActivity(intent); OverridePendingTransition.invoke(activity); - //CachedInflater.from(activity).clear(); + // CachedInflater.from(activity).clear(); } } @@ -63,10 +61,9 @@ public static void setDefaultDayNightMode(@NonNull Context context) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); } - //CachedInflater.from(context).clear(); + // CachedInflater.from(context).clear(); } - private @StyleRes int getSelectedTheme(Activity activity) { String theme = Prefs.getTheme(activity); if (isDarkTheme(activity)) { @@ -100,9 +97,7 @@ static boolean systemThemeAvailable() { return Build.VERSION.SDK_INT >= 29; } - /** - * Takes the system theme into account. - */ + /** Takes the system theme into account. */ public static boolean isDarkTheme(@NonNull Context context) { String theme = Prefs.getTheme(context); @@ -119,7 +114,8 @@ public static String getCheckmarkEmoji(@NonNull Context context) { } private static boolean isSystemInDarkTheme(@NonNull Context context) { - return (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + return (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; } private static final class OverridePendingTransition { diff --git a/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java b/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java index f3573b5dd..155b134d0 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/FileProviderUtil.java @@ -1,22 +1,18 @@ package org.thoughtcrime.securesms.util; - import android.content.Context; import android.net.Uri; - import androidx.annotation.NonNull; import androidx.core.content.FileProvider; - -import org.thoughtcrime.securesms.BuildConfig; - import java.io.File; +import org.thoughtcrime.securesms.BuildConfig; public class FileProviderUtil { - private static final String AUTHORITY = BuildConfig.APPLICATION_ID+".fileprovider"; + private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; - public static Uri getUriFor(@NonNull Context context, @NonNull File file) throws IllegalStateException, NullPointerException { + public static Uri getUriFor(@NonNull Context context, @NonNull File file) + throws IllegalStateException, NullPointerException { return FileProvider.getUriForFile(context, AUTHORITY, file); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/FutureTaskListener.java b/src/main/java/org/thoughtcrime/securesms/util/FutureTaskListener.java index e182dc6bc..f4eb288ce 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/FutureTaskListener.java +++ b/src/main/java/org/thoughtcrime/securesms/util/FutureTaskListener.java @@ -1,18 +1,16 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.util; @@ -20,5 +18,6 @@ public interface FutureTaskListener { public void onSuccess(V result); + public void onFailure(ExecutionException exception); } diff --git a/src/main/java/org/thoughtcrime/securesms/util/Hash.java b/src/main/java/org/thoughtcrime/securesms/util/Hash.java index f0de2c7fc..833ae5826 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Hash.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Hash.java @@ -7,16 +7,15 @@ public class Hash { - public static String sha256(String input) { - try { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - messageDigest.update(input.getBytes(Charset.forName("UTF-8"))); - byte[] digest = messageDigest.digest(); - return String.format("%064x", new BigInteger(1, digest)); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } - return null; + public static String sha256(String input) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(input.getBytes(Charset.forName("UTF-8"))); + byte[] digest = messageDigest.digest(); + return String.format("%064x", new BigInteger(1, digest)); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); } - + return null; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/Hex.java b/src/main/java/org/thoughtcrime/securesms/util/Hex.java index e6f06fb76..b5762a4ad 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Hex.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Hex.java @@ -1,35 +1,31 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.util; import java.io.IOException; -/** - * Utility for generating hex dumps. - */ +/** Utility for generating hex dumps. */ public class Hex { - private final static char[] HEX_DIGITS = { + private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; public static String toStringCondensed(byte[] bytes) { StringBuffer buf = new StringBuffer(); - for (int i=0;i> 4) & 0xf]); buf.append(HEX_DIGITS[b & 0xf]); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java b/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java index 70c4544cc..8f8c7ea5b 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java +++ b/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.util; - import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.Context; @@ -9,17 +8,15 @@ import android.net.Uri; import android.os.Build; import android.widget.Toast; - import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.R; - import java.util.List; +import org.thoughtcrime.securesms.R; public class IntentUtils { public static boolean isResolvable(@NonNull Context context, @NonNull Intent intent) { - List resolveInfoList = context.getPackageManager().queryIntentActivities(intent, 0); + List resolveInfoList = + context.getPackageManager().queryIntentActivities(intent, 0); return resolveInfoList != null && resolveInfoList.size() > 1; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java b/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java index c248527ad..9fe9b65a8 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java +++ b/src/main/java/org/thoughtcrime/securesms/util/JsonUtils.java @@ -1,17 +1,14 @@ package org.thoughtcrime.securesms.util; import android.util.Base64; - import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; - -import org.json.JSONException; -import org.json.JSONObject; - import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import org.json.JSONException; +import org.json.JSONObject; public class JsonUtils { @@ -57,7 +54,7 @@ public static ObjectMapper getMapper() { public static String optString(JSONObject obj, String name) { try { return obj.optString(name); - } catch(Exception e) { + } catch (Exception e) { return ""; } } @@ -65,7 +62,7 @@ public static String optString(JSONObject obj, String name) { public static boolean optBoolean(JSONObject obj, String name) { try { return obj.optBoolean(name); - } catch(Exception e) { + } catch (Exception e) { return false; } } @@ -80,7 +77,7 @@ public SaneJSONObject(JSONObject delegate) { public String getString(String name) throws JSONException { if (delegate.isNull(name)) return null; - else return delegate.getString(name); + else return delegate.getString(name); } public long getLong(String name) throws JSONException { diff --git a/src/main/java/org/thoughtcrime/securesms/util/LRUCache.java b/src/main/java/org/thoughtcrime/securesms/util/LRUCache.java index b89308918..b8948f1df 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/LRUCache.java +++ b/src/main/java/org/thoughtcrime/securesms/util/LRUCache.java @@ -3,7 +3,7 @@ import java.util.LinkedHashMap; import java.util.Map; -public class LRUCache extends LinkedHashMap { +public class LRUCache extends LinkedHashMap { private final int maxSize; @@ -12,7 +12,7 @@ public LRUCache(int maxSize) { } @Override - protected boolean removeEldestEntry (Map.Entry eldest) { + protected boolean removeEldestEntry(Map.Entry eldest) { return size() > maxSize; } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/Linkifier.java b/src/main/java/org/thoughtcrime/securesms/util/Linkifier.java index 88ee1a94f..824f91552 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Linkifier.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Linkifier.java @@ -5,27 +5,29 @@ import android.text.Spanned; import android.text.style.URLSpan; import android.text.util.Linkify; - import java.util.regex.Pattern; /* Utility for text linkify-ing */ public class Linkifier { - private static final Pattern CMD_PATTERN = Pattern.compile("(?<=^|\\s)/[a-zA-Z][a-zA-Z@\\d_/.-]{0,254}"); - private static final Pattern CUSTOM_PATTERN = Pattern.compile("(?<=^|\\s)(OPENPGP4FPR|openpgp4fpr|mumble|geo|gemini):[^ \\n]+"); - private static final Pattern PROXY_PATTERN = Pattern.compile("(?<=^|\\s)(SOCKS5|socks5|ss|SS):[^ \\n]+"); - private static final Pattern PHONE_PATTERN - = Pattern.compile( // sdd = space, dot, or dash - "(?<=^|\\s|\\.|\\()" // no letter at start - + "(\\+[0-9]+[\\- \\.]*)?" // +* - + "(\\([0-9]+\\)[\\- \\.]*)?" // ()* - + "([0-9][0-9\\- \\.]{3,}[0-9])" // + (5 characters min) - + "(?=$|\\s|\\.|\\))"); // no letter at end + private static final Pattern CMD_PATTERN = + Pattern.compile("(?<=^|\\s)/[a-zA-Z][a-zA-Z@\\d_/.-]{0,254}"); + private static final Pattern CUSTOM_PATTERN = + Pattern.compile("(?<=^|\\s)(OPENPGP4FPR|openpgp4fpr|mumble|geo|gemini):[^ \\n]+"); + private static final Pattern PROXY_PATTERN = + Pattern.compile("(?<=^|\\s)(SOCKS5|socks5|ss|SS):[^ \\n]+"); + private static final Pattern PHONE_PATTERN = + Pattern.compile( // sdd = space, dot, or dash + "(?<=^|\\s|\\.|\\()" // no letter at start + + "(\\+[0-9]+[\\- \\.]*)?" // +* + + "(\\([0-9]+\\)[\\- \\.]*)?" // ()* + + "([0-9][0-9\\- \\.]{3,}[0-9])" // + (5 characters min) + + "(?=$|\\s|\\.|\\))"); // no letter at end private static int brokenPhoneLinkifier = -1; private static boolean internalPhoneLinkifierNeeded() { if (brokenPhoneLinkifier == -1) { // unset - if(Linkify.addLinks(new SpannableString("a100b"), Linkify.PHONE_NUMBERS)) { + if (Linkify.addLinks(new SpannableString("a100b"), Linkify.PHONE_NUMBERS)) { brokenPhoneLinkifier = 1; // true } else { brokenPhoneLinkifier = 0; // false @@ -39,8 +41,10 @@ private static void replaceURLSpan(Spannable messageBody) { for (URLSpan urlSpan : urlSpans) { int start = messageBody.getSpanStart(urlSpan); int end = messageBody.getSpanEnd(urlSpan); - // LongClickCopySpan must not be derived from URLSpan, otherwise links will be removed on the next addLinks() call - messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // LongClickCopySpan must not be derived from URLSpan, otherwise links will be removed on the + // next addLinks() call + messageBody.setSpan( + new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } @@ -48,23 +52,31 @@ public static Spannable linkify(Spannable messageBody) { // linkify commands such as `/echo` - // do this first to avoid `/xkcd_123456` to be treated partly as a phone number Linkify.addLinks(messageBody, CMD_PATTERN, "cmd:", null, null); - replaceURLSpan(messageBody); // replace URLSpan so that it is not removed on the next addLinks() call + replaceURLSpan( + messageBody); // replace URLSpan so that it is not removed on the next addLinks() call Linkify.addLinks(messageBody, CUSTOM_PATTERN, null, null, null); replaceURLSpan(messageBody); if (Linkify.addLinks(messageBody, PROXY_PATTERN, null, null, null)) { - replaceURLSpan(messageBody); // replace URLSpan so that it is not removed on the next addLinks() call + replaceURLSpan( + messageBody); // replace URLSpan so that it is not removed on the next addLinks() call } int flags; if (internalPhoneLinkifierNeeded()) { - if (Linkify.addLinks(messageBody, PHONE_PATTERN, "tel:", Linkify.sPhoneNumberMatchFilter, Linkify.sPhoneNumberTransformFilter)) { - replaceURLSpan(messageBody); // replace URLSpan so that it is not removed on the next addLinks() call + if (Linkify.addLinks( + messageBody, + PHONE_PATTERN, + "tel:", + Linkify.sPhoneNumberMatchFilter, + Linkify.sPhoneNumberTransformFilter)) { + replaceURLSpan( + messageBody); // replace URLSpan so that it is not removed on the next addLinks() call } - flags = Linkify.EMAIL_ADDRESSES|Linkify.WEB_URLS; + flags = Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS; } else { - flags = Linkify.EMAIL_ADDRESSES|Linkify.WEB_URLS|Linkify.PHONE_NUMBERS; + flags = Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS | Linkify.PHONE_NUMBERS; } // linkyfiy urls etc., this removes all existing URLSpan @@ -74,5 +86,4 @@ public static Spannable linkify(Spannable messageBody) { return messageBody; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/ListenableFutureTask.java b/src/main/java/org/thoughtcrime/securesms/util/ListenableFutureTask.java index 068772aa4..5643d139b 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ListenableFutureTask.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ListenableFutureTask.java @@ -1,23 +1,20 @@ /** * 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.util; import androidx.annotation.Nullable; - import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; @@ -29,11 +26,9 @@ public class ListenableFutureTask extends FutureTask { private final List> listeners = new LinkedList<>(); - @Nullable - private final Object identifier; + @Nullable private final Object identifier; - @Nullable - private final Executor callbackExecutor; + @Nullable private final Executor callbackExecutor; public ListenableFutureTask(Callable callable) { this(callable, null); @@ -43,25 +38,26 @@ public ListenableFutureTask(Callable callable, @Nullable Object identifier) { this(callable, identifier, null); } - public ListenableFutureTask(Callable callable, @Nullable Object identifier, @Nullable Executor callbackExecutor) { + public ListenableFutureTask( + Callable callable, @Nullable Object identifier, @Nullable Executor callbackExecutor) { super(callable); - this.identifier = identifier; + this.identifier = identifier; this.callbackExecutor = callbackExecutor; } - public ListenableFutureTask(final V result) { this(result, null); } public ListenableFutureTask(final V result, @Nullable Object identifier) { - super(new Callable() { - @Override - public V call() throws Exception { - return result; - } - }); - this.identifier = identifier; + super( + new Callable() { + @Override + public V call() throws Exception { + return result; + } + }); + this.identifier = identifier; this.callbackExecutor = null; this.run(); } @@ -84,17 +80,18 @@ protected synchronized void done() { } private void callback() { - Runnable callbackRunnable = new Runnable() { - @Override - public void run() { - for (FutureTaskListener listener : listeners) { - callback(listener); - } - } - }; + Runnable callbackRunnable = + new Runnable() { + @Override + public void run() { + for (FutureTaskListener listener : listeners) { + callback(listener); + } + } + }; if (callbackExecutor == null) callbackRunnable.run(); - else callbackExecutor.execute(callbackRunnable); + else callbackExecutor.execute(callbackRunnable); } private void callback(FutureTaskListener listener) { @@ -121,6 +118,6 @@ public boolean equals(Object other) { @Override public int hashCode() { if (identifier != null) return identifier.hashCode(); - else return super.hashCode(); + else return super.hashCode(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java b/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java index bda2a2a66..8d0ebdeed 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java +++ b/src/main/java/org/thoughtcrime/securesms/util/LongClickCopySpan.java @@ -7,28 +7,23 @@ import android.text.style.ClickableSpan; import android.view.View; import android.widget.Toast; - import androidx.annotation.ColorInt; import androidx.appcompat.app.AlertDialog; - +import chat.delta.rpc.types.SecurejoinSource; import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; - import org.thoughtcrime.securesms.ConversationActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.qr.QrCodeHandler; -import chat.delta.rpc.types.SecurejoinSource; - public class LongClickCopySpan extends ClickableSpan { private static final String PREFIX_MAILTO = "mailto:"; private static final String PREFIX_TEL = "tel:"; private static final String PREFIX_CMD = "cmd:"; private boolean isHighlighted; - @ColorInt - private int highlightColor; + @ColorInt private int highlightColor; private final String url; public LongClickCopySpan(String url) { @@ -66,16 +61,21 @@ public void onClick(View widget) { contactId = dcContext.createContact(null, addr); } DcContact contact = dcContext.getContact(contactId); - if (contact.getId() != 0 && !contact.isBlocked() && dcContext.getChatIdByContactId(contact.getId()) != 0) { + if (contact.getId() != 0 + && !contact.isBlocked() + && dcContext.getChatIdByContactId(contact.getId()) != 0) { openChat(activity, contact); } else { new AlertDialog.Builder(activity) - .setMessage(activity.getString(R.string.ask_start_chat_with, contact.getDisplayName())) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { + .setMessage( + activity.getString(R.string.ask_start_chat_with, contact.getDisplayName())) + .setPositiveButton( + android.R.string.ok, + (dialog, which) -> { openChat(activity, contact); }) - .setNegativeButton(R.string.cancel, null) - .show(); + .setNegativeButton(R.string.cancel, null) + .show(); } } catch (Exception e) { e.printStackTrace(); @@ -96,17 +96,21 @@ void onLongClick(View widget) { if (url.startsWith(PREFIX_CMD)) { Util.writeTextToClipboard(context, url.substring(PREFIX_CMD.length())); - Toast.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show(); + Toast.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) + .show(); } else { String preparedUrl = prepareUrl(url); new AlertDialog.Builder(context) .setTitle(preparedUrl) - .setItems(new CharSequence[]{ - context.getString(R.string.menu_copy_to_clipboard) - }, + .setItems( + new CharSequence[] {context.getString(R.string.menu_copy_to_clipboard)}, (dialogInterface, i) -> { Util.writeTextToClipboard(context, preparedUrl); - Toast.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show(); + Toast.makeText( + context, + context.getString(R.string.copied_to_clipboard), + Toast.LENGTH_SHORT) + .show(); }) .setNegativeButton(R.string.cancel, null) .show(); diff --git a/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java b/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java index 359aa5907..30f3ca3fa 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java +++ b/src/main/java/org/thoughtcrime/securesms/util/LongClickMovementMethod.java @@ -11,9 +11,7 @@ import android.view.MotionEvent; import android.view.View; import android.widget.TextView; - import androidx.core.content.ContextCompat; - import org.thoughtcrime.securesms.R; public class LongClickMovementMethod extends LinkMovementMethod { @@ -25,34 +23,36 @@ public class LongClickMovementMethod extends LinkMovementMethod { private LongClickCopySpan currentSpan; private LongClickMovementMethod(final Context context) { - gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { - @Override - public void onLongPress(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onLongClick(widget); - widget = null; - currentSpan = null; - } - } + gestureDetector = + new GestureDetector( + context, + new GestureDetector.SimpleOnGestureListener() { + @Override + public void onLongPress(MotionEvent e) { + if (currentSpan != null && widget != null) { + currentSpan.onLongClick(widget); + widget = null; + currentSpan = null; + } + } - @Override - public boolean onSingleTapUp(MotionEvent e) { - if (currentSpan != null && widget != null) { - currentSpan.onClick(widget); - widget = null; - currentSpan = null; - } - return true; - } - }); + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (currentSpan != null && widget != null) { + currentSpan.onClick(widget); + widget = null; + currentSpan = null; + } + return true; + } + }); } @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int action = event.getAction(); - if (action == MotionEvent.ACTION_UP || - action == MotionEvent.ACTION_DOWN) { + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { int x = (int) event.getX(); int y = (int) event.getY(); @@ -70,10 +70,10 @@ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event if (longClickCopySpan.length != 0) { LongClickCopySpan aSingleSpan = longClickCopySpan[0]; if (action == MotionEvent.ACTION_DOWN) { - Selection.setSelection(buffer, buffer.getSpanStart(aSingleSpan), - buffer.getSpanEnd(aSingleSpan)); - aSingleSpan.setHighlighted(true, - ContextCompat.getColor(widget.getContext(), R.color.touch_highlight)); + Selection.setSelection( + buffer, buffer.getSpanStart(aSingleSpan), buffer.getSpanEnd(aSingleSpan)); + aSingleSpan.setHighlighted( + true, ContextCompat.getColor(widget.getContext(), R.color.touch_highlight)); } else { Selection.removeSelection(buffer); aSingleSpan.setHighlighted(false, Color.TRANSPARENT); @@ -85,8 +85,11 @@ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event } } else if (action == MotionEvent.ACTION_CANCEL) { // Remove Selections. - LongClickCopySpan[] spans = buffer.getSpans(Selection.getSelectionStart(buffer), - Selection.getSelectionEnd(buffer), LongClickCopySpan.class); + LongClickCopySpan[] spans = + buffer.getSpans( + Selection.getSelectionStart(buffer), + Selection.getSelectionEnd(buffer), + LongClickCopySpan.class); for (LongClickCopySpan aSpan : spans) { aSpan.setHighlighted(false, Color.TRANSPARENT); } diff --git a/src/main/java/org/thoughtcrime/securesms/util/MailtoUtil.java b/src/main/java/org/thoughtcrime/securesms/util/MailtoUtil.java index c2080270b..fbd891540 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/MailtoUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/MailtoUtil.java @@ -2,62 +2,61 @@ import android.net.MailTo; import android.net.Uri; - import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; public class MailtoUtil { - private static final String MAILTO = "mailto"; - private static final String SUBJECT = "subject"; - private static final String BODY = "body"; - private static final String QUERY_SEPARATOR = "&"; - private static final String KEY_VALUE_SEPARATOR = "="; + private static final String MAILTO = "mailto"; + private static final String SUBJECT = "subject"; + private static final String BODY = "body"; + private static final String QUERY_SEPARATOR = "&"; + private static final String KEY_VALUE_SEPARATOR = "="; - public static boolean isMailto(Uri uri) { - return uri != null && MAILTO.equals(uri.getScheme()); - } + public static boolean isMailto(Uri uri) { + return uri != null && MAILTO.equals(uri.getScheme()); + } - public static String[] getRecipients(Uri uri) { - String[] recipientsArray = new String[0]; - if (uri != null) { - MailTo mailto = MailTo.parse(uri.toString()); - String recipientsList = mailto.getTo(); - if(recipientsList != null && !recipientsList.trim().isEmpty()) { - recipientsArray = recipientsList.trim().split(","); - } - } - return recipientsArray; + public static String[] getRecipients(Uri uri) { + String[] recipientsArray = new String[0]; + if (uri != null) { + MailTo mailto = MailTo.parse(uri.toString()); + String recipientsList = mailto.getTo(); + if (recipientsList != null && !recipientsList.trim().isEmpty()) { + recipientsArray = recipientsList.trim().split(","); + } } + return recipientsArray; + } - public static String getText(Uri uri) { - Map mailtoQueryMap = getMailtoQueryMap(uri); - String textToShare = mailtoQueryMap.get(SUBJECT); - String body = mailtoQueryMap.get(BODY); - if (body != null && !body.isEmpty()) { - if (textToShare != null && !textToShare.isEmpty()) { - textToShare += "\n" + body; - } else { - textToShare = body; - } - } - return textToShare != null? textToShare : ""; + public static String getText(Uri uri) { + Map mailtoQueryMap = getMailtoQueryMap(uri); + String textToShare = mailtoQueryMap.get(SUBJECT); + String body = mailtoQueryMap.get(BODY); + if (body != null && !body.isEmpty()) { + if (textToShare != null && !textToShare.isEmpty()) { + textToShare += "\n" + body; + } else { + textToShare = body; + } } + return textToShare != null ? textToShare : ""; + } - private static Map getMailtoQueryMap(Uri uri) { - Map mailtoQueryMap = new HashMap<>(); - String query = uri.getEncodedQuery(); - if (query != null && !query.isEmpty()) { - String[] queryArray = query.split(QUERY_SEPARATOR); - for(String queryEntry : queryArray) { - String[] queryEntryArray = queryEntry.split(KEY_VALUE_SEPARATOR); - try { - mailtoQueryMap.put(queryEntryArray[0], URLDecoder.decode(queryEntryArray[1], "UTF-8")); - } catch (Exception e) { - e.printStackTrace(); - } - } + private static Map getMailtoQueryMap(Uri uri) { + Map mailtoQueryMap = new HashMap<>(); + String query = uri.getEncodedQuery(); + if (query != null && !query.isEmpty()) { + String[] queryArray = query.split(QUERY_SEPARATOR); + for (String queryEntry : queryArray) { + String[] queryEntryArray = queryEntry.split(KEY_VALUE_SEPARATOR); + try { + mailtoQueryMap.put(queryEntryArray[0], URLDecoder.decode(queryEntryArray[1], "UTF-8")); + } catch (Exception e) { + e.printStackTrace(); } - return mailtoQueryMap; + } } + return mailtoQueryMap; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java b/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java index 7b205b992..7bdd1d7ca 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java @@ -8,15 +8,18 @@ import android.util.Log; import android.util.Pair; import android.webkit.MimeTypeMap; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; - import com.b44t.messenger.DcMsg; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.gif.GifDrawable; - +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.ExecutionException; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DocumentSlide; @@ -30,27 +33,19 @@ import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.ExecutionException; - public class MediaUtil { private static final String TAG = MediaUtil.class.getSimpleName(); - public static final String IMAGE_WEBP = "image/webp"; - public static final String IMAGE_JPEG = "image/jpeg"; - public static final String IMAGE_GIF = "image/gif"; - public static final String AUDIO_AAC = "audio/aac"; + public static final String IMAGE_WEBP = "image/webp"; + public static final String IMAGE_JPEG = "image/jpeg"; + public static final String IMAGE_GIF = "image/gif"; + public static final String AUDIO_AAC = "audio/aac"; public static final String AUDIO_UNSPECIFIED = "audio/*"; public static final String VIDEO_UNSPECIFIED = "video/*"; - public static final String OCTET = "application/octet-stream"; - public static final String WEBXDC = "application/webxdc+zip"; - public static final String VCARD = "text/vcard"; - + public static final String OCTET = "application/octet-stream"; + public static final String WEBXDC = "application/webxdc+zip"; + public static final String VCARD = "text/vcard"; public static Slide getSlideForMsg(Context context, DcMsg dcMsg) { Slide slide = null; @@ -62,13 +57,11 @@ public static Slide getSlideForMsg(Context context, DcMsg dcMsg) { slide = new StickerSlide(context, dcMsg); } else if (dcMsg.getType() == DcMsg.DC_MSG_VIDEO) { slide = new VideoSlide(context, dcMsg); - } else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO - || dcMsg.getType() == DcMsg.DC_MSG_VOICE) { + } else if (dcMsg.getType() == DcMsg.DC_MSG_AUDIO || dcMsg.getType() == DcMsg.DC_MSG_VOICE) { slide = new AudioSlide(context, dcMsg); } else if (dcMsg.getType() == DcMsg.DC_MSG_VCARD) { slide = new VcardSlide(context, dcMsg); - } else if (dcMsg.getType() == DcMsg.DC_MSG_FILE - || dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) { + } else if (dcMsg.getType() == DcMsg.DC_MSG_FILE || dcMsg.getType() == DcMsg.DC_MSG_WEBXDC) { slide = new DocumentSlide(context, dcMsg); } @@ -96,20 +89,18 @@ public static Slide getSlideForMsg(Context context, DcMsg dcMsg) { public static @Nullable String getCorrectedMimeType(@Nullable String mimeType) { if (mimeType == null) return null; - switch(mimeType) { - case "image/jpg": - return MimeTypeMap.getSingleton().hasMimeType(IMAGE_JPEG) - ? IMAGE_JPEG - : mimeType; - default: - return mimeType; + switch (mimeType) { + case "image/jpg": + return MimeTypeMap.getSingleton().hasMimeType(IMAGE_JPEG) ? IMAGE_JPEG : mimeType; + default: + return mimeType; } } /** - * This is a version of android.webkit.MimeTypeMap.getFileExtensionFromUrl() that - * doesn't refuse to do its job when there are characters in the URL it doesn't know. - * Using MimeTypeMap.getFileExtensionFromUrl() led to bugs like this one: + * This is a version of android.webkit.MimeTypeMap.getFileExtensionFromUrl() that doesn't refuse + * to do its job when there are characters in the URL it doesn't know. Using + * MimeTypeMap.getFileExtensionFromUrl() led to bugs like this one: * https://github.com/deltachat/deltachat-android/issues/2306 * * @return The url's file extension, or "" if there is none. @@ -128,8 +119,7 @@ public static String getFileExtensionFromUrl(String url) { } int filenamePos = url.lastIndexOf('/'); - String filename = - 0 <= filenamePos ? url.substring(filenamePos + 1) : url; + String filename = 0 <= filenamePos ? url.substring(filenamePos + 1) : url; if (!filename.isEmpty()) { int dotPos = filename.lastIndexOf('.'); @@ -145,9 +135,9 @@ public static long getMediaSize(Context context, Uri uri) throws IOException { InputStream in = PartAuthority.getAttachmentStream(context, uri); if (in == null) throw new IOException("Couldn't obtain input stream."); - long size = 0; + long size = 0; byte[] buffer = new byte[4096]; - int read; + int read; while ((read = in.read(buffer)) != -1) { size += read; @@ -158,7 +148,8 @@ public static long getMediaSize(Context context, Uri uri) throws IOException { } @WorkerThread - public static Pair getDimensions(@NonNull Context context, @Nullable String contentType, @Nullable Uri uri) { + public static Pair getDimensions( + @NonNull Context context, @Nullable String contentType, @Nullable Uri uri) { if (uri == null || !MediaUtil.isImageType(contentType)) { return new Pair<>(0, 0); } @@ -167,7 +158,8 @@ public static Pair getDimensions(@NonNull Context context, @Nu if (MediaUtil.isGif(contentType)) { try { - GifDrawable drawable = GlideApp.with(context) + GifDrawable drawable = + GlideApp.with(context) .asGif() .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) @@ -257,22 +249,24 @@ public ThumbnailSize(int width, int height) { this.width = width; this.height = height; } + public int width; public int height; } - public static boolean createVideoThumbnailIfNeeded(Context context, Uri dataUri, Uri thumbnailUri, ThumbnailSize retWh) { + public static boolean createVideoThumbnailIfNeeded( + Context context, Uri dataUri, Uri thumbnailUri, ThumbnailSize retWh) { boolean success = false; try { File thumbnailFile = new File(thumbnailUri.getPath()); File dataFile = new File(dataUri.getPath()); - if (!thumbnailFile.exists() || dataFile.lastModified()>thumbnailFile.lastModified()) { + if (!thumbnailFile.exists() || dataFile.lastModified() > thumbnailFile.lastModified()) { Bitmap bitmap = null; MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(context, dataUri); bitmap = retriever.getFrameAtTime(-1); - if (retWh!=null) { + if (retWh != null) { retWh.width = bitmap.getWidth(); retWh.height = bitmap.getHeight(); } @@ -284,20 +278,19 @@ public static boolean createVideoThumbnailIfNeeded(Context context, Uri dataUri, success = true; } } - } - catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); } return success; } public static String getExtensionFromMimeType(String contentType) { - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(contentType); + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(contentType); if (extension != null) { return extension; } - //custom handling needed for unsupported extensions on Android 4.X + // custom handling needed for unsupported extensions on Android 4.X switch (contentType) { case AUDIO_AAC: return "aac"; @@ -310,5 +303,4 @@ public static String getExtensionFromMimeType(String contentType) { } return null; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/Pair.java b/src/main/java/org/thoughtcrime/securesms/util/Pair.java index 2492b1722..1b7a37b88 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Pair.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Pair.java @@ -1,7 +1,7 @@ /** * Copyright (C) 2014-2016 Open Whisper Systems * - * Licensed according to the LICENSE file in this repository. + *

Licensed according to the LICENSE file in this repository. */ package org.thoughtcrime.securesms.util; @@ -14,18 +14,18 @@ public Pair(T1 v1, T2 v2) { this.v2 = v2; } - public T1 first(){ + public T1 first() { return v1; } - public T2 second(){ + public T2 second() { return v2; } public boolean equals(Object o) { - return o instanceof Pair && - equal(((Pair) o).first(), first()) && - equal(((Pair) o).second(), second()); + return o instanceof Pair + && equal(((Pair) o).first(), first()) + && equal(((Pair) o).second(), second()); } public int hashCode() { diff --git a/src/main/java/org/thoughtcrime/securesms/util/ParcelUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ParcelUtil.java index c1eda1232..13055b0c6 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ParcelUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ParcelUtil.java @@ -24,5 +24,4 @@ public static T deserialize(byte[] bytes, Parcelable.Creator creator) { Parcel parcel = deserialize(bytes); return creator.createFromParcel(parcel); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/Prefs.java b/src/main/java/org/thoughtcrime/securesms/util/Prefs.java index 8a5fc4b1a..13e3f76b0 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Prefs.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Prefs.java @@ -7,84 +7,98 @@ import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.provider.Settings; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; - import com.b44t.messenger.DcContext; - -import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; public class Prefs { private static final String TAG = Prefs.class.getSimpleName(); - public static final String RELIABLE_SERVICE_PREF = "pref_reliable_service"; - public static final String DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"; - public static final String THEME_PREF = "pref_theme"; - public static final String BACKGROUND_PREF = "pref_chat_background"; - - private static final String DATABASE_ENCRYPTED_SECRET = "pref_database_encrypted_secret_"; // followed by account-id - private static final String DATABASE_UNENCRYPTED_SECRET = "pref_database_unencrypted_secret_"; // followed by account-id - - public static final String RINGTONE_PREF = "pref_key_ringtone"; - private static final String VIBRATE_PREF = "pref_key_vibrate"; - private static final String CHAT_VIBRATE = "pref_chat_vibrate_"; // followed by chat-id - public static final String LED_COLOR_PREF = "pref_led_color"; - private static final String CHAT_RINGTONE = "pref_chat_ringtone_"; // followed by chat-id - public static final String SCREEN_SECURITY_PREF = "pref_screen_security"; - private static final String ENTER_SENDS_PREF = "pref_enter_sends"; - private static final String PROMPTED_DOZE_MSG_ID_PREF = "pref_prompted_doze_msg_id"; - private static final String STATS_DEVICE_MSG_ID_PREF = "pref_stats_device_msg_id"; - public static final String DOZE_ASKED_DIRECTLY = "pref_doze_asked_directly"; - public static final String ASKED_FOR_NOTIFICATION_PERMISSION= "pref_asked_for_notification_permission"; - private static final String IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications"; - - public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy"; - public static final String NOTIFICATION_PRIORITY_PREF = "pref_notification_priority"; - - private static final String PROFILE_AVATAR_ID_PREF = "pref_profile_avatar_id"; - public static final String INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard"; + public static final String RELIABLE_SERVICE_PREF = "pref_reliable_service"; + public static final String DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"; + public static final String THEME_PREF = "pref_theme"; + public static final String BACKGROUND_PREF = "pref_chat_background"; + + private static final String DATABASE_ENCRYPTED_SECRET = + "pref_database_encrypted_secret_"; // followed by account-id + private static final String DATABASE_UNENCRYPTED_SECRET = + "pref_database_unencrypted_secret_"; // followed by account-id + + public static final String RINGTONE_PREF = "pref_key_ringtone"; + private static final String VIBRATE_PREF = "pref_key_vibrate"; + private static final String CHAT_VIBRATE = "pref_chat_vibrate_"; // followed by chat-id + public static final String LED_COLOR_PREF = "pref_led_color"; + private static final String CHAT_RINGTONE = "pref_chat_ringtone_"; // followed by chat-id + public static final String SCREEN_SECURITY_PREF = "pref_screen_security"; + private static final String ENTER_SENDS_PREF = "pref_enter_sends"; + private static final String PROMPTED_DOZE_MSG_ID_PREF = "pref_prompted_doze_msg_id"; + private static final String STATS_DEVICE_MSG_ID_PREF = "pref_stats_device_msg_id"; + public static final String DOZE_ASKED_DIRECTLY = "pref_doze_asked_directly"; + public static final String ASKED_FOR_NOTIFICATION_PERMISSION = + "pref_asked_for_notification_permission"; + private static final String IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications"; + + public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy"; + public static final String NOTIFICATION_PRIORITY_PREF = "pref_notification_priority"; + + private static final String PROFILE_AVATAR_ID_PREF = "pref_profile_avatar_id"; + public static final String INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard"; private static final String PREF_CONTACT_PHOTO_IDENTIFIERS = "pref_contact_photo_identifiers"; - public static final String ALWAYS_LOAD_REMOTE_CONTENT = "pref_always_load_remote_content"; - public static final boolean ALWAYS_LOAD_REMOTE_CONTENT_DEFAULT = false; + public static final String ALWAYS_LOAD_REMOTE_CONTENT = "pref_always_load_remote_content"; + public static final boolean ALWAYS_LOAD_REMOTE_CONTENT_DEFAULT = false; - public static final String LAST_DEVICE_MSG_LABEL = "pref_last_device_msg_id"; - public static final String WEBXDC_STORE_URL_PREF = "pref_webxdc_store_url"; - public static final String DEFAULT_WEBXDC_STORE_URL = "https://webxdc.org/apps/"; + public static final String LAST_DEVICE_MSG_LABEL = "pref_last_device_msg_id"; + public static final String WEBXDC_STORE_URL_PREF = "pref_webxdc_store_url"; + public static final String DEFAULT_WEBXDC_STORE_URL = "https://webxdc.org/apps/"; public enum VibrateState { - DEFAULT(0), ENABLED(1), DISABLED(2); + DEFAULT(0), + ENABLED(1), + DISABLED(2); private final int id; - VibrateState(int id) { this.id = id; } - public int getId() { return id; } - public static VibrateState fromId(int id) { return values()[id]; } + + VibrateState(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static VibrateState fromId(int id) { + return values()[id]; + } } - public static void setDatabaseEncryptedSecret(@NonNull Context context, @NonNull String secret, int accountId) { + public static void setDatabaseEncryptedSecret( + @NonNull Context context, @NonNull String secret, int accountId) { setStringPreference(context, DATABASE_ENCRYPTED_SECRET + accountId, secret); } - public static void setDatabaseUnencryptedSecret(@NonNull Context context, @Nullable String secret, int accountId) { + public static void setDatabaseUnencryptedSecret( + @NonNull Context context, @Nullable String secret, int accountId) { setStringPreference(context, DATABASE_UNENCRYPTED_SECRET + accountId, secret); } - public static @Nullable String getDatabaseUnencryptedSecret(@NonNull Context context, int accountId) { + public static @Nullable String getDatabaseUnencryptedSecret( + @NonNull Context context, int accountId) { return getStringPreference(context, DATABASE_UNENCRYPTED_SECRET + accountId, null); } - public static @Nullable String getDatabaseEncryptedSecret(@NonNull Context context, int accountId) { + public static @Nullable String getDatabaseEncryptedSecret( + @NonNull Context context, int accountId) { return getStringPreference(context, DATABASE_ENCRYPTED_SECRET + accountId, null); } @@ -101,11 +115,14 @@ public static int getProfileAvatarId(Context context) { } public static int getNotificationPriority(Context context) { - return Integer.valueOf(getStringPreference(context, NOTIFICATION_PRIORITY_PREF, String.valueOf(NotificationCompat.PRIORITY_HIGH))); + return Integer.valueOf( + getStringPreference( + context, NOTIFICATION_PRIORITY_PREF, String.valueOf(NotificationCompat.PRIORITY_HIGH))); } public static NotificationPrivacyPreference getNotificationPrivacy(Context context) { - return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")); + return new NotificationPrivacyPreference( + getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")); } public static boolean isInChatNotifications(Context context) { @@ -133,7 +150,10 @@ public static boolean isScreenSecurityEnabled(Context context) { } public static String getTheme(Context context) { - return getStringPreference(context, THEME_PREF, DynamicTheme.systemThemeAvailable() ? DynamicTheme.SYSTEM : DynamicTheme.LIGHT); + return getStringPreference( + context, + THEME_PREF, + DynamicTheme.systemThemeAvailable() ? DynamicTheme.SYSTEM : DynamicTheme.LIGHT); } public static String getWebxdcStoreUrl(Context context) { @@ -162,11 +182,12 @@ public static int getStatsDeviceMsgId(Context context) { } public static boolean isPushEnabled(Context context) { - return BuildConfig.USE_PLAY_SERVICES; + return BuildConfig.USE_PLAY_SERVICES; } public static boolean isHardCompressionEnabled(Context context) { - return DcHelper.getContext(context).getConfigInt(DcHelper.CONFIG_MEDIA_QUALITY) == DcContext.DC_MEDIA_QUALITY_WORSE; + return DcHelper.getContext(context).getConfigInt(DcHelper.CONFIG_MEDIA_QUALITY) + == DcContext.DC_MEDIA_QUALITY_WORSE; } public static boolean isLocationStreamingEnabled(Context context) { @@ -177,7 +198,6 @@ public static boolean isNewBroadcastAvailable(Context context) { return true; } - public static boolean isCallsEnabled(Context context) { return true; } @@ -185,7 +205,9 @@ public static boolean isCallsEnabled(Context context) { // ringtone public static @NonNull Uri getNotificationRingtone(Context context) { - String result = getStringPreference(context, RINGTONE_PREF, Settings.System.DEFAULT_NOTIFICATION_URI.toString()); + String result = + getStringPreference( + context, RINGTONE_PREF, Settings.System.DEFAULT_NOTIFICATION_URI.toString()); if (result != null && result.startsWith("file:")) { result = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); @@ -203,19 +225,20 @@ public static void setNotificationRingtone(Context context, Uri ringtone) { } public static void setChatRingtone(Context context, int accountId, int chatId, Uri ringtone) { - final String KEY = (accountId != 0 && chatId != 0)? CHAT_RINGTONE+accountId+"."+chatId : CHAT_RINGTONE; - if(ringtone!=null) { + final String KEY = + (accountId != 0 && chatId != 0) ? CHAT_RINGTONE + accountId + "." + chatId : CHAT_RINGTONE; + if (ringtone != null) { setStringPreference(context, KEY, ringtone.toString()); - } - else { + } else { removePreference(context, KEY); } } public static @Nullable Uri getChatRingtone(Context context, int accountId, int chatId) { - final String KEY = (accountId != 0 && chatId != 0)? CHAT_RINGTONE+accountId+"."+chatId : CHAT_RINGTONE; + final String KEY = + (accountId != 0 && chatId != 0) ? CHAT_RINGTONE + accountId + "." + chatId : CHAT_RINGTONE; String result = getStringPreference(context, KEY, null); - return result==null? null : Uri.parse(result); + return result == null ? null : Uri.parse(result); } public static void setReliableService(Context context, boolean value) { @@ -227,7 +250,8 @@ public static boolean reliableService(Context context) { if (prefs.contains(RELIABLE_SERVICE_PREF)) { try { return prefs.getBoolean(RELIABLE_SERVICE_PREF, true); - } catch(Exception e) {} + } catch (Exception e) { + } } // if the key was unset, then calculate default value @@ -240,18 +264,20 @@ public static boolean isNotificationVibrateEnabled(Context context) { return getBooleanPreference(context, VIBRATE_PREF, true); } - public static void setChatVibrate(Context context, int accountId, int chatId, VibrateState vibrateState) { - final String KEY = (accountId != 0 && chatId != 0)? CHAT_VIBRATE+accountId+"."+chatId : CHAT_VIBRATE; - if(vibrateState!=VibrateState.DEFAULT) { + public static void setChatVibrate( + Context context, int accountId, int chatId, VibrateState vibrateState) { + final String KEY = + (accountId != 0 && chatId != 0) ? CHAT_VIBRATE + accountId + "." + chatId : CHAT_VIBRATE; + if (vibrateState != VibrateState.DEFAULT) { setIntegerPreference(context, KEY, vibrateState.getId()); - } - else { + } else { removePreference(context, KEY); } } public static VibrateState getChatVibrate(Context context, int accountId, int chatId) { - final String KEY = (accountId != 0 && chatId != 0)? CHAT_VIBRATE+accountId+"."+chatId : CHAT_VIBRATE; + final String KEY = + (accountId != 0 && chatId != 0) ? CHAT_VIBRATE + accountId + "." + chatId : CHAT_VIBRATE; return VibrateState.fromId(getIntegerPreference(context, KEY, VibrateState.DEFAULT.getId())); } @@ -264,16 +290,16 @@ public static String getNotificationLedColor(Context context) { // misc. public static String getBackgroundImagePath(Context context, int accountId) { - return getStringPreference(context, BACKGROUND_PREF+accountId, ""); + return getStringPreference(context, BACKGROUND_PREF + accountId, ""); } public static void setBackgroundImagePath(Context context, int accountId, String path) { - setStringPreference(context, BACKGROUND_PREF+accountId, path); + setStringPreference(context, BACKGROUND_PREF + accountId, path); } public static boolean getAlwaysLoadRemoteContent(Context context) { - return getBooleanPreference(context, Prefs.ALWAYS_LOAD_REMOTE_CONTENT, - Prefs.ALWAYS_LOAD_REMOTE_CONTENT_DEFAULT); + return getBooleanPreference( + context, Prefs.ALWAYS_LOAD_REMOTE_CONTENT, Prefs.ALWAYS_LOAD_REMOTE_CONTENT_DEFAULT); } // generic preference functions @@ -314,7 +340,8 @@ public static void removePreference(Context context, String key) { PreferenceManager.getDefaultSharedPreferences(context).edit().remove(key).apply(); } - private static Set getStringSetPreference(Context context, String key, Set defaultValues) { + private static Set getStringSetPreference( + Context context, String key, Set defaultValues) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.contains(key)) { return prefs.getStringSet(key, Collections.emptySet()); @@ -324,12 +351,17 @@ private static Set getStringSetPreference(Context context, String key, S } public static void setSystemContactPhotos(Context context, Set contactPhotoIdentifiers) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putStringSet(PREF_CONTACT_PHOTO_IDENTIFIERS, contactPhotoIdentifiers).apply(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putStringSet(PREF_CONTACT_PHOTO_IDENTIFIERS, contactPhotoIdentifiers) + .apply(); } public static Uri getSystemContactPhoto(Context context, String identifier) { - List contactPhotoIdentifiers = new ArrayList<>(getStringSetPreference(context, PREF_CONTACT_PHOTO_IDENTIFIERS, new HashSet<>())); - for(String contactPhotoIdentifier : contactPhotoIdentifiers) { + List contactPhotoIdentifiers = + new ArrayList<>( + getStringSetPreference(context, PREF_CONTACT_PHOTO_IDENTIFIERS, new HashSet<>())); + for (String contactPhotoIdentifier : contactPhotoIdentifiers) { if (contactPhotoIdentifier.contains(identifier)) { String[] parts = contactPhotoIdentifier.split("\\|"); long contactId = Long.valueOf(parts[1]); @@ -338,5 +370,4 @@ public static Uri getSystemContactPhoto(Context context, String identifier) { } return null; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/ResUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ResUtil.java index 865fc3fcc..847b34c25 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ResUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ResUtil.java @@ -1,20 +1,17 @@ /** * Copyright (C) 2015 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ - package org.thoughtcrime.securesms.util; import android.content.Context; @@ -22,15 +19,14 @@ import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.TypedValue; - import androidx.annotation.AttrRes; import androidx.core.content.ContextCompat; public class ResUtil { public static int getColor(Context context, @AttrRes int attr) { - final TypedArray styledAttributes = context.obtainStyledAttributes(new int[]{attr}); - final int result = styledAttributes.getColor(0, -1); + final TypedArray styledAttributes = context.obtainStyledAttributes(new int[] {attr}); + final int result = styledAttributes.getColor(0, -1); styledAttributes.recycle(); return result; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java index 42d259ae7..c9a511653 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java +++ b/src/main/java/org/thoughtcrime/securesms/util/SaveAttachmentTask.java @@ -13,16 +13,10 @@ import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.loader.content.CursorLoader; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -34,21 +28,26 @@ import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; -public class SaveAttachmentTask extends ProgressDialogAsyncTask> { +public class SaveAttachmentTask + extends ProgressDialogAsyncTask> { private static final String TAG = SaveAttachmentTask.class.getSimpleName(); - static final int SUCCESS = 0; - private static final int FAILURE = 1; + static final int SUCCESS = 0; + private static final int FAILURE = 1; private static final int WRITE_ACCESS_FAILURE = 2; - private final WeakReference contextReference; + private final WeakReference contextReference; public SaveAttachmentTask(Context context) { - super(context, - context.getResources().getString(R.string.one_moment), - context.getResources().getString(R.string.one_moment)); - this.contextReference = new WeakReference<>(context); + super( + context, + context.getResources().getString(R.string.one_moment), + context.getResources().getString(R.string.one_moment)); + this.contextReference = new WeakReference<>(context); } @Override @@ -58,8 +57,8 @@ protected Pair doInBackground(SaveAttachmentTask.Attachment... att } try { - Context context = contextReference.get(); - Uri uri = null; + Context context = contextReference.get(); + Uri uri = null; if (!StorageUtil.canWriteToMediaStore(context)) { return new Pair<>(WRITE_ACCESS_FAILURE, null); @@ -76,23 +75,23 @@ protected Pair doInBackground(SaveAttachmentTask.Attachment... att } } if (attachments.length > 1) return new Pair<>(SUCCESS, null); - else return new Pair<>(SUCCESS, uri); + else return new Pair<>(SUCCESS, uri); } catch (IOException ioe) { Log.w(TAG, ioe); return new Pair<>(FAILURE, null); } } - private @Nullable Uri saveAttachment(Context context, Attachment attachment) throws IOException - { - String contentType = Objects.requireNonNull(MediaUtil.getCorrectedMimeType(attachment.contentType)); - String fileName = attachment.fileName; + private @Nullable Uri saveAttachment(Context context, Attachment attachment) throws IOException { + String contentType = + Objects.requireNonNull(MediaUtil.getCorrectedMimeType(attachment.contentType)); + String fileName = attachment.fileName; if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date); fileName = sanitizeOutputFileName(fileName); - Uri outputUri = getMediaStoreContentUriForType(contentType); - Uri mediaUri = createOutputUri(outputUri, contentType, fileName); + Uri outputUri = getMediaStoreContentUriForType(contentType); + Uri mediaUri = createOutputUri(outputUri, contentType, fileName); ContentValues updateValues = new ContentValues(); if (mediaUri == null) { @@ -109,10 +108,12 @@ protected Pair doInBackground(SaveAttachmentTask.Attachment... att if (Util.equals(outputUri.getScheme(), ContentResolver.SCHEME_FILE)) { try (OutputStream outputStream = new FileOutputStream(mediaUri.getPath())) { StreamUtil.copy(inputStream, outputStream); - MediaScannerConnection.scanFile(context, new String[]{mediaUri.getPath()}, new String[]{contentType}, null); + MediaScannerConnection.scanFile( + context, new String[] {mediaUri.getPath()}, new String[] {contentType}, null); } } else { - try (OutputStream outputStream = context.getContentResolver().openOutputStream(mediaUri, "w")) { + try (OutputStream outputStream = + context.getContentResolver().openOutputStream(mediaUri, "w")) { long total = StreamUtil.copy(inputStream, outputStream); if (total > 0) { updateValues.put(MediaStore.MediaColumns.SIZE, total); @@ -167,15 +168,15 @@ private boolean isMediaStoreImageType(@NonNull String contentType) { if (!contentType.startsWith("image/")) { return false; } - return contentType.equals("image/jpeg") || - contentType.equals("image/jpg") || - contentType.equals("image/png") || - contentType.equals("image/gif") || - contentType.equals("image/webp") || - contentType.equals("image/bmp") || - contentType.equals("image/heic") || - contentType.equals("image/heif") || - contentType.equals("image/avif"); + return contentType.equals("image/jpeg") + || contentType.equals("image/jpg") + || contentType.equals("image/png") + || contentType.equals("image/gif") + || contentType.equals("image/webp") + || contentType.equals("image/bmp") + || contentType.equals("image/heic") + || contentType.equals("image/heif") + || contentType.equals("image/avif"); } private @Nullable File ensureExternalPath(@Nullable File path) { @@ -184,7 +185,8 @@ private boolean isMediaStoreImageType(@NonNull String contentType) { } if (path == null) { - File documents = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File documents = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); if (documents.exists() || documents.mkdirs()) { return documents; } else { @@ -202,10 +204,10 @@ private boolean isMediaStoreImageType(@NonNull String contentType) { /** * Returns a path to a shared media (or documents) directory for the type of the file. * - * Note that this method attempts to create a directory if the path returned from - * Environment object does not exist yet. The attempt may fail in which case it attempts - * to return the default "Document" path. It finally returns null if it also fails. - * Otherwise it returns the absolute path to the directory. + *

Note that this method attempts to create a directory if the path returned from Environment + * object does not exist yet. The attempt may fail in which case it attempts to return the default + * "Document" path. It finally returns null if it also fails. Otherwise it returns the absolute + * path to the directory. * * @param contentType a MIME type of a file * @return an absolute path to a directory or null @@ -229,9 +231,9 @@ private boolean isMediaStoreImageType(@NonNull String contentType) { } private String generateOutputFileName(@NonNull String contentType, long timestamp) { - String extension = MediaUtil.getExtensionFromMimeType(contentType); + String extension = MediaUtil.getExtensionFromMimeType(contentType); SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); - String base = "deltachat-" + dateFormatter.format(timestamp); + String base = "deltachat-" + dateFormatter.format(timestamp); if (extension == null) extension = "attach"; @@ -242,30 +244,38 @@ private String sanitizeOutputFileName(@NonNull String fileName) { return new File(fileName).getName(); } - private @Nullable Uri createOutputUri(@NonNull Uri outputUri, @NonNull String contentType, @NonNull String fileName) - throws IOException - { + private @Nullable Uri createOutputUri( + @NonNull Uri outputUri, @NonNull String contentType, @NonNull String fileName) + throws IOException { String[] fileParts = getFileNameParts(fileName); - String base = fileParts[0]; - String extension = fileParts[1]; - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + String base = fileParts[0]; + String extension = fileParts[1]; + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); if (MediaUtil.isOctetStream(mimeType) && MediaUtil.isImageVideoOrAudioType(contentType)) { - Log.d(TAG, "MimeTypeMap returned octet stream for media, changing to provided content type [" + contentType + "] instead."); + Log.d( + TAG, + "MimeTypeMap returned octet stream for media, changing to provided content type [" + + contentType + + "] instead."); mimeType = contentType; } ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); - contentValues.put(MediaStore.MediaColumns.DATE_ADDED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); - contentValues.put(MediaStore.MediaColumns.DATE_MODIFIED, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); + contentValues.put( + MediaStore.MediaColumns.DATE_ADDED, + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); + contentValues.put( + MediaStore.MediaColumns.DATE_MODIFIED, + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); if (Build.VERSION.SDK_INT > 28) { contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1); } else if (Util.equals(outputUri.getScheme(), ContentResolver.SCHEME_FILE)) { File outputDirectory = new File(outputUri.getPath()); - File outputFile = new File(outputDirectory, base + "." + extension); + File outputFile = new File(outputDirectory, base + "." + extension); int i = 0; while (outputFile.exists()) { @@ -280,16 +290,17 @@ private String sanitizeOutputFileName(@NonNull String fileName) { } else { String dir = getExternalPathForType(contentType); if (dir == null) { - throw new IOException(String.format(Locale.ENGLISH, "Path for type: %s was not available", contentType)); + throw new IOException( + String.format(Locale.ENGLISH, "Path for type: %s was not available", contentType)); } String outputFileName = fileName; - String dataPath = String.format("%s/%s", dir, outputFileName); - int i = 0; + String dataPath = String.format("%s/%s", dir, outputFileName); + int i = 0; while (pathTaken(outputUri, dataPath)) { Log.d(TAG, "The content exists. Rename and check again."); outputFileName = base + "-" + (++i) + "." + extension; - dataPath = String.format("%s/%s", dir, outputFileName); + dataPath = String.format("%s/%s", dir, outputFileName); } contentValues.put(MediaStore.MediaColumns.DATA, dataPath); } @@ -298,12 +309,15 @@ private String sanitizeOutputFileName(@NonNull String fileName) { } private boolean pathTaken(@NonNull Uri outputUri, @NonNull String dataPath) throws IOException { - try (Cursor cursor = getContext().getContentResolver().query(outputUri, - new String[] { MediaStore.MediaColumns.DATA }, - MediaStore.MediaColumns.DATA + " = ?", - new String[] { dataPath }, - null)) - { + try (Cursor cursor = + getContext() + .getContentResolver() + .query( + outputUri, + new String[] {MediaStore.MediaColumns.DATA}, + MediaStore.MediaColumns.DATA + " = ?", + new String[] {dataPath}, + null)) { if (cursor == null) { throw new IOException("Something is wrong with the filename to save"); } @@ -318,7 +332,7 @@ private String[] getFileNameParts(String fileName) { result[0] = tokens[0]; if (tokens.length > 1) result[1] = tokens[1]; - else result[1] = ""; + else result[1] = ""; return result; } @@ -331,9 +345,8 @@ protected void onPostExecute(final Pair result) { switch (result.first()) { case FAILURE: - Toast.makeText(context, - context.getResources().getString(R.string.error), - Toast.LENGTH_LONG).show(); + Toast.makeText(context, context.getResources().getString(R.string.error), Toast.LENGTH_LONG) + .show(); break; case SUCCESS: Uri uri = result.second(); @@ -353,33 +366,35 @@ protected void onPostExecute(final Pair result) { } } - Toast.makeText(context, - dir==null? context.getString(R.string.done) : context.getString(R.string.file_saved_to, dir), - Toast.LENGTH_LONG).show(); + Toast.makeText( + context, + dir == null + ? context.getString(R.string.done) + : context.getString(R.string.file_saved_to, dir), + Toast.LENGTH_LONG) + .show(); break; case WRITE_ACCESS_FAILURE: - Toast.makeText(context, R.string.error, - Toast.LENGTH_LONG).show(); + Toast.makeText(context, R.string.error, Toast.LENGTH_LONG).show(); break; } } public static class Attachment { - public Uri uri; + public Uri uri; public String fileName; public String contentType; - public long date; + public long date; - public Attachment(@NonNull Uri uri, @NonNull String contentType, - long date, @Nullable String fileName) - { + public Attachment( + @NonNull Uri uri, @NonNull String contentType, long date, @Nullable String fileName) { if (uri == null || contentType == null || date < 0) { throw new AssertionError("uri, content type, and date must all be specified"); } - this.uri = uri; - this.fileName = fileName; + this.uri = uri; + this.fileName = fileName; this.contentType = contentType; - this.date = date; + this.date = date; } } @@ -392,4 +407,3 @@ public static void showWarningDialog(Context context, OnClickListener onAcceptLi builder.show(); } } - diff --git a/src/main/java/org/thoughtcrime/securesms/util/ScreenLockUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ScreenLockUtil.java index 3b07b0025..ae3cd96af 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ScreenLockUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ScreenLockUtil.java @@ -4,22 +4,22 @@ import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; - import androidx.activity.result.ActivityResultLauncher; public class ScreenLockUtil { - public static boolean applyScreenLock(Activity activity, String title, String descr, ActivityResultLauncher launcher) { - KeyguardManager keyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); - Intent intent; - if (keyguardManager != null) { - intent = keyguardManager.createConfirmDeviceCredentialIntent(title, descr); - if (intent != null) { - launcher.launch(intent); - return true; - } - } - return false; + public static boolean applyScreenLock( + Activity activity, String title, String descr, ActivityResultLauncher launcher) { + KeyguardManager keyguardManager = + (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); + Intent intent; + if (keyguardManager != null) { + intent = keyguardManager.createConfirmDeviceCredentialIntent(title, descr); + if (intent != null) { + launcher.launch(intent); + return true; + } } - + return false; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/SelectedContactsAdapter.java b/src/main/java/org/thoughtcrime/securesms/util/SelectedContactsAdapter.java index e45e53f1a..4446762dd 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/SelectedContactsAdapter.java +++ b/src/main/java/org/thoughtcrime/securesms/util/SelectedContactsAdapter.java @@ -12,42 +12,36 @@ import android.widget.BaseAdapter; import android.widget.ImageButton; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatTextView; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.components.AvatarImageView; -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.recipients.Recipient; - import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.AvatarImageView; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.recipients.Recipient; public class SelectedContactsAdapter extends BaseAdapter { - @NonNull private final Context context; - @Nullable private ItemClickListener itemClickListener; - @NonNull private final List contacts = new LinkedList<>(); + @NonNull private final Context context; + @Nullable private ItemClickListener itemClickListener; + @NonNull private final List contacts = new LinkedList<>(); private final boolean isBroadcast; - @NonNull private final DcContext dcContext; - @NonNull private final GlideRequests glideRequests; - - public SelectedContactsAdapter(@NonNull Context context, - @NonNull GlideRequests glideRequests, - boolean isBroadcast) - { - this.context = context; + @NonNull private final DcContext dcContext; + @NonNull private final GlideRequests glideRequests; + + public SelectedContactsAdapter( + @NonNull Context context, @NonNull GlideRequests glideRequests, boolean isBroadcast) { + this.context = context; this.glideRequests = glideRequests; - this.isBroadcast = isBroadcast; - this.dcContext = DcHelper.getContext(context); + this.isBroadcast = isBroadcast; + this.dcContext = DcHelper.getContext(context); } public void changeData(Collection contactIds) { @@ -105,14 +99,15 @@ public View getView(final int position, View v, final ViewGroup parent) { AvatarImageView avatar = v.findViewById(R.id.contact_photo_image); AppCompatTextView name = v.findViewById(R.id.name); - TextView phone = v.findViewById(R.id.phone); - ImageButton delete = v.findViewById(R.id.delete); + TextView phone = v.findViewById(R.id.phone); + ImageButton delete = v.findViewById(R.id.delete); - final int contactId = (int)getItem(position); - final boolean modifiable = contactId != DC_CONTACT_ID_ADD_MEMBER && contactId != DC_CONTACT_ID_SELF; + final int contactId = (int) getItem(position); + final boolean modifiable = + contactId != DC_CONTACT_ID_ADD_MEMBER && contactId != DC_CONTACT_ID_SELF; Recipient recipient = null; - if(contactId == DcContact.DC_CONTACT_ID_ADD_MEMBER) { + if (contactId == DcContact.DC_CONTACT_ID_ADD_MEMBER) { name.setText(context.getString(R.string.group_add_members)); name.setTypeface(null, Typeface.BOLD); phone.setVisibility(View.GONE); @@ -128,17 +123,19 @@ public View getView(final int position, View v, final ViewGroup parent) { avatar.clear(glideRequests); avatar.setAvatar(glideRequests, recipient, false); delete.setVisibility(modifiable ? View.VISIBLE : View.GONE); - delete.setColorFilter(DynamicTheme.isDarkTheme(context)? Color.WHITE : Color.BLACK); - delete.setOnClickListener(view -> { - if (itemClickListener != null) { - itemClickListener.onItemDeleteClick(contacts.get(position)); - } - }); - v.setOnClickListener(view -> { - if (itemClickListener != null) { - itemClickListener.onItemClick(contacts.get(position)); - } - }); + delete.setColorFilter(DynamicTheme.isDarkTheme(context) ? Color.WHITE : Color.BLACK); + delete.setOnClickListener( + view -> { + if (itemClickListener != null) { + itemClickListener.onItemDeleteClick(contacts.get(position)); + } + }); + v.setOnClickListener( + view -> { + if (itemClickListener != null) { + itemClickListener.onItemClick(contacts.get(position)); + } + }); return v; } @@ -149,6 +146,7 @@ public void setItemClickListener(@Nullable ItemClickListener listener) { public interface ItemClickListener { void onItemClick(int contactId); + void onItemDeleteClick(int contactId); } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/SendRelayedMessageUtil.java b/src/main/java/org/thoughtcrime/securesms/util/SendRelayedMessageUtil.java index 999d4cff4..f601106d1 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/SendRelayedMessageUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/SendRelayedMessageUtil.java @@ -13,34 +13,29 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; -import android.util.Log; import android.provider.OpenableColumns; - +import android.util.Log; +import chat.delta.rpc.Rpc; +import chat.delta.rpc.RpcException; import com.b44t.messenger.DcContext; import com.b44t.messenger.DcMsg; - -import org.thoughtcrime.securesms.ConversationListRelayingActivity; -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.providers.PersistentBlobProvider; - import java.io.BufferedReader; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; - -import chat.delta.rpc.Rpc; -import chat.delta.rpc.RpcException; +import org.thoughtcrime.securesms.ConversationListRelayingActivity; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.providers.PersistentBlobProvider; public class SendRelayedMessageUtil { private static final String TAG = SendRelayedMessageUtil.class.getSimpleName(); public static void immediatelyRelay(Activity activity, int chatId) { - immediatelyRelay(activity, new Long[]{(long) chatId}); + immediatelyRelay(activity, new Long[] {(long) chatId}); } public static void immediatelyRelay(Activity activity, final Long[] chatIds) { @@ -51,39 +46,39 @@ public static void immediatelyRelay(Activity activity, final Long[] chatIds) { resetRelayingMessageContent(activity); if (forwardedMessageIDs == null || forwardedMsgAccId <= 0) return; - Util.runOnAnyBackgroundThread(() -> { - DcContext dcContext = DcHelper.getContext(activity); - int accId = dcContext.getAccountId(); - if (forwardedMsgAccId != accId) { - Rpc rpc = DcHelper.getRpc(activity); - List list = Util.toList(forwardedMessageIDs); - for (long longChatId : chatIds) { - try { - rpc.forwardMessagesToAccount(forwardedMsgAccId, list, accId, (int)longChatId); - } catch (RpcException e) { - e.printStackTrace(); + Util.runOnAnyBackgroundThread( + () -> { + DcContext dcContext = DcHelper.getContext(activity); + int accId = dcContext.getAccountId(); + if (forwardedMsgAccId != accId) { + Rpc rpc = DcHelper.getRpc(activity); + List list = Util.toList(forwardedMessageIDs); + for (long longChatId : chatIds) { + try { + rpc.forwardMessagesToAccount(forwardedMsgAccId, list, accId, (int) longChatId); + } catch (RpcException e) { + e.printStackTrace(); + } + } + return; } - } - return; - } - for (long longChatId : chatIds) { - int chatId = (int) longChatId; - if (dcContext.getChat(chatId).isSelfTalk()) { - for (int msgId : forwardedMessageIDs) { - DcMsg msg = dcContext.getMsg(msgId); - if (msg.canSave() && msg.getSavedMsgId() == 0 && msg.getChatId() != chatId) { - dcContext.saveMsgs(new int[]{msgId}); + for (long longChatId : chatIds) { + int chatId = (int) longChatId; + if (dcContext.getChat(chatId).isSelfTalk()) { + for (int msgId : forwardedMessageIDs) { + DcMsg msg = dcContext.getMsg(msgId); + if (msg.canSave() && msg.getSavedMsgId() == 0 && msg.getChatId() != chatId) { + dcContext.saveMsgs(new int[] {msgId}); + } else { + handleForwarding(activity, chatId, new int[] {msgId}); + } + } } else { - handleForwarding(activity, chatId, new int[]{msgId}); + handleForwarding(activity, chatId, forwardedMessageIDs); } } - } else { - handleForwarding(activity, chatId, forwardedMessageIDs); - } - } - - }); + }); } else if (isSharing(activity)) { ArrayList sharedUris = getSharedUris(activity); String sharedText = getSharedText(activity); @@ -91,11 +86,13 @@ public static void immediatelyRelay(Activity activity, final Long[] chatIds) { String sharedHtml = getHtml(activity, ShareUtil.getSharedHtml(activity)); String msgType = ShareUtil.getSharedType(activity); resetRelayingMessageContent(activity); - Util.runOnAnyBackgroundThread(() -> { - for (long chatId : chatIds) { - sendMultipleMsgs(activity, (int) chatId, sharedUris, msgType, sharedHtml, subject, sharedText); - } - }); + Util.runOnAnyBackgroundThread( + () -> { + for (long chatId : chatIds) { + sendMultipleMsgs( + activity, (int) chatId, sharedUris, msgType, sharedHtml, subject, sharedText); + } + }); } } @@ -104,17 +101,26 @@ private static void handleForwarding(Context context, int chatId, int[] forwarde dcContext.forwardMsgs(forwardedMessageIDs, chatId); } - public static void sendMultipleMsgs(Context context, int chatId, ArrayList sharedUris, String sharedText) { + public static void sendMultipleMsgs( + Context context, int chatId, ArrayList sharedUris, String sharedText) { sendMultipleMsgs(context, chatId, sharedUris, null, null, null, sharedText); } - private static void sendMultipleMsgs(Context context, int chatId, ArrayList sharedUris, String msgType, String sharedHtml, String subject, String sharedText) { + private static void sendMultipleMsgs( + Context context, + int chatId, + ArrayList sharedUris, + String msgType, + String sharedHtml, + String subject, + String sharedText) { DcContext dcContext = DcHelper.getContext(context); ArrayList uris = sharedUris; String text = sharedText; if (uris.size() == 1) { - dcContext.sendMsg(chatId, createMessage(context, uris.get(0), msgType, sharedHtml, subject, text)); + dcContext.sendMsg( + chatId, createMessage(context, uris.get(0), msgType, sharedHtml, subject, text)); } else { if (text != null || sharedHtml != null) { dcContext.sendMsg(chatId, createMessage(context, null, null, sharedHtml, subject, text)); @@ -135,7 +141,9 @@ public static boolean containsVideoType(Context context, ArrayList uris) { return false; } - public static DcMsg createMessage(Context context, Uri uri, String type, String html, String subject, String text) throws NullPointerException { + public static DcMsg createMessage( + Context context, Uri uri, String type, String html, String subject, String text) + throws NullPointerException { DcContext dcContext = DcHelper.getContext(context); DcMsg message; String mimeType = MediaUtil.getMimeType(context, uri); @@ -172,7 +180,9 @@ public static DcMsg createMessage(Context context, Uri uri, String type, String private static void setFileFromUri(Context context, Uri uri, DcMsg message, String mimeType) { String path; DcContext dcContext = DcHelper.getContext(context); - String filename = "cannot-resolve.jpg"; // best guess, this still leads to most images being workable if OS does weird things + String filename = + "cannot-resolve.jpg"; // best guess, this still leads to most images being workable if OS + // does weird things try { if (PartAuthority.isLocalUri(uri)) { diff --git a/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java index 6d9356c01..0cc6ec7a9 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ServiceUtil.java @@ -8,7 +8,7 @@ public class ServiceUtil { public static InputMethodManager getInputMethodManager(Context context) { - return (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); } public static WindowManager getWindowManager(Context context) { @@ -16,6 +16,6 @@ public static WindowManager getWindowManager(Context context) { } public static Vibrator getVibrator(Context context) { - return (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); + return (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/ShareUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ShareUtil.java index 68737b27e..11d052659 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ShareUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ShareUtil.java @@ -1,238 +1,235 @@ package org.thoughtcrime.securesms.util; -import static org.thoughtcrime.securesms.ConversationActivity.TEXT_EXTRA; +import static org.thoughtcrime.securesms.ConversationActivity.MSG_HTML_EXTRA; import static org.thoughtcrime.securesms.ConversationActivity.MSG_SUBJECT_EXTRA; import static org.thoughtcrime.securesms.ConversationActivity.MSG_TYPE_EXTRA; -import static org.thoughtcrime.securesms.ConversationActivity.MSG_HTML_EXTRA; +import static org.thoughtcrime.securesms.ConversationActivity.TEXT_EXTRA; import android.app.Activity; import android.content.Intent; import android.net.Uri; - import androidx.annotation.NonNull; - import java.util.ArrayList; public class ShareUtil { - private static final String FORWARDED_MESSAGE_ACCID = "forwarded_message_accid"; - private static final String FORWARDED_MESSAGE_IDS = "forwarded_message_ids"; - private static final String SHARED_URIS = "shared_uris"; - private static final String SHARED_CONTACT_ID = "shared_contact_id"; - private static final String IS_SHARING = "is_sharing"; - private static final String IS_FROM_WEBXDC = "is_from_webxdc"; - private static final String SHARED_TITLE = "shared_title"; - private static final String DIRECT_SHARING_CHAT_ID = "direct_sharing_chat_id"; - - public static boolean isRelayingMessageContent(Activity activity) { - return isForwarding(activity) || isSharing(activity); - } - - public static boolean isForwarding(Activity activity) { - return getForwardedMessageAccountId(activity) > 0; - } - - public static boolean isSharing(Activity activity) { - try { - return activity.getIntent().getBooleanExtra(IS_SHARING, false); - } catch (NullPointerException npe) { - return false; - } - } - - public static boolean isFromWebxdc(Activity activity) { - try { - return activity.getIntent().getBooleanExtra(IS_FROM_WEBXDC, false); - } catch (NullPointerException npe) { - return false; - } - } - - public static boolean isDirectSharing(Activity activity) { - try { - return activity.getIntent().getIntExtra(DIRECT_SHARING_CHAT_ID, -1) != -1; - } catch (NullPointerException npe) { - return false; - } - } - - public static int getDirectSharingChatId(Activity activity) { - try { - return activity.getIntent().getIntExtra(DIRECT_SHARING_CHAT_ID, -1); - } catch (NullPointerException npe) { - return -1; - } - } - - public static int getForwardedMessageAccountId(Activity activity) { - try { - return activity.getIntent().getIntExtra(FORWARDED_MESSAGE_ACCID, -1); - } catch (NullPointerException npe) { - return -1; - } - } - - static int[] getForwardedMessageIDs(Activity activity) { - try { - return activity.getIntent().getIntArrayExtra(FORWARDED_MESSAGE_IDS); - } catch (NullPointerException npe) { - return null; - } - } - - public static @NonNull ArrayList getSharedUris(Activity activity) { - if (activity != null) { - Intent i = activity.getIntent(); - if (i != null) { - ArrayList uris = i.getParcelableArrayListExtra(SHARED_URIS); - if (uris != null) return uris; - } - } - return new ArrayList<>(); - } - - public static String getSharedType(Activity activity) { - try { - return activity.getIntent().getStringExtra(MSG_TYPE_EXTRA); - } catch (NullPointerException npe) { - return null; - } - } - - public static Uri getSharedHtml(Activity activity) { - try { - return activity.getIntent().getParcelableExtra(MSG_HTML_EXTRA); - } catch (NullPointerException npe) { - return null; - } - } - - public static String getSharedSubject(Activity activity) { - try { - return activity.getIntent().getStringExtra(MSG_SUBJECT_EXTRA); - } catch (NullPointerException npe) { - return null; - } - } - - public static int getSharedContactId(Activity activity) { - try { - return activity.getIntent().getIntExtra(SHARED_CONTACT_ID, 0); - } catch(Exception e) { - e.printStackTrace(); - return 0; + private static final String FORWARDED_MESSAGE_ACCID = "forwarded_message_accid"; + private static final String FORWARDED_MESSAGE_IDS = "forwarded_message_ids"; + private static final String SHARED_URIS = "shared_uris"; + private static final String SHARED_CONTACT_ID = "shared_contact_id"; + private static final String IS_SHARING = "is_sharing"; + private static final String IS_FROM_WEBXDC = "is_from_webxdc"; + private static final String SHARED_TITLE = "shared_title"; + private static final String DIRECT_SHARING_CHAT_ID = "direct_sharing_chat_id"; + + public static boolean isRelayingMessageContent(Activity activity) { + return isForwarding(activity) || isSharing(activity); + } + + public static boolean isForwarding(Activity activity) { + return getForwardedMessageAccountId(activity) > 0; + } + + public static boolean isSharing(Activity activity) { + try { + return activity.getIntent().getBooleanExtra(IS_SHARING, false); + } catch (NullPointerException npe) { + return false; + } + } + + public static boolean isFromWebxdc(Activity activity) { + try { + return activity.getIntent().getBooleanExtra(IS_FROM_WEBXDC, false); + } catch (NullPointerException npe) { + return false; + } + } + + public static boolean isDirectSharing(Activity activity) { + try { + return activity.getIntent().getIntExtra(DIRECT_SHARING_CHAT_ID, -1) != -1; + } catch (NullPointerException npe) { + return false; + } + } + + public static int getDirectSharingChatId(Activity activity) { + try { + return activity.getIntent().getIntExtra(DIRECT_SHARING_CHAT_ID, -1); + } catch (NullPointerException npe) { + return -1; + } + } + + public static int getForwardedMessageAccountId(Activity activity) { + try { + return activity.getIntent().getIntExtra(FORWARDED_MESSAGE_ACCID, -1); + } catch (NullPointerException npe) { + return -1; + } + } + + static int[] getForwardedMessageIDs(Activity activity) { + try { + return activity.getIntent().getIntArrayExtra(FORWARDED_MESSAGE_IDS); + } catch (NullPointerException npe) { + return null; + } + } + + public static @NonNull ArrayList getSharedUris(Activity activity) { + if (activity != null) { + Intent i = activity.getIntent(); + if (i != null) { + ArrayList uris = i.getParcelableArrayListExtra(SHARED_URIS); + if (uris != null) return uris; } } - - public static String getSharedText(Activity activity) { - try { - return activity.getIntent().getStringExtra(TEXT_EXTRA); - } catch (NullPointerException npe) { - return null; - } - } - - public static String getSharedTitle(Activity activity) { - try { - return activity.getIntent().getStringExtra(SHARED_TITLE); - } catch (NullPointerException npe) { - return null; - } - } - - - public static void resetRelayingMessageContent(Activity activity) { - try { - activity.getIntent().removeExtra(FORWARDED_MESSAGE_ACCID); - activity.getIntent().removeExtra(FORWARDED_MESSAGE_IDS); - activity.getIntent().removeExtra(SHARED_URIS); - activity.getIntent().removeExtra(SHARED_CONTACT_ID); - activity.getIntent().removeExtra(IS_SHARING); - activity.getIntent().removeExtra(DIRECT_SHARING_CHAT_ID); - activity.getIntent().removeExtra(TEXT_EXTRA); - activity.getIntent().removeExtra(MSG_TYPE_EXTRA); - activity.getIntent().removeExtra(MSG_HTML_EXTRA); - activity.getIntent().removeExtra(MSG_SUBJECT_EXTRA); - } catch (NullPointerException npe) { - npe.printStackTrace(); - } - } - - public static void acquireRelayMessageContent(Activity currentActivity, @NonNull Intent newActivityIntent) { - if (isForwarding(currentActivity)) { - int accId = getForwardedMessageAccountId(currentActivity); - setForwardingMessageIds(newActivityIntent, getForwardedMessageIDs(currentActivity), accId); - } else if (isSharing(currentActivity)) { - newActivityIntent.putExtra(IS_SHARING, true); - if (isDirectSharing(currentActivity)) { - newActivityIntent.putExtra(DIRECT_SHARING_CHAT_ID, getDirectSharingChatId(currentActivity)); - } - if (!getSharedUris(currentActivity).isEmpty()) { - newActivityIntent.putParcelableArrayListExtra(SHARED_URIS, getSharedUris(currentActivity)); - } - if (getSharedContactId(currentActivity) != 0) { - newActivityIntent.putExtra(SHARED_CONTACT_ID, getSharedContactId(currentActivity)); - } - if (getSharedText(currentActivity) != null) { - newActivityIntent.putExtra(TEXT_EXTRA, getSharedText(currentActivity)); - } - if (getSharedSubject(currentActivity) != null) { - newActivityIntent.putExtra(MSG_SUBJECT_EXTRA, getSharedSubject(currentActivity)); - } - if (getSharedHtml(currentActivity) != null) { - newActivityIntent.putExtra(MSG_HTML_EXTRA, getSharedHtml(currentActivity)); - } - if (getSharedType(currentActivity) != null) { - newActivityIntent.putExtra(MSG_TYPE_EXTRA, getSharedType(currentActivity)); - } - } - } - - public static void setForwardingMessageIds(Intent composeIntent, int[] messageIds, int accId) { - composeIntent.putExtra(FORWARDED_MESSAGE_ACCID, accId); - composeIntent.putExtra(FORWARDED_MESSAGE_IDS, messageIds); - } - - public static void setIsFromWebxdc(Intent composeIntent, boolean fromWebxdc) { - composeIntent.putExtra(IS_FROM_WEBXDC, fromWebxdc); - composeIntent.putExtra(IS_SHARING, true); - } - - public static void setSharedUris(Intent composeIntent, ArrayList uris) { - composeIntent.putParcelableArrayListExtra(SHARED_URIS, uris); - composeIntent.putExtra(IS_SHARING, true); - } - - public static void setSharedText(Intent composeIntent, String text) { - composeIntent.putExtra(TEXT_EXTRA, text); - composeIntent.putExtra(IS_SHARING, true); - } - - public static void setSharedSubject(Intent composeIntent, String subject) { - composeIntent.putExtra(MSG_SUBJECT_EXTRA, subject); - composeIntent.putExtra(IS_SHARING, true); - } - - public static void setSharedHtml(Intent composeIntent, Uri html) { - composeIntent.putExtra(MSG_HTML_EXTRA, html); - composeIntent.putExtra(IS_SHARING, true); - } - - public static void setSharedType(Intent composeIntent, String type) { - composeIntent.putExtra(MSG_TYPE_EXTRA, type); - composeIntent.putExtra(IS_SHARING, true); - } - - public static void setSharedContactId(Intent composeIntent, int contactId) { - composeIntent.putExtra(SHARED_CONTACT_ID, contactId); - composeIntent.putExtra(IS_SHARING, true); - } - - public static void setSharedTitle(Intent composeIntent, String text) { - composeIntent.putExtra(SHARED_TITLE, text); - } - - public static void setDirectSharing(Intent composeIntent, int chatId) { - composeIntent.putExtra(DIRECT_SHARING_CHAT_ID, chatId); + return new ArrayList<>(); + } + + public static String getSharedType(Activity activity) { + try { + return activity.getIntent().getStringExtra(MSG_TYPE_EXTRA); + } catch (NullPointerException npe) { + return null; + } + } + + public static Uri getSharedHtml(Activity activity) { + try { + return activity.getIntent().getParcelableExtra(MSG_HTML_EXTRA); + } catch (NullPointerException npe) { + return null; + } + } + + public static String getSharedSubject(Activity activity) { + try { + return activity.getIntent().getStringExtra(MSG_SUBJECT_EXTRA); + } catch (NullPointerException npe) { + return null; + } + } + + public static int getSharedContactId(Activity activity) { + try { + return activity.getIntent().getIntExtra(SHARED_CONTACT_ID, 0); + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + public static String getSharedText(Activity activity) { + try { + return activity.getIntent().getStringExtra(TEXT_EXTRA); + } catch (NullPointerException npe) { + return null; + } + } + + public static String getSharedTitle(Activity activity) { + try { + return activity.getIntent().getStringExtra(SHARED_TITLE); + } catch (NullPointerException npe) { + return null; + } + } + + public static void resetRelayingMessageContent(Activity activity) { + try { + activity.getIntent().removeExtra(FORWARDED_MESSAGE_ACCID); + activity.getIntent().removeExtra(FORWARDED_MESSAGE_IDS); + activity.getIntent().removeExtra(SHARED_URIS); + activity.getIntent().removeExtra(SHARED_CONTACT_ID); + activity.getIntent().removeExtra(IS_SHARING); + activity.getIntent().removeExtra(DIRECT_SHARING_CHAT_ID); + activity.getIntent().removeExtra(TEXT_EXTRA); + activity.getIntent().removeExtra(MSG_TYPE_EXTRA); + activity.getIntent().removeExtra(MSG_HTML_EXTRA); + activity.getIntent().removeExtra(MSG_SUBJECT_EXTRA); + } catch (NullPointerException npe) { + npe.printStackTrace(); + } + } + + public static void acquireRelayMessageContent( + Activity currentActivity, @NonNull Intent newActivityIntent) { + if (isForwarding(currentActivity)) { + int accId = getForwardedMessageAccountId(currentActivity); + setForwardingMessageIds(newActivityIntent, getForwardedMessageIDs(currentActivity), accId); + } else if (isSharing(currentActivity)) { + newActivityIntent.putExtra(IS_SHARING, true); + if (isDirectSharing(currentActivity)) { + newActivityIntent.putExtra(DIRECT_SHARING_CHAT_ID, getDirectSharingChatId(currentActivity)); + } + if (!getSharedUris(currentActivity).isEmpty()) { + newActivityIntent.putParcelableArrayListExtra(SHARED_URIS, getSharedUris(currentActivity)); + } + if (getSharedContactId(currentActivity) != 0) { + newActivityIntent.putExtra(SHARED_CONTACT_ID, getSharedContactId(currentActivity)); + } + if (getSharedText(currentActivity) != null) { + newActivityIntent.putExtra(TEXT_EXTRA, getSharedText(currentActivity)); + } + if (getSharedSubject(currentActivity) != null) { + newActivityIntent.putExtra(MSG_SUBJECT_EXTRA, getSharedSubject(currentActivity)); + } + if (getSharedHtml(currentActivity) != null) { + newActivityIntent.putExtra(MSG_HTML_EXTRA, getSharedHtml(currentActivity)); + } + if (getSharedType(currentActivity) != null) { + newActivityIntent.putExtra(MSG_TYPE_EXTRA, getSharedType(currentActivity)); + } } - + } + + public static void setForwardingMessageIds(Intent composeIntent, int[] messageIds, int accId) { + composeIntent.putExtra(FORWARDED_MESSAGE_ACCID, accId); + composeIntent.putExtra(FORWARDED_MESSAGE_IDS, messageIds); + } + + public static void setIsFromWebxdc(Intent composeIntent, boolean fromWebxdc) { + composeIntent.putExtra(IS_FROM_WEBXDC, fromWebxdc); + composeIntent.putExtra(IS_SHARING, true); + } + + public static void setSharedUris(Intent composeIntent, ArrayList uris) { + composeIntent.putParcelableArrayListExtra(SHARED_URIS, uris); + composeIntent.putExtra(IS_SHARING, true); + } + + public static void setSharedText(Intent composeIntent, String text) { + composeIntent.putExtra(TEXT_EXTRA, text); + composeIntent.putExtra(IS_SHARING, true); + } + + public static void setSharedSubject(Intent composeIntent, String subject) { + composeIntent.putExtra(MSG_SUBJECT_EXTRA, subject); + composeIntent.putExtra(IS_SHARING, true); + } + + public static void setSharedHtml(Intent composeIntent, Uri html) { + composeIntent.putExtra(MSG_HTML_EXTRA, html); + composeIntent.putExtra(IS_SHARING, true); + } + + public static void setSharedType(Intent composeIntent, String type) { + composeIntent.putExtra(MSG_TYPE_EXTRA, type); + composeIntent.putExtra(IS_SHARING, true); + } + + public static void setSharedContactId(Intent composeIntent, int contactId) { + composeIntent.putExtra(SHARED_CONTACT_ID, contactId); + composeIntent.putExtra(IS_SHARING, true); + } + + public static void setSharedTitle(Intent composeIntent, String text) { + composeIntent.putExtra(SHARED_TITLE, text); + } + + public static void setDirectSharing(Intent composeIntent, int chatId) { + composeIntent.putExtra(DIRECT_SHARING_CHAT_ID, chatId); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLogger.java b/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLogger.java index 3b41c55fb..5afb80a03 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLogger.java +++ b/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLogger.java @@ -1,18 +1,18 @@ /** * Copyright (C) 2014-2016 Open Whisper Systems * - * Licensed according to the LICENSE file in this repository. + *

Licensed according to the LICENSE file in this repository. */ package org.thoughtcrime.securesms.util; public interface SignalProtocolLogger { public static final int VERBOSE = 2; - public static final int DEBUG = 3; - public static final int INFO = 4; - public static final int WARN = 5; - public static final int ERROR = 6; - public static final int ASSERT = 7; + public static final int DEBUG = 3; + public static final int INFO = 4; + public static final int WARN = 5; + public static final int ERROR = 6; + public static final int ASSERT = 7; public void log(int priority, String tag, String message); } diff --git a/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLoggerProvider.java b/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLoggerProvider.java index b62b5f223..d85feb451 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLoggerProvider.java +++ b/src/main/java/org/thoughtcrime/securesms/util/SignalProtocolLoggerProvider.java @@ -1,7 +1,7 @@ /** * Copyright (C) 2014-2016 Open Whisper Systems * - * Licensed according to the LICENSE file in this repository. + *

Licensed according to the LICENSE file in this repository. */ package org.thoughtcrime.securesms.util; diff --git a/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java b/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java index 15a0b0c44..9a6d5f98f 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java @@ -15,25 +15,32 @@ public static CharSequence italic(CharSequence sequence) { public static CharSequence italic(CharSequence sequence, int length) { SpannableString spannable = new SpannableString(sequence); - spannable.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(android.graphics.Typeface.ITALIC), + 0, + length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } public static CharSequence small(CharSequence sequence) { SpannableString spannable = new SpannableString(sequence); - spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } public static CharSequence bold(CharSequence sequence) { SpannableString spannable = new SpannableString(sequence); - spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.BOLD), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } public static CharSequence color(int color, CharSequence sequence) { SpannableString spannable = new SpannableString(sequence); - spannable.setSpan(new ForegroundColorSpan(color), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new ForegroundColorSpan(color), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java b/src/main/java/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java index a043551db..080eb6dff 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java +++ b/src/main/java/org/thoughtcrime/securesms/util/StickyHeaderDecoration.java @@ -5,17 +5,15 @@ import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; - import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.ViewHolder; - import java.util.HashMap; import java.util.Map; /** - * A sticky header decoration for android's RecyclerView. - * Currently only supports LinearLayoutManager in VERTICAL orientation. + * A sticky header decoration for android's RecyclerView. Currently only supports + * LinearLayoutManager in VERTICAL orientation. */ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration { @@ -24,29 +22,26 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration { private static final long NO_HEADER_ID = -1L; private final Map headerCache; - private final StickyHeaderAdapter adapter; - private final boolean renderInline; - private final boolean sticky; - private int screenOrientation; + private final StickyHeaderAdapter adapter; + private final boolean renderInline; + private final boolean sticky; + private int screenOrientation; /** * @param adapter the sticky header adapter to use */ public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline, boolean sticky) { - this.adapter = adapter; - this.headerCache = new HashMap<>(); + this.adapter = adapter; + this.headerCache = new HashMap<>(); this.renderInline = renderInline; - this.sticky = sticky; + this.sticky = sticky; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, - RecyclerView.State state) - { - int position = parent.getChildAdapterPosition(view); + public void getItemOffsets( + Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + int position = parent.getChildAdapterPosition(view); int headerHeight = 0; if (position != RecyclerView.NO_POSITION && hasHeader(parent, adapter, position)) { @@ -59,19 +54,20 @@ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) { boolean isReverse = isReverseLayout(parent); - int itemCount = ((RecyclerView.Adapter)adapter).getItemCount(); + int itemCount = ((RecyclerView.Adapter) adapter).getItemCount(); - if ((isReverse && adapterPos == itemCount - 1 && adapter.getHeaderId(adapterPos) != -1) || - (!isReverse && adapterPos == 0)) - { + if ((isReverse && adapterPos == itemCount - 1 && adapter.getHeaderId(adapterPos) != -1) + || (!isReverse && adapterPos == 0)) { return true; } - int previous = adapterPos + (isReverse ? 1 : -1); - long headerId = adapter.getHeaderId(adapterPos); + int previous = adapterPos + (isReverse ? 1 : -1); + long headerId = adapter.getHeaderId(adapterPos); long previousHeaderId = adapter.getHeaderId(previous); - return headerId != NO_HEADER_ID && previousHeaderId != NO_HEADER_ID && headerId != previousHeaderId; + return headerId != NO_HEADER_ID + && previousHeaderId != NO_HEADER_ID + && headerId != previousHeaderId; } protected ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) { @@ -87,12 +83,19 @@ protected ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, adapter.onBindHeaderViewHolder(holder, position); int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); - int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); - - int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, - parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width); - int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, - parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height); + int heightSpec = + View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); + + int childWidth = + ViewGroup.getChildMeasureSpec( + widthSpec, + parent.getPaddingLeft() + parent.getPaddingRight(), + header.getLayoutParams().width); + int childHeight = + ViewGroup.getChildMeasureSpec( + heightSpec, + parent.getPaddingTop() + parent.getPaddingBottom(), + header.getLayoutParams().height); header.measure(childWidth, childHeight); header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight()); @@ -103,9 +106,7 @@ protected ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { final int count = parent.getChildCount(); @@ -115,7 +116,8 @@ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) final int adapterPos = parent.getChildAdapterPosition(child); - if (adapterPos != RecyclerView.NO_POSITION && ((layoutPos == 0 && sticky) || hasHeader(parent, adapter, adapterPos))) { + if (adapterPos != RecyclerView.NO_POSITION + && ((layoutPos == 0 && sticky) || hasHeader(parent, adapter, adapterPos))) { View header = getHeader(parent, adapter, adapterPos).itemView; c.save(); final int left = child.getLeft(); @@ -127,9 +129,8 @@ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) } } - protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, - int layoutPos) - { + protected int getHeaderTop( + RecyclerView parent, View child, View header, int adapterPos, int layoutPos) { int headerHeight = getHeaderHeightForLayout(header); int top = getChildY(parent, child) - headerHeight; if (sticky && layoutPos == 0) { @@ -137,12 +138,16 @@ protected int getHeaderTop(RecyclerView parent, View child, View header, int ada final long currentId = adapter.getHeaderId(adapterPos); // find next view with header and compute the offscreen push if needed for (int i = 1; i < count; i++) { - int adapterPosHere = parent.getChildAdapterPosition(parent.getChildAt(translatedChildPosition(parent, i))); + int adapterPosHere = + parent.getChildAdapterPosition(parent.getChildAt(translatedChildPosition(parent, i))); if (adapterPosHere != RecyclerView.NO_POSITION) { long nextId = adapter.getHeaderId(adapterPosHere); if (nextId != currentId) { final View next = parent.getChildAt(translatedChildPosition(parent, i)); - final int offset = getChildY(parent, next) - (headerHeight + getHeader(parent, adapter, adapterPosHere).itemView.getHeight()); + final int offset = + getChildY(parent, next) + - (headerHeight + + getHeader(parent, adapter, adapterPosHere).itemView.getHeight()); if (offset < 0) { return offset; } else { @@ -163,7 +168,7 @@ private int translatedChildPosition(RecyclerView parent, int position) { } private int getChildY(RecyclerView parent, View child) { - return (int) child.getY(); + return (int) child.getY(); } protected int getHeaderHeightForLayout(View header) { @@ -171,8 +176,8 @@ protected int getHeaderHeightForLayout(View header) { } private boolean isReverseLayout(final RecyclerView parent) { - return (parent.getLayoutManager() instanceof LinearLayoutManager) && - ((LinearLayoutManager)parent.getLayoutManager()).getReverseLayout(); + return (parent.getLayoutManager() instanceof LinearLayoutManager) + && ((LinearLayoutManager) parent.getLayoutManager()).getReverseLayout(); } public void onConfigurationChanged(Configuration configuration) { @@ -187,7 +192,8 @@ public void invalidateLayouts() { } /** - * The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header views. + * The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header + * views. * * @param the header view holder */ @@ -211,6 +217,7 @@ public interface StickyHeaderAdapter { /** * Updates the header view to reflect the header data for the given position + * * @param viewHolder the header view holder * @param position the header's item position */ diff --git a/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java b/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java index 08a91a3cb..1252113d5 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Stopwatch.java @@ -1,22 +1,20 @@ package org.thoughtcrime.securesms.util; import android.util.Log; - import androidx.annotation.NonNull; - import java.util.LinkedList; import java.util.List; public class Stopwatch { - private final long startTime; - private final String title; + private final long startTime; + private final String title; private final List splits; public Stopwatch(@NonNull String title) { this.startTime = System.currentTimeMillis(); - this.title = title; - this.splits = new LinkedList<>(); + this.title = title; + this.splits = new LinkedList<>(); } public void split(@NonNull String label) { @@ -47,11 +45,11 @@ public void stop(@NonNull String tag) { } private static class Split { - final long time; + final long time; final String label; Split(long time, String label) { - this.time = time; + this.time = time; this.label = label; } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java b/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java index e8114e20a..b2347291d 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/StorageUtil.java @@ -6,25 +6,22 @@ import android.os.Build; import android.os.Environment; import android.provider.MediaStore; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.permissions.Permissions; public class StorageUtil { public static boolean canWriteToMediaStore(Context context) { - return Build.VERSION.SDK_INT > 28 || - Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE); + return Build.VERSION.SDK_INT > 28 + || Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE); } public static @NonNull Uri getVideoUri() { return MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } - public static @NonNull - Uri getAudioUri() { + public static @NonNull Uri getAudioUri() { return MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/StreamUtil.java b/src/main/java/org/thoughtcrime/securesms/util/StreamUtil.java index 884fdb620..f6933f5b8 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/StreamUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/StreamUtil.java @@ -21,5 +21,4 @@ public static long copy(InputStream in, OutputStream out) throws IOException { return total; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/ThemeUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ThemeUtil.java index c72a79d52..ec942295a 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ThemeUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ThemeUtil.java @@ -4,10 +4,8 @@ import android.content.res.Resources; import android.graphics.Color; import android.util.TypedValue; - import androidx.annotation.AttrRes; import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.R; public class ThemeUtil { diff --git a/src/main/java/org/thoughtcrime/securesms/util/ThreadUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ThreadUtil.java index ee21bcdfc..67d13ca0f 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ThreadUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ThreadUtil.java @@ -8,11 +8,10 @@ public class ThreadUtil { public static ExecutorService newDynamicSingleThreadedExecutor() { - ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, - new LinkedBlockingQueue()); + ThreadPoolExecutor executor = + new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); executor.allowCoreThreadTimeOut(true); return executor; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/Util.java b/src/main/java/org/thoughtcrime/securesms/util/Util.java index 825420da7..8eab9ba56 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -37,15 +37,10 @@ import android.view.Menu; import android.view.MenuItem; import android.view.accessibility.AccessibilityManager; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.os.ConfigurationCompat; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.components.ComposeText; - import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -59,6 +54,8 @@ import java.util.Locale; import java.util.Objects; import java.util.concurrent.CountDownLatch; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.ComposeText; public class Util { private static final String TAG = Util.class.getSimpleName(); @@ -89,14 +86,14 @@ public static boolean isInviteURL(String url) { public static CharSequence getBoldedString(String value) { SpannableString spanned = new SpannableString(value); - spanned.setSpan(new StyleSpan(Typeface.BOLD), 0, - spanned.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spanned.setSpan( + new StyleSpan(Typeface.BOLD), 0, spanned.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spanned; } - private static final int redDestructiveColor = 0xffff0c16; // typical "destructive red" for light/dark mode + private static final int redDestructiveColor = + 0xffff0c16; // typical "destructive red" for light/dark mode public static void redMenuItem(Menu menu, int id) { MenuItem item = menu.findItem(id); @@ -127,7 +124,7 @@ public static List toList(int[] array) { public static @NonNull int[] appendInt(@Nullable int[] cur, int val) { if (cur == null) { - return new int[] { val }; + return new int[] {val}; } final int N = cur.length; int[] ret = new int[N + 1]; @@ -189,11 +186,10 @@ public static boolean moveFile(String fromPath, String toPath) { File fromFile = new File(fromPath); File toFile = new File(toPath); toFile.delete(); - if(fromFile.renameTo(toFile)) { + if (fromFile.renameTo(toFile)) { success = true; } - } - catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); } @@ -202,11 +198,10 @@ public static boolean moveFile(String fromPath, String toPath) { try { InputStream fromStream = new FileInputStream(fromPath); OutputStream toStream = new FileOutputStream(toPath); - if(Util.copy(fromStream, toStream)>0) { + if (Util.copy(fromStream, toStream) > 0) { success = true; } - } - catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -226,7 +221,7 @@ public static void assertMainThread() { public static void runOnMain(final @NonNull Runnable runnable) { if (isMainThread()) runnable.run(); - else handler.post(runnable); + else handler.post(runnable); } public static void runOnMainDelayed(final @NonNull Runnable runnable, long delayMillis) { @@ -238,13 +233,14 @@ public static void runOnMainSync(final @NonNull Runnable runnable) { runnable.run(); } else { final CountDownLatch sync = new CountDownLatch(1); - runOnMain(() -> { - try { - runnable.run(); - } finally { - sync.countDown(); - } - }); + runOnMain( + () -> { + try { + runnable.run(); + } finally { + sync.countDown(); + } + }); try { sync.await(); } catch (InterruptedException ie) { @@ -266,9 +262,11 @@ public static void runOnAnyBackgroundThread(final @NonNull Runnable runnable) { } public static void runOnBackgroundDelayed(final @NonNull Runnable runnable, long delayMillis) { - handler.postDelayed(() -> { - AsyncTask.THREAD_POOL_EXECUTOR.execute(runnable); - }, delayMillis); + handler.postDelayed( + () -> { + AsyncTask.THREAD_POOL_EXECUTOR.execute(runnable); + }, + delayMillis); } public static boolean equals(@Nullable Object a, @Nullable Object b) { @@ -280,7 +278,8 @@ public static int hashCode(@Nullable Object... objects) { } public static boolean isLowMemory(Context context) { - ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); return activityManager.isLowRamDevice() || activityManager.getLargeMemoryClass() <= 64; } @@ -295,15 +294,16 @@ public static float clamp(float value, float min, float max) { public static void writeTextToClipboard(@NonNull Context context, @NonNull String text) { android.content.ClipboardManager clipboard = - (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText(context.getString(R.string.app_name), text); clipboard.setPrimaryClip(clip); } public static String getTextFromClipboard(@NonNull Context context) { android.content.ClipboardManager clipboard = - (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - if (clipboard.hasPrimaryClip() && clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { + (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard.hasPrimaryClip() + && clipboard.getPrimaryClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); return item.getText().toString(); } @@ -311,25 +311,22 @@ public static String getTextFromClipboard(@NonNull Context context) { } public static int toIntExact(long value) { - if ((int)value != value) { + if ((int) value != value) { throw new ArithmeticException("integer overflow"); } - return (int)value; + return (int) value; } public static int objectToInt(Object value) { try { - if(value instanceof String) { - return Integer.parseInt((String)value); - } - else if (value instanceof Boolean) { - return (Boolean)value? 1 : 0; - } - else if (value instanceof Integer) { - return (Integer)value; - } - else if (value instanceof Long) { - return toIntExact((Long)value); + if (value instanceof String) { + return Integer.parseInt((String) value); + } else if (value instanceof Boolean) { + return (Boolean) value ? 1 : 0; + } else if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof Long) { + return toIntExact((Long) value); } } catch (Exception e) { } @@ -339,10 +336,12 @@ else if (value instanceof Long) { public static String getPrettyFileSize(long sizeBytes) { if (sizeBytes <= 0) return "0"; - String[] units = new String[]{"B", "kB", "MB", "GB", "TB"}; - int digitGroups = (int) (Math.log10(sizeBytes) / Math.log10(1024)); + String[] units = new String[] {"B", "kB", "MB", "GB", "TB"}; + int digitGroups = (int) (Math.log10(sizeBytes) / Math.log10(1024)); - return new DecimalFormat("#,##0.#").format(sizeBytes/Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + return new DecimalFormat("#,##0.#").format(sizeBytes / Math.pow(1024, digitGroups)) + + " " + + units[digitGroups]; } public static void sleep(long millis) { @@ -360,6 +359,7 @@ public static int rgbToArgbColor(int rgb) { } private static long lastClickTime = 0; + public static boolean isClickedRecently() { long now = System.currentTimeMillis(); if (now - lastClickTime < 500) { @@ -371,11 +371,14 @@ public static boolean isClickedRecently() { } private static AccessibilityManager accessibilityManager; + public static boolean isTouchExplorationEnabled(Context context) { try { if (accessibilityManager == null) { Context applicationContext = context.getApplicationContext(); - accessibilityManager = ((AccessibilityManager) applicationContext.getSystemService(Context.ACCESSIBILITY_SERVICE)); + accessibilityManager = + ((AccessibilityManager) + applicationContext.getSystemService(Context.ACCESSIBILITY_SERVICE)); } return accessibilityManager.isTouchExplorationEnabled(); } catch (Exception e) { @@ -385,15 +388,17 @@ public static boolean isTouchExplorationEnabled(Context context) { private static Locale lastLocale = null; - public synchronized static Locale getLocale() { + public static synchronized Locale getLocale() { if (lastLocale == null) { try { - lastLocale = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()).get(0); + lastLocale = + ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()).get(0); } catch (Exception e) { e.printStackTrace(); } if (lastLocale == null) { - // Locale.getDefault() returns the locale the App was STARTED in, not the locale of the system. + // Locale.getDefault() returns the locale the App was STARTED in, not the locale of the + // system. // It just happens to be the same for the majority of use cases. lastLocale = Locale.getDefault(); } @@ -401,7 +406,7 @@ public synchronized static Locale getLocale() { return lastLocale; } - public synchronized static void localeChanged() { + public static synchronized void localeChanged() { lastLocale = null; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java b/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java index c90480836..f767a37a5 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java +++ b/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java @@ -1,18 +1,16 @@ /** * Copyright (C) 2015 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 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 + *

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 . + *

You should have received a copy of the GNU General Public License along with this program. If + * not, see . */ package org.thoughtcrime.securesms.util; @@ -36,7 +34,6 @@ import android.view.animation.Animation; import android.widget.AbsSpinner; import android.widget.TextView; - import androidx.annotation.IdRes; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; @@ -47,15 +44,13 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.views.Stub; - import chat.delta.util.ListenableFuture; import chat.delta.util.SettableFuture; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.views.Stub; public class ViewUtil { - private final static String TAG = ViewUtil.class.getSimpleName(); + private static final String TAG = ViewUtil.class.getSimpleName(); @SuppressWarnings("deprecation") public static void setBackground(final @NonNull View v, final @Nullable Drawable drawable) { @@ -74,7 +69,8 @@ public static float getX(final @NonNull View v) { return ViewCompat.getX(v); } - public static void swapChildInPlace(ViewGroup parent, View toRemove, View toAdd, int defaultIndex) { + public static void swapChildInPlace( + ViewGroup parent, View toRemove, View toAdd, int defaultIndex) { int childIndex = parent.indexOfChild(toRemove); if (childIndex > -1) parent.removeView(toRemove); parent.addView(toAdd, childIndex > -1 ? childIndex : defaultIndex); @@ -82,7 +78,7 @@ public static void swapChildInPlace(ViewGroup parent, View toRemove, View toAdd, @SuppressWarnings("unchecked") public static T inflateStub(@NonNull View parent, @IdRes int stubId) { - return (T)((ViewStub)parent.findViewById(stubId)).inflate(); + return (T) ((ViewStub) parent.findViewById(stubId)).inflate(); } @SuppressWarnings("unchecked") @@ -96,7 +92,7 @@ public static T findById(@NonNull Activity parent, @IdRes int r } public static Stub findStubById(@NonNull Activity parent, @IdRes int resId) { - return new Stub((ViewStub)parent.findViewById(resId)); + return new Stub((ViewStub) parent.findViewById(resId)); } private static Animation getAlphaAnimation(float from, float to, int duration) { @@ -114,11 +110,13 @@ public static ListenableFuture fadeOut(final @NonNull View view, final return fadeOut(view, duration, View.GONE); } - public static ListenableFuture fadeOut(@NonNull View view, int duration, int visibility) { + public static ListenableFuture fadeOut( + @NonNull View view, int duration, int visibility) { return animateOut(view, getAlphaAnimation(1f, 0f, duration), visibility); } - public static ListenableFuture animateOut(final @NonNull View view, final @NonNull Animation animation, final int visibility) { + public static ListenableFuture animateOut( + final @NonNull View view, final @NonNull Animation animation, final int visibility) { final SettableFuture future = new SettableFuture(); if (view.getVisibility() == visibility) { future.set(true); @@ -129,19 +127,20 @@ public static ListenableFuture animateOut(final @NonNull View view, fin view.clearAnimation(); animation.reset(); animation.setStartTime(0); - animation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) {} - - @Override - public void onAnimationRepeat(Animation animation) {} - - @Override - public void onAnimationEnd(Animation animation) { - view.setVisibility(visibility); - future.set(true); - } - }); + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationRepeat(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + view.setVisibility(visibility); + future.set(true); + } + }); view.startAnimation(animation); } return future; @@ -163,15 +162,14 @@ public static void animateIn(final @NonNull View view, final @NonNull Animation } @SuppressWarnings("unchecked") - public static T inflate(@NonNull LayoutInflater inflater, - @NonNull ViewGroup parent, - @LayoutRes int layoutResId) - { - return (T)(inflater.inflate(layoutResId, parent, false)); + public static T inflate( + @NonNull LayoutInflater inflater, @NonNull ViewGroup parent, @LayoutRes int layoutResId) { + return (T) (inflater.inflate(layoutResId, parent, false)); } @SuppressLint("RtlHardcoded") - public static void setTextViewGravityStart(final @NonNull TextView textView, @NonNull Context context) { + public static void setTextViewGravityStart( + final @NonNull TextView textView, @NonNull Context context) { if (Util.getLayoutDirection(context) == View.LAYOUT_DIRECTION_RTL) { textView.setGravity(Gravity.RIGHT); } else { @@ -202,7 +200,7 @@ public static boolean isRtl(@NonNull Context context) { } public static int dpToPx(Context context, int dp) { - return (int)((dp * context.getResources().getDisplayMetrics().density) + 0.5); + return (int) ((dp * context.getResources().getDisplayMetrics().density) + 0.5); } public static float pxToSp(Context context, int px) { @@ -218,7 +216,7 @@ public static float pxToSp(Context context, int px) { } public static void updateLayoutParams(@NonNull View view, int width, int height) { - view.getLayoutParams().width = width; + view.getLayoutParams().width = width; view.getLayoutParams().height = height; view.requestLayout(); } @@ -263,7 +261,8 @@ public static void setTopMargin(@NonNull View view, int margin) { } public static void setPaddingTop(@NonNull View view, int padding) { - view.setPadding(view.getPaddingLeft(), padding, view.getPaddingRight(), view.getPaddingBottom()); + view.setPadding( + view.getPaddingLeft(), padding, view.getPaddingRight(), view.getPaddingBottom()); } public static void setPaddingBottom(@NonNull View view, int padding) { @@ -279,7 +278,7 @@ public static int getStatusBarHeight(@NonNull View view) { if (Build.VERSION.SDK_INT > 29 && rootWindowInsets != null) { return rootWindowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top; } else { - int result = 0; + int result = 0; int resourceId = view.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = view.getResources().getDimensionPixelSize(resourceId); @@ -290,7 +289,8 @@ public static int getStatusBarHeight(@NonNull View view) { // Checks if a selection is valid for a given Spinner view. // Returns given selection if valid. - // Otherwise, to avoid ArrayIndexOutOfBoundsException, 0 is returned, assuming to refer to a good default. + // Otherwise, to avoid ArrayIndexOutOfBoundsException, 0 is returned, assuming to refer to a good + // default. public static int checkBounds(int selection, AbsSpinner view) { if (selection < 0 || selection >= view.getCount()) { Log.w(TAG, "index " + selection + " out of bounds of " + view.toString()); @@ -306,7 +306,7 @@ public static boolean isEdgeToEdgeSupported() { /** * Get combined insets from status bar, navigation bar and display cutout areas. - * + * * @param windowInsets The window insets to extract from * @return Combined insets using the maximum values from system bars and display cutout */ @@ -319,27 +319,29 @@ private static Insets getCombinedInsets(@NonNull WindowInsetsCompat windowInsets /** * Apply window insets to a view by adding margin to avoid drawing it behind system bars. * Convenience method that applies insets to all sides. - * + * * @param view The view to apply insets to */ public static void applyWindowInsetsAsMargin(@NonNull View view) { applyWindowInsetsAsMargin(view, true, true, true, true, false); } - public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { + public static void applyWindowInsetsAsMargin( + @NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { applyWindowInsetsAsMargin(view, left, top, right, bottom, false); } - public static void forceApplyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { + public static void forceApplyWindowInsetsAsMargin( + @NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { applyWindowInsetsAsMargin(view, left, top, right, bottom, true); } /** * Apply window insets to a view by adding margin to avoid drawing it behind system bars. - * - * This method stores the original margin values in view tags to ensure that - * margin doesn't accumulate on multiple inset applications. - * + * + *

This method stores the original margin values in view tags to ensure that margin doesn't + * accumulate on multiple inset applications. + * * @param view The view to apply insets to * @param left Whether to apply left inset * @param top Whether to apply top inset @@ -347,7 +349,13 @@ public static void forceApplyWindowInsetsAsMargin(@NonNull View view, boolean le * @param bottom Whether to apply bottom inset * @param forceDispatch Whether to force application of insets */ - public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean forceDispatch) { + public static void applyWindowInsetsAsMargin( + @NonNull View view, + boolean left, + boolean top, + boolean right, + boolean bottom, + boolean forceDispatch) { // Only enable on API 30+ where WindowInsets APIs work correctly if (!isEdgeToEdgeSupported()) return; @@ -364,31 +372,34 @@ public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, b } } - ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> { - Insets insets = getCombinedInsets(windowInsets); - - // Retrieve the original margin values from tags with null checks - Integer leftTag = (Integer) v.getTag(R.id.tag_window_insets_margin_left); - Integer topTag = (Integer) v.getTag(R.id.tag_window_insets_margin_top); - Integer rightTag = (Integer) v.getTag(R.id.tag_window_insets_margin_right); - Integer bottomTag = (Integer) v.getTag(R.id.tag_window_insets_margin_bottom); - int baseMarginLeft = leftTag != null ? leftTag : 0; - int baseMarginTop = topTag != null ? topTag : 0; - int baseMarginRight = rightTag != null ? rightTag : 0; - int baseMarginBottom = bottomTag != null ? bottomTag : 0; - - ViewGroup.LayoutParams layoutParams = v.getLayoutParams(); - if (layoutParams instanceof ViewGroup.MarginLayoutParams) { - ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) layoutParams; - marginParams.leftMargin = left ? baseMarginLeft + insets.left : baseMarginLeft; - marginParams.topMargin = top ? baseMarginTop + insets.top : baseMarginTop; - marginParams.rightMargin = right ? baseMarginRight + insets.right : baseMarginRight; - marginParams.bottomMargin = bottom ? baseMarginBottom + insets.bottom : baseMarginBottom; - v.setLayoutParams(marginParams); - } - - return windowInsets; - }); + ViewCompat.setOnApplyWindowInsetsListener( + view, + (v, windowInsets) -> { + Insets insets = getCombinedInsets(windowInsets); + + // Retrieve the original margin values from tags with null checks + Integer leftTag = (Integer) v.getTag(R.id.tag_window_insets_margin_left); + Integer topTag = (Integer) v.getTag(R.id.tag_window_insets_margin_top); + Integer rightTag = (Integer) v.getTag(R.id.tag_window_insets_margin_right); + Integer bottomTag = (Integer) v.getTag(R.id.tag_window_insets_margin_bottom); + int baseMarginLeft = leftTag != null ? leftTag : 0; + int baseMarginTop = topTag != null ? topTag : 0; + int baseMarginRight = rightTag != null ? rightTag : 0; + int baseMarginBottom = bottomTag != null ? bottomTag : 0; + + ViewGroup.LayoutParams layoutParams = v.getLayoutParams(); + if (layoutParams instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) layoutParams; + marginParams.leftMargin = left ? baseMarginLeft + insets.left : baseMarginLeft; + marginParams.topMargin = top ? baseMarginTop + insets.top : baseMarginTop; + marginParams.rightMargin = right ? baseMarginRight + insets.right : baseMarginRight; + marginParams.bottomMargin = + bottom ? baseMarginBottom + insets.bottom : baseMarginBottom; + v.setLayoutParams(marginParams); + } + + return windowInsets; + }); // Request the initial insets to be dispatched if the view is attached if (view.isAttachedToWindow()) { @@ -404,10 +415,9 @@ public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, b } /** - * Apply window insets to a view by adding padding to avoid drawing elements behind system bars. - * Convenience method that applies insets to all sides. - * IME insets are propagated to child views. - * + * Apply window insets to a view by adding padding to avoid drawing elements behind system bars. + * Convenience method that applies insets to all sides. IME insets are propagated to child views. + * * @param view The view to apply insets to */ public static void applyWindowInsets(@NonNull View view) { @@ -417,7 +427,7 @@ public static void applyWindowInsets(@NonNull View view) { /** * Apply window insets to a view by adding padding to avoid drawing elements behind system bars. * - * IME insets are propagated to child views. + *

IME insets are propagated to child views. * * @param view The view to apply insets to * @param left Whether to apply left inset @@ -425,12 +435,14 @@ public static void applyWindowInsets(@NonNull View view) { * @param right Whether to apply right inset * @param bottom Whether to apply bottom inset */ - public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { + public static void applyWindowInsets( + @NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { applyWindowInsets(view, left, top, right, bottom, false, false); } /** - * Force applying window insets to a view by adding padding to avoid drawing elements behind system bars. + * Force applying window insets to a view by adding padding to avoid drawing elements behind + * system bars. * * @param view The view to apply insets to * @param left Whether to apply left inset @@ -438,16 +450,17 @@ public static void applyWindowInsets(@NonNull View view, boolean left, boolean t * @param right Whether to apply right inset * @param bottom Whether to apply bottom inset */ - public static void forceApplyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { + public static void forceApplyWindowInsets( + @NonNull View view, boolean left, boolean top, boolean right, boolean bottom) { applyWindowInsets(view, left, top, right, bottom, false, true); } /** * Apply window insets to a view by adding padding to avoid drawing elements behind system bars. - * - * This method stores the original padding values in view tags to ensure that - * padding doesn't accumulate on multiple inset applications. - * + * + *

This method stores the original padding values in view tags to ensure that padding doesn't + * accumulate on multiple inset applications. + * * @param view The view to apply insets to * @param left Whether to apply left inset * @param top Whether to apply top inset @@ -456,7 +469,14 @@ public static void forceApplyWindowInsets(@NonNull View view, boolean left, bool * @param consumeImeInsets Whether to consume IME insets so they don't propagate to child views * @param forceDispatch Force application of Insets, regardless if system think it shall dispatch */ - public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean consumeImeInsets, boolean forceDispatch) { + public static void applyWindowInsets( + @NonNull View view, + boolean left, + boolean top, + boolean right, + boolean bottom, + boolean consumeImeInsets, + boolean forceDispatch) { // Only enable on API 30+ where WindowInsets APIs work correctly if (!isEdgeToEdgeSupported()) return; @@ -469,33 +489,35 @@ public static void applyWindowInsets(@NonNull View view, boolean left, boolean t view.setTag(R.id.tag_window_insets_padding_bottom, view.getPaddingBottom()); } - ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> { - Insets insets = getCombinedInsets(windowInsets); - - // Retrieve the original padding values from tags with null checks - Integer leftTag = (Integer) v.getTag(R.id.tag_window_insets_padding_left); - Integer topTag = (Integer) v.getTag(R.id.tag_window_insets_padding_top); - Integer rightTag = (Integer) v.getTag(R.id.tag_window_insets_padding_right); - Integer bottomTag = (Integer) v.getTag(R.id.tag_window_insets_padding_bottom); - int basePaddingLeft = leftTag != null ? leftTag : 0; - int basePaddingTop = topTag != null ? topTag : 0; - int basePaddingRight = rightTag != null ? rightTag : 0; - int basePaddingBottom = bottomTag != null ? bottomTag : 0; - - v.setPadding( - left ? basePaddingLeft + insets.left : basePaddingLeft, - top ? basePaddingTop + insets.top : basePaddingTop, - right ? basePaddingRight + insets.right : basePaddingRight, - bottom ? basePaddingBottom + insets.bottom : basePaddingBottom - ); - - if (consumeImeInsets) { - windowInsets = new WindowInsetsCompat.Builder(windowInsets) - .setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE) - .build(); - } - return windowInsets; - }); + ViewCompat.setOnApplyWindowInsetsListener( + view, + (v, windowInsets) -> { + Insets insets = getCombinedInsets(windowInsets); + + // Retrieve the original padding values from tags with null checks + Integer leftTag = (Integer) v.getTag(R.id.tag_window_insets_padding_left); + Integer topTag = (Integer) v.getTag(R.id.tag_window_insets_padding_top); + Integer rightTag = (Integer) v.getTag(R.id.tag_window_insets_padding_right); + Integer bottomTag = (Integer) v.getTag(R.id.tag_window_insets_padding_bottom); + int basePaddingLeft = leftTag != null ? leftTag : 0; + int basePaddingTop = topTag != null ? topTag : 0; + int basePaddingRight = rightTag != null ? rightTag : 0; + int basePaddingBottom = bottomTag != null ? bottomTag : 0; + + v.setPadding( + left ? basePaddingLeft + insets.left : basePaddingLeft, + top ? basePaddingTop + insets.top : basePaddingTop, + right ? basePaddingRight + insets.right : basePaddingRight, + bottom ? basePaddingBottom + insets.bottom : basePaddingBottom); + + if (consumeImeInsets) { + windowInsets = + new WindowInsetsCompat.Builder(windowInsets) + .setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE) + .build(); + } + return windowInsets; + }); // Request the initial insets to be dispatched if the view is attached if (view.isAttachedToWindow()) { @@ -510,21 +532,21 @@ public static void applyWindowInsets(@NonNull View view, boolean left, boolean t } } - /** - * Apply the top status bar inset as the height of a view. - */ + /** Apply the top status bar inset as the height of a view. */ private static void applyTopInsetAsHeight(@NonNull View view) { - ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> { - Insets insets = getCombinedInsets(windowInsets); + ViewCompat.setOnApplyWindowInsetsListener( + view, + (v, windowInsets) -> { + Insets insets = getCombinedInsets(windowInsets); - android.view.ViewGroup.LayoutParams params = v.getLayoutParams(); - if (params != null) { - params.height = insets.top; - v.setLayoutParams(params); - } + android.view.ViewGroup.LayoutParams params = v.getLayoutParams(); + if (params != null) { + params.height = insets.top; + v.setLayoutParams(params); + } - return windowInsets; - }); + return windowInsets; + }); // Request the initial insets to be dispatched if the view is attached if (view.isAttachedToWindow()) { @@ -533,8 +555,9 @@ private static void applyTopInsetAsHeight(@NonNull View view) { } /** - * Apply adjustments to the activity's custom toolbar or set height of R.id.status_bar_background for proper Edge-to-Edge display. - * + * Apply adjustments to the activity's custom toolbar or set height of R.id.status_bar_background + * for proper Edge-to-Edge display. + * * @param activity The activity to apply the adjustments to */ public static void adjustToolbarForE2E(@NonNull AppCompatActivity activity) { @@ -565,5 +588,4 @@ public static void adjustToolbarForE2E(@NonNull AppCompatActivity activity) { } } } - } diff --git a/src/main/java/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java b/src/main/java/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java index 90a4eedb4..90abcd27a 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java +++ b/src/main/java/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java @@ -1,8 +1,7 @@ package org.thoughtcrime.securesms.util.concurrent; -import java.util.concurrent.ExecutionException; - import chat.delta.util.ListenableFuture; +import java.util.concurrent.ExecutionException; public abstract class AssertedSuccessListener implements ListenableFuture.Listener { @Override diff --git a/src/main/java/org/thoughtcrime/securesms/util/guava/Absent.java b/src/main/java/org/thoughtcrime/securesms/util/guava/Absent.java index e45b1422b..59763aca4 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/guava/Absent.java +++ b/src/main/java/org/thoughtcrime/securesms/util/guava/Absent.java @@ -21,41 +21,43 @@ import java.util.Collections; import java.util.Set; - -/** - * Implementation of an {@link Optional} not containing a reference. - */ - +/** Implementation of an {@link Optional} not containing a reference. */ final class Absent extends Optional { static final Absent INSTANCE = new Absent(); - @Override public boolean isPresent() { + @Override + public boolean isPresent() { return false; } - @Override public Object get() { + @Override + public Object get() { throw new IllegalStateException("value is absent"); } - @Override public Object or(Object defaultValue) { + @Override + public Object or(Object defaultValue) { return checkNotNull(defaultValue, "use orNull() instead of or(null)"); } @SuppressWarnings("unchecked") // safe covariant cast - @Override public Optional or(Optional secondChoice) { + @Override + public Optional or(Optional secondChoice) { return (Optional) checkNotNull(secondChoice); } - @Override public Object or(Supplier supplier) { - return checkNotNull(supplier.get(), - "use orNull() instead of a Supplier that returns null"); + @Override + public Object or(Supplier supplier) { + return checkNotNull(supplier.get(), "use orNull() instead of a Supplier that returns null"); } - @Override public Object orNull() { + @Override + public Object orNull() { return null; } - @Override public Set asSet() { + @Override + public Set asSet() { return Collections.emptySet(); } @@ -65,15 +67,18 @@ public Optional transform(Function function) { return Optional.absent(); } - @Override public boolean equals(Object object) { + @Override + public boolean equals(Object object) { return object == this; } - @Override public int hashCode() { + @Override + public int hashCode() { return 0x598df91c; } - @Override public String toString() { + @Override + public String toString() { return "Optional.absent()"; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/guava/Function.java b/src/main/java/org/thoughtcrime/securesms/util/guava/Function.java index bd1d4c598..5584f7cc8 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/guava/Function.java +++ b/src/main/java/org/thoughtcrime/securesms/util/guava/Function.java @@ -16,29 +16,26 @@ package org.thoughtcrime.securesms.util.guava; - - /** * Determines an output value based on an input value. - * + * *

See the Guava User Guide article on the use of {@code * Function}. - * + * * @author Kevin Bourrillion * @since 2.0 (imported from Google Collections Library) */ - public interface Function { /** * Returns the result of applying this function to {@code input}. This method is generally * expected, but not absolutely required, to have the following properties: * *

    - *
  • Its execution does not cause any observable side effects. - *
  • The computation is consistent with equals; that is, {@link Objects#equal - * Objects.equal}{@code (a, b)} implies that {@code Objects.equal(function.apply(a), - * function.apply(b))}. + *
  • Its execution does not cause any observable side effects. + *
  • The computation is consistent with equals; that is, {@link Objects#equal + * Objects.equal}{@code (a, b)} implies that {@code Objects.equal(function.apply(a), + * function.apply(b))}. *
* * @throws NullPointerException if {@code input} is null and this function does not accept null diff --git a/src/main/java/org/thoughtcrime/securesms/util/guava/Optional.java b/src/main/java/org/thoughtcrime/securesms/util/guava/Optional.java index 185e1f07e..1108ba909 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/guava/Optional.java +++ b/src/main/java/org/thoughtcrime/securesms/util/guava/Optional.java @@ -21,61 +21,53 @@ import java.io.Serializable; import java.util.Set; - /** - * An immutable object that may contain a non-null reference to another object. Each - * instance of this type either contains a non-null reference, or contains nothing (in - * which case we say that the reference is "absent"); it is never said to "contain {@code - * null}". + * An immutable object that may contain a non-null reference to another object. Each instance of + * this type either contains a non-null reference, or contains nothing (in which case we say that + * the reference is "absent"); it is never said to "contain {@code null}". * - *

A non-null {@code Optional} reference can be used as a replacement for a nullable - * {@code T} reference. It allows you to represent "a {@code T} that must be present" and - * a "a {@code T} that might be absent" as two distinct types in your program, which can - * aid clarity. + *

A non-null {@code Optional} reference can be used as a replacement for a nullable {@code T} + * reference. It allows you to represent "a {@code T} that must be present" and a "a {@code T} that + * might be absent" as two distinct types in your program, which can aid clarity. * *

Some uses of this class include * *

    - *
  • As a method return type, as an alternative to returning {@code null} to indicate - * that no value was available - *
  • To distinguish between "unknown" (for example, not present in a map) and "known to - * have no value" (present in the map, with value {@code Optional.absent()}) - *
  • To wrap nullable references for storage in a collection that does not support - * {@code null} (though there are - * - * several other approaches to this that should be considered first) + *
  • As a method return type, as an alternative to returning {@code null} to indicate that no + * value was available + *
  • To distinguish between "unknown" (for example, not present in a map) and "known to have no + * value" (present in the map, with value {@code Optional.absent()}) + *
  • To wrap nullable references for storage in a collection that does not support {@code null} + * (though there are + * several other approaches to this that should be considered first) *
* - *

A common alternative to using this class is to find or create a suitable - * null object for the - * type in question. + *

A common alternative to using this class is to find or create a suitable null object for the type in question. * - *

This class is not intended as a direct analogue of any existing "option" or "maybe" - * construct from other programming environments, though it may bear some similarities. + *

This class is not intended as a direct analogue of any existing "option" or "maybe" construct + * from other programming environments, though it may bear some similarities. * *

See the Guava User Guide article on - * using {@code Optional}. + * href="http://code.google.com/p/guava-libraries/wiki/UsingAndAvoidingNullExplained#Optional">using + * {@code Optional}. * - * @param the type of instance that can be contained. {@code Optional} is naturally - * covariant on this type, so it is safe to cast an {@code Optional} to {@code - * Optional} for any supertype {@code S} of {@code T}. + * @param the type of instance that can be contained. {@code Optional} is naturally covariant on + * this type, so it is safe to cast an {@code Optional} to {@code Optional} for any + * supertype {@code S} of {@code T}. * @author Kurt Alfred Kluever * @author Kevin Bourrillion * @since 10.0 */ public abstract class Optional implements Serializable { - /** - * Returns an {@code Optional} instance with no contained reference. - */ + /** Returns an {@code Optional} instance with no contained reference. */ @SuppressWarnings("unchecked") public static Optional absent() { return (Optional) Absent.INSTANCE; } - /** - * Returns an {@code Optional} instance containing the given non-null reference. - */ + /** Returns an {@code Optional} instance containing the given non-null reference. */ public static Optional of(T reference) { return new Present(checkNotNull(reference)); } @@ -85,62 +77,57 @@ public static Optional of(T reference) { * reference; otherwise returns {@link Optional#absent}. */ public static Optional fromNullable(T nullableReference) { - return (nullableReference == null) - ? Optional.absent() - : new Present(nullableReference); + return (nullableReference == null) ? Optional.absent() : new Present(nullableReference); } Optional() {} - /** - * Returns {@code true} if this holder contains a (non-null) instance. - */ + /** Returns {@code true} if this holder contains a (non-null) instance. */ public abstract boolean isPresent(); /** - * Returns the contained instance, which must be present. If the instance might be - * absent, use {@link #or(Object)} or {@link #orNull} instead. + * Returns the contained instance, which must be present. If the instance might be absent, use + * {@link #or(Object)} or {@link #orNull} instead. * - * @throws IllegalStateException if the instance is absent ({@link #isPresent} returns - * {@code false}) + * @throws IllegalStateException if the instance is absent ({@link #isPresent} returns {@code + * false}) */ public abstract T get(); /** - * Returns the contained instance if it is present; {@code defaultValue} otherwise. If - * no default value should be required because the instance is known to be present, use - * {@link #get()} instead. For a default value of {@code null}, use {@link #orNull}. + * Returns the contained instance if it is present; {@code defaultValue} otherwise. If no default + * value should be required because the instance is known to be present, use {@link #get()} + * instead. For a default value of {@code null}, use {@link #orNull}. * *

Note about generics: The signature {@code public T or(T defaultValue)} is overly * restrictive. However, the ideal signature, {@code public S or(S)}, is not legal * Java. As a result, some sensible operations involving subtypes are compile errors: - *

   {@code
    *
-   *   Optional optionalInt = getSomeOptionalInt();
-   *   Number value = optionalInt.or(0.5); // error
+   * 
{@code
+   * Optional optionalInt = getSomeOptionalInt();
+   * Number value = optionalInt.or(0.5); // error
    *
-   *   FluentIterable numbers = getSomeNumbers();
-   *   Optional first = numbers.first();
-   *   Number value = first.or(0.5); // error}
+ * FluentIterable numbers = getSomeNumbers(); + * Optional first = numbers.first(); + * Number value = first.or(0.5); // error + * }
* * As a workaround, it is always safe to cast an {@code Optional} to {@code * Optional}. Casting either of the above example {@code Optional} instances to {@code * Optional} (where {@code Number} is the desired output type) solves the problem: - *
   {@code
    *
-   *   Optional optionalInt = (Optional) getSomeOptionalInt();
-   *   Number value = optionalInt.or(0.5); // fine
+   * 
{@code
+   * Optional optionalInt = (Optional) getSomeOptionalInt();
+   * Number value = optionalInt.or(0.5); // fine
    *
-   *   FluentIterable numbers = getSomeNumbers();
-   *   Optional first = (Optional) numbers.first();
-   *   Number value = first.or(0.5); // fine}
+ * FluentIterable numbers = getSomeNumbers(); + * Optional first = (Optional) numbers.first(); + * Number value = first.or(0.5); // fine + * }
*/ public abstract T or(T defaultValue); - /** - * Returns this {@code Optional} if it has a value present; {@code secondChoice} - * otherwise. - */ + /** Returns this {@code Optional} if it has a value present; {@code secondChoice} otherwise. */ public abstract Optional or(Optional secondChoice); /** @@ -152,14 +139,14 @@ public static Optional fromNullable(T nullableReference) { public abstract T or(Supplier supplier); /** - * Returns the contained instance if it is present; {@code null} otherwise. If the - * instance is known to be present, use {@link #get()} instead. + * Returns the contained instance if it is present; {@code null} otherwise. If the instance is + * known to be present, use {@link #get()} instead. */ public abstract T orNull(); /** - * Returns an immutable singleton {@link Set} whose only element is the contained instance - * if it is present; an empty immutable {@link Set} otherwise. + * Returns an immutable singleton {@link Set} whose only element is the contained instance if it + * is present; an empty immutable {@link Set} otherwise. * * @since 11.0 */ @@ -167,34 +154,32 @@ public static Optional fromNullable(T nullableReference) { /** * If the instance is present, it is transformed with the given {@link Function}; otherwise, - * {@link Optional#absent} is returned. If the function returns {@code null}, a - * {@link NullPointerException} is thrown. + * {@link Optional#absent} is returned. If the function returns {@code null}, a {@link + * NullPointerException} is thrown. * * @throws NullPointerException if the function returns {@code null} - * * @since 12.0 */ - public abstract Optional transform(Function function); /** - * Returns {@code true} if {@code object} is an {@code Optional} instance, and either - * the contained references are {@linkplain Object#equals equal} to each other or both - * are absent. Note that {@code Optional} instances of differing parameterized types can - * be equal. + * Returns {@code true} if {@code object} is an {@code Optional} instance, and either the + * contained references are {@linkplain Object#equals equal} to each other or both are absent. + * Note that {@code Optional} instances of differing parameterized types can be equal. */ - @Override public abstract boolean equals(Object object); + @Override + public abstract boolean equals(Object object); - /** - * Returns a hash code for this instance. - */ - @Override public abstract int hashCode(); + /** Returns a hash code for this instance. */ + @Override + public abstract int hashCode(); /** - * Returns a string representation for this instance. The form of this string - * representation is unspecified. + * Returns a string representation for this instance. The form of this string representation is + * unspecified. */ - @Override public abstract String toString(); + @Override + public abstract String toString(); /** * Returns the value of each present instance from the supplied {@code optionals}, in order, @@ -204,28 +189,28 @@ public static Optional fromNullable(T nullableReference) { * @since 11.0 (generics widened in 13.0) */ -// public static Iterable presentInstances( -// final Iterable> optionals) { -// checkNotNull(optionals); -// return new Iterable() { -// @Override public Iterator iterator() { -// return new AbstractIterator() { -// private final Iterator> iterator = -// checkNotNull(optionals.iterator()); -// -// @Override protected T computeNext() { -// while (iterator.hasNext()) { -// Optional optional = iterator.next(); -// if (optional.isPresent()) { -// return optional.get(); -// } -// } -// return endOfData(); -// } -// }; -// }; -// }; -// } + // public static Iterable presentInstances( + // final Iterable> optionals) { + // checkNotNull(optionals); + // return new Iterable() { + // @Override public Iterator iterator() { + // return new AbstractIterator() { + // private final Iterator> iterator = + // checkNotNull(optionals.iterator()); + // + // @Override protected T computeNext() { + // while (iterator.hasNext()) { + // Optional optional = iterator.next(); + // if (optional.isPresent()) { + // return optional.get(); + // } + // } + // return endOfData(); + // } + // }; + // }; + // }; + // } private static final long serialVersionUID = 0; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/guava/Preconditions.java b/src/main/java/org/thoughtcrime/securesms/util/guava/Preconditions.java index 1c32bb54c..011ad5d6f 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/guava/Preconditions.java +++ b/src/main/java/org/thoughtcrime/securesms/util/guava/Preconditions.java @@ -16,55 +16,50 @@ package org.thoughtcrime.securesms.util.guava; - import java.util.NoSuchElementException; - - /** - * Simple static methods to be called at the start of your own methods to verify - * correct arguments and state. This allows constructs such as + * Simple static methods to be called at the start of your own methods to verify correct arguments + * and state. This allows constructs such as + * *
  *     if (count <= 0) {
  *       throw new IllegalArgumentException("must be positive: " + count);
  *     }
* * to be replaced with the more compact + * *
  *     checkArgument(count > 0, "must be positive: %s", count);
* - * Note that the sense of the expression is inverted; with {@code Preconditions} - * you declare what you expect to be true, just as you do with an - * - * {@code assert} or a JUnit {@code assertTrue} call. + * Note that the sense of the expression is inverted; with {@code Preconditions} you declare what + * you expect to be true, just as you do with an {@code assert} or a + * JUnit {@code assertTrue} call. * - *

Warning: only the {@code "%s"} specifier is recognized as a - * placeholder in these messages, not the full range of {@link - * String#format(String, Object[])} specifiers. + *

Warning: only the {@code "%s"} specifier is recognized as a placeholder in these + * messages, not the full range of {@link String#format(String, Object[])} specifiers. * - *

Take care not to confuse precondition checking with other similar types - * of checks! Precondition exceptions -- including those provided here, but also - * {@link IndexOutOfBoundsException}, {@link NoSuchElementException}, {@link - * UnsupportedOperationException} and others -- are used to signal that the - * calling method has made an error. This tells the caller that it should - * not have invoked the method when it did, with the arguments it did, or - * perhaps ever. Postcondition or other invariant failures should not throw - * these types of exceptions. + *

Take care not to confuse precondition checking with other similar types of checks! + * Precondition exceptions -- including those provided here, but also {@link + * IndexOutOfBoundsException}, {@link NoSuchElementException}, {@link UnsupportedOperationException} + * and others -- are used to signal that the calling method has made an error. This tells the + * caller that it should not have invoked the method when it did, with the arguments it did, or + * perhaps ever. Postcondition or other invariant failures should not throw these types of + * exceptions. * *

See the Guava User Guide on - * using {@code Preconditions}. + * "http://code.google.com/p/guava-libraries/wiki/PreconditionsExplained"> using {@code + * Preconditions}. * * @author Kevin Bourrillion * @since 2.0 (imported from Google Collections Library) */ - public final class Preconditions { private Preconditions() {} /** - * Ensures the truth of an expression involving one or more parameters to the - * calling method. + * Ensures the truth of an expression involving one or more parameters to the calling method. * * @param expression a boolean expression * @throws IllegalArgumentException if {@code expression} is false @@ -76,52 +71,44 @@ public static void checkArgument(boolean expression) { } /** - * Ensures the truth of an expression involving one or more parameters to the - * calling method. + * Ensures the truth of an expression involving one or more parameters to the calling method. * * @param expression a boolean expression - * @param errorMessage the exception message to use if the check fails; will - * be converted to a string using {@link String#valueOf(Object)} + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} * @throws IllegalArgumentException if {@code expression} is false */ - public static void checkArgument( - boolean expression, Object errorMessage) { + public static void checkArgument(boolean expression, Object errorMessage) { if (!expression) { throw new IllegalArgumentException(String.valueOf(errorMessage)); } } /** - * Ensures the truth of an expression involving one or more parameters to the - * calling method. + * Ensures the truth of an expression involving one or more parameters to the calling method. * * @param expression a boolean expression - * @param errorMessageTemplate a template for the exception message should the - * check fail. The message is formed by replacing each {@code %s} - * placeholder in the template with an argument. These are matched by - * position - the first {@code %s} gets {@code errorMessageArgs[0]}, etc. - * Unmatched arguments will be appended to the formatted message in square - * braces. Unmatched placeholders will be left as-is. - * @param errorMessageArgs the arguments to be substituted into the message - * template. Arguments are converted to strings using - * {@link String#valueOf(Object)}. + * @param errorMessageTemplate a template for the exception message should the check fail. The + * message is formed by replacing each {@code %s} placeholder in the template with an + * argument. These are matched by position - the first {@code %s} gets {@code + * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in + * square braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. * @throws IllegalArgumentException if {@code expression} is false - * @throws NullPointerException if the check fails and either {@code - * errorMessageTemplate} or {@code errorMessageArgs} is null (don't let - * this happen) + * @throws NullPointerException if the check fails and either {@code errorMessageTemplate} or + * {@code errorMessageArgs} is null (don't let this happen) */ - public static void checkArgument(boolean expression, - String errorMessageTemplate, - Object... errorMessageArgs) { + public static void checkArgument( + boolean expression, String errorMessageTemplate, Object... errorMessageArgs) { if (!expression) { - throw new IllegalArgumentException( - format(errorMessageTemplate, errorMessageArgs)); + throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs)); } } /** - * Ensures the truth of an expression involving the state of the calling - * instance, but not involving any parameters to the calling method. + * Ensures the truth of an expression involving the state of the calling instance, but not + * involving any parameters to the calling method. * * @param expression a boolean expression * @throws IllegalStateException if {@code expression} is false @@ -133,52 +120,45 @@ public static void checkState(boolean expression) { } /** - * Ensures the truth of an expression involving the state of the calling - * instance, but not involving any parameters to the calling method. + * Ensures the truth of an expression involving the state of the calling instance, but not + * involving any parameters to the calling method. * * @param expression a boolean expression - * @param errorMessage the exception message to use if the check fails; will - * be converted to a string using {@link String#valueOf(Object)} + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} * @throws IllegalStateException if {@code expression} is false */ - public static void checkState( - boolean expression, Object errorMessage) { + public static void checkState(boolean expression, Object errorMessage) { if (!expression) { throw new IllegalStateException(String.valueOf(errorMessage)); } } /** - * Ensures the truth of an expression involving the state of the calling - * instance, but not involving any parameters to the calling method. + * Ensures the truth of an expression involving the state of the calling instance, but not + * involving any parameters to the calling method. * * @param expression a boolean expression - * @param errorMessageTemplate a template for the exception message should the - * check fail. The message is formed by replacing each {@code %s} - * placeholder in the template with an argument. These are matched by - * position - the first {@code %s} gets {@code errorMessageArgs[0]}, etc. - * Unmatched arguments will be appended to the formatted message in square - * braces. Unmatched placeholders will be left as-is. - * @param errorMessageArgs the arguments to be substituted into the message - * template. Arguments are converted to strings using - * {@link String#valueOf(Object)}. + * @param errorMessageTemplate a template for the exception message should the check fail. The + * message is formed by replacing each {@code %s} placeholder in the template with an + * argument. These are matched by position - the first {@code %s} gets {@code + * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in + * square braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. * @throws IllegalStateException if {@code expression} is false - * @throws NullPointerException if the check fails and either {@code - * errorMessageTemplate} or {@code errorMessageArgs} is null (don't let - * this happen) + * @throws NullPointerException if the check fails and either {@code errorMessageTemplate} or + * {@code errorMessageArgs} is null (don't let this happen) */ - public static void checkState(boolean expression, - String errorMessageTemplate, - Object... errorMessageArgs) { + public static void checkState( + boolean expression, String errorMessageTemplate, Object... errorMessageArgs) { if (!expression) { - throw new IllegalStateException( - format(errorMessageTemplate, errorMessageArgs)); + throw new IllegalStateException(format(errorMessageTemplate, errorMessageArgs)); } } /** - * Ensures that an object reference passed as a parameter to the calling - * method is not null. + * Ensures that an object reference passed as a parameter to the calling method is not null. * * @param reference an object reference * @return the non-null reference that was validated @@ -192,12 +172,11 @@ public static T checkNotNull(T reference) { } /** - * Ensures that an object reference passed as a parameter to the calling - * method is not null. + * Ensures that an object reference passed as a parameter to the calling method is not null. * * @param reference an object reference - * @param errorMessage the exception message to use if the check fails; will - * be converted to a string using {@link String#valueOf(Object)} + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} * @return the non-null reference that was validated * @throws NullPointerException if {@code reference} is null */ @@ -209,29 +188,24 @@ public static T checkNotNull(T reference, Object errorMessage) { } /** - * Ensures that an object reference passed as a parameter to the calling - * method is not null. + * Ensures that an object reference passed as a parameter to the calling method is not null. * * @param reference an object reference - * @param errorMessageTemplate a template for the exception message should the - * check fail. The message is formed by replacing each {@code %s} - * placeholder in the template with an argument. These are matched by - * position - the first {@code %s} gets {@code errorMessageArgs[0]}, etc. - * Unmatched arguments will be appended to the formatted message in square - * braces. Unmatched placeholders will be left as-is. - * @param errorMessageArgs the arguments to be substituted into the message - * template. Arguments are converted to strings using - * {@link String#valueOf(Object)}. + * @param errorMessageTemplate a template for the exception message should the check fail. The + * message is formed by replacing each {@code %s} placeholder in the template with an + * argument. These are matched by position - the first {@code %s} gets {@code + * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in + * square braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. * @return the non-null reference that was validated * @throws NullPointerException if {@code reference} is null */ - public static T checkNotNull(T reference, - String errorMessageTemplate, - Object... errorMessageArgs) { + public static T checkNotNull( + T reference, String errorMessageTemplate, Object... errorMessageArgs) { if (reference == null) { // If either of these parameters is null, the right thing happens anyway - throw new NullPointerException( - format(errorMessageTemplate, errorMessageArgs)); + throw new NullPointerException(format(errorMessageTemplate, errorMessageArgs)); } return reference; } @@ -266,16 +240,13 @@ public static T checkNotNull(T reference, */ /** - * Ensures that {@code index} specifies a valid element in an array, - * list or string of size {@code size}. An element index may range from zero, - * inclusive, to {@code size}, exclusive. + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. * - * @param index a user-supplied index identifying an element of an array, list - * or string + * @param index a user-supplied index identifying an element of an array, list or string * @param size the size of that array, list or string * @return the value of {@code index} - * @throws IndexOutOfBoundsException if {@code index} is negative or is not - * less than {@code size} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} * @throws IllegalArgumentException if {@code size} is negative */ public static int checkElementIndex(int index, int size) { @@ -283,21 +254,17 @@ public static int checkElementIndex(int index, int size) { } /** - * Ensures that {@code index} specifies a valid element in an array, - * list or string of size {@code size}. An element index may range from zero, - * inclusive, to {@code size}, exclusive. + * Ensures that {@code index} specifies a valid element in an array, list or string of size + * {@code size}. An element index may range from zero, inclusive, to {@code size}, exclusive. * - * @param index a user-supplied index identifying an element of an array, list - * or string + * @param index a user-supplied index identifying an element of an array, list or string * @param size the size of that array, list or string * @param desc the text to use to describe this index in an error message * @return the value of {@code index} - * @throws IndexOutOfBoundsException if {@code index} is negative or is not - * less than {@code size} + * @throws IndexOutOfBoundsException if {@code index} is negative or is not less than {@code size} * @throws IllegalArgumentException if {@code size} is negative */ - public static int checkElementIndex( - int index, int size, String desc) { + public static int checkElementIndex(int index, int size, String desc) { // Carefully optimized for execution by hotspot (explanatory comment above) if (index < 0 || index >= size) { throw new IndexOutOfBoundsException(badElementIndex(index, size, desc)); @@ -316,16 +283,13 @@ private static String badElementIndex(int index, int size, String desc) { } /** - * Ensures that {@code index} specifies a valid position in an array, - * list or string of size {@code size}. A position index may range from zero - * to {@code size}, inclusive. + * Ensures that {@code index} specifies a valid position in an array, list or string of + * size {@code size}. A position index may range from zero to {@code size}, inclusive. * - * @param index a user-supplied index identifying a position in an array, list - * or string + * @param index a user-supplied index identifying a position in an array, list or string * @param size the size of that array, list or string * @return the value of {@code index} - * @throws IndexOutOfBoundsException if {@code index} is negative or is - * greater than {@code size} + * @throws IndexOutOfBoundsException if {@code index} is negative or is greater than {@code size} * @throws IllegalArgumentException if {@code size} is negative */ public static int checkPositionIndex(int index, int size) { @@ -333,21 +297,17 @@ public static int checkPositionIndex(int index, int size) { } /** - * Ensures that {@code index} specifies a valid position in an array, - * list or string of size {@code size}. A position index may range from zero - * to {@code size}, inclusive. + * Ensures that {@code index} specifies a valid position in an array, list or string of + * size {@code size}. A position index may range from zero to {@code size}, inclusive. * - * @param index a user-supplied index identifying a position in an array, list - * or string + * @param index a user-supplied index identifying a position in an array, list or string * @param size the size of that array, list or string * @param desc the text to use to describe this index in an error message * @return the value of {@code index} - * @throws IndexOutOfBoundsException if {@code index} is negative or is - * greater than {@code size} + * @throws IndexOutOfBoundsException if {@code index} is negative or is greater than {@code size} * @throws IllegalArgumentException if {@code size} is negative */ - public static int checkPositionIndex( - int index, int size, String desc) { + public static int checkPositionIndex(int index, int size, String desc) { // Carefully optimized for execution by hotspot (explanatory comment above) if (index < 0 || index > size) { throw new IndexOutOfBoundsException(badPositionIndex(index, size, desc)); @@ -361,23 +321,20 @@ private static String badPositionIndex(int index, int size, String desc) { } else if (size < 0) { throw new IllegalArgumentException("negative size: " + size); } else { // index > size - return format("%s (%s) must not be greater than size (%s)", - desc, index, size); + return format("%s (%s) must not be greater than size (%s)", desc, index, size); } } /** - * Ensures that {@code start} and {@code end} specify a valid positions - * in an array, list or string of size {@code size}, and are in order. A - * position index may range from zero to {@code size}, inclusive. + * Ensures that {@code start} and {@code end} specify a valid positions in an array, list + * or string of size {@code size}, and are in order. A position index may range from zero to + * {@code size}, inclusive. * - * @param start a user-supplied index identifying a starting position in an - * array, list or string - * @param end a user-supplied index identifying a ending position in an array, - * list or string + * @param start a user-supplied index identifying a starting position in an array, list or string + * @param end a user-supplied index identifying a ending position in an array, list or string * @param size the size of that array, list or string - * @throws IndexOutOfBoundsException if either index is negative or is - * greater than {@code size}, or if {@code end} is less than {@code start} + * @throws IndexOutOfBoundsException if either index is negative or is greater than {@code size}, + * or if {@code end} is less than {@code start} * @throws IllegalArgumentException if {@code size} is negative */ public static void checkPositionIndexes(int start, int end, int size) { @@ -395,29 +352,24 @@ private static String badPositionIndexes(int start, int end, int size) { return badPositionIndex(end, size, "end index"); } // end < start - return format("end index (%s) must not be less than start index (%s)", - end, start); + return format("end index (%s) must not be less than start index (%s)", end, start); } /** - * Substitutes each {@code %s} in {@code template} with an argument. These - * are matched by position - the first {@code %s} gets {@code args[0]}, etc. - * If there are more arguments than placeholders, the unmatched arguments will - * be appended to the end of the formatted message in square braces. + * Substitutes each {@code %s} in {@code template} with an argument. These are matched by position + * - the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than + * placeholders, the unmatched arguments will be appended to the end of the formatted message in + * square braces. * - * @param template a non-null string containing 0 or more {@code %s} - * placeholders. - * @param args the arguments to be substituted into the message - * template. Arguments are converted to strings using - * {@link String#valueOf(Object)}. Arguments can be null. + * @param template a non-null string containing 0 or more {@code %s} placeholders. + * @param args the arguments to be substituted into the message template. Arguments are converted + * to strings using {@link String#valueOf(Object)}. Arguments can be null. */ - static String format(String template, - Object... args) { + static String format(String template, Object... args) { template = String.valueOf(template); // null -> "null" // start substituting the arguments into the '%s' placeholders - StringBuilder builder = new StringBuilder( - template.length() + 16 * args.length); + StringBuilder builder = new StringBuilder(template.length() + 16 * args.length); int templateStart = 0; int i = 0; while (i < args.length) { diff --git a/src/main/java/org/thoughtcrime/securesms/util/guava/Present.java b/src/main/java/org/thoughtcrime/securesms/util/guava/Present.java index 4bf3d1548..d97a6e9ce 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/guava/Present.java +++ b/src/main/java/org/thoughtcrime/securesms/util/guava/Present.java @@ -21,10 +21,7 @@ import java.util.Collections; import java.util.Set; -/** - * Implementation of an {@link Optional} containing a reference. - */ - +/** Implementation of an {@link Optional} containing a reference. */ final class Present extends Optional { private final T reference; @@ -32,43 +29,52 @@ final class Present extends Optional { this.reference = reference; } - @Override public boolean isPresent() { + @Override + public boolean isPresent() { return true; } - @Override public T get() { + @Override + public T get() { return reference; } - @Override public T or(T defaultValue) { + @Override + public T or(T defaultValue) { checkNotNull(defaultValue, "use orNull() instead of or(null)"); return reference; } - @Override public Optional or(Optional secondChoice) { + @Override + public Optional or(Optional secondChoice) { checkNotNull(secondChoice); return this; } - @Override public T or(Supplier supplier) { + @Override + public T or(Supplier supplier) { checkNotNull(supplier); return reference; } - @Override public T orNull() { + @Override + public T orNull() { return reference; } - @Override public Set asSet() { + @Override + public Set asSet() { return Collections.singleton(reference); } - - @Override public Optional transform(Function function) { - return new Present(checkNotNull(function.apply(reference), - "Transformation function cannot return null.")); + + @Override + public Optional transform(Function function) { + return new Present( + checkNotNull(function.apply(reference), "Transformation function cannot return null.")); } - @Override public boolean equals(Object object) { + @Override + public boolean equals(Object object) { if (object instanceof Present) { Present other = (Present) object; return reference.equals(other.reference); @@ -76,11 +82,13 @@ final class Present extends Optional { return false; } - @Override public int hashCode() { + @Override + public int hashCode() { return 0x598df91c + reference.hashCode(); } - @Override public String toString() { + @Override + public String toString() { return "Optional.of(" + reference + ")"; } diff --git a/src/main/java/org/thoughtcrime/securesms/util/guava/Supplier.java b/src/main/java/org/thoughtcrime/securesms/util/guava/Supplier.java index 8a3683c42..14a837d9d 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/guava/Supplier.java +++ b/src/main/java/org/thoughtcrime/securesms/util/guava/Supplier.java @@ -16,19 +16,18 @@ package org.thoughtcrime.securesms.util.guava; - /** - * A class that can supply objects of a single type. Semantically, this could - * be a factory, generator, builder, closure, or something else entirely. No - * guarantees are implied by this interface. + * A class that can supply objects of a single type. Semantically, this could be a factory, + * generator, builder, closure, or something else entirely. No guarantees are implied by this + * interface. * * @author Harry Heymann * @since 2.0 (imported from Google Collections Library) */ public interface Supplier { /** - * Retrieves an instance of the appropriate type. The returned object may or - * may not be a new instance, depending on the implementation. + * Retrieves an instance of the appropriate type. The returned object may or may not be a new + * instance, depending on the implementation. * * @return an instance of the appropriate type */ diff --git a/src/main/java/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java b/src/main/java/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java index b15c96137..f2c47b9ce 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java +++ b/src/main/java/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.util.spans; - import android.text.TextPaint; import android.text.style.MetricAffectingSpan; diff --git a/src/main/java/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java b/src/main/java/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java index 23d76efcd..5c1f2c0a5 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java +++ b/src/main/java/org/thoughtcrime/securesms/util/task/ProgressDialogAsyncTask.java @@ -3,27 +3,25 @@ import android.content.Context; import android.content.DialogInterface.OnCancelListener; import android.os.AsyncTask; - import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.util.views.ProgressDialog; - import java.lang.ref.WeakReference; +import org.thoughtcrime.securesms.util.views.ProgressDialog; -public abstract class ProgressDialogAsyncTask extends AsyncTask { +public abstract class ProgressDialogAsyncTask + extends AsyncTask { private final WeakReference contextReference; - private ProgressDialog progress; - private final String title; - private final String message; - private boolean cancelable; - private OnCancelListener onCancelListener; + private ProgressDialog progress; + private final String title; + private final String message; + private boolean cancelable; + private OnCancelListener onCancelListener; public ProgressDialogAsyncTask(Context context, String title, String message) { super(); this.contextReference = new WeakReference<>(context); - this.title = title; - this.message = message; + this.title = title; + this.message = message; } public void setCancelable(@Nullable OnCancelListener onCancelListener) { @@ -43,7 +41,7 @@ protected void onPreExecute() { protected void onPostExecute(Result result) { try { if (progress != null) progress.dismiss(); - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -52,4 +50,3 @@ protected Context getContext() { return contextReference.get(); } } - diff --git a/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java b/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java index 19e644a81..f0e8d0201 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java +++ b/src/main/java/org/thoughtcrime/securesms/util/task/SnackbarAsyncTask.java @@ -2,48 +2,43 @@ import android.os.AsyncTask; import android.view.View; - import androidx.annotation.Nullable; - import com.google.android.material.snackbar.Snackbar; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.views.ProgressDialog; -public abstract class SnackbarAsyncTask - extends AsyncTask - implements View.OnClickListener -{ +public abstract class SnackbarAsyncTask extends AsyncTask + implements View.OnClickListener { - private final View view; - private final String snackbarText; - private final String snackbarActionText; - private final int snackbarDuration; + private final View view; + private final String snackbarText; + private final String snackbarActionText; + private final int snackbarDuration; private final boolean showProgress; - private @Nullable Params reversibleParameter; + private @Nullable Params reversibleParameter; private @Nullable ProgressDialog progressDialog; - public SnackbarAsyncTask(View view, - String snackbarText, - String snackbarActionText, - int snackbarDuration, - boolean showProgress) - { - this.view = view; - this.snackbarText = snackbarText; - this.snackbarActionText = snackbarActionText; - this.snackbarDuration = snackbarDuration; - this.showProgress = showProgress; + public SnackbarAsyncTask( + View view, + String snackbarText, + String snackbarActionText, + int snackbarDuration, + boolean showProgress) { + this.view = view; + this.snackbarText = snackbarText; + this.snackbarActionText = snackbarActionText; + this.snackbarDuration = snackbarDuration; + this.showProgress = showProgress; } @Override protected void onPreExecute() { if (this.showProgress) { - this.progressDialog = ProgressDialog.show(view.getContext(), - "", view.getContext().getString(R.string.one_moment), true, false); - } - else { + this.progressDialog = + ProgressDialog.show( + view.getContext(), "", view.getContext().getString(R.string.one_moment), true, false); + } else { this.progressDialog = null; } } @@ -51,7 +46,7 @@ protected void onPreExecute() { @SafeVarargs @Override protected final Void doInBackground(Params... params) { - this.reversibleParameter = params != null && params.length > 0 ?params[0] : null; + this.reversibleParameter = params != null && params.length > 0 ? params[0] : null; executeAction(reversibleParameter); return null; } @@ -64,9 +59,9 @@ protected void onPostExecute(Void result) { } Snackbar.make(view, snackbarText, snackbarDuration) - .setAction(snackbarActionText, this) - .setActionTextColor(view.getResources().getColor(R.color.white)) - .show(); + .setAction(snackbarActionText, this) + .setActionTextColor(view.getResources().getColor(R.color.white)) + .show(); } @Override @@ -75,7 +70,7 @@ public void onClick(View v) { @Override protected void onPreExecute() { if (showProgress) progressDialog = ProgressDialog.show(view.getContext(), "", "", true); - else progressDialog = null; + else progressDialog = null; } @Override @@ -95,6 +90,6 @@ protected void onPostExecute(Void result) { } protected abstract void executeAction(@Nullable Params parameter); - protected abstract void reverseAction(@Nullable Params parameter); + protected abstract void reverseAction(@Nullable Params parameter); } diff --git a/src/main/java/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java b/src/main/java/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java index df2ba39b4..c02abd5ed 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java +++ b/src/main/java/org/thoughtcrime/securesms/util/views/ConversationAdaptiveActionsToolbar.java @@ -5,22 +5,23 @@ import android.util.AttributeSet; import android.view.Menu; import android.view.MenuItem; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ViewUtil; /** - * This class was pulled from Signal (AdaptiveActionsToolbar) and then adapted to the ConversationActivity. + * This class was pulled from Signal (AdaptiveActionsToolbar) and then adapted to the + * ConversationActivity. */ public class ConversationAdaptiveActionsToolbar extends Toolbar { - private static final int NAVIGATION_DP = 56; - private static final int TITLE_DP = 48; // estimated, only a number (if >1 items are selected there is more room anyway as there are fewer options) - private static final int ACTION_VIEW_WIDTH_DP = 48; + private static final int NAVIGATION_DP = 56; + private static final int TITLE_DP = + 48; // estimated, only a number (if >1 items are selected there is more room anyway as there + // are fewer options) + private static final int ACTION_VIEW_WIDTH_DP = 48; private static final int OVERFLOW_VIEW_WIDTH_DP = 36; private static final int ID_ACTION_1 = R.id.menu_context_edit; @@ -29,20 +30,23 @@ public class ConversationAdaptiveActionsToolbar extends Toolbar { private static final int ID_ACTION_4 = R.id.menu_context_forward; private static final int ID_ACTION_5 = R.id.menu_context_delete_message; - private final int maxShown; + private final int maxShown; public ConversationAdaptiveActionsToolbar(@NonNull Context context) { this(context, null); } - public ConversationAdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs) { + public ConversationAdaptiveActionsToolbar( + @NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.toolbarStyle); } - public ConversationAdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public ConversationAdaptiveActionsToolbar( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ConversationAdaptiveActionsToolbar); + TypedArray array = + context.obtainStyledAttributes(attrs, R.styleable.ConversationAdaptiveActionsToolbar); maxShown = array.getInteger(R.styleable.ConversationAdaptiveActionsToolbar_aat_max_shown, 100); @@ -77,11 +81,12 @@ public static void adjustMenuActions(@NonNull Menu menu, int maxToShow, int tool for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); - boolean showAsAction = item.getItemId() == ID_ACTION_1 - || item.getItemId() == ID_ACTION_2 - || item.getItemId() == ID_ACTION_3 - || item.getItemId() == ID_ACTION_4 - || item.getItemId() == ID_ACTION_5; + boolean showAsAction = + item.getItemId() == ID_ACTION_1 + || item.getItemId() == ID_ACTION_2 + || item.getItemId() == ID_ACTION_3 + || item.getItemId() == ID_ACTION_4 + || item.getItemId() == ID_ACTION_5; if (showAsAction && item.isVisible() && nItemsToShow > 0) { item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); diff --git a/src/main/java/org/thoughtcrime/securesms/util/views/ProgressDialog.java b/src/main/java/org/thoughtcrime/securesms/util/views/ProgressDialog.java index 583c0f893..bebbe79ff 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/views/ProgressDialog.java +++ b/src/main/java/org/thoughtcrime/securesms/util/views/ProgressDialog.java @@ -9,111 +9,125 @@ import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; - import org.thoughtcrime.securesms.R; public class ProgressDialog extends AlertDialog { - private boolean indeterminate; - private String message; - private TextView textView; - private ProgressBar progressBar; - - public ProgressDialog(@NonNull Context context) { - super(context); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - View dialogView = View.inflate(getContext(), R.layout.dialog_progress, null); - setView(dialogView); - super.onCreate(savedInstanceState); - } - - @Override - public void setMessage(CharSequence message) { - this.message = message.toString(); - if (textView != null) { - textView.setText(message); - } + private boolean indeterminate; + private String message; + private TextView textView; + private ProgressBar progressBar; + + public ProgressDialog(@NonNull Context context) { + super(context); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + View dialogView = View.inflate(getContext(), R.layout.dialog_progress, null); + setView(dialogView); + super.onCreate(savedInstanceState); + } + + @Override + public void setMessage(CharSequence message) { + this.message = message.toString(); + if (textView != null) { + textView.setText(message); } + } - private boolean isButtonVisible(int which) { - Button button = getButton(which); - if (button==null) { - return false; - } - return button.getVisibility()==View.VISIBLE; + private boolean isButtonVisible(int which) { + Button button = getButton(which); + if (button == null) { + return false; } + return button.getVisibility() == View.VISIBLE; + } - @Override - public void show() { - super.show(); + @Override + public void show() { + super.show(); - if (isButtonVisible(Dialog.BUTTON_POSITIVE) || isButtonVisible(Dialog.BUTTON_NEGATIVE) || isButtonVisible(Dialog.BUTTON_NEUTRAL)) { - findViewById(R.id.noButtonsSpacer).setVisibility(View.GONE); - } - - progressBar = findViewById(R.id.progressBar); - textView = findViewById(R.id.text); - setupProgressBar(); - setupTextView(); - } - - private void setupProgressBar() { - if (progressBar != null) { - progressBar.getIndeterminateDrawable() - .setColorFilter(ContextCompat.getColor(getContext(), R.color.def_accent), PorterDuff.Mode.SRC_IN); - progressBar.setIndeterminate(indeterminate); - } + if (isButtonVisible(Dialog.BUTTON_POSITIVE) + || isButtonVisible(Dialog.BUTTON_NEGATIVE) + || isButtonVisible(Dialog.BUTTON_NEUTRAL)) { + findViewById(R.id.noButtonsSpacer).setVisibility(View.GONE); } - private void setupTextView() { - if (textView != null && message != null && !message.isEmpty()) { - textView.setText(message); - } + progressBar = findViewById(R.id.progressBar); + textView = findViewById(R.id.text); + setupProgressBar(); + setupTextView(); + } + + private void setupProgressBar() { + if (progressBar != null) { + progressBar + .getIndeterminateDrawable() + .setColorFilter( + ContextCompat.getColor(getContext(), R.color.def_accent), PorterDuff.Mode.SRC_IN); + progressBar.setIndeterminate(indeterminate); } + } - private void setIndeterminate(boolean indeterminate) { - this.indeterminate = indeterminate; - if (progressBar != null) { - progressBar.setIndeterminate(indeterminate); - } + private void setupTextView() { + if (textView != null && message != null && !message.isEmpty()) { + textView.setText(message); } + } - // Source: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/ProgressDialog.java - public static ProgressDialog show(Context context, CharSequence title, - CharSequence message, boolean indeterminate) { - return show(context, title, message, indeterminate, false, null); + private void setIndeterminate(boolean indeterminate) { + this.indeterminate = indeterminate; + if (progressBar != null) { + progressBar.setIndeterminate(indeterminate); } - - // Source: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/ProgressDialog.java - public static ProgressDialog show(Context context, CharSequence title, - CharSequence message, boolean indeterminate, boolean cancelable) { - return show(context, title, message, indeterminate, cancelable, null); + } + + // Source: + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/ProgressDialog.java + public static ProgressDialog show( + Context context, CharSequence title, CharSequence message, boolean indeterminate) { + return show(context, title, message, indeterminate, false, null); + } + + // Source: + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/ProgressDialog.java + public static ProgressDialog show( + Context context, + CharSequence title, + CharSequence message, + boolean indeterminate, + boolean cancelable) { + return show(context, title, message, indeterminate, cancelable, null); + } + + // Source: + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/ProgressDialog.java + public static ProgressDialog show( + Context context, + CharSequence title, + CharSequence message, + boolean indeterminate, + boolean cancelable, + OnCancelListener cancelListener) { + ProgressDialog dialog = new ProgressDialog(context); + dialog.setTitle(title); + dialog.setMessage(message); + dialog.setIndeterminate(indeterminate); + dialog.setCancelable(cancelable); + dialog.setOnCancelListener(cancelListener); + if (cancelable) { + dialog.setCanceledOnTouchOutside(false); + dialog.setButton( + DialogInterface.BUTTON_NEGATIVE, + context.getString(R.string.cancel), + ((dialog1, which) -> cancelListener.onCancel(dialog))); } - - // Source: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/ProgressDialog.java - public static ProgressDialog show(Context context, CharSequence title, - CharSequence message, boolean indeterminate, - boolean cancelable, OnCancelListener cancelListener) { - ProgressDialog dialog = new ProgressDialog(context); - dialog.setTitle(title); - dialog.setMessage(message); - dialog.setIndeterminate(indeterminate); - dialog.setCancelable(cancelable); - dialog.setOnCancelListener(cancelListener); - if (cancelable) { - dialog.setCanceledOnTouchOutside(false); - dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), - ((dialog1, which) -> cancelListener.onCancel(dialog))); - } - dialog.show(); - return dialog; - } - + dialog.show(); + return dialog; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java b/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java index 7d9648444..1ba1dbd93 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java +++ b/src/main/java/org/thoughtcrime/securesms/util/views/Stub.java @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.util.views; - import android.view.ViewStub; - import androidx.annotation.NonNull; public class Stub { @@ -16,7 +14,7 @@ public Stub(@NonNull ViewStub viewStub) { public T get() { if (view == null) { - view = (T)viewStub.inflate(); + view = (T) viewStub.inflate(); viewStub = null; } @@ -26,5 +24,4 @@ public T get() { public boolean resolved() { return view != null; } - } diff --git a/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java b/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java index eb46b5f8f..f0f11f1d2 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java +++ b/src/main/java/org/thoughtcrime/securesms/video/VideoPlayer.java @@ -21,10 +21,8 @@ import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.MediaItem; @@ -41,7 +39,6 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.util.ViewUtil; @@ -51,8 +48,8 @@ public class VideoPlayer extends FrameLayout { @Nullable private final StyledPlayerView exoView; - @Nullable private SimpleExoPlayer exoPlayer; - @Nullable private Window window; + @Nullable private SimpleExoPlayer exoPlayer; + @Nullable private Window window; public VideoPlayer(Context context) { this(context, null); @@ -70,8 +67,7 @@ public VideoPlayer(Context context, AttributeSet attrs, int defStyleAttr) { this.exoView = ViewUtil.findById(this, R.id.video_view); } - public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay) - { + public void setVideoSource(@NonNull VideoSlide videoSource, boolean autoplay) { setExoViewSource(videoSource, autoplay); } @@ -91,27 +87,30 @@ public void setWindow(@Nullable Window window) { this.window = window; } - private void setExoViewSource(@NonNull VideoSlide videoSource, boolean autoplay) - { - BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(getContext()).build(); - TrackSelector trackSelector = new DefaultTrackSelector(getContext()); - LoadControl loadControl = new DefaultLoadControl(); - - exoPlayer = new SimpleExoPlayer.Builder(getContext()) - .setTrackSelector(trackSelector) - .setBandwidthMeter(bandwidthMeter) - .setLoadControl(loadControl) - .build(); + private void setExoViewSource(@NonNull VideoSlide videoSource, boolean autoplay) { + BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(getContext()).build(); + TrackSelector trackSelector = new DefaultTrackSelector(getContext()); + LoadControl loadControl = new DefaultLoadControl(); + + exoPlayer = + new SimpleExoPlayer.Builder(getContext()) + .setTrackSelector(trackSelector) + .setBandwidthMeter(bandwidthMeter) + .setLoadControl(loadControl) + .build(); exoPlayer.addListener(new ExoPlayerListener(window)); //noinspection ConstantConditions exoView.setPlayer(exoPlayer); - DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null); - AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory); - ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + DefaultDataSourceFactory defaultDataSourceFactory = + new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null); + AttachmentDataSourceFactory attachmentDataSourceFactory = + new AttachmentDataSourceFactory(defaultDataSourceFactory); + ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - MediaSource mediaSource = new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory) - .createMediaSource(MediaItem.fromUri(videoSource.getUri())); + MediaSource mediaSource = + new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory) + .createMediaSource(MediaItem.fromUri(videoSource.getUri())); exoPlayer.prepare(new LoopingMediaSource(mediaSource)); exoPlayer.setPlayWhenReady(autoplay); @@ -126,7 +125,7 @@ private static class ExoPlayerListener implements Player.Listener { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - switch(playbackState) { + switch (playbackState) { case Player.STATE_IDLE: case Player.STATE_BUFFERING: case Player.STATE_ENDED: @@ -144,6 +143,4 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { } } } - - } diff --git a/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java b/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java index af37ca8d0..656e1cf45 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java +++ b/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java @@ -1,13 +1,10 @@ package org.thoughtcrime.securesms.video.exo; - import android.net.Uri; - import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.TransferListener; - import java.io.IOException; import java.util.Collections; import java.util.List; @@ -24,8 +21,7 @@ public AttachmentDataSource(DefaultDataSource defaultDataSource) { } @Override - public void addTransferListener(TransferListener transferListener) { - } + public void addTransferListener(TransferListener transferListener) {} @Override public long open(DataSpec dataSpec) throws IOException { diff --git a/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java b/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java index 1638db7d4..a9a243abe 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java +++ b/src/main/java/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.video.exo; - import androidx.annotation.NonNull; - import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -10,8 +8,7 @@ public class AttachmentDataSourceFactory implements DataSource.Factory { private final DefaultDataSourceFactory defaultDataSourceFactory; - public AttachmentDataSourceFactory(@NonNull DefaultDataSourceFactory defaultDataSourceFactory) - { + public AttachmentDataSourceFactory(@NonNull DefaultDataSourceFactory defaultDataSourceFactory) { this.defaultDataSourceFactory = defaultDataSourceFactory; } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/InputSurface.java b/src/main/java/org/thoughtcrime/securesms/video/recode/InputSurface.java index 07ef7fdc0..c649ac81c 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/InputSurface.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/InputSurface.java @@ -28,107 +28,109 @@ @TargetApi(17) public class InputSurface { - private static final int EGL_RECORDABLE_ANDROID = 0x3142; - private static final int EGL_OPENGL_ES2_BIT = 4; - private EGLDisplay mEGLDisplay; - private EGLContext mEGLContext; - private EGLSurface mEGLSurface; - private Surface mSurface; - - public InputSurface(Surface surface) { - if (surface == null) { - throw new NullPointerException(); - } - mSurface = surface; - eglSetup(); + private static final int EGL_RECORDABLE_ANDROID = 0x3142; + private static final int EGL_OPENGL_ES2_BIT = 4; + private EGLDisplay mEGLDisplay; + private EGLContext mEGLContext; + private EGLSurface mEGLSurface; + private Surface mSurface; + + public InputSurface(Surface surface) { + if (surface == null) { + throw new NullPointerException(); } - - private void eglSetup() { - mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { - throw new RuntimeException("unable to get EGL14 display"); - } - int[] version = new int[2]; - if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { - mEGLDisplay = null; - throw new RuntimeException("unable to initialize EGL14"); - } - - int[] attribList = { - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_RECORDABLE_ANDROID, 1, - EGL14.EGL_NONE - }; - EGLConfig[] configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, - numConfigs, 0)) { - throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config"); - } - - int[] attrib_list = { - EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, - EGL14.EGL_NONE - }; - - mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0); - checkEglError("eglCreateContext"); - if (mEGLContext == null) { - throw new RuntimeException("null context"); - } - - int[] surfaceAttribs = { - EGL14.EGL_NONE - }; - mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface, - surfaceAttribs, 0); - checkEglError("eglCreateWindowSurface"); - if (mEGLSurface == null) { - throw new RuntimeException("surface was null"); - } + mSurface = surface; + eglSetup(); + } + + private void eglSetup() { + mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { + throw new RuntimeException("unable to get EGL14 display"); } - - public void release() { - if (EGL14.eglGetCurrentContext().equals(mEGLContext)) { - EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); - } - EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface); - EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); - mSurface.release(); - mEGLDisplay = null; - mEGLContext = null; - mEGLSurface = null; - mSurface = null; + int[] version = new int[2]; + if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { + mEGLDisplay = null; + throw new RuntimeException("unable to initialize EGL14"); } - public void makeCurrent() { - if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { - throw new RuntimeException("eglMakeCurrent failed"); - } + int[] attribList = { + EGL14.EGL_RED_SIZE, + 8, + EGL14.EGL_GREEN_SIZE, + 8, + EGL14.EGL_BLUE_SIZE, + 8, + EGL14.EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_RECORDABLE_ANDROID, + 1, + EGL14.EGL_NONE + }; + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + if (!EGL14.eglChooseConfig( + mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) { + throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config"); } - public boolean swapBuffers() { - return EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface); + int[] attrib_list = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + + mEGLContext = + EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0); + checkEglError("eglCreateContext"); + if (mEGLContext == null) { + throw new RuntimeException("null context"); } - public Surface getSurface() { - return mSurface; + int[] surfaceAttribs = {EGL14.EGL_NONE}; + mEGLSurface = + EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface, surfaceAttribs, 0); + checkEglError("eglCreateWindowSurface"); + if (mEGLSurface == null) { + throw new RuntimeException("surface was null"); } + } - public void setPresentationTime(long nsecs) { - EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs); + public void release() { + if (EGL14.eglGetCurrentContext().equals(mEGLContext)) { + EGL14.eglMakeCurrent( + mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + } + EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface); + EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); + mSurface.release(); + mEGLDisplay = null; + mEGLContext = null; + mEGLSurface = null; + mSurface = null; + } + + public void makeCurrent() { + if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { + throw new RuntimeException("eglMakeCurrent failed"); } + } - private void checkEglError(String msg) { - boolean failed = false; - while (EGL14.eglGetError() != EGL14.EGL_SUCCESS) { - failed = true; - } - if (failed) { - throw new RuntimeException("EGL error encountered (see log)"); - } + public boolean swapBuffers() { + return EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface); + } + + public Surface getSurface() { + return mSurface; + } + + public void setPresentationTime(long nsecs) { + EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs); + } + + private void checkEglError(String msg) { + boolean failed = false; + while (EGL14.eglGetError() != EGL14.EGL_SUCCESS) { + failed = true; + } + if (failed) { + throw new RuntimeException("EGL error encountered (see log)"); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/MP4Builder.java b/src/main/java/org/thoughtcrime/securesms/video/recode/MP4Builder.java index b5502928a..06f1989b8 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/MP4Builder.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/MP4Builder.java @@ -3,7 +3,6 @@ import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.MediaFormat; - import com.coremedia.iso.BoxParser; import com.coremedia.iso.IsoFile; import com.coremedia.iso.IsoTypeWriter; @@ -29,7 +28,6 @@ import com.coremedia.iso.boxes.TrackHeaderBox; import com.googlecode.mp4parser.DataSource; import com.googlecode.mp4parser.util.Matrix; - import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; @@ -44,394 +42,396 @@ @TargetApi(16) public class MP4Builder { - private InterleaveChunkMdat mdat = null; - private Mp4Movie currentMp4Movie = null; - private FileOutputStream fos = null; - private FileChannel fc = null; - private long dataOffset = 0; - private long writedSinceLastMdat = 0; - private boolean writeNewMdat = true; - private final HashMap track2SampleSizes = new HashMap<>(); - private ByteBuffer sizeBuffer = null; + private InterleaveChunkMdat mdat = null; + private Mp4Movie currentMp4Movie = null; + private FileOutputStream fos = null; + private FileChannel fc = null; + private long dataOffset = 0; + private long writedSinceLastMdat = 0; + private boolean writeNewMdat = true; + private final HashMap track2SampleSizes = new HashMap<>(); + private ByteBuffer sizeBuffer = null; + + public MP4Builder createMovie(Mp4Movie mp4Movie) throws Exception { + currentMp4Movie = mp4Movie; + + fos = new FileOutputStream(mp4Movie.getCacheFile()); + fc = fos.getChannel(); + + FileTypeBox fileTypeBox = createFileTypeBox(); + fileTypeBox.getBox(fc); + dataOffset += fileTypeBox.getSize(); + writedSinceLastMdat += dataOffset; + + mdat = new InterleaveChunkMdat(); + + sizeBuffer = ByteBuffer.allocateDirect(4); + + return this; + } + + private void flushCurrentMdat() throws Exception { + long oldPosition = fc.position(); + fc.position(mdat.getOffset()); + mdat.getBox(fc); + fc.position(oldPosition); + mdat.setDataOffset(0); + mdat.setContentSize(0); + fos.flush(); + } + + public boolean writeSampleData( + int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo, boolean isAudio) + throws Exception { + if (writeNewMdat) { + mdat.setContentSize(0); + mdat.getBox(fc); + mdat.setDataOffset(dataOffset); + dataOffset += 16; + writedSinceLastMdat += 16; + writeNewMdat = false; + } - public MP4Builder createMovie(Mp4Movie mp4Movie) throws Exception { - currentMp4Movie = mp4Movie; + mdat.setContentSize(mdat.getContentSize() + bufferInfo.size); + writedSinceLastMdat += bufferInfo.size; - fos = new FileOutputStream(mp4Movie.getCacheFile()); - fc = fos.getChannel(); + boolean flush = false; + if (writedSinceLastMdat >= 32 * 1024) { + flushCurrentMdat(); + writeNewMdat = true; + flush = true; + writedSinceLastMdat -= 32 * 1024; + } - FileTypeBox fileTypeBox = createFileTypeBox(); - fileTypeBox.getBox(fc); - dataOffset += fileTypeBox.getSize(); - writedSinceLastMdat += dataOffset; + currentMp4Movie.addSample(trackIndex, dataOffset, bufferInfo); + byteBuf.position(bufferInfo.offset + (isAudio ? 0 : 4)); + byteBuf.limit(bufferInfo.offset + bufferInfo.size); - mdat = new InterleaveChunkMdat(); + if (!isAudio) { + sizeBuffer.position(0); + sizeBuffer.putInt(bufferInfo.size - 4); + sizeBuffer.position(0); + fc.write(sizeBuffer); + } - sizeBuffer = ByteBuffer.allocateDirect(4); + fc.write(byteBuf); + dataOffset += bufferInfo.size; - return this; + if (flush) { + fos.flush(); } + return flush; + } - private void flushCurrentMdat() throws Exception { - long oldPosition = fc.position(); - fc.position(mdat.getOffset()); - mdat.getBox(fc); - fc.position(oldPosition); - mdat.setDataOffset(0); - mdat.setContentSize(0); - fos.flush(); - } + public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception { + return currentMp4Movie.addTrack(mediaFormat, isAudio); + } - public boolean writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo, boolean isAudio) throws Exception { - if (writeNewMdat) { - mdat.setContentSize(0); - mdat.getBox(fc); - mdat.setDataOffset(dataOffset); - dataOffset += 16; - writedSinceLastMdat += 16; - writeNewMdat = false; - } + public void finishMovie(boolean error) throws Exception { + if (mdat.getContentSize() != 0) { + flushCurrentMdat(); + } - mdat.setContentSize(mdat.getContentSize() + bufferInfo.size); - writedSinceLastMdat += bufferInfo.size; + for (Track track : currentMp4Movie.getTracks()) { + List samples = track.getSamples(); + long[] sizes = new long[samples.size()]; + for (int i = 0; i < sizes.length; i++) { + sizes[i] = samples.get(i).getSize(); + } + track2SampleSizes.put(track, sizes); + } - boolean flush = false; - if (writedSinceLastMdat >= 32 * 1024) { - flushCurrentMdat(); - writeNewMdat = true; - flush = true; - writedSinceLastMdat -= 32 * 1024; - } + Box moov = createMovieBox(currentMp4Movie); + moov.getBox(fc); + fos.flush(); - currentMp4Movie.addSample(trackIndex, dataOffset, bufferInfo); - byteBuf.position(bufferInfo.offset + (isAudio ? 0 : 4)); - byteBuf.limit(bufferInfo.offset + bufferInfo.size); + fc.close(); + fos.close(); + } - if (!isAudio) { - sizeBuffer.position(0); - sizeBuffer.putInt(bufferInfo.size - 4); - sizeBuffer.position(0); - fc.write(sizeBuffer); - } + protected FileTypeBox createFileTypeBox() { + LinkedList minorBrands = new LinkedList<>(); + minorBrands.add("isom"); + minorBrands.add("3gp4"); + return new FileTypeBox("isom", 0, minorBrands); + } - fc.write(byteBuf); - dataOffset += bufferInfo.size; + private class InterleaveChunkMdat implements Box { + private Container parent; + private long contentSize = 1024 * 1024 * 1024; + private long dataOffset = 0; - if (flush) { - fos.flush(); - } - return flush; + public Container getParent() { + return parent; } - public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception { - return currentMp4Movie.addTrack(mediaFormat, isAudio); + public long getOffset() { + return dataOffset; } - public void finishMovie(boolean error) throws Exception { - if (mdat.getContentSize() != 0) { - flushCurrentMdat(); - } - - for (Track track : currentMp4Movie.getTracks()) { - List samples = track.getSamples(); - long[] sizes = new long[samples.size()]; - for (int i = 0; i < sizes.length; i++) { - sizes[i] = samples.get(i).getSize(); - } - track2SampleSizes.put(track, sizes); - } - - Box moov = createMovieBox(currentMp4Movie); - moov.getBox(fc); - fos.flush(); - - fc.close(); - fos.close(); + public void setDataOffset(long offset) { + dataOffset = offset; } - protected FileTypeBox createFileTypeBox() { - LinkedList minorBrands = new LinkedList<>(); - minorBrands.add("isom"); - minorBrands.add("3gp4"); - return new FileTypeBox("isom", 0, minorBrands); + public void setParent(Container parent) { + this.parent = parent; } - private class InterleaveChunkMdat implements Box { - private Container parent; - private long contentSize = 1024 * 1024 * 1024; - private long dataOffset = 0; - - public Container getParent() { - return parent; - } - - public long getOffset() { - return dataOffset; - } - - public void setDataOffset(long offset) { - dataOffset = offset; - } - - public void setParent(Container parent) { - this.parent = parent; - } - - public void setContentSize(long contentSize) { - this.contentSize = contentSize; - } - - public long getContentSize() { - return contentSize; - } - - public String getType() { - return "mdat"; - } - - public long getSize() { - return 16 + contentSize; - } + public void setContentSize(long contentSize) { + this.contentSize = contentSize; + } - private boolean isSmallBox(long contentSize) { - return (contentSize + 8) < 4294967296L; - } + public long getContentSize() { + return contentSize; + } - @Override - public void parse(DataSource dataSource, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { + public String getType() { + return "mdat"; + } - } + public long getSize() { + return 16 + contentSize; + } - public void getBox(WritableByteChannel writableByteChannel) throws IOException { - ByteBuffer bb = ByteBuffer.allocate(16); - long size = getSize(); - if (isSmallBox(size)) { - IsoTypeWriter.writeUInt32(bb, size); - } else { - IsoTypeWriter.writeUInt32(bb, 1); - } - bb.put(IsoFile.fourCCtoBytes("mdat")); - if (isSmallBox(size)) { - bb.put(new byte[8]); - } else { - IsoTypeWriter.writeUInt64(bb, size); - } - bb.rewind(); - writableByteChannel.write(bb); - } + private boolean isSmallBox(long contentSize) { + return (contentSize + 8) < 4294967296L; } - public static long gcd(long a, long b) { - if (b == 0) { - return a; - } - return gcd(b, a % b); + @Override + public void parse( + DataSource dataSource, ByteBuffer header, long contentSize, BoxParser boxParser) + throws IOException {} + + public void getBox(WritableByteChannel writableByteChannel) throws IOException { + ByteBuffer bb = ByteBuffer.allocate(16); + long size = getSize(); + if (isSmallBox(size)) { + IsoTypeWriter.writeUInt32(bb, size); + } else { + IsoTypeWriter.writeUInt32(bb, 1); + } + bb.put(IsoFile.fourCCtoBytes("mdat")); + if (isSmallBox(size)) { + bb.put(new byte[8]); + } else { + IsoTypeWriter.writeUInt64(bb, size); + } + bb.rewind(); + writableByteChannel.write(bb); } + } - public long getTimescale(Mp4Movie mp4Movie) { - long timescale = 0; - if (!mp4Movie.getTracks().isEmpty()) { - timescale = mp4Movie.getTracks().iterator().next().getTimeScale(); - } - for (Track track : mp4Movie.getTracks()) { - timescale = gcd(track.getTimeScale(), timescale); - } - return timescale; + public static long gcd(long a, long b) { + if (b == 0) { + return a; } + return gcd(b, a % b); + } - protected MovieBox createMovieBox(Mp4Movie movie) { - MovieBox movieBox = new MovieBox(); - MovieHeaderBox mvhd = new MovieHeaderBox(); - - mvhd.setCreationTime(new Date()); - mvhd.setModificationTime(new Date()); - mvhd.setMatrix(Matrix.ROTATE_0); - long movieTimeScale = getTimescale(movie); - long duration = 0; - - for (Track track : movie.getTracks()) { - long tracksDuration = track.getDuration() * movieTimeScale / track.getTimeScale(); - if (tracksDuration > duration) { - duration = tracksDuration; - } - } + public long getTimescale(Mp4Movie mp4Movie) { + long timescale = 0; + if (!mp4Movie.getTracks().isEmpty()) { + timescale = mp4Movie.getTracks().iterator().next().getTimeScale(); + } + for (Track track : mp4Movie.getTracks()) { + timescale = gcd(track.getTimeScale(), timescale); + } + return timescale; + } + + protected MovieBox createMovieBox(Mp4Movie movie) { + MovieBox movieBox = new MovieBox(); + MovieHeaderBox mvhd = new MovieHeaderBox(); + + mvhd.setCreationTime(new Date()); + mvhd.setModificationTime(new Date()); + mvhd.setMatrix(Matrix.ROTATE_0); + long movieTimeScale = getTimescale(movie); + long duration = 0; + + for (Track track : movie.getTracks()) { + long tracksDuration = track.getDuration() * movieTimeScale / track.getTimeScale(); + if (tracksDuration > duration) { + duration = tracksDuration; + } + } - mvhd.setDuration(duration); - mvhd.setTimescale(movieTimeScale); - mvhd.setNextTrackId(movie.getTracks().size() + 1); + mvhd.setDuration(duration); + mvhd.setTimescale(movieTimeScale); + mvhd.setNextTrackId(movie.getTracks().size() + 1); - movieBox.addBox(mvhd); - for (Track track : movie.getTracks()) { - movieBox.addBox(createTrackBox(track, movie)); - } - return movieBox; + movieBox.addBox(mvhd); + for (Track track : movie.getTracks()) { + movieBox.addBox(createTrackBox(track, movie)); } - - protected TrackBox createTrackBox(Track track, Mp4Movie movie) { - TrackBox trackBox = new TrackBox(); - TrackHeaderBox tkhd = new TrackHeaderBox(); - - tkhd.setEnabled(true); - tkhd.setInMovie(true); - tkhd.setInPreview(true); - if (track.isAudio()) { - tkhd.setMatrix(Matrix.ROTATE_0); - } else { - tkhd.setMatrix(movie.getMatrix()); - } - tkhd.setAlternateGroup(0); - tkhd.setCreationTime(track.getCreationTime()); - tkhd.setDuration(track.getDuration() * getTimescale(movie) / track.getTimeScale()); - tkhd.setHeight(track.getHeight()); - tkhd.setWidth(track.getWidth()); - tkhd.setLayer(0); - tkhd.setModificationTime(new Date()); - tkhd.setTrackId(track.getTrackId() + 1); - tkhd.setVolume(track.getVolume()); - - trackBox.addBox(tkhd); - - MediaBox mdia = new MediaBox(); - trackBox.addBox(mdia); - MediaHeaderBox mdhd = new MediaHeaderBox(); - mdhd.setCreationTime(track.getCreationTime()); - mdhd.setDuration(track.getDuration()); - mdhd.setTimescale(track.getTimeScale()); - mdhd.setLanguage("eng"); - mdia.addBox(mdhd); - HandlerBox hdlr = new HandlerBox(); - hdlr.setName(track.isAudio() ? "SoundHandle" : "VideoHandle"); - hdlr.setHandlerType(track.getHandler()); - - mdia.addBox(hdlr); - - MediaInformationBox minf = new MediaInformationBox(); - minf.addBox(track.getMediaHeaderBox()); - - DataInformationBox dinf = new DataInformationBox(); - DataReferenceBox dref = new DataReferenceBox(); - dinf.addBox(dref); - DataEntryUrlBox url = new DataEntryUrlBox(); - url.setFlags(1); - dref.addBox(url); - minf.addBox(dinf); - - Box stbl = createStbl(track); - minf.addBox(stbl); - mdia.addBox(minf); - - return trackBox; + return movieBox; + } + + protected TrackBox createTrackBox(Track track, Mp4Movie movie) { + TrackBox trackBox = new TrackBox(); + TrackHeaderBox tkhd = new TrackHeaderBox(); + + tkhd.setEnabled(true); + tkhd.setInMovie(true); + tkhd.setInPreview(true); + if (track.isAudio()) { + tkhd.setMatrix(Matrix.ROTATE_0); + } else { + tkhd.setMatrix(movie.getMatrix()); + } + tkhd.setAlternateGroup(0); + tkhd.setCreationTime(track.getCreationTime()); + tkhd.setDuration(track.getDuration() * getTimescale(movie) / track.getTimeScale()); + tkhd.setHeight(track.getHeight()); + tkhd.setWidth(track.getWidth()); + tkhd.setLayer(0); + tkhd.setModificationTime(new Date()); + tkhd.setTrackId(track.getTrackId() + 1); + tkhd.setVolume(track.getVolume()); + + trackBox.addBox(tkhd); + + MediaBox mdia = new MediaBox(); + trackBox.addBox(mdia); + MediaHeaderBox mdhd = new MediaHeaderBox(); + mdhd.setCreationTime(track.getCreationTime()); + mdhd.setDuration(track.getDuration()); + mdhd.setTimescale(track.getTimeScale()); + mdhd.setLanguage("eng"); + mdia.addBox(mdhd); + HandlerBox hdlr = new HandlerBox(); + hdlr.setName(track.isAudio() ? "SoundHandle" : "VideoHandle"); + hdlr.setHandlerType(track.getHandler()); + + mdia.addBox(hdlr); + + MediaInformationBox minf = new MediaInformationBox(); + minf.addBox(track.getMediaHeaderBox()); + + DataInformationBox dinf = new DataInformationBox(); + DataReferenceBox dref = new DataReferenceBox(); + dinf.addBox(dref); + DataEntryUrlBox url = new DataEntryUrlBox(); + url.setFlags(1); + dref.addBox(url); + minf.addBox(dinf); + + Box stbl = createStbl(track); + minf.addBox(stbl); + mdia.addBox(minf); + + return trackBox; + } + + protected Box createStbl(Track track) { + SampleTableBox stbl = new SampleTableBox(); + + createStsd(track, stbl); + createStts(track, stbl); + createStss(track, stbl); + createStsc(track, stbl); + createStsz(track, stbl); + createStco(track, stbl); + + return stbl; + } + + protected void createStsd(Track track, SampleTableBox stbl) { + stbl.addBox(track.getSampleDescriptionBox()); + } + + protected void createStts(Track track, SampleTableBox stbl) { + TimeToSampleBox.Entry lastEntry = null; + List entries = new ArrayList<>(); + + for (long delta : track.getSampleDurations()) { + if (lastEntry != null && lastEntry.getDelta() == delta) { + lastEntry.setCount(lastEntry.getCount() + 1); + } else { + lastEntry = new TimeToSampleBox.Entry(1, delta); + entries.add(lastEntry); + } + } + TimeToSampleBox stts = new TimeToSampleBox(); + stts.setEntries(entries); + stbl.addBox(stts); + } + + protected void createStss(Track track, SampleTableBox stbl) { + long[] syncSamples = track.getSyncSamples(); + if (syncSamples != null && syncSamples.length > 0) { + SyncSampleBox stss = new SyncSampleBox(); + stss.setSampleNumber(syncSamples); + stbl.addBox(stss); } + } - protected Box createStbl(Track track) { - SampleTableBox stbl = new SampleTableBox(); + protected void createStsc(Track track, SampleTableBox stbl) { + SampleToChunkBox stsc = new SampleToChunkBox(); + stsc.setEntries(new LinkedList()); - createStsd(track, stbl); - createStts(track, stbl); - createStss(track, stbl); - createStsc(track, stbl); - createStsz(track, stbl); - createStco(track, stbl); + long lastOffset; + int lastChunkNumber = 1; + int lastSampleCount = 0; - return stbl; - } + int previousWritedChunkCount = -1; - protected void createStsd(Track track, SampleTableBox stbl) { - stbl.addBox(track.getSampleDescriptionBox()); - } + int samplesCount = track.getSamples().size(); + for (int a = 0; a < samplesCount; a++) { + Sample sample = track.getSamples().get(a); + long offset = sample.getOffset(); + long size = sample.getSize(); - protected void createStts(Track track, SampleTableBox stbl) { - TimeToSampleBox.Entry lastEntry = null; - List entries = new ArrayList<>(); - - for (long delta : track.getSampleDurations()) { - if (lastEntry != null && lastEntry.getDelta() == delta) { - lastEntry.setCount(lastEntry.getCount() + 1); - } else { - lastEntry = new TimeToSampleBox.Entry(1, delta); - entries.add(lastEntry); - } - } - TimeToSampleBox stts = new TimeToSampleBox(); - stts.setEntries(entries); - stbl.addBox(stts); - } + lastOffset = offset + size; + lastSampleCount++; - protected void createStss(Track track, SampleTableBox stbl) { - long[] syncSamples = track.getSyncSamples(); - if (syncSamples != null && syncSamples.length > 0) { - SyncSampleBox stss = new SyncSampleBox(); - stss.setSampleNumber(syncSamples); - stbl.addBox(stss); + boolean write = false; + if (a != samplesCount - 1) { + Sample nextSample = track.getSamples().get(a + 1); + if (lastOffset != nextSample.getOffset()) { + write = true; } - } - - protected void createStsc(Track track, SampleTableBox stbl) { - SampleToChunkBox stsc = new SampleToChunkBox(); - stsc.setEntries(new LinkedList()); - - long lastOffset; - int lastChunkNumber = 1; - int lastSampleCount = 0; - - int previousWritedChunkCount = -1; - - int samplesCount = track.getSamples().size(); - for (int a = 0; a < samplesCount; a++) { - Sample sample = track.getSamples().get(a); - long offset = sample.getOffset(); - long size = sample.getSize(); - - lastOffset = offset + size; - lastSampleCount++; - - boolean write = false; - if (a != samplesCount - 1) { - Sample nextSample = track.getSamples().get(a + 1); - if (lastOffset != nextSample.getOffset()) { - write = true; - } - } else { - write = true; - } - if (write) { - if (previousWritedChunkCount != lastSampleCount) { - stsc.getEntries().add(new SampleToChunkBox.Entry(lastChunkNumber, lastSampleCount, 1)); - previousWritedChunkCount = lastSampleCount; - } - lastSampleCount = 0; - lastChunkNumber++; - } + } else { + write = true; + } + if (write) { + if (previousWritedChunkCount != lastSampleCount) { + stsc.getEntries().add(new SampleToChunkBox.Entry(lastChunkNumber, lastSampleCount, 1)); + previousWritedChunkCount = lastSampleCount; } - stbl.addBox(stsc); + lastSampleCount = 0; + lastChunkNumber++; + } } - - protected void createStsz(Track track, SampleTableBox stbl) { - SampleSizeBox stsz = new SampleSizeBox(); - stsz.setSampleSizes(track2SampleSizes.get(track)); - stbl.addBox(stsz); + stbl.addBox(stsc); + } + + protected void createStsz(Track track, SampleTableBox stbl) { + SampleSizeBox stsz = new SampleSizeBox(); + stsz.setSampleSizes(track2SampleSizes.get(track)); + stbl.addBox(stsz); + } + + protected void createStco(Track track, SampleTableBox stbl) { + ArrayList chunksOffsets = new ArrayList<>(); + long lastOffset = -1; + for (Sample sample : track.getSamples()) { + long offset = sample.getOffset(); + if (lastOffset != -1 && lastOffset != offset) { + lastOffset = -1; + } + if (lastOffset == -1) { + chunksOffsets.add(offset); + } + lastOffset = offset + sample.getSize(); } - - protected void createStco(Track track, SampleTableBox stbl) { - ArrayList chunksOffsets = new ArrayList<>(); - long lastOffset = -1; - for (Sample sample : track.getSamples()) { - long offset = sample.getOffset(); - if (lastOffset != -1 && lastOffset != offset) { - lastOffset = -1; - } - if (lastOffset == -1) { - chunksOffsets.add(offset); - } - lastOffset = offset + sample.getSize(); - } - long[] chunkOffsetsLong = new long[chunksOffsets.size()]; - for (int a = 0; a < chunksOffsets.size(); a++) { - chunkOffsetsLong[a] = chunksOffsets.get(a); - } - - StaticChunkOffsetBox stco = new StaticChunkOffsetBox(); - stco.setChunkOffsets(chunkOffsetsLong); - stbl.addBox(stco); + long[] chunkOffsetsLong = new long[chunksOffsets.size()]; + for (int a = 0; a < chunksOffsets.size(); a++) { + chunkOffsetsLong[a] = chunksOffsets.get(a); } + + StaticChunkOffsetBox stco = new StaticChunkOffsetBox(); + stco.setChunkOffsets(chunkOffsetsLong); + stbl.addBox(stco); + } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/Mp4Movie.java b/src/main/java/org/thoughtcrime/securesms/video/recode/Mp4Movie.java index 6d2a9b001..fa9eaf57c 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/Mp4Movie.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/Mp4Movie.java @@ -3,71 +3,70 @@ import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.MediaFormat; - import com.googlecode.mp4parser.util.Matrix; - import java.io.File; import java.util.ArrayList; @TargetApi(16) public class Mp4Movie { - private Matrix matrix = Matrix.ROTATE_0; - private final ArrayList tracks = new ArrayList<>(); - private File cacheFile; - private int width; - private int height; + private Matrix matrix = Matrix.ROTATE_0; + private final ArrayList tracks = new ArrayList<>(); + private File cacheFile; + private int width; + private int height; - public Matrix getMatrix() { - return matrix; - } + public Matrix getMatrix() { + return matrix; + } - public int getWidth() { - return width; - } + public int getWidth() { + return width; + } - public int getHeight() { - return height; - } + public int getHeight() { + return height; + } - public void setCacheFile(File file) { - cacheFile = file; - } + public void setCacheFile(File file) { + cacheFile = file; + } - public void setRotation(int angle) { - if (angle == 0) { - matrix = Matrix.ROTATE_0; - } else if (angle == 90) { - matrix = Matrix.ROTATE_90; - } else if (angle == 180) { - matrix = Matrix.ROTATE_180; - } else if (angle == 270) { - matrix = Matrix.ROTATE_270; - } + public void setRotation(int angle) { + if (angle == 0) { + matrix = Matrix.ROTATE_0; + } else if (angle == 90) { + matrix = Matrix.ROTATE_90; + } else if (angle == 180) { + matrix = Matrix.ROTATE_180; + } else if (angle == 270) { + matrix = Matrix.ROTATE_270; } + } - public void setSize(int w, int h) { - width = w; - height = h; - } + public void setSize(int w, int h) { + width = w; + height = h; + } - public ArrayList getTracks() { - return tracks; - } + public ArrayList getTracks() { + return tracks; + } - public File getCacheFile() { - return cacheFile; - } + public File getCacheFile() { + return cacheFile; + } - public void addSample(int trackIndex, long offset, MediaCodec.BufferInfo bufferInfo) throws Exception { - if (trackIndex < 0 || trackIndex >= tracks.size()) { - return; - } - Track track = tracks.get(trackIndex); - track.addSample(offset, bufferInfo); + public void addSample(int trackIndex, long offset, MediaCodec.BufferInfo bufferInfo) + throws Exception { + if (trackIndex < 0 || trackIndex >= tracks.size()) { + return; } + Track track = tracks.get(trackIndex); + track.addSample(offset, bufferInfo); + } - public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception { - tracks.add(new Track(tracks.size(), mediaFormat, isAudio)); - return tracks.size() - 1; - } + public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception { + tracks.add(new Track(tracks.size(), mediaFormat, isAudio)); + return tracks.size() - 1; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/OutputSurface.java b/src/main/java/org/thoughtcrime/securesms/video/recode/OutputSurface.java index dd6937288..c73a9cff7 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/OutputSurface.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/OutputSurface.java @@ -20,10 +20,8 @@ import android.graphics.SurfaceTexture; import android.opengl.GLES20; import android.view.Surface; - import java.nio.ByteBuffer; import java.nio.ByteOrder; - import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; @@ -33,175 +31,173 @@ @TargetApi(16) public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { - private static final int EGL_OPENGL_ES2_BIT = 4; - private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - private EGL10 mEGL; - private EGLDisplay mEGLDisplay = null; - private EGLContext mEGLContext = null; - private EGLSurface mEGLSurface = null; - private SurfaceTexture mSurfaceTexture; - private Surface mSurface; - private final Object mFrameSyncObject = new Object(); - private boolean mFrameAvailable; - private TextureRenderer mTextureRender; - private int mWidth; - private int mHeight; - private int rotateRender = 0; - private ByteBuffer mPixelBuf; - - public OutputSurface(int width, int height, int rotate) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException(); - } - mWidth = width; - mHeight = height; - rotateRender = rotate; - mPixelBuf = ByteBuffer.allocateDirect(mWidth * mHeight * 4); - mPixelBuf.order(ByteOrder.LITTLE_ENDIAN); - eglSetup(width, height); - makeCurrent(); - setup(); + private static final int EGL_OPENGL_ES2_BIT = 4; + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private EGL10 mEGL; + private EGLDisplay mEGLDisplay = null; + private EGLContext mEGLContext = null; + private EGLSurface mEGLSurface = null; + private SurfaceTexture mSurfaceTexture; + private Surface mSurface; + private final Object mFrameSyncObject = new Object(); + private boolean mFrameAvailable; + private TextureRenderer mTextureRender; + private int mWidth; + private int mHeight; + private int rotateRender = 0; + private ByteBuffer mPixelBuf; + + public OutputSurface(int width, int height, int rotate) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException(); } - - public OutputSurface() { - setup(); + mWidth = width; + mHeight = height; + rotateRender = rotate; + mPixelBuf = ByteBuffer.allocateDirect(mWidth * mHeight * 4); + mPixelBuf.order(ByteOrder.LITTLE_ENDIAN); + eglSetup(width, height); + makeCurrent(); + setup(); + } + + public OutputSurface() { + setup(); + } + + private void setup() { + mTextureRender = new TextureRenderer(rotateRender); + mTextureRender.surfaceCreated(); + mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId()); + mSurfaceTexture.setOnFrameAvailableListener(this); + mSurface = new Surface(mSurfaceTexture); + } + + private void eglSetup(int width, int height) { + mEGL = (EGL10) EGLContext.getEGL(); + mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("unable to get EGL10 display"); } - private void setup() { - mTextureRender = new TextureRenderer(rotateRender); - mTextureRender.surfaceCreated(); - mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId()); - mSurfaceTexture.setOnFrameAvailableListener(this); - mSurface = new Surface(mSurfaceTexture); + if (!mEGL.eglInitialize(mEGLDisplay, null)) { + mEGLDisplay = null; + throw new RuntimeException("unable to initialize EGL10"); } - private void eglSetup(int width, int height) { - mEGL = (EGL10) EGLContext.getEGL(); - mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); - - if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) { - throw new RuntimeException("unable to get EGL10 display"); - } - - if (!mEGL.eglInitialize(mEGLDisplay, null)) { - mEGLDisplay = null; - throw new RuntimeException("unable to initialize EGL10"); - } - - int[] attribList = { - EGL10.EGL_RED_SIZE, 8, - EGL10.EGL_GREEN_SIZE, 8, - EGL10.EGL_BLUE_SIZE, 8, - EGL10.EGL_ALPHA_SIZE, 8, - EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT, - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL10.EGL_NONE - }; - EGLConfig[] configs = new EGLConfig[1]; - int[] numConfigs = new int[1]; - if (!mEGL.eglChooseConfig(mEGLDisplay, attribList, configs, configs.length, numConfigs)) { - throw new RuntimeException("unable to find RGB888+pbuffer EGL config"); - } - int[] attrib_list = { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL10.EGL_NONE - }; - mEGLContext = mEGL.eglCreateContext(mEGLDisplay, configs[0], EGL10.EGL_NO_CONTEXT, attrib_list); - checkEglError("eglCreateContext"); - if (mEGLContext == null) { - throw new RuntimeException("null context"); - } - int[] surfaceAttribs = { - EGL10.EGL_WIDTH, width, - EGL10.EGL_HEIGHT, height, - EGL10.EGL_NONE - }; - mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs); - checkEglError("eglCreatePbufferSurface"); - if (mEGLSurface == null) { - throw new RuntimeException("surface was null"); - } + int[] attribList = { + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 8, + EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + }; + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + if (!mEGL.eglChooseConfig(mEGLDisplay, attribList, configs, configs.length, numConfigs)) { + throw new RuntimeException("unable to find RGB888+pbuffer EGL config"); } - - public void release() { - if (mEGL != null) { - if (mEGL.eglGetCurrentContext().equals(mEGLContext)) { - mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); - } - mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface); - mEGL.eglDestroyContext(mEGLDisplay, mEGLContext); - } - mSurface.release(); - mEGLDisplay = null; - mEGLContext = null; - mEGLSurface = null; - mEGL = null; - mTextureRender = null; - mSurface = null; - mSurfaceTexture = null; + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE}; + mEGLContext = mEGL.eglCreateContext(mEGLDisplay, configs[0], EGL10.EGL_NO_CONTEXT, attrib_list); + checkEglError("eglCreateContext"); + if (mEGLContext == null) { + throw new RuntimeException("null context"); } - - public void makeCurrent() { - if (mEGL == null) { - throw new RuntimeException("not configured for makeCurrent"); - } - checkEglError("before makeCurrent"); - if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { - throw new RuntimeException("eglMakeCurrent failed"); - } + int[] surfaceAttribs = { + EGL10.EGL_WIDTH, width, + EGL10.EGL_HEIGHT, height, + EGL10.EGL_NONE + }; + mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs); + checkEglError("eglCreatePbufferSurface"); + if (mEGLSurface == null) { + throw new RuntimeException("surface was null"); } - - public Surface getSurface() { - return mSurface; + } + + public void release() { + if (mEGL != null) { + if (mEGL.eglGetCurrentContext().equals(mEGLContext)) { + mEGL.eglMakeCurrent( + mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + } + mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface); + mEGL.eglDestroyContext(mEGLDisplay, mEGLContext); } - - public void changeFragmentShader(String fragmentShader) { - mTextureRender.changeFragmentShader(fragmentShader); + mSurface.release(); + mEGLDisplay = null; + mEGLContext = null; + mEGLSurface = null; + mEGL = null; + mTextureRender = null; + mSurface = null; + mSurfaceTexture = null; + } + + public void makeCurrent() { + if (mEGL == null) { + throw new RuntimeException("not configured for makeCurrent"); } - - public void awaitNewImage() { - final int TIMEOUT_MS = 2500; - synchronized (mFrameSyncObject) { - while (!mFrameAvailable) { - try { - mFrameSyncObject.wait(TIMEOUT_MS); - if (!mFrameAvailable) { - throw new RuntimeException("Surface frame wait timed out"); - } - } catch (InterruptedException ie) { - throw new RuntimeException(ie); - } - } - mFrameAvailable = false; - } - mTextureRender.checkGlError("before updateTexImage"); - mSurfaceTexture.updateTexImage(); + checkEglError("before makeCurrent"); + if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { + throw new RuntimeException("eglMakeCurrent failed"); } - - public void drawImage(boolean invert) { - mTextureRender.drawFrame(mSurfaceTexture, invert); - } - - @Override - public void onFrameAvailable(SurfaceTexture st) { - synchronized (mFrameSyncObject) { - if (mFrameAvailable) { - throw new RuntimeException("mFrameAvailable already set, frame could be dropped"); - } - mFrameAvailable = true; - mFrameSyncObject.notifyAll(); + } + + public Surface getSurface() { + return mSurface; + } + + public void changeFragmentShader(String fragmentShader) { + mTextureRender.changeFragmentShader(fragmentShader); + } + + public void awaitNewImage() { + final int TIMEOUT_MS = 2500; + synchronized (mFrameSyncObject) { + while (!mFrameAvailable) { + try { + mFrameSyncObject.wait(TIMEOUT_MS); + if (!mFrameAvailable) { + throw new RuntimeException("Surface frame wait timed out"); + } + } catch (InterruptedException ie) { + throw new RuntimeException(ie); } + } + mFrameAvailable = false; } - - public ByteBuffer getFrame() { - mPixelBuf.rewind(); - GLES20.glReadPixels(0, 0, mWidth, mHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf); - return mPixelBuf; + mTextureRender.checkGlError("before updateTexImage"); + mSurfaceTexture.updateTexImage(); + } + + public void drawImage(boolean invert) { + mTextureRender.drawFrame(mSurfaceTexture, invert); + } + + @Override + public void onFrameAvailable(SurfaceTexture st) { + synchronized (mFrameSyncObject) { + if (mFrameAvailable) { + throw new RuntimeException("mFrameAvailable already set, frame could be dropped"); + } + mFrameAvailable = true; + mFrameSyncObject.notifyAll(); } + } - private void checkEglError(String msg) { - if (mEGL.eglGetError() != EGL10.EGL_SUCCESS) { - throw new RuntimeException("EGL error encountered (see log)"); - } + public ByteBuffer getFrame() { + mPixelBuf.rewind(); + GLES20.glReadPixels(0, 0, mWidth, mHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf); + return mPixelBuf; + } + + private void checkEglError(String msg) { + if (mEGL.eglGetError() != EGL10.EGL_SUCCESS) { + throw new RuntimeException("EGL error encountered (see log)"); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/Sample.java b/src/main/java/org/thoughtcrime/securesms/video/recode/Sample.java index 9b1b3da1d..8ec136701 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/Sample.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/Sample.java @@ -1,19 +1,19 @@ package org.thoughtcrime.securesms.video.recode; public class Sample { - private long offset = 0; - private long size = 0; + private long offset = 0; + private long size = 0; - public Sample(long offset, long size) { - this.offset = offset; - this.size = size; - } + public Sample(long offset, long size) { + this.offset = offset; + this.size = size; + } - public long getOffset() { - return offset; - } + public long getOffset() { + return offset; + } - public long getSize() { - return size; - } + public long getSize() { + return size; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/TextureRenderer.java b/src/main/java/org/thoughtcrime/securesms/video/recode/TextureRenderer.java index 03c98dc6a..b0e3567da 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/TextureRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/TextureRenderer.java @@ -21,7 +21,6 @@ import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.opengl.Matrix; - import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; @@ -29,185 +28,204 @@ @TargetApi(16) public class TextureRenderer { - private static final int FLOAT_SIZE_BYTES = 4; - private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; - private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; - private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; - private static final float[] mTriangleVerticesData = { - -1.0f, -1.0f, 0, 0.f, 0.f, - 1.0f, -1.0f, 0, 1.f, 0.f, - -1.0f, 1.0f, 0, 0.f, 1.f, - 1.0f, 1.0f, 0, 1.f, 1.f, - }; - private final FloatBuffer mTriangleVertices; - - private static final String VERTEX_SHADER = - "uniform mat4 uMVPMatrix;\n" + - "uniform mat4 uSTMatrix;\n" + - "attribute vec4 aPosition;\n" + - "attribute vec4 aTextureCoord;\n" + - "varying vec2 vTextureCoord;\n" + - "void main() {\n" + - " gl_Position = uMVPMatrix * aPosition;\n" + - " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + - "}\n"; - - private static final String FRAGMENT_SHADER = - "#extension GL_OES_EGL_image_external : require\n" + - "precision mediump float;\n" + - "varying vec2 vTextureCoord;\n" + - "uniform samplerExternalOES sTexture;\n" + - "void main() {\n" + - " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + - "}\n"; - - private final float[] mMVPMatrix = new float[16]; - private final float[] mSTMatrix = new float[16]; - private int mProgram; - private int mTextureID = -12345; - private int muMVPMatrixHandle; - private int muSTMatrixHandle; - private int maPositionHandle; - private int maTextureHandle; - private int rotationAngle = 0; - - public TextureRenderer(int rotation) { - rotationAngle = rotation; - mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); - mTriangleVertices.put(mTriangleVerticesData).position(0); - Matrix.setIdentityM(mSTMatrix, 0); + private static final int FLOAT_SIZE_BYTES = 4; + private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; + private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; + private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; + private static final float[] mTriangleVerticesData = { + -1.0f, -1.0f, 0, 0.f, 0.f, + 1.0f, -1.0f, 0, 1.f, 0.f, + -1.0f, 1.0f, 0, 0.f, 1.f, + 1.0f, 1.0f, 0, 1.f, 1.f, + }; + private final FloatBuffer mTriangleVertices; + + private static final String VERTEX_SHADER = + "uniform mat4 uMVPMatrix;\n" + + "uniform mat4 uSTMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec4 aTextureCoord;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " gl_Position = uMVPMatrix * aPosition;\n" + + " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + + "}\n"; + + private static final String FRAGMENT_SHADER = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + + private final float[] mMVPMatrix = new float[16]; + private final float[] mSTMatrix = new float[16]; + private int mProgram; + private int mTextureID = -12345; + private int muMVPMatrixHandle; + private int muSTMatrixHandle; + private int maPositionHandle; + private int maTextureHandle; + private int rotationAngle = 0; + + public TextureRenderer(int rotation) { + rotationAngle = rotation; + mTriangleVertices = + ByteBuffer.allocateDirect(mTriangleVerticesData.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + mTriangleVertices.put(mTriangleVerticesData).position(0); + Matrix.setIdentityM(mSTMatrix, 0); + } + + public int getTextureId() { + return mTextureID; + } + + public void drawFrame(SurfaceTexture st, boolean invert) { + checkGlError("onDrawFrame start"); + st.getTransformMatrix(mSTMatrix); + + if (invert) { + mSTMatrix[5] = -mSTMatrix[5]; + mSTMatrix[13] = 1.0f - mSTMatrix[13]; } - public int getTextureId() { - return mTextureID; + GLES20.glUseProgram(mProgram); + checkGlError("glUseProgram"); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + GLES20.glVertexAttribPointer( + maPositionHandle, + 3, + GLES20.GL_FLOAT, + false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, + mTriangleVertices); + checkGlError("glVertexAttribPointer maPosition"); + GLES20.glEnableVertexAttribArray(maPositionHandle); + checkGlError("glEnableVertexAttribArray maPositionHandle"); + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); + GLES20.glVertexAttribPointer( + maTextureHandle, + 2, + GLES20.GL_FLOAT, + false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, + mTriangleVertices); + checkGlError("glVertexAttribPointer maTextureHandle"); + GLES20.glEnableVertexAttribArray(maTextureHandle); + checkGlError("glEnableVertexAttribArray maTextureHandle"); + GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); + GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + checkGlError("glDrawArrays"); + GLES20.glFinish(); + } + + public void surfaceCreated() { + mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); + if (mProgram == 0) { + throw new RuntimeException("failed creating program"); } - - public void drawFrame(SurfaceTexture st, boolean invert) { - checkGlError("onDrawFrame start"); - st.getTransformMatrix(mSTMatrix); - - if (invert) { - mSTMatrix[5] = -mSTMatrix[5]; - mSTMatrix[13] = 1.0f - mSTMatrix[13]; - } - - GLES20.glUseProgram(mProgram); - checkGlError("glUseProgram"); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); - mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); - GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); - checkGlError("glVertexAttribPointer maPosition"); - GLES20.glEnableVertexAttribArray(maPositionHandle); - checkGlError("glEnableVertexAttribArray maPositionHandle"); - mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); - GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); - checkGlError("glVertexAttribPointer maTextureHandle"); - GLES20.glEnableVertexAttribArray(maTextureHandle); - checkGlError("glEnableVertexAttribArray maTextureHandle"); - GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); - GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - checkGlError("glDrawArrays"); - GLES20.glFinish(); + maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); + checkGlError("glGetAttribLocation aPosition"); + if (maPositionHandle == -1) { + throw new RuntimeException("Could not get attrib location for aPosition"); } - - public void surfaceCreated() { - mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); - if (mProgram == 0) { - throw new RuntimeException("failed creating program"); - } - maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); - checkGlError("glGetAttribLocation aPosition"); - if (maPositionHandle == -1) { - throw new RuntimeException("Could not get attrib location for aPosition"); - } - maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); - checkGlError("glGetAttribLocation aTextureCoord"); - if (maTextureHandle == -1) { - throw new RuntimeException("Could not get attrib location for aTextureCoord"); - } - muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); - checkGlError("glGetUniformLocation uMVPMatrix"); - if (muMVPMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uMVPMatrix"); - } - muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); - checkGlError("glGetUniformLocation uSTMatrix"); - if (muSTMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uSTMatrix"); - } - int[] textures = new int[1]; - GLES20.glGenTextures(1, textures, 0); - mTextureID = textures[0]; - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); - checkGlError("glBindTexture mTextureID"); - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); - GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); - GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); - checkGlError("glTexParameter"); - - Matrix.setIdentityM(mMVPMatrix, 0); - if (rotationAngle != 0) { - Matrix.rotateM(mMVPMatrix, 0, rotationAngle, 0, 0, 1); - } + maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); + checkGlError("glGetAttribLocation aTextureCoord"); + if (maTextureHandle == -1) { + throw new RuntimeException("Could not get attrib location for aTextureCoord"); } - - public void changeFragmentShader(String fragmentShader) { - GLES20.glDeleteProgram(mProgram); - mProgram = createProgram(VERTEX_SHADER, fragmentShader); - if (mProgram == 0) { - throw new RuntimeException("failed creating program"); - } + muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + checkGlError("glGetUniformLocation uMVPMatrix"); + if (muMVPMatrixHandle == -1) { + throw new RuntimeException("Could not get attrib location for uMVPMatrix"); + } + muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); + checkGlError("glGetUniformLocation uSTMatrix"); + if (muSTMatrixHandle == -1) { + throw new RuntimeException("Could not get attrib location for uSTMatrix"); } + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + mTextureID = textures[0]; + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + checkGlError("glBindTexture mTextureID"); + GLES20.glTexParameterf( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameterf( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + checkGlError("glTexParameter"); + + Matrix.setIdentityM(mMVPMatrix, 0); + if (rotationAngle != 0) { + Matrix.rotateM(mMVPMatrix, 0, rotationAngle, 0, 0, 1); + } + } - private int loadShader(int shaderType, String source) { - int shader = GLES20.glCreateShader(shaderType); - checkGlError("glCreateShader type=" + shaderType); - GLES20.glShaderSource(shader, source); - GLES20.glCompileShader(shader); - int[] compiled = new int[1]; - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); - if (compiled[0] == 0) { - GLES20.glDeleteShader(shader); - shader = 0; - } - return shader; + public void changeFragmentShader(String fragmentShader) { + GLES20.glDeleteProgram(mProgram); + mProgram = createProgram(VERTEX_SHADER, fragmentShader); + if (mProgram == 0) { + throw new RuntimeException("failed creating program"); + } + } + + private int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + checkGlError("glCreateShader type=" + shaderType); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + GLES20.glDeleteShader(shader); + shader = 0; } + return shader; + } - private int createProgram(String vertexSource, String fragmentSource) { - int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); - if (vertexShader == 0) { - return 0; - } - int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); - if (pixelShader == 0) { - return 0; - } - int program = GLES20.glCreateProgram(); - checkGlError("glCreateProgram"); - if (program == 0) { - return 0; - } - GLES20.glAttachShader(program, vertexShader); - checkGlError("glAttachShader"); - GLES20.glAttachShader(program, pixelShader); - checkGlError("glAttachShader"); - GLES20.glLinkProgram(program); - int[] linkStatus = new int[1]; - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); - if (linkStatus[0] != GLES20.GL_TRUE) { - GLES20.glDeleteProgram(program); - program = 0; - } - return program; + private int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + int program = GLES20.glCreateProgram(); + checkGlError("glCreateProgram"); + if (program == 0) { + return 0; + } + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + GLES20.glDeleteProgram(program); + program = 0; } + return program; + } - public void checkGlError(String op) { - int error; - if ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { - throw new RuntimeException(op + ": glError " + error); - } + public void checkGlError(String op) { + int error; + if ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + throw new RuntimeException(op + ": glError " + error); } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/Track.java b/src/main/java/org/thoughtcrime/securesms/video/recode/Track.java index 523208690..d3c0db584 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/Track.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/Track.java @@ -3,7 +3,6 @@ import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.MediaFormat; - import com.coremedia.iso.boxes.AbstractMediaHeaderBox; import com.coremedia.iso.boxes.SampleDescriptionBox; import com.coremedia.iso.boxes.SoundMediaHeaderBox; @@ -16,7 +15,6 @@ import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.ESDescriptor; import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.SLConfigDescriptor; import com.mp4parser.iso14496.part15.AvcConfigurationBox; - import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Date; @@ -26,231 +24,232 @@ @TargetApi(16) public class Track { - private long trackId = 0; - private final ArrayList samples = new ArrayList<>(); - private long duration = 0; - private String handler; - private AbstractMediaHeaderBox headerBox = null; - private SampleDescriptionBox sampleDescriptionBox = null; - private LinkedList syncSamples = null; - private int timeScale; - private final Date creationTime = new Date(); - private int height; - private int width; - private float volume = 0; - private final ArrayList sampleDurations = new ArrayList<>(); - private boolean isAudio = false; - private static final Map samplingFrequencyIndexMap = new HashMap<>(); - private long lastPresentationTimeUs = 0; - private boolean first = true; - - static { - samplingFrequencyIndexMap.put(96000, 0x0); - samplingFrequencyIndexMap.put(88200, 0x1); - samplingFrequencyIndexMap.put(64000, 0x2); - samplingFrequencyIndexMap.put(48000, 0x3); - samplingFrequencyIndexMap.put(44100, 0x4); - samplingFrequencyIndexMap.put(32000, 0x5); - samplingFrequencyIndexMap.put(24000, 0x6); - samplingFrequencyIndexMap.put(22050, 0x7); - samplingFrequencyIndexMap.put(16000, 0x8); - samplingFrequencyIndexMap.put(12000, 0x9); - samplingFrequencyIndexMap.put(11025, 0xa); - samplingFrequencyIndexMap.put(8000, 0xb); - } - - public Track(int id, MediaFormat format, boolean audio) throws Exception { - trackId = id; - isAudio = audio; - if (!isAudio) { - sampleDurations.add((long) 3015); - duration = 3015; - width = format.getInteger(MediaFormat.KEY_WIDTH); - height = format.getInteger(MediaFormat.KEY_HEIGHT); - timeScale = 90000; - syncSamples = new LinkedList<>(); - handler = "vide"; - headerBox = new VideoMediaHeaderBox(); - sampleDescriptionBox = new SampleDescriptionBox(); - String mime = format.getString(MediaFormat.KEY_MIME); - if (mime.equals("video/avc")) { - VisualSampleEntry visualSampleEntry = new VisualSampleEntry("avc1"); - visualSampleEntry.setDataReferenceIndex(1); - visualSampleEntry.setDepth(24); - visualSampleEntry.setFrameCount(1); - visualSampleEntry.setHorizresolution(72); - visualSampleEntry.setVertresolution(72); - visualSampleEntry.setWidth(width); - visualSampleEntry.setHeight(height); - - AvcConfigurationBox avcConfigurationBox = new AvcConfigurationBox(); - - if (format.getByteBuffer("csd-0") != null) { - ArrayList spsArray = new ArrayList<>(); - ByteBuffer spsBuff = format.getByteBuffer("csd-0"); - spsBuff.position(4); - byte[] spsBytes = new byte[spsBuff.remaining()]; - spsBuff.get(spsBytes); - spsArray.add(spsBytes); - - ArrayList ppsArray = new ArrayList<>(); - ByteBuffer ppsBuff = format.getByteBuffer("csd-1"); - ppsBuff.position(4); - byte[] ppsBytes = new byte[ppsBuff.remaining()]; - ppsBuff.get(ppsBytes); - ppsArray.add(ppsBytes); - avcConfigurationBox.setSequenceParameterSets(spsArray); - avcConfigurationBox.setPictureParameterSets(ppsArray); - } - - avcConfigurationBox.setAvcLevelIndication(13); - avcConfigurationBox.setAvcProfileIndication(100); - avcConfigurationBox.setBitDepthLumaMinus8(-1); - avcConfigurationBox.setBitDepthChromaMinus8(-1); - avcConfigurationBox.setChromaFormat(-1); - avcConfigurationBox.setConfigurationVersion(1); - avcConfigurationBox.setLengthSizeMinusOne(3); - avcConfigurationBox.setProfileCompatibility(0); - - visualSampleEntry.addBox(avcConfigurationBox); - sampleDescriptionBox.addBox(visualSampleEntry); - } else if (mime.equals("video/mp4v")) { - VisualSampleEntry visualSampleEntry = new VisualSampleEntry("mp4v"); - visualSampleEntry.setDataReferenceIndex(1); - visualSampleEntry.setDepth(24); - visualSampleEntry.setFrameCount(1); - visualSampleEntry.setHorizresolution(72); - visualSampleEntry.setVertresolution(72); - visualSampleEntry.setWidth(width); - visualSampleEntry.setHeight(height); - - sampleDescriptionBox.addBox(visualSampleEntry); - } - } else { - sampleDurations.add((long) 1024); - duration = 1024; - volume = 1; - timeScale = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - handler = "soun"; - headerBox = new SoundMediaHeaderBox(); - sampleDescriptionBox = new SampleDescriptionBox(); - AudioSampleEntry audioSampleEntry = new AudioSampleEntry("mp4a"); - audioSampleEntry.setChannelCount(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)); - audioSampleEntry.setSampleRate(format.getInteger(MediaFormat.KEY_SAMPLE_RATE)); - audioSampleEntry.setDataReferenceIndex(1); - audioSampleEntry.setSampleSize(16); - - ESDescriptorBox esds = new ESDescriptorBox(); - ESDescriptor descriptor = new ESDescriptor(); - descriptor.setEsId(0); - - SLConfigDescriptor slConfigDescriptor = new SLConfigDescriptor(); - slConfigDescriptor.setPredefined(2); - descriptor.setSlConfigDescriptor(slConfigDescriptor); - - DecoderConfigDescriptor decoderConfigDescriptor = new DecoderConfigDescriptor(); - decoderConfigDescriptor.setObjectTypeIndication(0x40); - decoderConfigDescriptor.setStreamType(5); - decoderConfigDescriptor.setBufferSizeDB(1536); - decoderConfigDescriptor.setMaxBitRate(96000); - decoderConfigDescriptor.setAvgBitRate(96000); - - AudioSpecificConfig audioSpecificConfig = new AudioSpecificConfig(); - audioSpecificConfig.setAudioObjectType(2); - audioSpecificConfig.setSamplingFrequencyIndex(samplingFrequencyIndexMap.get((int) audioSampleEntry.getSampleRate())); - audioSpecificConfig.setChannelConfiguration(audioSampleEntry.getChannelCount()); - decoderConfigDescriptor.setAudioSpecificInfo(audioSpecificConfig); - - descriptor.setDecoderConfigDescriptor(decoderConfigDescriptor); - - ByteBuffer data = descriptor.serialize(); - esds.setEsDescriptor(descriptor); - esds.setData(data); - audioSampleEntry.addBox(esds); - sampleDescriptionBox.addBox(audioSampleEntry); - } - } - - public long getTrackId() { - return trackId; - } - - public void addSample(long offset, MediaCodec.BufferInfo bufferInfo) { - long delta = bufferInfo.presentationTimeUs - lastPresentationTimeUs; - if (delta < 0) { - return; - } - boolean isSyncFrame = !isAudio && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; - samples.add(new Sample(offset, bufferInfo.size)); - if (syncSamples != null && isSyncFrame) { - syncSamples.add(samples.size()); - } - - delta = (delta * timeScale + 500000L) / 1000000L; - lastPresentationTimeUs = bufferInfo.presentationTimeUs; - if (!first) { - sampleDurations.add(sampleDurations.size() - 1, delta); - duration += delta; - } - first = false; - } - - public ArrayList getSamples() { - return samples; - } - - public long getDuration() { - return duration; - } - - public String getHandler() { - return handler; - } - - public AbstractMediaHeaderBox getMediaHeaderBox() { - return headerBox; - } - - public SampleDescriptionBox getSampleDescriptionBox() { - return sampleDescriptionBox; - } - - public long[] getSyncSamples() { - if (syncSamples == null || syncSamples.isEmpty()) { - return null; + private long trackId = 0; + private final ArrayList samples = new ArrayList<>(); + private long duration = 0; + private String handler; + private AbstractMediaHeaderBox headerBox = null; + private SampleDescriptionBox sampleDescriptionBox = null; + private LinkedList syncSamples = null; + private int timeScale; + private final Date creationTime = new Date(); + private int height; + private int width; + private float volume = 0; + private final ArrayList sampleDurations = new ArrayList<>(); + private boolean isAudio = false; + private static final Map samplingFrequencyIndexMap = new HashMap<>(); + private long lastPresentationTimeUs = 0; + private boolean first = true; + + static { + samplingFrequencyIndexMap.put(96000, 0x0); + samplingFrequencyIndexMap.put(88200, 0x1); + samplingFrequencyIndexMap.put(64000, 0x2); + samplingFrequencyIndexMap.put(48000, 0x3); + samplingFrequencyIndexMap.put(44100, 0x4); + samplingFrequencyIndexMap.put(32000, 0x5); + samplingFrequencyIndexMap.put(24000, 0x6); + samplingFrequencyIndexMap.put(22050, 0x7); + samplingFrequencyIndexMap.put(16000, 0x8); + samplingFrequencyIndexMap.put(12000, 0x9); + samplingFrequencyIndexMap.put(11025, 0xa); + samplingFrequencyIndexMap.put(8000, 0xb); + } + + public Track(int id, MediaFormat format, boolean audio) throws Exception { + trackId = id; + isAudio = audio; + if (!isAudio) { + sampleDurations.add((long) 3015); + duration = 3015; + width = format.getInteger(MediaFormat.KEY_WIDTH); + height = format.getInteger(MediaFormat.KEY_HEIGHT); + timeScale = 90000; + syncSamples = new LinkedList<>(); + handler = "vide"; + headerBox = new VideoMediaHeaderBox(); + sampleDescriptionBox = new SampleDescriptionBox(); + String mime = format.getString(MediaFormat.KEY_MIME); + if (mime.equals("video/avc")) { + VisualSampleEntry visualSampleEntry = new VisualSampleEntry("avc1"); + visualSampleEntry.setDataReferenceIndex(1); + visualSampleEntry.setDepth(24); + visualSampleEntry.setFrameCount(1); + visualSampleEntry.setHorizresolution(72); + visualSampleEntry.setVertresolution(72); + visualSampleEntry.setWidth(width); + visualSampleEntry.setHeight(height); + + AvcConfigurationBox avcConfigurationBox = new AvcConfigurationBox(); + + if (format.getByteBuffer("csd-0") != null) { + ArrayList spsArray = new ArrayList<>(); + ByteBuffer spsBuff = format.getByteBuffer("csd-0"); + spsBuff.position(4); + byte[] spsBytes = new byte[spsBuff.remaining()]; + spsBuff.get(spsBytes); + spsArray.add(spsBytes); + + ArrayList ppsArray = new ArrayList<>(); + ByteBuffer ppsBuff = format.getByteBuffer("csd-1"); + ppsBuff.position(4); + byte[] ppsBytes = new byte[ppsBuff.remaining()]; + ppsBuff.get(ppsBytes); + ppsArray.add(ppsBytes); + avcConfigurationBox.setSequenceParameterSets(spsArray); + avcConfigurationBox.setPictureParameterSets(ppsArray); } - long[] returns = new long[syncSamples.size()]; - for (int i = 0; i < syncSamples.size(); i++) { - returns[i] = syncSamples.get(i); - } - return returns; - } - - public int getTimeScale() { - return timeScale; - } - - public Date getCreationTime() { - return creationTime; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - public float getVolume() { - return volume; - } - - public ArrayList getSampleDurations() { - return sampleDurations; - } - - public boolean isAudio() { - return isAudio; - } + avcConfigurationBox.setAvcLevelIndication(13); + avcConfigurationBox.setAvcProfileIndication(100); + avcConfigurationBox.setBitDepthLumaMinus8(-1); + avcConfigurationBox.setBitDepthChromaMinus8(-1); + avcConfigurationBox.setChromaFormat(-1); + avcConfigurationBox.setConfigurationVersion(1); + avcConfigurationBox.setLengthSizeMinusOne(3); + avcConfigurationBox.setProfileCompatibility(0); + + visualSampleEntry.addBox(avcConfigurationBox); + sampleDescriptionBox.addBox(visualSampleEntry); + } else if (mime.equals("video/mp4v")) { + VisualSampleEntry visualSampleEntry = new VisualSampleEntry("mp4v"); + visualSampleEntry.setDataReferenceIndex(1); + visualSampleEntry.setDepth(24); + visualSampleEntry.setFrameCount(1); + visualSampleEntry.setHorizresolution(72); + visualSampleEntry.setVertresolution(72); + visualSampleEntry.setWidth(width); + visualSampleEntry.setHeight(height); + + sampleDescriptionBox.addBox(visualSampleEntry); + } + } else { + sampleDurations.add((long) 1024); + duration = 1024; + volume = 1; + timeScale = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + handler = "soun"; + headerBox = new SoundMediaHeaderBox(); + sampleDescriptionBox = new SampleDescriptionBox(); + AudioSampleEntry audioSampleEntry = new AudioSampleEntry("mp4a"); + audioSampleEntry.setChannelCount(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)); + audioSampleEntry.setSampleRate(format.getInteger(MediaFormat.KEY_SAMPLE_RATE)); + audioSampleEntry.setDataReferenceIndex(1); + audioSampleEntry.setSampleSize(16); + + ESDescriptorBox esds = new ESDescriptorBox(); + ESDescriptor descriptor = new ESDescriptor(); + descriptor.setEsId(0); + + SLConfigDescriptor slConfigDescriptor = new SLConfigDescriptor(); + slConfigDescriptor.setPredefined(2); + descriptor.setSlConfigDescriptor(slConfigDescriptor); + + DecoderConfigDescriptor decoderConfigDescriptor = new DecoderConfigDescriptor(); + decoderConfigDescriptor.setObjectTypeIndication(0x40); + decoderConfigDescriptor.setStreamType(5); + decoderConfigDescriptor.setBufferSizeDB(1536); + decoderConfigDescriptor.setMaxBitRate(96000); + decoderConfigDescriptor.setAvgBitRate(96000); + + AudioSpecificConfig audioSpecificConfig = new AudioSpecificConfig(); + audioSpecificConfig.setAudioObjectType(2); + audioSpecificConfig.setSamplingFrequencyIndex( + samplingFrequencyIndexMap.get((int) audioSampleEntry.getSampleRate())); + audioSpecificConfig.setChannelConfiguration(audioSampleEntry.getChannelCount()); + decoderConfigDescriptor.setAudioSpecificInfo(audioSpecificConfig); + + descriptor.setDecoderConfigDescriptor(decoderConfigDescriptor); + + ByteBuffer data = descriptor.serialize(); + esds.setEsDescriptor(descriptor); + esds.setData(data); + audioSampleEntry.addBox(esds); + sampleDescriptionBox.addBox(audioSampleEntry); + } + } + + public long getTrackId() { + return trackId; + } + + public void addSample(long offset, MediaCodec.BufferInfo bufferInfo) { + long delta = bufferInfo.presentationTimeUs - lastPresentationTimeUs; + if (delta < 0) { + return; + } + boolean isSyncFrame = !isAudio && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; + samples.add(new Sample(offset, bufferInfo.size)); + if (syncSamples != null && isSyncFrame) { + syncSamples.add(samples.size()); + } + + delta = (delta * timeScale + 500000L) / 1000000L; + lastPresentationTimeUs = bufferInfo.presentationTimeUs; + if (!first) { + sampleDurations.add(sampleDurations.size() - 1, delta); + duration += delta; + } + first = false; + } + + public ArrayList getSamples() { + return samples; + } + + public long getDuration() { + return duration; + } + + public String getHandler() { + return handler; + } + + public AbstractMediaHeaderBox getMediaHeaderBox() { + return headerBox; + } + + public SampleDescriptionBox getSampleDescriptionBox() { + return sampleDescriptionBox; + } + + public long[] getSyncSamples() { + if (syncSamples == null || syncSamples.isEmpty()) { + return null; + } + long[] returns = new long[syncSamples.size()]; + for (int i = 0; i < syncSamples.size(); i++) { + returns[i] = syncSamples.get(i); + } + return returns; + } + + public int getTimeScale() { + return timeScale; + } + + public Date getCreationTime() { + return creationTime; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public float getVolume() { + return volume; + } + + public ArrayList getSampleDurations() { + return sampleDurations; + } + + public boolean isAudio() { + return isAudio; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/video/recode/VideoRecoder.java b/src/main/java/org/thoughtcrime/securesms/video/recode/VideoRecoder.java index a8b1cbd65..a2c8a1792 100644 --- a/src/main/java/org/thoughtcrime/securesms/video/recode/VideoRecoder.java +++ b/src/main/java/org/thoughtcrime/securesms/video/recode/VideoRecoder.java @@ -6,9 +6,7 @@ import android.media.MediaExtractor; import android.media.MediaFormat; import android.util.Log; - import androidx.appcompat.app.AlertDialog; - import com.b44t.messenger.DcMsg; import com.coremedia.iso.IsoFile; import com.coremedia.iso.boxes.Box; @@ -19,20 +17,18 @@ import com.coremedia.iso.boxes.TrackHeaderBox; import com.googlecode.mp4parser.util.Matrix; import com.googlecode.mp4parser.util.Path; - -import org.thoughtcrime.securesms.connect.DcHelper; -import org.thoughtcrime.securesms.util.Prefs; -import org.thoughtcrime.securesms.util.Util; - import java.io.File; import java.nio.ByteBuffer; import java.util.List; +import org.thoughtcrime.securesms.connect.DcHelper; +import org.thoughtcrime.securesms.util.Prefs; +import org.thoughtcrime.securesms.util.Util; public class VideoRecoder { private static final String TAG = VideoRecoder.class.getSimpleName(); - private final static String MIME_TYPE = "video/avc"; + private static final String MIME_TYPE = "video/avc"; private final boolean cancelCurrentVideoConversion = false; private final Object videoConvertSync = new Object(); @@ -64,7 +60,15 @@ private int selectTrack(MediaExtractor extractor, boolean audio) { return -5; } - private long readAndWriteTrack(MediaExtractor extractor, MP4Builder mediaMuxer, MediaCodec.BufferInfo info, long start, long end, File file, boolean isAudio) throws Exception { + private long readAndWriteTrack( + MediaExtractor extractor, + MP4Builder mediaMuxer, + MediaCodec.BufferInfo info, + long start, + long end, + File file, + boolean isAudio) + throws Exception { int trackIndex = selectTrack(extractor, isAudio); if (trackIndex >= 0) { extractor.selectTrack(trackIndex); @@ -106,7 +110,7 @@ private long readAndWriteTrack(MediaExtractor extractor, MP4Builder mediaMuxer, info.offset = 0; info.flags = extractor.getSampleFlags(); if (mediaMuxer.writeSampleData(muxerTrackIndex, buffer, info, isAudio)) { - //didWriteData(messageObject, file, false, false); + // didWriteData(messageObject, file, false, false); } } lastTimestamp = info.presentationTimeUs; @@ -166,7 +170,7 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP File inputFile = new File(videoEditedInfo.originalPath); if (!inputFile.canRead()) { - //didWriteData(messageObject, cacheFile, true, true); + // didWriteData(messageObject, cacheFile, true, true); Log.w(TAG, "Could not read video file to be recoded"); return false; } @@ -192,7 +196,10 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP checkConversionCanceled(); - if (resultVideoBitrate= 0) { @@ -210,7 +217,7 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP int colorFormat; colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; - //Log.i("DeltaChat", "colorFormat = " + colorFormat); + // Log.i("DeltaChat", "colorFormat = " + colorFormat); extractor.selectTrack(videoIndex); if (startTime > 0) { @@ -220,9 +227,11 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP } MediaFormat inputFormat = extractor.getTrackFormat(videoIndex); - MediaFormat outputFormat = MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight); + MediaFormat outputFormat = + MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight); outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); - outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, resultVideoBitrate != 0 ? resultVideoBitrate : 921600); + outputFormat.setInteger( + MediaFormat.KEY_BIT_RATE, resultVideoBitrate != 0 ? resultVideoBitrate : 921600); outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25); outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); @@ -255,10 +264,12 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP inputBuf = decoder.getInputBuffer(inputBufIndex); int chunkSize = extractor.readSampleData(inputBuf, 0); if (chunkSize < 0) { - decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer( + inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM); inputDone = true; } else { - decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, extractor.getSampleTime(), 0); + decoder.queueInputBuffer( + inputBufIndex, 0, chunkSize, extractor.getSampleTime(), 0); extractor.advance(); } } @@ -268,7 +279,8 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP if (eof) { int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC); if (inputBufIndex >= 0) { - decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer( + inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM); inputDone = true; } } @@ -288,17 +300,19 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP videoTrackIndex = mediaMuxer.addTrack(newFormat, false); } } else if (encoderStatus < 0) { - throw new RuntimeException("unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus); + throw new RuntimeException( + "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus); } else { ByteBuffer encodedData; encodedData = encoder.getOutputBuffer(encoderStatus); if (encodedData == null) { - throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); + throw new RuntimeException( + "encoderOutputBuffer " + encoderStatus + " was null"); } if (info.size > 1) { if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { if (mediaMuxer.writeSampleData(videoTrackIndex, encodedData, info, false)) { - //didWriteData(messageObject, cacheFile, false, false); + // didWriteData(messageObject, cacheFile, false, false); } } else if (videoTrackIndex == -5) { byte[] csd = new byte[info.size]; @@ -309,7 +323,10 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP ByteBuffer pps = null; for (int a = info.size - 1; a >= 0; a--) { if (a > 3) { - if (csd[a] == 1 && csd[a - 1] == 0 && csd[a - 2] == 0 && csd[a - 3] == 0) { + if (csd[a] == 1 + && csd[a - 1] == 0 + && csd[a - 2] == 0 + && csd[a - 3] == 0) { sps = ByteBuffer.allocate(a - 3); pps = ByteBuffer.allocate(info.size - (a - 3)); sps.put(csd, 0, a - 3).position(0); @@ -321,7 +338,8 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP } } - MediaFormat newFormat = MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight); + MediaFormat newFormat = + MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight); if (sps != null && pps != null) { newFormat.setByteBuffer("csd-0", sps); newFormat.setByteBuffer("csd-1", pps); @@ -344,9 +362,10 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = decoder.getOutputFormat(); - //Log.i("DeltaChat", "newFormat = " + newFormat); + // Log.i("DeltaChat", "newFormat = " + newFormat); } else if (decoderStatus < 0) { - throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus); + throw new RuntimeException( + "unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus); } else { boolean doRender; doRender = info.size != 0; @@ -359,7 +378,8 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP if (startTime > 0 && videoTime == -1) { if (info.presentationTimeUs < startTime) { doRender = false; - //Log.i("DeltaChat", "drop frame startTime = " + startTime + " present time = " + info.presentationTimeUs); + // Log.i("DeltaChat", "drop frame startTime = " + startTime + " present + // time = " + info.presentationTimeUs); } else { videoTime = info.presentationTimeUs; } @@ -381,7 +401,7 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { decoderOutputAvailable = false; - //Log.i("DeltaChat", "decoder stream end"); + // Log.i("DeltaChat", "decoder stream end"); encoder.signalEndOfInputStream(); } } @@ -392,7 +412,7 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP videoStartTime = videoTime; } } catch (Exception e) { - Log.w(TAG,"Recoding video failed unexpectedly", e); + Log.w(TAG, "Recoding video failed unexpectedly", e); error = true; } @@ -416,7 +436,8 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP checkConversionCanceled(); } } else { - long videoTime = readAndWriteTrack(extractor, mediaMuxer, info, startTime, endTime, cacheFile, false); + long videoTime = + readAndWriteTrack(extractor, mediaMuxer, info, startTime, endTime, cacheFile, false); if (videoTime != -1) { videoStartTime = videoTime; } @@ -425,7 +446,7 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP readAndWriteTrack(extractor, mediaMuxer, info, videoStartTime, endTime, cacheFile, true); } } catch (Exception e) { - Log.w(TAG,"Recoding video failed unexpectedly/2", e); + Log.w(TAG, "Recoding video failed unexpectedly/2", e); error = true; } finally { if (extractor != null) { @@ -435,38 +456,38 @@ private boolean convertVideo(final VideoEditedInfo videoEditedInfo, String destP try { mediaMuxer.finishMovie(false); } catch (Exception e) { - Log.w(TAG,"Flushing video failed unexpectedly", e); + Log.w(TAG, "Flushing video failed unexpectedly", e); } } - //Log.i("DeltaChat", "time = " + (System.currentTimeMillis() - time)); + // Log.i("DeltaChat", "time = " + (System.currentTimeMillis() - time)); } } else { - //didWriteData(messageObject, cacheFile, true, true); - Log.w(TAG,"Video width or height are 0, refusing recode."); + // didWriteData(messageObject, cacheFile, true, true); + Log.w(TAG, "Video width or height are 0, refusing recode."); return false; } - //didWriteData(messageObject, cacheFile, true, error); + // didWriteData(messageObject, cacheFile, true, error); return true; } private static class VideoEditedInfo { String originalPath; - float originalDurationMs; - long originalAudioBytes; - int originalRotationValue; - int originalWidth; - int originalHeight; - int originalVideoBitrate; - - long startTime; - long endTime; - int rotationValue; - - int resultWidth; - int resultHeight; - int resultVideoBitrate; - - int estimatedBytes; + float originalDurationMs; + long originalAudioBytes; + int originalRotationValue; + int originalWidth; + int originalHeight; + int originalVideoBitrate; + + long startTime; + long endTime; + int rotationValue; + + int resultWidth; + int resultHeight; + int resultVideoBitrate; + + int estimatedBytes; } private static VideoEditedInfo getVideoEditInfoFromFile(String videoPath) { @@ -492,11 +513,13 @@ private static VideoEditedInfo getVideoEditInfoFromFile(String videoPath) { try { MediaBox mediaBox = trackBox.getMediaBox(); MediaHeaderBox mediaHeaderBox = mediaBox.getMediaHeaderBox(); - SampleSizeBox sampleSizeBox = mediaBox.getMediaInformationBox().getSampleTableBox().getSampleSizeBox(); + SampleSizeBox sampleSizeBox = + mediaBox.getMediaInformationBox().getSampleTableBox().getSampleSizeBox(); for (long size : sampleSizeBox.getSampleSizes()) { sampleSizes += size; } - float originalVideoSeconds = (float) mediaHeaderBox.getDuration() / (float) mediaHeaderBox.getTimescale(); + float originalVideoSeconds = + (float) mediaHeaderBox.getDuration() / (float) mediaHeaderBox.getTimescale(); trackBitrate = (int) (sampleSizes * 8 / originalVideoSeconds); vei.originalDurationMs = originalVideoSeconds * 1000; } catch (Exception e) { @@ -534,20 +557,22 @@ private static VideoEditedInfo getVideoEditInfoFromFile(String videoPath) { return vei; } - private static int calculateEstimatedSize(float timeDelta, int resultBitrate, float originalDurationMs, long originalAudioBytes) { - long videoFramesSize = (long) (resultBitrate / 8 * (originalDurationMs /1000)); + private static int calculateEstimatedSize( + float timeDelta, int resultBitrate, float originalDurationMs, long originalAudioBytes) { + long videoFramesSize = (long) (resultBitrate / 8 * (originalDurationMs / 1000)); int size = (int) ((originalAudioBytes + videoFramesSize) * timeDelta); return size; } - private static void alert(Context context, String str) - { + private static void alert(Context context, String str) { Log.e(TAG, str); - Util.runOnMain(() -> new AlertDialog.Builder(context) - .setCancelable(false) - .setMessage(str) - .setPositiveButton(android.R.string.ok, null) - .show()); + Util.runOnMain( + () -> + new AlertDialog.Builder(context) + .setCancelable(false) + .setMessage(str) + .setPositiveButton(android.R.string.ok, null) + .show()); } // prepareVideo() assumes the msg object is set up properly to being sent; @@ -555,7 +580,8 @@ private static void alert(Context context, String str) // return: true=video might be prepared, can be sent, false=error public static boolean prepareVideo(Context context, int chatId, DcMsg msg) { final long MAX_BYTES = DcHelper.getInt(context, "sys.msgsize_max_recommended"); - final String TOO_BIG_FILE = "Video cannot be compressed to a reasonable size. Try a shorter video or a lower quality."; + final String TOO_BIG_FILE = + "Video cannot be compressed to a reasonable size. Try a shorter video or a lower quality."; try { String inPath = msg.getFile(); Log.i(TAG, "Preparing video: " + inPath); @@ -564,7 +590,7 @@ public static boolean prepareVideo(Context context, int chatId, DcMsg msg) { VideoEditedInfo vei = getVideoEditInfoFromFile(inPath); if (vei == null) { Log.w(TAG, String.format("Recoding failed for %s: cannot get info", inPath)); - if (msg.getFilebytes() > MAX_BYTES+MAX_BYTES/4) { + if (msg.getFilebytes() > MAX_BYTES + MAX_BYTES / 4) { alert(context, TOO_BIG_FILE); return false; } @@ -582,19 +608,27 @@ public static boolean prepareVideo(Context context, int chatId, DcMsg msg) { } else { msg.setDimension(vei.originalWidth, vei.originalHeight); } - msg.setDuration((int)vei.originalDurationMs); + msg.setDuration((int) vei.originalDurationMs); // check if video bitrate is already reasonable - final int MAX_KBPS = 1500000; + final int MAX_KBPS = 1500000; long inBytes = new File(inPath).length(); - if (inBytes > 0 && inBytes <= MAX_BYTES && vei.originalVideoBitrate <= MAX_KBPS*2 /*be tolerant as long the file size matches*/) { - Log.i(TAG, String.format("recoding for %s is not needed, %d bytes and %d kbps are ok", inPath, inBytes, vei.originalVideoBitrate)); + if (inBytes > 0 + && inBytes <= MAX_BYTES + && vei.originalVideoBitrate + <= MAX_KBPS * 2 /*be tolerant as long the file size matches*/) { + Log.i( + TAG, + String.format( + "recoding for %s is not needed, %d bytes and %d kbps are ok", + inPath, inBytes, vei.originalVideoBitrate)); return true; } // calculate new video bitrate, sth. between 200 kbps and 1500 kbps long resultDurationMs = (long) vei.originalDurationMs; - long maxVideoBytes = MAX_BYTES - vei.originalAudioBytes - resultDurationMs /*10 kbps codec overhead*/; + long maxVideoBytes = + MAX_BYTES - vei.originalAudioBytes - resultDurationMs /*10 kbps codec overhead*/; vei.resultVideoBitrate = (int) (maxVideoBytes / Math.max(1, resultDurationMs / 1000) * 8); if (vei.resultVideoBitrate < 200000) { @@ -615,7 +649,10 @@ public static boolean prepareVideo(Context context, int chatId, DcMsg msg) { vei.resultWidth = vei.originalWidth; vei.resultHeight = vei.originalHeight; if (vei.resultWidth > maxSide || vei.resultHeight > maxSide) { - float scale = vei.resultWidth > vei.resultHeight ? (float) maxSide / vei.resultWidth : (float) maxSide / vei.resultHeight; + float scale = + vei.resultWidth > vei.resultHeight + ? (float) maxSide / vei.resultWidth + : (float) maxSide / vei.resultHeight; vei.resultWidth *= scale; vei.resultHeight *= scale; } @@ -629,10 +666,14 @@ public static boolean prepareVideo(Context context, int chatId, DcMsg msg) { msg.setDuration((int) resultDurationMs); // calculate bytes - vei.estimatedBytes = VideoRecoder.calculateEstimatedSize((float) resultDurationMs / vei.originalDurationMs, - vei.resultVideoBitrate, vei.originalDurationMs, vei.originalAudioBytes); - - if (vei.estimatedBytes > MAX_BYTES+MAX_BYTES/4) { + vei.estimatedBytes = + VideoRecoder.calculateEstimatedSize( + (float) resultDurationMs / vei.originalDurationMs, + vei.resultVideoBitrate, + vei.originalDurationMs, + vei.originalAudioBytes); + + if (vei.estimatedBytes > MAX_BYTES + MAX_BYTES / 4) { alert(context, TOO_BIG_FILE); return false; } @@ -641,15 +682,17 @@ public static boolean prepareVideo(Context context, int chatId, DcMsg msg) { String tempPath = DcHelper.getBlobdirFile(DcHelper.getContext(context), inPath); VideoRecoder videoRecoder = new VideoRecoder(); if (!videoRecoder.convertVideo(vei, tempPath)) { - alert(context, String.format("Recoding failed for %s: cannot convert to temporary file %s", inPath, tempPath)); + alert( + context, + String.format( + "Recoding failed for %s: cannot convert to temporary file %s", inPath, tempPath)); return false; } msg.setFileAndDeduplicate(tempPath, msg.getFilename(), msg.getFilemime()); Log.i(TAG, String.format("recoding for %s done", inPath)); - } - catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } diff --git a/src/main/java/org/thoughtcrime/securesms/webxdc/WebxdcGarbageCollectionWorker.java b/src/main/java/org/thoughtcrime/securesms/webxdc/WebxdcGarbageCollectionWorker.java index b5ce3ae4b..a773f3a75 100644 --- a/src/main/java/org/thoughtcrime/securesms/webxdc/WebxdcGarbageCollectionWorker.java +++ b/src/main/java/org/thoughtcrime/securesms/webxdc/WebxdcGarbageCollectionWorker.java @@ -10,7 +10,6 @@ import chat.delta.rpc.Rpc; import chat.delta.rpc.RpcException; import com.google.common.util.concurrent.ListenableFuture; - import java.util.Collections; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -30,54 +29,59 @@ public WebxdcGarbageCollectionWorker(Context context, WorkerParameters params) { Log.i(TAG, "Running Webxdc storage garbage collection..."); final Pattern WEBXDC_URL_PATTERN = - Pattern.compile("^https?://acc(\\d+)-msg(\\d+)\\.localhost/?"); + Pattern.compile("^https?://acc(\\d+)-msg(\\d+)\\.localhost/?"); - return CallbackToFutureAdapter.getFuture(completer -> { - WebStorage webStorage = WebStorage.getInstance(); + return CallbackToFutureAdapter.getFuture( + completer -> { + WebStorage webStorage = WebStorage.getInstance(); - webStorage.getOrigins((origins) -> { - if (origins == null || origins.isEmpty()) { - Log.i(TAG, "Done, no WebView origins found."); - completer.set(Result.success()); - return; - } + webStorage.getOrigins( + (origins) -> { + if (origins == null || origins.isEmpty()) { + Log.i(TAG, "Done, no WebView origins found."); + completer.set(Result.success()); + return; + } - Rpc rpc = DcHelper.getRpc(context); - if (rpc == null) { - Log.e(TAG, "Failed to get access to RPC, Webxdc storage garbage collection aborted."); - completer.set(Result.failure()); - return; - } + Rpc rpc = DcHelper.getRpc(context); + if (rpc == null) { + Log.e( + TAG, + "Failed to get access to RPC, Webxdc storage garbage collection aborted."); + completer.set(Result.failure()); + return; + } - for (Object key : origins.keySet()) { - String url = (String)key; - Matcher m = WEBXDC_URL_PATTERN.matcher(url); - if (m.matches()) { - int accId = Integer.parseInt(m.group(1)); - int msgId = Integer.parseInt(m.group(2)); - try { - if (rpc.getExistingMsgIds(accId, Collections.singletonList(msgId)).isEmpty()) { - webStorage.deleteOrigin(url); - Log.i(TAG, String.format("Deleted webxdc origin: %s", url)); - } else { - Log.i(TAG, String.format("Existing webxdc origin: %s", url)); - } - } catch (RpcException e) { - Log.e(TAG, "error calling rpc.getExistingMsgIds()", e); - completer.set(Result.failure()); - return; - } - } else { // old webxdc URL schemes, etc - webStorage.deleteOrigin(url); - Log.i(TAG, String.format("Deleted unknown origin: %s", url)); - } - } + for (Object key : origins.keySet()) { + String url = (String) key; + Matcher m = WEBXDC_URL_PATTERN.matcher(url); + if (m.matches()) { + int accId = Integer.parseInt(m.group(1)); + int msgId = Integer.parseInt(m.group(2)); + try { + if (rpc.getExistingMsgIds(accId, Collections.singletonList(msgId)) + .isEmpty()) { + webStorage.deleteOrigin(url); + Log.i(TAG, String.format("Deleted webxdc origin: %s", url)); + } else { + Log.i(TAG, String.format("Existing webxdc origin: %s", url)); + } + } catch (RpcException e) { + Log.e(TAG, "error calling rpc.getExistingMsgIds()", e); + completer.set(Result.failure()); + return; + } + } else { // old webxdc URL schemes, etc + webStorage.deleteOrigin(url); + Log.i(TAG, String.format("Deleted unknown origin: %s", url)); + } + } - Log.i(TAG, "Done running Webxdc storage garbage collection."); - completer.set(Result.success()); - }); + Log.i(TAG, "Done running Webxdc storage garbage collection."); + completer.set(Result.success()); + }); - return "Webxdc Garbage Collector"; - }); + return "Webxdc Garbage Collector"; + }); } } diff --git a/src/main/res/anim/animation_toggle_in.xml b/src/main/res/anim/animation_toggle_in.xml index ffffd02d1..ea9ea31f2 100644 --- a/src/main/res/anim/animation_toggle_in.xml +++ b/src/main/res/anim/animation_toggle_in.xml @@ -12,4 +12,4 @@ android:duration="150" android:fromAlpha="0.0" android:toAlpha="1.0" /> - \ No newline at end of file + diff --git a/src/main/res/anim/animation_toggle_out.xml b/src/main/res/anim/animation_toggle_out.xml index 1e6f7ed63..5f30d2592 100644 --- a/src/main/res/anim/animation_toggle_out.xml +++ b/src/main/res/anim/animation_toggle_out.xml @@ -12,4 +12,4 @@ android:duration="150" android:fromAlpha="1.0" android:toAlpha="0.0" /> - \ No newline at end of file + diff --git a/src/main/res/anim/fade_scale_in.xml b/src/main/res/anim/fade_scale_in.xml index 0f2def07d..8a29147b1 100644 --- a/src/main/res/anim/fade_scale_in.xml +++ b/src/main/res/anim/fade_scale_in.xml @@ -1,6 +1,7 @@ - - \ No newline at end of file + diff --git a/src/main/res/anim/fade_scale_out.xml b/src/main/res/anim/fade_scale_out.xml index 2ee729071..a5610588f 100644 --- a/src/main/res/anim/fade_scale_out.xml +++ b/src/main/res/anim/fade_scale_out.xml @@ -1,6 +1,7 @@ - - \ No newline at end of file + diff --git a/src/main/res/anim/slide_from_right.xml b/src/main/res/anim/slide_from_right.xml index 7dbec61f3..b6348e740 100644 --- a/src/main/res/anim/slide_from_right.xml +++ b/src/main/res/anim/slide_from_right.xml @@ -1,9 +1,10 @@ - + - \ No newline at end of file + diff --git a/src/main/res/anim/slide_to_right.xml b/src/main/res/anim/slide_to_right.xml index c655fcd12..4621f980e 100644 --- a/src/main/res/anim/slide_to_right.xml +++ b/src/main/res/anim/slide_to_right.xml @@ -1,9 +1,10 @@ - + - \ No newline at end of file + diff --git a/src/main/res/animator/bottom_pause_to_play_animation.xml b/src/main/res/animator/bottom_pause_to_play_animation.xml index f5b474bb7..7d0dba8c8 100644 --- a/src/main/res/animator/bottom_pause_to_play_animation.xml +++ b/src/main/res/animator/bottom_pause_to_play_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/animator/bottom_play_to_pause_animation.xml b/src/main/res/animator/bottom_play_to_pause_animation.xml index 4f2778d68..31f41823d 100644 --- a/src/main/res/animator/bottom_play_to_pause_animation.xml +++ b/src/main/res/animator/bottom_play_to_pause_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/animator/rotate_90_animation.xml b/src/main/res/animator/rotate_90_animation.xml index 7d44ce690..88dd56cc5 100644 --- a/src/main/res/animator/rotate_90_animation.xml +++ b/src/main/res/animator/rotate_90_animation.xml @@ -6,4 +6,4 @@ android:propertyName="rotation" android:valueFrom="0" android:valueTo="90" - android:valueType="floatType"/> \ No newline at end of file + android:valueType="floatType" /> diff --git a/src/main/res/animator/rotate_minus_90_animation.xml b/src/main/res/animator/rotate_minus_90_animation.xml index ef9e1b6f1..501e5f98b 100644 --- a/src/main/res/animator/rotate_minus_90_animation.xml +++ b/src/main/res/animator/rotate_minus_90_animation.xml @@ -6,4 +6,4 @@ android:propertyName="rotation" android:valueFrom="90" android:valueTo="0" - android:valueType="floatType"/> \ No newline at end of file + android:valueType="floatType" /> diff --git a/src/main/res/animator/upper_pause_to_play_animation.xml b/src/main/res/animator/upper_pause_to_play_animation.xml index 880c7b0b8..8dd9a7ca7 100644 --- a/src/main/res/animator/upper_pause_to_play_animation.xml +++ b/src/main/res/animator/upper_pause_to_play_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/animator/upper_play_to_pause_animation.xml b/src/main/res/animator/upper_play_to_pause_animation.xml index ffa933231..8a5aee5cb 100644 --- a/src/main/res/animator/upper_play_to_pause_animation.xml +++ b/src/main/res/animator/upper_play_to_pause_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/color/text_color_dark_theme.xml b/src/main/res/color/text_color_dark_theme.xml index 95308fe1a..d74b3e6c4 100644 --- a/src/main/res/color/text_color_dark_theme.xml +++ b/src/main/res/color/text_color_dark_theme.xml @@ -1,5 +1,7 @@ - - - - \ No newline at end of file + + + + diff --git a/src/main/res/color/text_color_secondary_dark_theme.xml b/src/main/res/color/text_color_secondary_dark_theme.xml index 5510b324c..9dd39f59a 100644 --- a/src/main/res/color/text_color_secondary_dark_theme.xml +++ b/src/main/res/color/text_color_secondary_dark_theme.xml @@ -1,5 +1,7 @@ - - - - \ No newline at end of file + + + + diff --git a/src/main/res/drawable-anydpi-v24/icon_notification.xml b/src/main/res/drawable-anydpi-v24/icon_notification.xml index d59648bca..4ad08a820 100644 --- a/src/main/res/drawable-anydpi-v24/icon_notification.xml +++ b/src/main/res/drawable-anydpi-v24/icon_notification.xml @@ -1,18 +1,20 @@ - - - - - + + + + diff --git a/src/main/res/drawable-anydpi-v24/notification_permanent.xml b/src/main/res/drawable-anydpi-v24/notification_permanent.xml index 939f87946..0283db96a 100644 --- a/src/main/res/drawable-anydpi-v24/notification_permanent.xml +++ b/src/main/res/drawable-anydpi-v24/notification_permanent.xml @@ -1,18 +1,25 @@ - - > + + > - + - + - + diff --git a/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml b/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml index fff6b14b6..d586ef368 100644 --- a/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml +++ b/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml @@ -1,6 +1,5 @@ - + @@ -9,7 +8,8 @@ android:topLeftRadius="@dimen/message_corner_radius" android:topRightRadius="@dimen/message_corner_radius" android:bottomLeftRadius="@dimen/message_corner_radius" /> - + diff --git a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml index 196688a09..e45cf3247 100644 --- a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml +++ b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml @@ -1,6 +1,5 @@ - + @@ -9,7 +8,8 @@ android:topLeftRadius="@dimen/message_corner_radius" android:topRightRadius="@dimen/message_corner_radius" android:bottomRightRadius="@dimen/message_corner_radius" /> - + diff --git a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml index 315f63174..3da4f5a41 100644 --- a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml +++ b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml @@ -1,11 +1,12 @@ - + - + - - - + + + diff --git a/src/main/res/drawable-night/jumpto_btn_bg.xml b/src/main/res/drawable-night/jumpto_btn_bg.xml index be9119122..3f435878e 100644 --- a/src/main/res/drawable-night/jumpto_btn_bg.xml +++ b/src/main/res/drawable-night/jumpto_btn_bg.xml @@ -1,5 +1,6 @@ - - + + diff --git a/src/main/res/drawable-night/pinned_list_item_background.xml b/src/main/res/drawable-night/pinned_list_item_background.xml index dc98397b0..a4a4cc66b 100644 --- a/src/main/res/drawable-night/pinned_list_item_background.xml +++ b/src/main/res/drawable-night/pinned_list_item_background.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable-night/pinned_list_item_background_blue.xml b/src/main/res/drawable-night/pinned_list_item_background_blue.xml index 084144a4b..b618da3f7 100644 --- a/src/main/res/drawable-night/pinned_list_item_background_blue.xml +++ b/src/main/res/drawable-night/pinned_list_item_background_blue.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable-night/pinned_list_item_background_gray.xml b/src/main/res/drawable-night/pinned_list_item_background_gray.xml index c0e41aaf0..c2be44695 100644 --- a/src/main/res/drawable-night/pinned_list_item_background_gray.xml +++ b/src/main/res/drawable-night/pinned_list_item_background_gray.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable-night/pinned_list_item_background_green.xml b/src/main/res/drawable-night/pinned_list_item_background_green.xml index 683182c4e..aec174734 100644 --- a/src/main/res/drawable-night/pinned_list_item_background_green.xml +++ b/src/main/res/drawable-night/pinned_list_item_background_green.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable-night/pinned_list_item_background_pink.xml b/src/main/res/drawable-night/pinned_list_item_background_pink.xml index 6004e6211..72abb7e67 100644 --- a/src/main/res/drawable-night/pinned_list_item_background_pink.xml +++ b/src/main/res/drawable-night/pinned_list_item_background_pink.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable-night/pinned_list_item_background_purple.xml b/src/main/res/drawable-night/pinned_list_item_background_purple.xml index e9b33c6fb..dec42d0bb 100644 --- a/src/main/res/drawable-night/pinned_list_item_background_purple.xml +++ b/src/main/res/drawable-night/pinned_list_item_background_purple.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable-night/pinned_list_item_background_red.xml b/src/main/res/drawable-night/pinned_list_item_background_red.xml index 6a27959db..aaef90864 100644 --- a/src/main/res/drawable-night/pinned_list_item_background_red.xml +++ b/src/main/res/drawable-night/pinned_list_item_background_red.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/archived_indicator_background.xml b/src/main/res/drawable/archived_indicator_background.xml index 046602984..2ab541f68 100644 --- a/src/main/res/drawable/archived_indicator_background.xml +++ b/src/main/res/drawable/archived_indicator_background.xml @@ -1,6 +1,11 @@ - - - - + + + + diff --git a/src/main/res/drawable/attachment_selector_shadow.xml b/src/main/res/drawable/attachment_selector_shadow.xml index 34e2aa8cd..2e7d4a13c 100644 --- a/src/main/res/drawable/attachment_selector_shadow.xml +++ b/src/main/res/drawable/attachment_selector_shadow.xml @@ -6,6 +6,6 @@ + android:angle="90" /> - \ No newline at end of file + diff --git a/src/main/res/drawable/badge_divider.xml b/src/main/res/drawable/badge_divider.xml index 9db9b4665..c741317f5 100644 --- a/src/main/res/drawable/badge_divider.xml +++ b/src/main/res/drawable/badge_divider.xml @@ -1,5 +1,6 @@ - + diff --git a/src/main/res/drawable/baseline_bookmark_24.xml b/src/main/res/drawable/baseline_bookmark_24.xml index 77251324b..81fd358b1 100644 --- a/src/main/res/drawable/baseline_bookmark_24.xml +++ b/src/main/res/drawable/baseline_bookmark_24.xml @@ -1,3 +1,11 @@ - - + + diff --git a/src/main/res/drawable/baseline_bookmark_border_24.xml b/src/main/res/drawable/baseline_bookmark_border_24.xml index d64b0ea27..def67c538 100644 --- a/src/main/res/drawable/baseline_bookmark_border_24.xml +++ b/src/main/res/drawable/baseline_bookmark_border_24.xml @@ -1,5 +1,13 @@ - - - - + + + + diff --git a/src/main/res/drawable/baseline_bookmark_remove_24.xml b/src/main/res/drawable/baseline_bookmark_remove_24.xml index 1fb5c5a92..d28658778 100644 --- a/src/main/res/drawable/baseline_bookmark_remove_24.xml +++ b/src/main/res/drawable/baseline_bookmark_remove_24.xml @@ -1,3 +1,11 @@ - - + + diff --git a/src/main/res/drawable/baseline_call_24.xml b/src/main/res/drawable/baseline_call_24.xml index 2aba8addc..4e81fb715 100644 --- a/src/main/res/drawable/baseline_call_24.xml +++ b/src/main/res/drawable/baseline_call_24.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/baseline_call_end_24.xml b/src/main/res/drawable/baseline_call_end_24.xml index fa4cd2c10..82d2d30cc 100644 --- a/src/main/res/drawable/baseline_call_end_24.xml +++ b/src/main/res/drawable/baseline_call_end_24.xml @@ -1,5 +1,13 @@ - - - - + + + + diff --git a/src/main/res/drawable/button_bg.xml b/src/main/res/drawable/button_bg.xml index 9e5b10938..c1c526333 100644 --- a/src/main/res/drawable/button_bg.xml +++ b/src/main/res/drawable/button_bg.xml @@ -1,5 +1,7 @@ - - - + + + diff --git a/src/main/res/drawable/button_secondary_background.xml b/src/main/res/drawable/button_secondary_background.xml index 8fe84fca5..fa4d7cad1 100644 --- a/src/main/res/drawable/button_secondary_background.xml +++ b/src/main/res/drawable/button_secondary_background.xml @@ -1,6 +1,10 @@ - - - + + + diff --git a/src/main/res/drawable/circle_alpha.xml b/src/main/res/drawable/circle_alpha.xml index 82142dd34..3c8344cd4 100644 --- a/src/main/res/drawable/circle_alpha.xml +++ b/src/main/res/drawable/circle_alpha.xml @@ -1,5 +1,6 @@ - - + + diff --git a/src/main/res/drawable/circle_tintable.xml b/src/main/res/drawable/circle_tintable.xml index 6c5c36063..60c36468e 100644 --- a/src/main/res/drawable/circle_tintable.xml +++ b/src/main/res/drawable/circle_tintable.xml @@ -1,5 +1,6 @@ - + diff --git a/src/main/res/drawable/circle_touch_highlight_background.xml b/src/main/res/drawable/circle_touch_highlight_background.xml index fe392b45e..35ec52275 100644 --- a/src/main/res/drawable/circle_touch_highlight_background.xml +++ b/src/main/res/drawable/circle_touch_highlight_background.xml @@ -1,9 +1,10 @@ - + - \ No newline at end of file + diff --git a/src/main/res/drawable/circle_universal_overlay.xml b/src/main/res/drawable/circle_universal_overlay.xml index 32c516245..39fc33611 100644 --- a/src/main/res/drawable/circle_universal_overlay.xml +++ b/src/main/res/drawable/circle_universal_overlay.xml @@ -1,4 +1,6 @@ - - - \ No newline at end of file + + + diff --git a/src/main/res/drawable/circle_white.xml b/src/main/res/drawable/circle_white.xml index 631057c46..60c36468e 100644 --- a/src/main/res/drawable/circle_white.xml +++ b/src/main/res/drawable/circle_white.xml @@ -1,4 +1,6 @@ - - - \ No newline at end of file + + + diff --git a/src/main/res/drawable/contact_blocked_24.xml b/src/main/res/drawable/contact_blocked_24.xml index 3368f8111..370a3d81d 100644 --- a/src/main/res/drawable/contact_blocked_24.xml +++ b/src/main/res/drawable/contact_blocked_24.xml @@ -1,5 +1,13 @@ - - - - + + + + diff --git a/src/main/res/drawable/contact_list_divider_dark.xml b/src/main/res/drawable/contact_list_divider_dark.xml index ab63ce24a..5b294af13 100644 --- a/src/main/res/drawable/contact_list_divider_dark.xml +++ b/src/main/res/drawable/contact_list_divider_dark.xml @@ -3,9 +3,13 @@ - + - + diff --git a/src/main/res/drawable/contact_list_divider_light.xml b/src/main/res/drawable/contact_list_divider_light.xml index f28cf73f6..a5462769b 100644 --- a/src/main/res/drawable/contact_list_divider_light.xml +++ b/src/main/res/drawable/contact_list_divider_light.xml @@ -3,10 +3,14 @@ - + - + - \ No newline at end of file + diff --git a/src/main/res/drawable/contact_photo_background.xml b/src/main/res/drawable/contact_photo_background.xml index ff1153bf2..befe35636 100644 --- a/src/main/res/drawable/contact_photo_background.xml +++ b/src/main/res/drawable/contact_photo_background.xml @@ -7,4 +7,4 @@ - \ No newline at end of file + diff --git a/src/main/res/drawable/conversation_attachment_close_circle.xml b/src/main/res/drawable/conversation_attachment_close_circle.xml index 86741f86e..158d39631 100644 --- a/src/main/res/drawable/conversation_attachment_close_circle.xml +++ b/src/main/res/drawable/conversation_attachment_close_circle.xml @@ -2,14 +2,18 @@ - - - + + + - + diff --git a/src/main/res/drawable/conversation_attachment_edit.xml b/src/main/res/drawable/conversation_attachment_edit.xml index a72bb14ed..d1515d84a 100644 --- a/src/main/res/drawable/conversation_attachment_edit.xml +++ b/src/main/res/drawable/conversation_attachment_edit.xml @@ -2,15 +2,18 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_item_background.xml b/src/main/res/drawable/conversation_item_background.xml index a1e2111d1..34c7a12a6 100644 --- a/src/main/res/drawable/conversation_item_background.xml +++ b/src/main/res/drawable/conversation_item_background.xml @@ -1,5 +1,9 @@ - - + + diff --git a/src/main/res/drawable/conversation_item_background_animated.xml b/src/main/res/drawable/conversation_item_background_animated.xml index 632882d7c..1bf8dfb5e 100644 --- a/src/main/res/drawable/conversation_item_background_animated.xml +++ b/src/main/res/drawable/conversation_item_background_animated.xml @@ -1,5 +1,11 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_animated_blue.xml b/src/main/res/drawable/conversation_item_background_animated_blue.xml index d7af7cdbb..927ec9286 100644 --- a/src/main/res/drawable/conversation_item_background_animated_blue.xml +++ b/src/main/res/drawable/conversation_item_background_animated_blue.xml @@ -1,5 +1,11 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_animated_gray.xml b/src/main/res/drawable/conversation_item_background_animated_gray.xml index 825fb3655..c069b7b29 100644 --- a/src/main/res/drawable/conversation_item_background_animated_gray.xml +++ b/src/main/res/drawable/conversation_item_background_animated_gray.xml @@ -1,5 +1,11 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_animated_green.xml b/src/main/res/drawable/conversation_item_background_animated_green.xml index a1204f69d..1aad84f07 100644 --- a/src/main/res/drawable/conversation_item_background_animated_green.xml +++ b/src/main/res/drawable/conversation_item_background_animated_green.xml @@ -1,5 +1,11 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_animated_pink.xml b/src/main/res/drawable/conversation_item_background_animated_pink.xml index eb8f54f85..1d7d7796a 100644 --- a/src/main/res/drawable/conversation_item_background_animated_pink.xml +++ b/src/main/res/drawable/conversation_item_background_animated_pink.xml @@ -1,5 +1,11 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_animated_purple.xml b/src/main/res/drawable/conversation_item_background_animated_purple.xml index 319cf63bb..db8a24c64 100644 --- a/src/main/res/drawable/conversation_item_background_animated_purple.xml +++ b/src/main/res/drawable/conversation_item_background_animated_purple.xml @@ -1,5 +1,11 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_animated_red.xml b/src/main/res/drawable/conversation_item_background_animated_red.xml index c4e9681b9..64325cc27 100644 --- a/src/main/res/drawable/conversation_item_background_animated_red.xml +++ b/src/main/res/drawable/conversation_item_background_animated_red.xml @@ -1,5 +1,11 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_blue.xml b/src/main/res/drawable/conversation_item_background_blue.xml index 1660d3233..5a7cab587 100644 --- a/src/main/res/drawable/conversation_item_background_blue.xml +++ b/src/main/res/drawable/conversation_item_background_blue.xml @@ -1,5 +1,9 @@ - - + + diff --git a/src/main/res/drawable/conversation_item_background_gray.xml b/src/main/res/drawable/conversation_item_background_gray.xml index 2648813cb..d696cda75 100644 --- a/src/main/res/drawable/conversation_item_background_gray.xml +++ b/src/main/res/drawable/conversation_item_background_gray.xml @@ -1,5 +1,9 @@ - - + + diff --git a/src/main/res/drawable/conversation_item_background_green.xml b/src/main/res/drawable/conversation_item_background_green.xml index f0dc1d9bd..d85b5dde7 100644 --- a/src/main/res/drawable/conversation_item_background_green.xml +++ b/src/main/res/drawable/conversation_item_background_green.xml @@ -1,5 +1,9 @@ - - + + diff --git a/src/main/res/drawable/conversation_item_background_pink.xml b/src/main/res/drawable/conversation_item_background_pink.xml index 48b31c922..3d91394ee 100644 --- a/src/main/res/drawable/conversation_item_background_pink.xml +++ b/src/main/res/drawable/conversation_item_background_pink.xml @@ -1,5 +1,9 @@ - - + + diff --git a/src/main/res/drawable/conversation_item_background_purple.xml b/src/main/res/drawable/conversation_item_background_purple.xml index b0463a491..b4860321f 100644 --- a/src/main/res/drawable/conversation_item_background_purple.xml +++ b/src/main/res/drawable/conversation_item_background_purple.xml @@ -1,5 +1,9 @@ - - + + diff --git a/src/main/res/drawable/conversation_item_background_red.xml b/src/main/res/drawable/conversation_item_background_red.xml index 489892e73..0cd2471d4 100644 --- a/src/main/res/drawable/conversation_item_background_red.xml +++ b/src/main/res/drawable/conversation_item_background_red.xml @@ -1,5 +1,9 @@ - - + + diff --git a/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml b/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml index 59da5934a..9ebc6bb6f 100644 --- a/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml +++ b/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml @@ -2,7 +2,7 @@ - + @@ -11,7 +11,9 @@ - + diff --git a/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml b/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml index d02a39156..486b1a5d6 100644 --- a/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml +++ b/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml @@ -2,7 +2,7 @@ - + @@ -11,7 +11,9 @@ - + diff --git a/src/main/res/drawable/conversation_item_update_background.xml b/src/main/res/drawable/conversation_item_update_background.xml index 6a7c72c11..12ac1ed7a 100644 --- a/src/main/res/drawable/conversation_item_update_background.xml +++ b/src/main/res/drawable/conversation_item_update_background.xml @@ -1,7 +1,9 @@ - - + diff --git a/src/main/res/drawable/conversation_list_item_background.xml b/src/main/res/drawable/conversation_list_item_background.xml index 82a85b420..cb0cd0c6a 100644 --- a/src/main/res/drawable/conversation_list_item_background.xml +++ b/src/main/res/drawable/conversation_list_item_background.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_list_item_background_blue.xml b/src/main/res/drawable/conversation_list_item_background_blue.xml index b4b4d54a6..2b5854a01 100644 --- a/src/main/res/drawable/conversation_list_item_background_blue.xml +++ b/src/main/res/drawable/conversation_list_item_background_blue.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_list_item_background_gray.xml b/src/main/res/drawable/conversation_list_item_background_gray.xml index 55e2c74fb..6296b1257 100644 --- a/src/main/res/drawable/conversation_list_item_background_gray.xml +++ b/src/main/res/drawable/conversation_list_item_background_gray.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_list_item_background_green.xml b/src/main/res/drawable/conversation_list_item_background_green.xml index cd4e19115..a3db4bcec 100644 --- a/src/main/res/drawable/conversation_list_item_background_green.xml +++ b/src/main/res/drawable/conversation_list_item_background_green.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_list_item_background_pink.xml b/src/main/res/drawable/conversation_list_item_background_pink.xml index 07e223232..3dc0266d6 100644 --- a/src/main/res/drawable/conversation_list_item_background_pink.xml +++ b/src/main/res/drawable/conversation_list_item_background_pink.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_list_item_background_purple.xml b/src/main/res/drawable/conversation_list_item_background_purple.xml index 010a9a07e..a26253bce 100644 --- a/src/main/res/drawable/conversation_list_item_background_purple.xml +++ b/src/main/res/drawable/conversation_list_item_background_purple.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_list_item_background_red.xml b/src/main/res/drawable/conversation_list_item_background_red.xml index 1a6dcfd4f..ede176b4a 100644 --- a/src/main/res/drawable/conversation_list_item_background_red.xml +++ b/src/main/res/drawable/conversation_list_item_background_red.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/delete_account_item_background.xml b/src/main/res/drawable/delete_account_item_background.xml index 583b62b94..76d7b797d 100644 --- a/src/main/res/drawable/delete_account_item_background.xml +++ b/src/main/res/drawable/delete_account_item_background.xml @@ -1,7 +1,9 @@ - - + diff --git a/src/main/res/drawable/dismiss_background.xml b/src/main/res/drawable/dismiss_background.xml index d373de262..97addfaa3 100644 --- a/src/main/res/drawable/dismiss_background.xml +++ b/src/main/res/drawable/dismiss_background.xml @@ -1,5 +1,6 @@ - - + + diff --git a/src/main/res/drawable/divider_end.xml b/src/main/res/drawable/divider_end.xml index be4c65380..bd3c2460a 100644 --- a/src/main/res/drawable/divider_end.xml +++ b/src/main/res/drawable/divider_end.xml @@ -1,10 +1,11 @@ - + - + diff --git a/src/main/res/drawable/divider_start.xml b/src/main/res/drawable/divider_start.xml index 220c9aba0..7a31661ad 100644 --- a/src/main/res/drawable/divider_start.xml +++ b/src/main/res/drawable/divider_start.xml @@ -1,10 +1,11 @@ - + - + diff --git a/src/main/res/drawable/floating_mini_bg_dark.xml b/src/main/res/drawable/floating_mini_bg_dark.xml index 617e35d5e..5761ae569 100644 --- a/src/main/res/drawable/floating_mini_bg_dark.xml +++ b/src/main/res/drawable/floating_mini_bg_dark.xml @@ -1,6 +1,8 @@ - - + + diff --git a/src/main/res/drawable/floating_mini_bg_light.xml b/src/main/res/drawable/floating_mini_bg_light.xml index 3b24b52c7..6ab43ff68 100644 --- a/src/main/res/drawable/floating_mini_bg_light.xml +++ b/src/main/res/drawable/floating_mini_bg_light.xml @@ -1,6 +1,8 @@ - - + + diff --git a/src/main/res/drawable/ic_advanced_24dp.xml b/src/main/res/drawable/ic_advanced_24dp.xml index 4fb9e1ab0..a3028d6f9 100644 --- a/src/main/res/drawable/ic_advanced_24dp.xml +++ b/src/main/res/drawable/ic_advanced_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_alternate_email_24.xml b/src/main/res/drawable/ic_alternate_email_24.xml index e6182db41..505891890 100644 --- a/src/main/res/drawable/ic_alternate_email_24.xml +++ b/src/main/res/drawable/ic_alternate_email_24.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_apps_24.xml b/src/main/res/drawable/ic_apps_24.xml index f578daeb2..b2427cca5 100644 --- a/src/main/res/drawable/ic_apps_24.xml +++ b/src/main/res/drawable/ic_apps_24.xml @@ -1,3 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_baseline_devices_24.xml b/src/main/res/drawable/ic_baseline_devices_24.xml index 28a718506..b0e486b8b 100644 --- a/src/main/res/drawable/ic_baseline_devices_24.xml +++ b/src/main/res/drawable/ic_baseline_devices_24.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_baseline_eye.xml b/src/main/res/drawable/ic_baseline_eye.xml index c74c5a758..94b4ec1ad 100644 --- a/src/main/res/drawable/ic_baseline_eye.xml +++ b/src/main/res/drawable/ic_baseline_eye.xml @@ -1,9 +1,10 @@ - - + diff --git a/src/main/res/drawable/ic_brightness_6_24dp.xml b/src/main/res/drawable/ic_brightness_6_24dp.xml index 548ad986f..aaa84c514 100644 --- a/src/main/res/drawable/ic_brightness_6_24dp.xml +++ b/src/main/res/drawable/ic_brightness_6_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_chevron_up.xml b/src/main/res/drawable/ic_chevron_up.xml index c54db3bcc..97f0d74d8 100644 --- a/src/main/res/drawable/ic_chevron_up.xml +++ b/src/main/res/drawable/ic_chevron_up.xml @@ -1,9 +1,10 @@ - - + diff --git a/src/main/res/drawable/ic_delivery_status_failed.xml b/src/main/res/drawable/ic_delivery_status_failed.xml index fbd4009fd..8823caf0c 100644 --- a/src/main/res/drawable/ic_delivery_status_failed.xml +++ b/src/main/res/drawable/ic_delivery_status_failed.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_donate_24dp.xml b/src/main/res/drawable/ic_donate_24dp.xml index 07ab6aae9..9020e3e22 100644 --- a/src/main/res/drawable/ic_donate_24dp.xml +++ b/src/main/res/drawable/ic_donate_24dp.xml @@ -1,3 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_forum_24dp.xml b/src/main/res/drawable/ic_forum_24dp.xml index e48c05fd4..7318e8df8 100644 --- a/src/main/res/drawable/ic_forum_24dp.xml +++ b/src/main/res/drawable/ic_forum_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_forward_white_24dp.xml b/src/main/res/drawable/ic_forward_white_24dp.xml index 4b84d9a42..3cdfcb028 100644 --- a/src/main/res/drawable/ic_forward_white_24dp.xml +++ b/src/main/res/drawable/ic_forward_white_24dp.xml @@ -1,3 +1,12 @@ - - + + diff --git a/src/main/res/drawable/ic_help_24dp.xml b/src/main/res/drawable/ic_help_24dp.xml index 66449b72f..b733dcf5f 100644 --- a/src/main/res/drawable/ic_help_24dp.xml +++ b/src/main/res/drawable/ic_help_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_launcher_foreground.xml b/src/main/res/drawable/ic_launcher_foreground.xml index 22648ce29..13450c9e2 100644 --- a/src/main/res/drawable/ic_launcher_foreground.xml +++ b/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,17 +1,19 @@ - - - - - + + + + diff --git a/src/main/res/drawable/ic_launcher_foreground_monochrome.xml b/src/main/res/drawable/ic_launcher_foreground_monochrome.xml index 8ef65e2a8..213abaa99 100644 --- a/src/main/res/drawable/ic_launcher_foreground_monochrome.xml +++ b/src/main/res/drawable/ic_launcher_foreground_monochrome.xml @@ -1,15 +1,17 @@ - - - - + + + diff --git a/src/main/res/drawable/ic_link_24.xml b/src/main/res/drawable/ic_link_24.xml index 54d9934a3..a7164796d 100644 --- a/src/main/res/drawable/ic_link_24.xml +++ b/src/main/res/drawable/ic_link_24.xml @@ -1,8 +1,16 @@ - + - + diff --git a/src/main/res/drawable/ic_lock.xml b/src/main/res/drawable/ic_lock.xml index b642ae75e..94153f58b 100644 --- a/src/main/res/drawable/ic_lock.xml +++ b/src/main/res/drawable/ic_lock.xml @@ -1,9 +1,10 @@ - - + diff --git a/src/main/res/drawable/ic_lock_24dp.xml b/src/main/res/drawable/ic_lock_24dp.xml index da2a77be2..42a7b7d99 100644 --- a/src/main/res/drawable/ic_lock_24dp.xml +++ b/src/main/res/drawable/ic_lock_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_map_white_24dp.xml b/src/main/res/drawable/ic_map_white_24dp.xml index efd6dc37e..56a0c3b90 100644 --- a/src/main/res/drawable/ic_map_white_24dp.xml +++ b/src/main/res/drawable/ic_map_white_24dp.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_notifications_24dp.xml b/src/main/res/drawable/ic_notifications_24dp.xml index 56cd14d7a..9788adcc3 100644 --- a/src/main/res/drawable/ic_notifications_24dp.xml +++ b/src/main/res/drawable/ic_notifications_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_outline_email.xml b/src/main/res/drawable/ic_outline_email.xml index 19ab62ba4..9d0a435c2 100644 --- a/src/main/res/drawable/ic_outline_email.xml +++ b/src/main/res/drawable/ic_outline_email.xml @@ -6,6 +6,8 @@ android:viewportWidth="24" android:tint="#FFFFFF"> - + - \ No newline at end of file + diff --git a/src/main/res/drawable/ic_person_large.xml b/src/main/res/drawable/ic_person_large.xml index c3324be1d..2c9b9ecbe 100644 --- a/src/main/res/drawable/ic_person_large.xml +++ b/src/main/res/drawable/ic_person_large.xml @@ -1,4 +1,10 @@ - - + + diff --git a/src/main/res/drawable/ic_proxy_disabled_24.xml b/src/main/res/drawable/ic_proxy_disabled_24.xml index 287bc46d2..290bdf840 100644 --- a/src/main/res/drawable/ic_proxy_disabled_24.xml +++ b/src/main/res/drawable/ic_proxy_disabled_24.xml @@ -1,5 +1,13 @@ - - - - + + + + diff --git a/src/main/res/drawable/ic_proxy_enabled_24.xml b/src/main/res/drawable/ic_proxy_enabled_24.xml index 81c20e883..c559c67de 100644 --- a/src/main/res/drawable/ic_proxy_enabled_24.xml +++ b/src/main/res/drawable/ic_proxy_enabled_24.xml @@ -1,5 +1,13 @@ - - - - + + + + diff --git a/src/main/res/drawable/ic_qr_code_24.xml b/src/main/res/drawable/ic_qr_code_24.xml index f95c18d61..f5ca18c7e 100644 --- a/src/main/res/drawable/ic_qr_code_24.xml +++ b/src/main/res/drawable/ic_qr_code_24.xml @@ -1,3 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_qr_code_scanner_24.xml b/src/main/res/drawable/ic_qr_code_scanner_24.xml index 725b35ea7..5164cc16a 100644 --- a/src/main/res/drawable/ic_qr_code_scanner_24.xml +++ b/src/main/res/drawable/ic_qr_code_scanner_24.xml @@ -1,5 +1,13 @@ - + - + diff --git a/src/main/res/drawable/ic_share_white_24dp.xml b/src/main/res/drawable/ic_share_white_24dp.xml index 045bbc0c0..02eea7642 100644 --- a/src/main/res/drawable/ic_share_white_24dp.xml +++ b/src/main/res/drawable/ic_share_white_24dp.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_sticker_24.xml b/src/main/res/drawable/ic_sticker_24.xml index 102dedfd5..4259002f5 100644 --- a/src/main/res/drawable/ic_sticker_24.xml +++ b/src/main/res/drawable/ic_sticker_24.xml @@ -1,3 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_swap_vert_24dp.xml b/src/main/res/drawable/ic_swap_vert_24dp.xml index c54dc343d..a67392a3d 100644 --- a/src/main/res/drawable/ic_swap_vert_24dp.xml +++ b/src/main/res/drawable/ic_swap_vert_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_timer_gray_18dp.xml b/src/main/res/drawable/ic_timer_gray_18dp.xml index 52a5c9218..3ca1d3e82 100644 --- a/src/main/res/drawable/ic_timer_gray_18dp.xml +++ b/src/main/res/drawable/ic_timer_gray_18dp.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/ic_verified.xml b/src/main/res/drawable/ic_verified.xml index c8b706df6..280717156 100644 --- a/src/main/res/drawable/ic_verified.xml +++ b/src/main/res/drawable/ic_verified.xml @@ -1,8 +1,16 @@ - - + - + android:strokeColor="#00000000" + android:strokeWidth="206.103" /> + diff --git a/src/main/res/drawable/ic_videocam_white_24dp.xml b/src/main/res/drawable/ic_videocam_white_24dp.xml index f75fa71ae..552a06766 100644 --- a/src/main/res/drawable/ic_videocam_white_24dp.xml +++ b/src/main/res/drawable/ic_videocam_white_24dp.xml @@ -1,5 +1,11 @@ - - + + diff --git a/src/main/res/drawable/image_shade.xml b/src/main/res/drawable/image_shade.xml index 07a18cb4d..54993da2b 100644 --- a/src/main/res/drawable/image_shade.xml +++ b/src/main/res/drawable/image_shade.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/src/main/res/drawable/jumpto_btn_bg.xml b/src/main/res/drawable/jumpto_btn_bg.xml index 82142dd34..3c8344cd4 100644 --- a/src/main/res/drawable/jumpto_btn_bg.xml +++ b/src/main/res/drawable/jumpto_btn_bg.xml @@ -1,5 +1,6 @@ - - + + diff --git a/src/main/res/drawable/message_bubble_background_received_alone.xml b/src/main/res/drawable/message_bubble_background_received_alone.xml index 196688a09..e45cf3247 100644 --- a/src/main/res/drawable/message_bubble_background_received_alone.xml +++ b/src/main/res/drawable/message_bubble_background_received_alone.xml @@ -1,6 +1,5 @@ - + @@ -9,7 +8,8 @@ android:topLeftRadius="@dimen/message_corner_radius" android:topRightRadius="@dimen/message_corner_radius" android:bottomRightRadius="@dimen/message_corner_radius" /> - + diff --git a/src/main/res/drawable/message_bubble_background_sent_alone.xml b/src/main/res/drawable/message_bubble_background_sent_alone.xml index fff6b14b6..d586ef368 100644 --- a/src/main/res/drawable/message_bubble_background_sent_alone.xml +++ b/src/main/res/drawable/message_bubble_background_sent_alone.xml @@ -1,6 +1,5 @@ - + @@ -9,7 +8,8 @@ android:topLeftRadius="@dimen/message_corner_radius" android:topRightRadius="@dimen/message_corner_radius" android:bottomLeftRadius="@dimen/message_corner_radius" /> - + diff --git a/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml b/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml index e8ac29c9a..713b86bc6 100644 --- a/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml +++ b/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml @@ -1,11 +1,12 @@ - + - + + android:strokeMiterLimit="10" /> + android:strokeMiterLimit="10" /> - \ No newline at end of file + diff --git a/src/main/res/drawable/pause_to_play_animation.xml b/src/main/res/drawable/pause_to_play_animation.xml index aa010cee9..d359bd161 100644 --- a/src/main/res/drawable/pause_to_play_animation.xml +++ b/src/main/res/drawable/pause_to_play_animation.xml @@ -1,16 +1,17 @@ - + android:animation="@animator/rotate_minus_90_animation" /> + android:animation="@animator/upper_pause_to_play_animation" /> + android:animation="@animator/bottom_pause_to_play_animation" /> diff --git a/src/main/res/drawable/pinned_list_item_background.xml b/src/main/res/drawable/pinned_list_item_background.xml index 0df99189d..01d71cee9 100644 --- a/src/main/res/drawable/pinned_list_item_background.xml +++ b/src/main/res/drawable/pinned_list_item_background.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/pinned_list_item_background_blue.xml b/src/main/res/drawable/pinned_list_item_background_blue.xml index ca3c9c8fd..2924a9475 100644 --- a/src/main/res/drawable/pinned_list_item_background_blue.xml +++ b/src/main/res/drawable/pinned_list_item_background_blue.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/pinned_list_item_background_gray.xml b/src/main/res/drawable/pinned_list_item_background_gray.xml index ae332ca2f..b8cc3a41f 100644 --- a/src/main/res/drawable/pinned_list_item_background_gray.xml +++ b/src/main/res/drawable/pinned_list_item_background_gray.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/pinned_list_item_background_green.xml b/src/main/res/drawable/pinned_list_item_background_green.xml index 3264071d4..19105d3fd 100644 --- a/src/main/res/drawable/pinned_list_item_background_green.xml +++ b/src/main/res/drawable/pinned_list_item_background_green.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/pinned_list_item_background_pink.xml b/src/main/res/drawable/pinned_list_item_background_pink.xml index 7279114a9..1becb7b0c 100644 --- a/src/main/res/drawable/pinned_list_item_background_pink.xml +++ b/src/main/res/drawable/pinned_list_item_background_pink.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/pinned_list_item_background_purple.xml b/src/main/res/drawable/pinned_list_item_background_purple.xml index 5761632a4..2c86c1070 100644 --- a/src/main/res/drawable/pinned_list_item_background_purple.xml +++ b/src/main/res/drawable/pinned_list_item_background_purple.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/pinned_list_item_background_red.xml b/src/main/res/drawable/pinned_list_item_background_red.xml index 0e30584d4..7310639ad 100644 --- a/src/main/res/drawable/pinned_list_item_background_red.xml +++ b/src/main/res/drawable/pinned_list_item_background_red.xml @@ -1,10 +1,15 @@ - - + + - + diff --git a/src/main/res/drawable/play_icon.xml b/src/main/res/drawable/play_icon.xml index 7472ac7de..ea4da4c40 100644 --- a/src/main/res/drawable/play_icon.xml +++ b/src/main/res/drawable/play_icon.xml @@ -18,7 +18,7 @@ android:fillColor="@android:color/white" android:strokeLineCap="butt" android:strokeLineJoin="miter" - android:strokeMiterLimit="10"/> + android:strokeMiterLimit="10" /> + android:strokeMiterLimit="10" /> - \ No newline at end of file + diff --git a/src/main/res/drawable/play_to_pause_animation.xml b/src/main/res/drawable/play_to_pause_animation.xml index 173fed50f..3c7a99a06 100644 --- a/src/main/res/drawable/play_to_pause_animation.xml +++ b/src/main/res/drawable/play_to_pause_animation.xml @@ -1,16 +1,17 @@ - + android:animation="@animator/rotate_90_animation" /> + android:animation="@animator/upper_play_to_pause_animation" /> + android:animation="@animator/bottom_play_to_pause_animation" /> diff --git a/src/main/res/drawable/reaction_pill_background.xml b/src/main/res/drawable/reaction_pill_background.xml index bad81d54c..6e4faca03 100644 --- a/src/main/res/drawable/reaction_pill_background.xml +++ b/src/main/res/drawable/reaction_pill_background.xml @@ -2,5 +2,7 @@ - + diff --git a/src/main/res/drawable/reaction_pill_background_selected.xml b/src/main/res/drawable/reaction_pill_background_selected.xml index b52af9b3d..af768f065 100644 --- a/src/main/res/drawable/reaction_pill_background_selected.xml +++ b/src/main/res/drawable/reaction_pill_background_selected.xml @@ -2,5 +2,7 @@ - + diff --git a/src/main/res/drawable/recording_lock_background_dark.xml b/src/main/res/drawable/recording_lock_background_dark.xml index 5dda37904..7be556aee 100644 --- a/src/main/res/drawable/recording_lock_background_dark.xml +++ b/src/main/res/drawable/recording_lock_background_dark.xml @@ -3,10 +3,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - + - + diff --git a/src/main/res/drawable/recording_lock_background_light.xml b/src/main/res/drawable/recording_lock_background_light.xml index d2e3a753e..929a5fa79 100644 --- a/src/main/res/drawable/recording_lock_background_light.xml +++ b/src/main/res/drawable/recording_lock_background_light.xml @@ -3,10 +3,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - + - + diff --git a/src/main/res/drawable/rounded_arrow_forward_24.xml b/src/main/res/drawable/rounded_arrow_forward_24.xml index 29b2576d4..d48d0e596 100644 --- a/src/main/res/drawable/rounded_arrow_forward_24.xml +++ b/src/main/res/drawable/rounded_arrow_forward_24.xml @@ -1,3 +1,12 @@ - - + + diff --git a/src/main/res/drawable/search_toolbar_shadow.xml b/src/main/res/drawable/search_toolbar_shadow.xml index 5afdc2a2d..f4bc37dea 100644 --- a/src/main/res/drawable/search_toolbar_shadow.xml +++ b/src/main/res/drawable/search_toolbar_shadow.xml @@ -1,7 +1,8 @@ - + - \ No newline at end of file + android:startColor="@android:color/transparent" + android:endColor="#40000000" + android:angle="90" /> + diff --git a/src/main/res/drawable/send_button_bg.xml b/src/main/res/drawable/send_button_bg.xml index 128666193..3fe9e7a11 100644 --- a/src/main/res/drawable/send_button_bg.xml +++ b/src/main/res/drawable/send_button_bg.xml @@ -1,5 +1,6 @@ - + diff --git a/src/main/res/drawable/sticky_date_header_background_dark.xml b/src/main/res/drawable/sticky_date_header_background_dark.xml index d5be2c288..93db9138a 100644 --- a/src/main/res/drawable/sticky_date_header_background_dark.xml +++ b/src/main/res/drawable/sticky_date_header_background_dark.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/src/main/res/drawable/sticky_date_header_background_light.xml b/src/main/res/drawable/sticky_date_header_background_light.xml index b015b381b..b1415dd9c 100644 --- a/src/main/res/drawable/sticky_date_header_background_light.xml +++ b/src/main/res/drawable/sticky_date_header_background_light.xml @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/src/main/res/drawable/touch_highlight_background.xml b/src/main/res/drawable/touch_highlight_background.xml index 3c7a58ba7..73163d3a6 100644 --- a/src/main/res/drawable/touch_highlight_background.xml +++ b/src/main/res/drawable/touch_highlight_background.xml @@ -1,5 +1,6 @@ - - + android:fillColor="#ffffff" /> diff --git a/src/main/res/layout-land/reaction_picker.xml b/src/main/res/layout-land/reaction_picker.xml index 0115e91fa..39294de5e 100644 --- a/src/main/res/layout-land/reaction_picker.xml +++ b/src/main/res/layout-land/reaction_picker.xml @@ -10,5 +10,5 @@ android:id="@+id/emoji_picker" android:layout_width="match_parent" android:layout_height="match_parent" - app:emojiGridColumns="10"/> + app:emojiGridColumns="10" /> diff --git a/src/main/res/layout/account_selection_list_fragment.xml b/src/main/res/layout/account_selection_list_fragment.xml index dfe5274fa..b5200d2a4 100644 --- a/src/main/res/layout/account_selection_list_fragment.xml +++ b/src/main/res/layout/account_selection_list_fragment.xml @@ -1,7 +1,8 @@ - + diff --git a/src/main/res/layout/account_selection_list_item.xml b/src/main/res/layout/account_selection_list_item.xml index 0192093b8..88f4cdc0a 100644 --- a/src/main/res/layout/account_selection_list_item.xml +++ b/src/main/res/layout/account_selection_list_item.xml @@ -1,40 +1,41 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="60dp" + android:orientation="horizontal" + android:gravity="center_vertical" + android:focusable="true" + android:background="?attr/conversation_list_item_background" + android:paddingLeft="16dp" + android:paddingStart="16dp"> + android:id="@+id/contact_photo_image" + android:layout_width="40dp" + android:layout_height="40dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:contentDescription="@null" /> - + - + - + - + - + + android:paddingEnd="16dp" /> diff --git a/src/main/res/layout/activity_application_preferences.xml b/src/main/res/layout/activity_application_preferences.xml index acddaa01c..9263e45bf 100644 --- a/src/main/res/layout/activity_application_preferences.xml +++ b/src/main/res/layout/activity_application_preferences.xml @@ -4,11 +4,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + diff --git a/src/main/res/layout/activity_blocked_contacts.xml b/src/main/res/layout/activity_blocked_contacts.xml index acddaa01c..9263e45bf 100644 --- a/src/main/res/layout/activity_blocked_contacts.xml +++ b/src/main/res/layout/activity_blocked_contacts.xml @@ -4,11 +4,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + diff --git a/src/main/res/layout/activity_conversation_list_archive.xml b/src/main/res/layout/activity_conversation_list_archive.xml index acddaa01c..9263e45bf 100644 --- a/src/main/res/layout/activity_conversation_list_archive.xml +++ b/src/main/res/layout/activity_conversation_list_archive.xml @@ -4,11 +4,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + diff --git a/src/main/res/layout/activity_edittransport.xml b/src/main/res/layout/activity_edittransport.xml index 3e393d856..668d33dc2 100644 --- a/src/main/res/layout/activity_edittransport.xml +++ b/src/main/res/layout/activity_edittransport.xml @@ -7,392 +7,389 @@ android:layout_height="match_parent" tools:context=".EditTransportActivity"> - + - - - - - - - - - + + - - - - - - + + + + + + + + + + + + + + + + + app:layout_constraintEnd_toEndOf="@id/guideline_root_end" + app:layout_constraintStart_toStartOf="@id/guideline_root_start" + app:layout_constraintTop_toBottomOf="@id/email"> - + - + - - - - - + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:layout_marginTop="12dp" + android:orientation="vertical" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/password"> + + + + + + + + + android:visibility="gone" + tools:visibility="visible" + app:constraint_referenced_ids="inbox, imap_login, imap_server, imap_port, imap_security_label, imap_security, outbox_view_spacer_top, + outbox, smtp_login, smtp_password, smtp_server, smtp_port, smtp_security_label, smtp_security, cert_check_label, cert_check, view_log_button, proxy_settings" /> + + + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toEndOf="@id/advanced_icon" + app:layout_constraintTop_toBottomOf="@id/no_servers_hint" /> - - - - - + - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + - - - - - - + + + + + + - - + app:layout_constraintEnd_toEndOf="@id/guideline_root_end" + app:layout_constraintStart_toStartOf="@id/guideline_root_start" + app:layout_constraintTop_toBottomOf="@id/smtp_login"> + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + android:text="@string/login_smtp_security" + app:layout_constraintEnd_toEndOf="@id/guideline_root_end" + app:layout_constraintStart_toStartOf="@id/guideline_root_start" + app:layout_constraintTop_toBottomOf="@id/smtp_port" /> + + - - - - - - - - - - - - - - - + + + + + android:text="@string/pref_view_log" + android:textColor="?attr/colorAccent" + android:textSize="16sp" + android:textStyle="normal" + android:typeface="sans" + android:paddingTop="16dp" + android:paddingBottom="32dp" + app:layout_constraintEnd_toEndOf="@id/guideline_root_end" + app:layout_constraintStart_toStartOf="@id/guideline_root_start" + app:layout_constraintTop_toBottomOf="@id/cert_check" /> - + - - - - - - - - - - - - - + diff --git a/src/main/res/layout/activity_qr.xml b/src/main/res/layout/activity_qr.xml index 68530d76c..e62a9e388 100644 --- a/src/main/res/layout/activity_qr.xml +++ b/src/main/res/layout/activity_qr.xml @@ -18,7 +18,7 @@ app:contentInsetStart="14dp" app:contentInsetLeft="14dp" android:elevation="4dp" - android:theme="?attr/actionBarStyle"/> + android:theme="?attr/actionBarStyle" /> + app:tabSelectedTextColor="@color/white" /> @@ -41,6 +41,6 @@ android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior"/> + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> diff --git a/src/main/res/layout/activity_qr_show.xml b/src/main/res/layout/activity_qr_show.xml index 473de0968..33dda999e 100644 --- a/src/main/res/layout/activity_qr_show.xml +++ b/src/main/res/layout/activity_qr_show.xml @@ -4,12 +4,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + diff --git a/src/main/res/layout/activity_registration_2nd_device_qr.xml b/src/main/res/layout/activity_registration_2nd_device_qr.xml index 01241e2b5..b5c237638 100644 --- a/src/main/res/layout/activity_registration_2nd_device_qr.xml +++ b/src/main/res/layout/activity_registration_2nd_device_qr.xml @@ -7,7 +7,7 @@ android:orientation="vertical" android:gravity="center_horizontal"> - + + android:textSize="16sp" /> + android:textSize="16sp" /> + android:textSize="16sp" /> + android:textSize="16sp" /> @@ -65,7 +65,7 @@ + android:layout_height="0dp" /> + android:layout_height="0dp" /> diff --git a/src/main/res/layout/activity_registration_qr.xml b/src/main/res/layout/activity_registration_qr.xml index 95880e3da..54887db3a 100644 --- a/src/main/res/layout/activity_registration_qr.xml +++ b/src/main/res/layout/activity_registration_qr.xml @@ -7,12 +7,12 @@ android:orientation="vertical" android:gravity="center_horizontal"> - + + android:layout_height="0dp" /> + android:layout_height="0dp" /> diff --git a/src/main/res/layout/activity_relay_list.xml b/src/main/res/layout/activity_relay_list.xml index 243fc3ccb..44a6bf55b 100644 --- a/src/main/res/layout/activity_relay_list.xml +++ b/src/main/res/layout/activity_relay_list.xml @@ -6,26 +6,26 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + - + diff --git a/src/main/res/layout/activity_select_chat_background.xml b/src/main/res/layout/activity_select_chat_background.xml index e61fb0b58..16df3fa71 100644 --- a/src/main/res/layout/activity_select_chat_background.xml +++ b/src/main/res/layout/activity_select_chat_background.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:layout_width="match_parent" + android:layout_height="wrap_content"> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:minHeight="?attr/actionBarSize" + android:background="?attr/colorPrimary" + app:contentInsetStart="14dp" + app:contentInsetLeft="14dp" + android:elevation="4dp" + android:theme="?attr/actionBarStyle" /> + android:id="@+id/tab_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="4dp" + android:layout_gravity="top" + android:background="?attr/colorPrimary" + app:tabMode="scrollable" + app:tabPaddingStart="8dp" + app:tabPaddingEnd="8dp" + app:tabBackground="?attr/colorPrimary" + app:tabIndicatorColor="@color/white" + app:tabTextColor="@color/gray10" + app:tabSelectedTextColor="@color/white" /> + android:id="@+id/pager" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> diff --git a/src/main/res/layout/attachment_type_selector.xml b/src/main/res/layout/attachment_type_selector.xml index 2afa0402e..eaaeca1f7 100644 --- a/src/main/res/layout/attachment_type_selector.xml +++ b/src/main/res/layout/attachment_type_selector.xml @@ -1,25 +1,28 @@ - - - - - + + + + + + android:id="@+id/recent_photos" + android:layout_width="match_parent" + android:layout_height="160dp" + android:padding="4dp" /> + app:circleColor="#00FFFFFF" /> + android:text="@string/video" /> @@ -79,7 +82,7 @@ android:src="@drawable/ic_image_white_24dp" android:scaleType="center" android:contentDescription="@string/gallery" - app:circleColor="@color/gallery_icon"/> + app:circleColor="@color/gallery_icon" /> + android:text="@string/gallery" /> @@ -108,7 +111,7 @@ android:src="@drawable/ic_insert_drive_file_white_24dp" android:scaleType="center" android:contentDescription="@string/file" - app:circleColor="@color/document_icon"/> + app:circleColor="@color/document_icon" /> + android:text="@string/file" /> @@ -137,7 +140,7 @@ android:src="@drawable/ic_apps_24" android:scaleType="center" android:contentDescription="@string/webxdc_app" - app:circleColor="@color/apps_icon"/> + app:circleColor="@color/apps_icon" /> + android:text="@string/webxdc_app" /> @@ -166,7 +169,7 @@ android:src="@drawable/ic_person_white_24dp" android:scaleType="center" android:contentDescription="@string/contact" - app:circleColor="@color/contact_icon"/> + app:circleColor="@color/contact_icon" /> + android:text="@string/contact" /> @@ -197,7 +200,7 @@ android:scaleType="center" android:visibility="visible" android:contentDescription="@string/location" - app:circleColor="@color/location_icon"/> + app:circleColor="@color/location_icon" /> + android:text="@string/location" /> diff --git a/src/main/res/layout/audio_view.xml b/src/main/res/layout/audio_view.xml index 7be81f6ed..770e1b951 100644 --- a/src/main/res/layout/audio_view.xml +++ b/src/main/res/layout/audio_view.xml @@ -1,73 +1,81 @@ - + - + - + - + - + - + - + - + diff --git a/src/main/res/layout/avatar_selector.xml b/src/main/res/layout/avatar_selector.xml index 32f96bc2d..94d07abc0 100644 --- a/src/main/res/layout/avatar_selector.xml +++ b/src/main/res/layout/avatar_selector.xml @@ -1,136 +1,147 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/layout/avatar_view.xml b/src/main/res/layout/avatar_view.xml index cb28e84dc..53e2b7947 100644 --- a/src/main/res/layout/avatar_view.xml +++ b/src/main/res/layout/avatar_view.xml @@ -8,13 +8,13 @@ tools:parentTag="org.thoughtcrime.securesms.components.AvatarView"> + android:id="@+id/avatar_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:foreground="@drawable/contact_photo_background" + android:cropToPadding="true" + tools:src="@color/blue_600" + android:contentDescription="@null" /> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/start_guideline" + app:layout_constraintGuide_percent=".69" + android:orientation="vertical" /> - + diff --git a/src/main/res/layout/backup_provider_activity.xml b/src/main/res/layout/backup_provider_activity.xml index af418cf29..a6da0b67d 100644 --- a/src/main/res/layout/backup_provider_activity.xml +++ b/src/main/res/layout/backup_provider_activity.xml @@ -4,12 +4,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + diff --git a/src/main/res/layout/backup_provider_fragment.xml b/src/main/res/layout/backup_provider_fragment.xml index 1dcb83823..0d2f8e7dd 100644 --- a/src/main/res/layout/backup_provider_fragment.xml +++ b/src/main/res/layout/backup_provider_fragment.xml @@ -1,112 +1,112 @@ - + - + - + - + - - - - + + + + - - - - + + + + - - - - + + + + - + - + diff --git a/src/main/res/layout/backup_receiver_fragment.xml b/src/main/res/layout/backup_receiver_fragment.xml index 14e5ac473..4bd0c6439 100644 --- a/src/main/res/layout/backup_receiver_fragment.xml +++ b/src/main/res/layout/backup_receiver_fragment.xml @@ -1,38 +1,38 @@ - + - + - + - + diff --git a/src/main/res/layout/call_item_view.xml b/src/main/res/layout/call_item_view.xml index 84eabaea9..616bac31a 100644 --- a/src/main/res/layout/call_item_view.xml +++ b/src/main/res/layout/call_item_view.xml @@ -9,42 +9,43 @@ android:gravity="center_vertical" android:focusable="true"> - - - - - + + - - - - + android:layout_weight="1" + android:layout_marginStart="6dp" + android:layout_marginEnd="6dp" + android:orientation="vertical"> + + + + + + diff --git a/src/main/res/layout/contact_filter_toolbar.xml b/src/main/res/layout/contact_filter_toolbar.xml index 94237fcef..48e60aba7 100644 --- a/src/main/res/layout/contact_filter_toolbar.xml +++ b/src/main/res/layout/contact_filter_toolbar.xml @@ -1,14 +1,16 @@ - + - + - + diff --git a/src/main/res/layout/contact_selection_activity.xml b/src/main/res/layout/contact_selection_activity.xml index d32366fdf..9b7662922 100644 --- a/src/main/res/layout/contact_selection_activity.xml +++ b/src/main/res/layout/contact_selection_activity.xml @@ -1,24 +1,26 @@ - + + android:id="@+id/toolbar" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:minHeight="?attr/actionBarSize" + android:background="?attr/colorPrimary" + android:elevation="4dp" + android:theme="?attr/actionBarStyle" + app:contentInsetStartWithNavigation="0dp" /> - + diff --git a/src/main/res/layout/contact_selection_list_divider.xml b/src/main/res/layout/contact_selection_list_divider.xml index f0e6a54d0..8eab9aa2f 100644 --- a/src/main/res/layout/contact_selection_list_divider.xml +++ b/src/main/res/layout/contact_selection_list_divider.xml @@ -1,20 +1,22 @@ - + - + - \ No newline at end of file + diff --git a/src/main/res/layout/contact_selection_list_fragment.xml b/src/main/res/layout/contact_selection_list_fragment.xml index 15323ff69..8cdb6e134 100644 --- a/src/main/res/layout/contact_selection_list_fragment.xml +++ b/src/main/res/layout/contact_selection_list_fragment.xml @@ -1,24 +1,26 @@ - + - + - + diff --git a/src/main/res/layout/contact_selection_list_item.xml b/src/main/res/layout/contact_selection_list_item.xml index 7670373c2..f89777599 100644 --- a/src/main/res/layout/contact_selection_list_item.xml +++ b/src/main/res/layout/contact_selection_list_item.xml @@ -1,29 +1,30 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="60dp" + android:orientation="horizontal" + android:gravity="center_vertical" + android:focusable="true" + android:background="?attr/conversation_list_item_background" + android:paddingLeft="16dp" + android:paddingRight="16dp"> + android:id="@+id/avatar" + android:layout_width="40dp" + android:layout_height="40dp" /> - + - + - + - + - + - + diff --git a/src/main/res/layout/conversation_activity.xml b/src/main/res/layout/conversation_activity.xml index 434bd3568..041773310 100644 --- a/src/main/res/layout/conversation_activity.xml +++ b/src/main/res/layout/conversation_activity.xml @@ -4,9 +4,10 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root_layout" android:background="?attr/input_panel_bg_color" - android:layout_height="match_parent" android:layout_width="match_parent"> + android:layout_height="match_parent" + android:layout_width="match_parent"> - + + android:id="@+id/layout_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> - + - + + android:id="@+id/attachment_editor_stub" + android:inflatedId="@+id/attachment_editor" + android:layout="@layout/conversation_activity_attachment_editor_stub" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> - + + android:layout_height="wrap_content" /> diff --git a/src/main/res/layout/conversation_activity_attachment_editor_stub.xml b/src/main/res/layout/conversation_activity_attachment_editor_stub.xml index 33087a412..412feeeaa 100644 --- a/src/main/res/layout/conversation_activity_attachment_editor_stub.xml +++ b/src/main/res/layout/conversation_activity_attachment_editor_stub.xml @@ -1,87 +1,82 @@ - + + android:id="@+id/removable_media_view" + android:paddingBottom="12dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center"> - + + android:id="@+id/attachment_thumbnail" + android:layout_width="230dp" + android:layout_height="150dp" + android:layout_gravity="center_horizontal" + android:visibility="gone" + android:contentDescription="@string/menu_add_attachment" + app:thumbnail_radius="@dimen/message_corner_radius" + app:minWidth="100dp" + app:maxWidth="300dp" + app:minHeight="100dp" + app:maxHeight="300dp" /> + android:id="@+id/attachment_audio" + android:layout_width="230dp" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_marginTop="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:padding="8dp" + android:background="@drawable/message_bubble_background_sent_alone_with_border" /> + android:id="@+id/attachment_document" + android:layout_width="230dp" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_marginTop="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:padding="8dp" + android:background="@drawable/message_bubble_background_sent_alone_with_border" /> + android:id="@+id/attachment_webxdc" + android:layout_width="230dp" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_marginTop="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:padding="8dp" + android:background="@drawable/message_bubble_background_sent_alone_with_border" /> + android:id="@+id/attachment_vcard" + android:layout_width="230dp" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_marginTop="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:padding="8dp" + android:background="@drawable/message_bubble_background_sent_alone_with_border" /> - diff --git a/src/main/res/layout/conversation_activity_emojidrawer_stub.xml b/src/main/res/layout/conversation_activity_emojidrawer_stub.xml index 02514ac2c..6eec69ce0 100644 --- a/src/main/res/layout/conversation_activity_emojidrawer_stub.xml +++ b/src/main/res/layout/conversation_activity_emojidrawer_stub.xml @@ -1,69 +1,68 @@ + + + android:layout_height="0dp" + android:layout_weight="1" + android:background="?emoji_drawer_background"> - + - + - + - + - + - - - + diff --git a/src/main/res/layout/conversation_fragment.xml b/src/main/res/layout/conversation_fragment.xml index 1990b6d07..4b86ec65e 100644 --- a/src/main/res/layout/conversation_fragment.xml +++ b/src/main/res/layout/conversation_fragment.xml @@ -1,20 +1,21 @@ - + + android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="8dp" + android:scrollbars="vertical" + android:cacheColorHint="?conversation_background" + android:clipChildren="false" + android:clipToPadding="false" /> + tools:text="@string/chat_new_one_to_one_hint" /> + android:layout_height="2dp" + android:layout_gravity="bottom" + android:background="@drawable/compose_divider_background" + android:alpha="1" /> + android:tint="@color/location_icon" /> + android:src="@drawable/ic_search_down" /> + style="@style/AddReaction" /> + style="@style/AddReaction" /> + style="@style/AddReaction" /> + style="@style/AddReaction" /> + style="@style/AddReaction" /> + style="@style/AddReaction" /> diff --git a/src/main/res/layout/conversation_input_panel.xml b/src/main/res/layout/conversation_input_panel.xml index 03aafc764..2a8b5df98 100644 --- a/src/main/res/layout/conversation_input_panel.xml +++ b/src/main/res/layout/conversation_input_panel.xml @@ -88,7 +88,7 @@ android:nextFocusForward="@+id/send_button" android:nextFocusRight="@+id/send_button" tools:visibility="invisible" - tools:hint="Send message" > + tools:hint="Send message"> @@ -122,7 +122,7 @@ android:clipChildren="false" android:clipToPadding="false"> - + @@ -130,7 +130,7 @@ - + diff --git a/src/main/res/layout/conversation_item_call.xml b/src/main/res/layout/conversation_item_call.xml index 15a28934f..462433a29 100644 --- a/src/main/res/layout/conversation_item_call.xml +++ b/src/main/res/layout/conversation_item_call.xml @@ -1,9 +1,9 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/call_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + tools:visibility="visible" /> diff --git a/src/main/res/layout/conversation_item_received.xml b/src/main/res/layout/conversation_item_received.xml index 2e3e5d2a9..c65a39673 100644 --- a/src/main/res/layout/conversation_item_received.xml +++ b/src/main/res/layout/conversation_item_received.xml @@ -13,265 +13,263 @@ android:clipToPadding="false" android:clipChildren="false"> - + - - - - - + android:layout_marginRight="@dimen/conversation_individual_right_gutter" + android:layout_marginEnd="@dimen/conversation_individual_right_gutter" + android:layout_marginBottom="@dimen/below_bubble" + android:paddingLeft="6dp" + android:paddingStart="6dp" + android:clipToPadding="false" + android:clipChildren="false" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/jumpto_icon"> + + - + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignBottom="@id/body_bubble"> - + - + + android:layout_marginRight="@dimen/message_bubble_edge_margin" + android:layout_marginEnd="@dimen/message_bubble_edge_margin" + android:layout_marginLeft="6dp" + android:layout_marginStart="6dp" + android:layout_toRightOf="@id/contact_photo_container" + android:layout_toEndOf="@id/contact_photo_container" + android:orientation="vertical" + android:clipToPadding="false" + android:clipChildren="false" + android:background="@color/white" + tools:backgroundTint="@color/core_light_10"> - + android:layout_marginTop="@dimen/message_bubble_top_padding" + android:layout_marginBottom="2dp" + android:layout_marginLeft="@dimen/message_bubble_horizontal_padding" + android:layout_marginRight="@dimen/message_bubble_horizontal_padding" + android:orientation="horizontal" + android:visibility="gone" + tools:visibility="visible"> - + - + - + - + - + - + - + - + - + - + -