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"> + + - + + +