Add support for persisting wallpaper selection.

This commit is contained in:
Greyson Parrelli
2021-01-20 09:03:21 -05:00
parent 80651d2425
commit 6bcb0de43d
13 changed files with 447 additions and 37 deletions

View File

@@ -5,6 +5,8 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
import java.util.Arrays;
import java.util.List;
@@ -26,4 +28,6 @@ public interface ChatWallpaper extends Parcelable {
GradientChatWallpaper.GRADIENT_2);
void loadInto(@NonNull ImageView imageView);
@NonNull Wallpaper serialize();
}

View File

@@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.wallpaper;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
/**
* Converts persisted models of wallpaper into usable {@link ChatWallpaper} instances.
*/
public class ChatWallpaperFactory {
public static @NonNull ChatWallpaper create(@NonNull Wallpaper model) {
if (model.hasSingleColor()) {
return new GradientChatWallpaper(model.getSingleColor().getColor());
} else if (model.hasLinearGradient()) {
return buildForLinearGradinent(model.getLinearGradient());
} else if (model.hasFile()) {
return buildForFile(model.getFile());
} else {
throw new IllegalArgumentException();
}
}
public static @NonNull ChatWallpaper create(@NonNull Uri uri) {
return new UriChatWallpaper(uri);
}
private static @NonNull ChatWallpaper buildForLinearGradinent(@NonNull Wallpaper.LinearGradient gradient) {
int[] colors = new int[gradient.getColorsCount()];
for (int i = 0; i < colors.length; i++) {
colors[i] = gradient.getColors(i);
}
float[] positions = new float[gradient.getPositionsCount()];
for (int i = 0; i < positions.length; i++) {
positions[i] = gradient.getPositions(i);
}
return new GradientChatWallpaper(gradient.getRotation(), colors, positions);
}
private static @NonNull ChatWallpaper buildForFile(@NonNull Wallpaper.File file) {
Uri uri = Uri.parse(file.getUri());
return new UriChatWallpaper(uri);
}
}

View File

@@ -16,6 +16,8 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
import java.util.Arrays;
import java.util.Objects;
@@ -81,6 +83,25 @@ final class GradientChatWallpaper implements ChatWallpaper, Parcelable {
imageView.setImageDrawable(buildDrawable());
}
@Override
public @NonNull Wallpaper serialize() {
Wallpaper.LinearGradient.Builder builder = Wallpaper.LinearGradient.newBuilder();
builder.setRotation(degrees);
for (int color : colors) {
builder.addColors(color);
}
for (float position : positions) {
builder.addPositions(position);
}
return Wallpaper.newBuilder()
.setLinearGradient(builder)
.build();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -7,46 +7,50 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
import org.thoughtcrime.securesms.mms.GlideApp;
final class UriChatWallpaper implements ChatWallpaper, Parcelable {
private final Uri uri;
UriChatWallpaper(@NonNull Uri uri) {
public 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<UriChatWallpaper> CREATOR = new Creator<UriChatWallpaper>() {
@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);
}
@Override
public @NonNull Wallpaper serialize() {
return Wallpaper.newBuilder()
.setFile(Wallpaper.File.newBuilder().setUri(uri.toString()))
.build();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(uri.toString());
}
public static final Creator<UriChatWallpaper> CREATOR = new Creator<UriChatWallpaper>() {
@Override
public UriChatWallpaper createFromParcel(Parcel in) {
return new UriChatWallpaper(Uri.parse(in.readString()));
}
@Override
public UriChatWallpaper[] newArray(int size) {
return new UriChatWallpaper[size];
}
};
}

View File

@@ -0,0 +1,105 @@
package org.thoughtcrime.securesms.wallpaper;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.PartAuthority;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Objects;
/**
* Manages the storage of custom wallpaper files.
*/
public final class WallpaperStorage {
private static final String TAG = Log.tag(WallpaperStorage.class);
private static final String DIRECTORY = "wallpapers";
private static final String FILENAME_BASE = "wallpaper";
/**
* Saves the provided input stream as a new wallpaper file.
*/
@WorkerThread
public static @NonNull ChatWallpaper save(@NonNull Context context, @NonNull InputStream wallpaperStream) throws IOException {
File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File file = File.createTempFile(FILENAME_BASE, "", directory);
StreamUtil.copy(wallpaperStream, getOutputStream(context, file));
return ChatWallpaperFactory.create(PartAuthority.getWallpaperUri(file.getName()));
}
@WorkerThread
public static @NonNull InputStream read(@NonNull Context context, String filename) throws IOException {
File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File wallpaperFile = new File(directory, filename);
return getInputStream(context, wallpaperFile);
}
@WorkerThread
public static @NonNull List<ChatWallpaper> getAll(@NonNull Context context) {
File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File[] allFiles = directory.listFiles(pathname -> pathname.getName().contains(FILENAME_BASE));
return Stream.of(allFiles)
.map(File::getName)
.map(PartAuthority::getWallpaperUri)
.map(ChatWallpaperFactory::create)
.toList();
}
/**
* Called when wallpaper is deselected. This will check anywhere the wallpaper could be used, and
* if we discover it's unused, we'll delete the file.
*/
@WorkerThread
public static void onWallpaperDeselected(@NonNull Context context, @NonNull Uri uri) {
Uri globalUri = SignalStore.wallpaper().getCurrentWallpaperUri();
if (Objects.equals(uri, globalUri)) {
return;
}
int recipientCount = DatabaseFactory.getRecipientDatabase(context).getWallpaperUriUsageCount(uri);
if (recipientCount > 0) {
return;
}
String filename = PartAuthority.getWallpaperFilename(uri);
File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File wallpaperFile = new File(directory, filename);
if (!wallpaperFile.delete()) {
Log.w(TAG, "Failed to delete " + filename + "!");
}
}
private static @NonNull OutputStream getOutputStream(@NonNull Context context, File outputFile) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second;
}
private static @NonNull InputStream getInputStream(@NonNull Context context, File inputFile) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
return ModernDecryptingPartInputStream.createFor(attachmentSecret, inputFile, 0);
}
}