diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 845c664a5..d82267a0a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -357,6 +357,16 @@
+
+
+
+
+
+
{
+ startActivity(ChatWallpaperActivity.getIntent(requireContext()));
+ return true;
+ });
initializeListSummary((ListPreference)findPreference(TextSecurePreferences.THEME_PREF));
initializeListSummary((ListPreference)findPreference(TextSecurePreferences.LANGUAGE_PREF));
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaper.java
new file mode 100644
index 000000000..5a0710e7c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaper.java
@@ -0,0 +1,29 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.os.Parcelable;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+public interface ChatWallpaper extends Parcelable {
+
+ List BUILTINS = Arrays.asList(GradientChatWallpaper.SOLID_1,
+ GradientChatWallpaper.SOLID_2,
+ GradientChatWallpaper.SOLID_3,
+ GradientChatWallpaper.SOLID_4,
+ GradientChatWallpaper.SOLID_5,
+ GradientChatWallpaper.SOLID_6,
+ GradientChatWallpaper.SOLID_7,
+ GradientChatWallpaper.SOLID_8,
+ GradientChatWallpaper.SOLID_9,
+ GradientChatWallpaper.SOLID_10,
+ GradientChatWallpaper.SOLID_11,
+ GradientChatWallpaper.SOLID_12,
+ GradientChatWallpaper.GRADIENT_1,
+ GradientChatWallpaper.GRADIENT_2);
+
+ void loadInto(@NonNull ImageView imageView);
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperActivity.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperActivity.java
new file mode 100644
index 000000000..c10cb73f3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperActivity.java
@@ -0,0 +1,66 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.navigation.NavGraph;
+import androidx.navigation.Navigation;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+
+public final class ChatWallpaperActivity extends PassphraseRequiredActivity {
+
+ private static final String EXTRA_RECIPIENT_ID = "extra.recipient.id";
+
+ private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
+
+ public static @NonNull Intent getIntent(@NonNull Context context) {
+ return getIntent(context, null);
+ }
+
+ public static @NonNull Intent getIntent(@NonNull Context context, @Nullable RecipientId recipientId) {
+ Intent intent = new Intent(context, ChatWallpaperActivity.class);
+ intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ ChatWallpaperViewModel.Factory factory = new ChatWallpaperViewModel.Factory(getIntent(this).getParcelableExtra(EXTRA_RECIPIENT_ID));
+
+ ViewModelProviders.of(this, factory).get(ChatWallpaperViewModel.class);
+
+ dynamicTheme.onCreate(this);
+ setContentView(R.layout.chat_wallpaper_activity);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+
+ toolbar.setNavigationOnClickListener(unused -> {
+ if (!Navigation.findNavController(this, R.id.nav_host_fragment).popBackStack()) {
+ finish();
+ }
+ });
+
+ if (savedInstanceState == null) {
+ Bundle extras = getIntent().getExtras();
+ NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
+
+ Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, extras != null ? extras : new Bundle());
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperFragment.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperFragment.java
new file mode 100644
index 000000000..cb5f40661
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperFragment.java
@@ -0,0 +1,43 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.navigation.Navigation;
+
+import org.thoughtcrime.securesms.R;
+
+public class ChatWallpaperFragment extends Fragment {
+ @Override
+ public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.chat_wallpaper_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ ChatWallpaperViewModel viewModel = ViewModelProviders.of(requireActivity()).get(ChatWallpaperViewModel.class);
+ ImageView chatWallpaperPreview = view.findViewById(R.id.chat_wallpaper_preview_background);
+ View setWallpaper = view.findViewById(R.id.chat_wallpaper_set_wallpaper);
+
+ viewModel.setWallpaper(GradientChatWallpaper.GRADIENT_1);
+
+ viewModel.getCurrentWallpaper().observe(getViewLifecycleOwner(), wallpaper -> {
+ if (wallpaper.isPresent()) {
+ wallpaper.get().loadInto(chatWallpaperPreview);
+ } else {
+ chatWallpaperPreview.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.signal_background_primary));
+ }
+ });
+
+ setWallpaper.setOnClickListener(unused -> Navigation.findNavController(view)
+ .navigate(R.id.action_chatWallpaperFragment_to_chatWallpaperSelectionFragment));
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperPreviewActivity.java
new file mode 100644
index 000000000..249dc15cb
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperPreviewActivity.java
@@ -0,0 +1,76 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.viewpager2.widget.ViewPager2;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.FullscreenHelper;
+
+import java.util.Collections;
+
+public class ChatWallpaperPreviewActivity extends PassphraseRequiredActivity {
+
+ private static final String EXTRA_CHAT_WALLPAPER = "extra.chat.wallpaper";
+ private static final String EXTRA_RECIPIENT_ID = "extra.recipient.id";
+
+ private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
+
+ public static @NonNull Intent create(@NonNull Context context, @NonNull ChatWallpaper selection, @Nullable RecipientId recipientId) {
+ Intent intent = new Intent(context, ChatWallpaperPreviewActivity.class);
+
+ intent.putExtra(EXTRA_CHAT_WALLPAPER, selection);
+ intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
+
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ dynamicTheme.onCreate(this);
+
+ setContentView(R.layout.chat_wallpaper_preview_activity);
+
+ ViewPager2 viewPager = findViewById(R.id.preview_pager);
+ ChatWallpaperPreviewAdapter adapter = new ChatWallpaperPreviewAdapter();
+ View submit = findViewById(R.id.preview_set_wallpaper);
+ ChatWallpaperViewModel.Factory factory = new ChatWallpaperViewModel.Factory(getIntent().getParcelableExtra(EXTRA_RECIPIENT_ID));
+ ChatWallpaperViewModel viewModel = ViewModelProviders.of(this, factory).get(ChatWallpaperViewModel.class);
+ ChatWallpaper selected = getIntent().getParcelableExtra(EXTRA_CHAT_WALLPAPER);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+
+ toolbar.setNavigationOnClickListener(unused -> finish());
+
+ viewPager.setAdapter(adapter);
+
+ adapter.submitList(Collections.singletonList(new ChatWallpaperSelectionMappingModel(selected)));
+ viewModel.getWallpapers().observe(this, adapter::submitList);
+
+ submit.setOnClickListener(unused -> {
+ ChatWallpaperSelectionMappingModel model = (ChatWallpaperSelectionMappingModel) adapter.getCurrentList().get(viewPager.getCurrentItem());
+
+ viewModel.saveWallpaperSelection(model.getWallpaper());
+ setResult(RESULT_OK);
+ finish();
+ });
+
+ new FullscreenHelper(this).showSystemUI();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperPreviewAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperPreviewAdapter.java
new file mode 100644
index 000000000..45a9a253c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperPreviewAdapter.java
@@ -0,0 +1,10 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.MappingAdapter;
+
+class ChatWallpaperPreviewAdapter extends MappingAdapter {
+ ChatWallpaperPreviewAdapter() {
+ registerFactory(ChatWallpaperSelectionMappingModel.class, ChatWallpaperViewHolder.createFactory(R.layout.chat_wallpaper_preview_fragment_adapter_item, null));
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperRepository.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperRepository.java
new file mode 100644
index 000000000..e95ce8b02
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperRepository.java
@@ -0,0 +1,14 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import java.util.List;
+
+class ChatWallpaperRepository {
+
+ void getAllWallpaper(@NonNull Consumer> consumer) {
+ consumer.accept(ChatWallpaper.BUILTINS);
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionAdapter.java
new file mode 100644
index 000000000..9643a97bc
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionAdapter.java
@@ -0,0 +1,12 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.MappingAdapter;
+
+class ChatWallpaperSelectionAdapter extends MappingAdapter {
+ ChatWallpaperSelectionAdapter(@Nullable ChatWallpaperViewHolder.EventListener eventListener) {
+ registerFactory(ChatWallpaperSelectionMappingModel.class, ChatWallpaperViewHolder.createFactory(R.layout.chat_wallpaper_selection_fragment_adapter_item, eventListener));
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionFragment.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionFragment.java
new file mode 100644
index 000000000..9e04ecd4c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionFragment.java
@@ -0,0 +1,74 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+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 androidx.lifecycle.ViewModelProviders;
+import androidx.navigation.Navigation;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.flexbox.FlexboxLayoutManager;
+import com.google.android.flexbox.JustifyContent;
+
+import org.thoughtcrime.securesms.R;
+
+public class ChatWallpaperSelectionFragment extends Fragment {
+
+ private static final short CHOOSE_PHOTO = 1;
+ private static final short PREVIEW = 2;
+
+ private ChatWallpaperViewModel viewModel;
+
+ @Override
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.chat_wallpaper_selection_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ View chooseFromPhotos = view.findViewById(R.id.chat_wallpaper_choose_from_photos);
+ RecyclerView recyclerView = view.findViewById(R.id.chat_wallpaper_recycler);
+ FlexboxLayoutManager flexboxLayoutManager = new FlexboxLayoutManager(requireContext());
+
+ chooseFromPhotos.setOnClickListener(unused -> {
+ // Navigate to photo selection (akin to what we did for profile avatar selection.)
+ //startActivityForResult(..., CHOOSE_PHOTO);
+ });
+
+ @SuppressWarnings("CodeBlock2Expr")
+ ChatWallpaperSelectionAdapter adapter = new ChatWallpaperSelectionAdapter(chatWallpaper -> {
+ startActivityForResult(ChatWallpaperPreviewActivity.create(requireActivity(), chatWallpaper, viewModel.getRecipientId()), PREVIEW);
+ });
+
+ flexboxLayoutManager.setJustifyContent(JustifyContent.SPACE_AROUND);
+ recyclerView.setLayoutManager(flexboxLayoutManager);
+ recyclerView.setAdapter(adapter);
+
+ viewModel = ViewModelProviders.of(requireActivity()).get(ChatWallpaperViewModel.class);
+ viewModel.getWallpapers().observe(getViewLifecycleOwner(), adapter::submitList);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == CHOOSE_PHOTO && resultCode == Activity.RESULT_OK && data != null) {
+ Uri uri = data.getData();
+ if (uri == null || uri == Uri.EMPTY) {
+ throw new AssertionError("Should never have an empty uri.");
+ } else {
+ startActivityForResult(ChatWallpaperPreviewActivity.create(requireActivity(), new UriChatWallpaper(uri), viewModel.getRecipientId()), PREVIEW);
+ }
+ } else if (requestCode == PREVIEW && resultCode == Activity.RESULT_OK) {
+ Navigation.findNavController(requireView()).popBackStack();
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionMappingModel.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionMappingModel.java
new file mode 100644
index 000000000..75f08bc8a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionMappingModel.java
@@ -0,0 +1,34 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.util.MappingModel;
+
+class ChatWallpaperSelectionMappingModel implements MappingModel {
+
+ private final ChatWallpaper chatWallpaper;
+
+ ChatWallpaperSelectionMappingModel(@NonNull ChatWallpaper chatWallpaper) {
+ this.chatWallpaper = chatWallpaper;
+ }
+
+ ChatWallpaper getWallpaper() {
+ return chatWallpaper;
+ }
+
+ public void loadInto(@NonNull ImageView imageView) {
+ chatWallpaper.loadInto(imageView);
+ }
+
+ @Override
+ public boolean areItemsTheSame(@NonNull ChatWallpaperSelectionMappingModel newItem) {
+ return areContentsTheSame(newItem);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull ChatWallpaperSelectionMappingModel newItem) {
+ return chatWallpaper.equals(newItem.chatWallpaper);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewHolder.java
new file mode 100644
index 000000000..120e6e697
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewHolder.java
@@ -0,0 +1,50 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.MappingAdapter;
+import org.thoughtcrime.securesms.util.MappingViewHolder;
+
+class ChatWallpaperViewHolder extends MappingViewHolder {
+
+ private final ImageView preview;
+ private final EventListener eventListener;
+
+ public ChatWallpaperViewHolder(@NonNull View itemView, @Nullable EventListener eventListener) {
+ super(itemView);
+ this.preview = itemView.findViewById(R.id.chat_wallpaper_preview);
+ this.eventListener = eventListener;
+ }
+
+ @Override
+ public void bind(@NonNull ChatWallpaperSelectionMappingModel model) {
+ model.loadInto(preview);
+
+ if (eventListener != null) {
+ preview.setOnClickListener(unused -> {
+ if (getAdapterPosition() != RecyclerView.NO_POSITION) {
+ eventListener.onModelClick(model);
+ }
+ });
+ }
+ }
+
+ public static @NonNull MappingAdapter.Factory createFactory(@LayoutRes int layout, @Nullable EventListener listener) {
+ return new MappingAdapter.LayoutFactory<>(view -> new ChatWallpaperViewHolder(view, listener), layout);
+ }
+
+ public interface EventListener {
+ default void onModelClick(@NonNull ChatWallpaperSelectionMappingModel model) {
+ onClick(model.getWallpaper());
+ }
+
+ void onClick(@NonNull ChatWallpaper chatWallpaper);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewModel.java
new file mode 100644
index 000000000..8acbc4dc9
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewModel.java
@@ -0,0 +1,67 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.MappingModel;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.List;
+import java.util.Objects;
+
+public class ChatWallpaperViewModel extends ViewModel {
+
+ private final ChatWallpaperRepository repository = new ChatWallpaperRepository();
+ private final MutableLiveData> wallpaper = new MutableLiveData<>();
+ private final MutableLiveData> builtins = new MutableLiveData<>();
+ private final RecipientId recipientId;
+
+ private ChatWallpaperViewModel(@Nullable RecipientId recipientId) {
+ this.recipientId = recipientId;
+
+ repository.getAllWallpaper(builtins::postValue);
+ }
+
+ void setWallpaper(@Nullable ChatWallpaper chatWallpaper) {
+ wallpaper.setValue(Optional.fromNullable(chatWallpaper));
+ }
+
+ void saveWallpaperSelection(@NonNull ChatWallpaper selected) {
+ // TODO
+ }
+
+ public @Nullable RecipientId getRecipientId() {
+ return recipientId;
+ }
+
+ LiveData> getCurrentWallpaper() {
+ return wallpaper;
+ }
+
+ LiveData>> getWallpapers() {
+ return Transformations.map(Transformations.distinctUntilChanged(builtins),
+ wallpapers -> Stream.of(wallpapers).>map(ChatWallpaperSelectionMappingModel::new).toList());
+ }
+
+ public static class Factory implements ViewModelProvider.Factory {
+
+ private final RecipientId recipientId;
+
+ public Factory(@Nullable RecipientId recipientId) {
+ this.recipientId = recipientId;
+ }
+
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ return Objects.requireNonNull(modelClass.cast(new ChatWallpaperViewModel(recipientId)));
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/GradientChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/GradientChatWallpaper.java
new file mode 100644
index 000000000..b1db74de5
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/GradientChatWallpaper.java
@@ -0,0 +1,205 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+final class GradientChatWallpaper implements ChatWallpaper, Parcelable {
+
+ public static final GradientChatWallpaper SOLID_1 = new GradientChatWallpaper(0xFFE26983);
+ public static final GradientChatWallpaper SOLID_2 = new GradientChatWallpaper(0xFFDF9171);
+ public static final GradientChatWallpaper SOLID_3 = new GradientChatWallpaper(0xFF9E9887);
+ public static final GradientChatWallpaper SOLID_4 = new GradientChatWallpaper(0xFF89AE8F);
+ public static final GradientChatWallpaper SOLID_5 = new GradientChatWallpaper(0xFF32C7E2);
+ public static final GradientChatWallpaper SOLID_6 = new GradientChatWallpaper(0xFF7C99B6);
+ public static final GradientChatWallpaper SOLID_7 = new GradientChatWallpaper(0xFFC988E7);
+ public static final GradientChatWallpaper SOLID_8 = new GradientChatWallpaper(0xFFE297C3);
+ public static final GradientChatWallpaper SOLID_9 = new GradientChatWallpaper(0xFFA2A2AA);
+ public static final GradientChatWallpaper SOLID_10 = new GradientChatWallpaper(0xFF146148);
+ public static final GradientChatWallpaper SOLID_11 = new GradientChatWallpaper(0xFF403B91);
+ public static final GradientChatWallpaper SOLID_12 = new GradientChatWallpaper(0xFF624249);
+ public static final GradientChatWallpaper GRADIENT_1 = new GradientChatWallpaper(167.96f,
+ new int[] { 0xFFF3DC47, 0xFFF3DA47, 0xFFF2D546, 0xFFF2CC46, 0xFFF1C146, 0xFFEFB445, 0xFFEEA544, 0xFFEC9644, 0xFFEB8743, 0xFFE97743, 0xFFE86942, 0xFFE65C41, 0xFFE55041, 0xFFE54841, 0xFFE44240, 0xFFE44040 },
+ new float[] { 0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1f });
+ public static final GradientChatWallpaper GRADIENT_2 = new GradientChatWallpaper(180f,
+ new int[] { 0xFF16161D, 0xFF17171E, 0xFF1A1A22, 0xFF1F1F28, 0xFF26262F, 0xFF2D2D38, 0xFF353542, 0xFF3E3E4C, 0xFF474757, 0xFF4F4F61, 0xFF57576B, 0xFF5F5F74, 0xFF65657C, 0xFF6A6A82, 0xFF6D6D85, 0xFF6E6E87 },
+ new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f });
+
+ private final float degrees;
+ private final int[] colors;
+ private final float[] positions;
+
+ GradientChatWallpaper(int color) {
+ this(0f, new int[]{color, color}, null);
+ }
+
+ GradientChatWallpaper(float degrees, int[] colors, float[] positions) {
+ this.degrees = degrees;
+ this.colors = colors;
+ this.positions = positions;
+ }
+
+ private GradientChatWallpaper(Parcel in) {
+ degrees = in.readFloat();
+ colors = in.createIntArray();
+ positions = in.createFloatArray();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeFloat(degrees);
+ dest.writeIntArray(colors);
+ dest.writeFloatArray(positions);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private @NonNull Drawable buildDrawable() {
+ return new RotatableGradientDrawable(degrees, colors, positions);
+ }
+
+ @Override
+ public void loadInto(@NonNull ImageView imageView) {
+ imageView.setImageDrawable(buildDrawable());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GradientChatWallpaper that = (GradientChatWallpaper) o;
+ return Float.compare(that.degrees, degrees) == 0 &&
+ Arrays.equals(colors, that.colors) &&
+ Arrays.equals(positions, that.positions);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(degrees);
+ result = 31 * result + Arrays.hashCode(colors);
+ result = 31 * result + Arrays.hashCode(positions);
+ return result;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public GradientChatWallpaper createFromParcel(Parcel in) {
+ return new GradientChatWallpaper(in);
+ }
+
+ @Override
+ public GradientChatWallpaper[] newArray(int size) {
+ return new GradientChatWallpaper[size];
+ }
+ };
+
+ private static final class RotatableGradientDrawable extends Drawable {
+
+ private final float degrees;
+ private final int[] colors;
+ private final float[] positions;
+
+ private final Rect fillRect = new Rect();
+ private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private RotatableGradientDrawable(float degrees, int[] colors, @Nullable float[] positions) {
+ this.degrees = degrees + 225f;
+ this.colors = colors;
+ this.positions = positions;
+ }
+
+ @Override
+ public void setBounds(int left, int top, int right, int bottom) {
+ super.setBounds(left, top, right, bottom);
+
+ Point topLeft = new Point(left, top);
+ Point topRight = new Point(right, top);
+ Point bottomLeft = new Point(left, bottom);
+ Point bottomRight = new Point(right, bottom);
+ Point origin = new Point(getBounds().width() / 2, getBounds().height() / 2);
+
+ Point rotationTopLeft = cornerPrime(origin, topLeft, degrees);
+ Point rotationTopRight = cornerPrime(origin, topRight, degrees);
+ Point rotationBottomLeft = cornerPrime(origin, bottomLeft, degrees);
+ Point rotationBottomRight = cornerPrime(origin, bottomRight, degrees);
+
+ fillRect.left = Integer.MAX_VALUE;
+ fillRect.top = Integer.MAX_VALUE;
+ fillRect.right = Integer.MIN_VALUE;
+ fillRect.bottom = Integer.MIN_VALUE;
+
+ for (Point point : Arrays.asList(topLeft, topRight, bottomLeft, bottomRight, rotationTopLeft, rotationTopRight, rotationBottomLeft, rotationBottomRight)) {
+ if (point.x < fillRect.left) {
+ fillRect.left = point.x;
+ }
+
+ if (point.x > fillRect.right) {
+ fillRect.right = point.x;
+ }
+
+ if (point.y < fillRect.top) {
+ fillRect.top = point.y;
+ }
+
+ if (point.y > fillRect.bottom) {
+ fillRect.bottom = point.y;
+ }
+ }
+
+ fillPaint.setShader(new LinearGradient(fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, colors, positions, Shader.TileMode.CLAMP));
+ }
+
+ private static Point cornerPrime(@NonNull Point origin, @NonNull Point corner, float degrees) {
+ return new Point(xPrime(origin, corner, Math.toRadians(degrees)), yPrime(origin, corner, Math.toRadians(degrees)));
+ }
+
+ private static int xPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
+ return (int) Math.ceil(((corner.x - origin.x) * Math.cos(theta)) - ((corner.y - origin.y) * Math.sin(theta)) + origin.x);
+ }
+
+ private static int yPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
+ return (int) Math.ceil(((corner.x - origin.x) * Math.sin(theta)) + ((corner.y - origin.y) * Math.cos(theta)) + origin.y);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ int save = canvas.save();
+ canvas.rotate(degrees, getBounds().width() / 2f, getBounds().height() / 2f);
+ canvas.drawRect(fillRect, fillPaint);
+ canvas.restoreToCount(save);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // Not supported
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ // Not supported
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java
new file mode 100644
index 000000000..c27340731
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java
@@ -0,0 +1,52 @@
+package org.thoughtcrime.securesms.wallpaper;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.mms.GlideApp;
+
+final class UriChatWallpaper implements ChatWallpaper, Parcelable {
+
+ private final Uri uri;
+
+ UriChatWallpaper(@NonNull Uri uri) {
+ this.uri = uri;
+ }
+
+ protected UriChatWallpaper(Parcel in) {
+ uri = in.readParcelable(Uri.class.getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(uri, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public UriChatWallpaper createFromParcel(Parcel in) {
+ return new UriChatWallpaper(in);
+ }
+
+ @Override
+ public UriChatWallpaper[] newArray(int size) {
+ return new UriChatWallpaper[size];
+ }
+ };
+
+ @Override
+ public void loadInto(@NonNull ImageView imageView) {
+ GlideApp.with(imageView)
+ .load(uri)
+ .into(imageView);
+ }
+}
diff --git a/app/src/main/res/drawable/chat_wallpaper_preview_bottom_bar.xml b/app/src/main/res/drawable/chat_wallpaper_preview_bottom_bar.xml
new file mode 100644
index 000000000..b449b34d7
--- /dev/null
+++ b/app/src/main/res/drawable/chat_wallpaper_preview_bottom_bar.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/chat_wallpaper_preview_bubble.xml b/app/src/main/res/drawable/chat_wallpaper_preview_bubble.xml
new file mode 100644
index 000000000..0974eab3d
--- /dev/null
+++ b/app/src/main/res/drawable/chat_wallpaper_preview_bubble.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/chat_wallpaper_preview_bubble_background.xml b/app/src/main/res/drawable/chat_wallpaper_preview_bubble_background.xml
new file mode 100644
index 000000000..4dfe8d84a
--- /dev/null
+++ b/app/src/main/res/drawable/chat_wallpaper_preview_bubble_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/chat_wallpaper_preview_bubble_background_accent.xml b/app/src/main/res/drawable/chat_wallpaper_preview_bubble_background_accent.xml
new file mode 100644
index 000000000..6f94ec3ab
--- /dev/null
+++ b/app/src/main/res/drawable/chat_wallpaper_preview_bubble_background_accent.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/chat_wallpaper_preview_date_background.xml b/app/src/main/res/drawable/chat_wallpaper_preview_date_background.xml
new file mode 100644
index 000000000..47b735abf
--- /dev/null
+++ b/app/src/main/res/drawable/chat_wallpaper_preview_date_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/chat_wallpaper_preview_outline.xml b/app/src/main/res/drawable/chat_wallpaper_preview_outline.xml
new file mode 100644
index 000000000..fead04325
--- /dev/null
+++ b/app/src/main/res/drawable/chat_wallpaper_preview_outline.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/chat_wallpaper_preview_top_bar.xml b/app/src/main/res/drawable/chat_wallpaper_preview_top_bar.xml
new file mode 100644
index 000000000..c2d3fbffb
--- /dev/null
+++ b/app/src/main/res/drawable/chat_wallpaper_preview_top_bar.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/test_gradient.xml b/app/src/main/res/drawable/test_gradient.xml
new file mode 100644
index 000000000..655faf877
--- /dev/null
+++ b/app/src/main/res/drawable/test_gradient.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/app/src/main/res/layout/chat_wallpaper_activity.xml b/app/src/main/res/layout/chat_wallpaper_activity.xml
new file mode 100644
index 000000000..af0690daa
--- /dev/null
+++ b/app/src/main/res/layout/chat_wallpaper_activity.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/chat_wallpaper_fragment.xml b/app/src/main/res/layout/chat_wallpaper_fragment.xml
new file mode 100644
index 000000000..695839a65
--- /dev/null
+++ b/app/src/main/res/layout/chat_wallpaper_fragment.xml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_wallpaper_preview_activity.xml b/app/src/main/res/layout/chat_wallpaper_preview_activity.xml
new file mode 100644
index 000000000..1dd3229f6
--- /dev/null
+++ b/app/src/main/res/layout/chat_wallpaper_preview_activity.xml
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_wallpaper_preview_fragment_adapter_item.xml b/app/src/main/res/layout/chat_wallpaper_preview_fragment_adapter_item.xml
new file mode 100644
index 000000000..d6600fb95
--- /dev/null
+++ b/app/src/main/res/layout/chat_wallpaper_preview_fragment_adapter_item.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_wallpaper_selection_fragment.xml b/app/src/main/res/layout/chat_wallpaper_selection_fragment.xml
new file mode 100644
index 000000000..ea4712cb6
--- /dev/null
+++ b/app/src/main/res/layout/chat_wallpaper_selection_fragment.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_wallpaper_selection_fragment_adapter_item.xml b/app/src/main/res/layout/chat_wallpaper_selection_fragment_adapter_item.xml
new file mode 100644
index 000000000..feee33914
--- /dev/null
+++ b/app/src/main/res/layout/chat_wallpaper_selection_fragment_adapter_item.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/chat_wallpaper.xml b/app/src/main/res/navigation/chat_wallpaper.xml
new file mode 100644
index 000000000..026f457ac
--- /dev/null
+++ b/app/src/main/res/navigation/chat_wallpaper.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index b84376281..83019dd8c 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -14,4 +14,7 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5cfec63ac..7b9a7bd52 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2207,6 +2207,7 @@
Dark
Appearance
Theme
+ Chat wallpaper
Disable PIN
Enable PIN
If you disable the PIN, you will lose all data when you re-register Signal unless you manually back up and restore. You can not turn on Registration Lock while the PIN is disabled.
@@ -2819,6 +2820,26 @@
Forward message
+
+ Chat wallpaper
+
+
+ Set wallpaper
+ Dark theme dims wallpaper
+ Clear wallpaper
+ Reset all wallpapers
+
+
+ Choose from photos
+ Presets
+
+
+ Preview
+ Set wallpaper
+ 10:49 am
+ This wallpaper will be set for all chats
+ Besides those you manually override
+
diff --git a/app/src/main/res/xml-v29/preferences_appearance.xml b/app/src/main/res/xml-v29/preferences_appearance.xml
index 5eb45ac39..56ea9cc53 100644
--- a/app/src/main/res/xml-v29/preferences_appearance.xml
+++ b/app/src/main/res/xml-v29/preferences_appearance.xml
@@ -9,6 +9,9 @@
android:defaultValue="system">
+
+
-
+
+
+