per conversation custom backgrounds

This commit is contained in:
kosyak 2024-07-14 17:01:50 +02:00
parent 4453ad71ac
commit 481f5ebfc1
8 changed files with 447 additions and 4 deletions

View file

@ -70,6 +70,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.databinding.DataBindingUtil;
@ -80,6 +81,7 @@ import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
@ -135,11 +137,13 @@ import eu.siacs.conversations.ui.util.ScrollState;
import eu.siacs.conversations.ui.util.SendButtonAction;
import eu.siacs.conversations.ui.util.SendButtonTool;
import eu.siacs.conversations.ui.util.ShareUtil;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.ui.util.ViewUtil;
import eu.siacs.conversations.ui.widget.EditMessage;
import eu.siacs.conversations.ui.widget.HighlighterView;
import eu.siacs.conversations.ui.widget.TabLayout;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.ChatBackgroundHelper;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper;
@ -1321,6 +1325,12 @@ public class ConversationFragment extends XmppFragment
} else {
this.postponedActivityResult.push(activityResult);
}
ChatBackgroundHelper.onActivityResult(activity, requestCode, resultCode, data, conversation.getUuid());
if (requestCode == ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND) {
refresh();
}
}
public void unblockConversation(final Blockable conversation) {
@ -1367,6 +1377,7 @@ public class ConversationFragment extends XmppFragment
final MenuItem menuOngoingCall = menu.findItem(R.id.action_ongoing_call);
final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call);
final MenuItem menuTogglePinned = menu.findItem(R.id.action_toggle_pinned);
final MenuItem deleteCustomBg = menu.findItem(R.id.action_delete_custom_bg);
if (conversation != null) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
@ -1417,6 +1428,8 @@ public class ConversationFragment extends XmppFragment
} else {
menuTogglePinned.setTitle(R.string.add_to_favorites);
}
deleteCustomBg.setVisible(ChatBackgroundHelper.getBgFile(activity, conversation.getUuid()).exists());
}
Fragment secondaryFragment = activity.getFragmentManager().findFragmentById(R.id.secondary_fragment);
@ -2032,6 +2045,27 @@ public class ConversationFragment extends XmppFragment
case R.id.action_throttle:
throttleNoisyNoftificationsDialog(conversation);
break;
case R.id.action_set_custom_bg:
if (activity.hasStoragePermission(ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND)) {
ChatBackgroundHelper.openBGPicker(this);
}
break;
case R.id.action_delete_custom_bg:
try {
File bgfile = ChatBackgroundHelper.getBgFile(activity, conversation.getUuid());
if (bgfile.exists()) {
bgfile.delete();
Toast.makeText(activity, R.string.delete_background_success,Toast.LENGTH_LONG).show();
} else {
Toast.makeText(activity, R.string.no_background_set,Toast.LENGTH_LONG).show();
}
refresh();
} catch (Exception e) {
Toast.makeText(activity, R.string.delete_background_failed,Toast.LENGTH_LONG).show();
throw new RuntimeException(e);
}
break;
case R.id.action_block:
case R.id.action_unblock:
final Activity activity = getActivity();
@ -2416,6 +2450,8 @@ public class ConversationFragment extends XmppFragment
Toast.LENGTH_SHORT)
.show();
}
ChatBackgroundHelper.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
if (writeGranted(grantResults, permissions)) {
if (activity != null && activity.xmppConnectionService != null) {
@ -2429,6 +2465,18 @@ public class ConversationFragment extends XmppFragment
}
}
private void updateChatBG() {
if (activity != null) {
Uri uri = ChatBackgroundHelper.getBgUri(activity, conversation.getUuid());
if (uri != null) {
binding.backgroundImage.setImageURI(uri);
binding.backgroundImage.setVisibility(View.VISIBLE);
} else {
binding.backgroundImage.setVisibility(View.GONE);
}
}
}
public void startDownloadable(Message message) {
if (!hasPermissions(REQUEST_START_DOWNLOAD, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
this.mPendingDownloadableMessage = message;
@ -2648,6 +2696,7 @@ public class ConversationFragment extends XmppFragment
public void onResume() {
super.onResume();
binding.messagesView.post(this::fireReadEvent);
updateChatBG();
}
private void fireReadEvent() {
@ -2982,6 +3031,8 @@ public class ConversationFragment extends XmppFragment
findAndReInitByUuidOrArchive(uuid);
}
}
updateChatBG();
}
@Override
@ -3514,6 +3565,7 @@ public class ConversationFragment extends XmppFragment
"ConversationFragment.refresh() skipped updated because view binding was null");
return;
}
updateChatBG();
if (this.conversation != null
&& this.activity != null
&& this.activity.xmppConnectionService != null) {

View file

@ -6,6 +6,10 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -22,12 +26,19 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.exifinterface.media.ExifInterface;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyStoreException;
@ -48,6 +59,7 @@ import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.UnifiedPushDistributor;
import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.ChatBackgroundHelper;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
@ -362,6 +374,37 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
privacyCategory.removePreference(omemoPreference);
}
}
final Preference importBackgroundPreference = mSettingsFragment.findPreference("import_background");
if (importBackgroundPreference != null) {
importBackgroundPreference.setSummary(getString(R.string.pref_chat_background_summary));
importBackgroundPreference.setOnPreferenceClickListener(preference -> {
if (hasStoragePermission(ChatBackgroundHelper.REQUEST_IMPORT_BACKGROUND)) {
ChatBackgroundHelper.openBGPicker(this);
}
return true;
});
}
final Preference deleteBackgroundPreference = mSettingsFragment.findPreference("delete_background");
if (deleteBackgroundPreference != null) {
deleteBackgroundPreference.setSummary(getString(R.string.pref_delete_background_summary));
deleteBackgroundPreference.setOnPreferenceClickListener(preference -> {
try {
File bgfile = ChatBackgroundHelper.getBgFile(this, null);
if (bgfile.exists()) {
bgfile.delete();
Toast.makeText(this,R.string.delete_background_success,Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this,R.string.no_background_set,Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
Toast.makeText(this,R.string.delete_background_failed,Toast.LENGTH_LONG).show();
throw new RuntimeException(e);
}
return true;
});
}
}
private void changeOmemoSettingSummary() {
@ -588,6 +631,13 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
SettingsUtils.applyScreenshotPreventionSetting(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
ChatBackgroundHelper.onActivityResult(this, requestCode, resultCode, data, null);
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
@ -597,6 +647,8 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
if (requestCode == REQUEST_CREATE_BACKUP) {
createBackup();
}
ChatBackgroundHelper.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
} else {
Toast.makeText(
this,

View file

@ -0,0 +1,305 @@
package eu.siacs.conversations.utils;
import static android.app.Activity.RESULT_OK;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
import org.checkerframework.checker.units.qual.N;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
public class ChatBackgroundHelper {
public static final int REQUEST_IMPORT_BACKGROUND = 0xbf8704;
public static void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data, @Nullable String conversationUUID) {
if(requestCode == REQUEST_IMPORT_BACKGROUND) {
if (resultCode == RESULT_OK) {
Uri bguri = data.getData();
onPickFile(activity, bguri, conversationUUID);
}
}
}
public static void onRequestPermissionsResult(Activity activity,
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && requestCode == REQUEST_IMPORT_BACKGROUND) {
openBGPicker(activity);
}
}
public static void onRequestPermissionsResult(Fragment fragment,
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && requestCode == REQUEST_IMPORT_BACKGROUND) {
openBGPicker(fragment);
}
}
public static File getBgFile(Activity activity, @Nullable String conversationUUID) {
if (conversationUUID == null) {
return new File(activity.getFilesDir() + File.separator + "backgrounds" + File.separator + "bg.jpg");
} else {
return new File(activity.getFilesDir() + File.separator + "backgrounds" + File.separator + "bg_" + conversationUUID + ".jpg");
}
}
@Nullable
public static Uri getBgUri(Activity activity, String conversationUUID) {
File chatBgfileUri = new File(activity.getFilesDir() + File.separator + "backgrounds" + File.separator + "bg_" + conversationUUID + ".jpg");
File bgfileUri = new File(activity.getFilesDir() + File.separator + "backgrounds" + File.separator + "bg.jpg");
if (chatBgfileUri.exists()) {
return Uri.fromFile(chatBgfileUri);
} else if (bgfileUri.exists()) {
return Uri.fromFile(bgfileUri);
} else {
return null;
}
}
public static void openBGPicker(Activity activity) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
activity.startActivityForResult(Intent.createChooser(intent, "Select image"), REQUEST_IMPORT_BACKGROUND);
}
public static void openBGPicker(Fragment fragment) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
fragment.startActivityForResult(Intent.createChooser(intent, "Select image"), REQUEST_IMPORT_BACKGROUND);
}
private static void onPickFile(Activity activity, Uri uri, @Nullable String conversationUUID) {
if (uri != null) {
InputStream in;
OutputStream out;
try {
File bgfolder = new File(activity.getFilesDir() + File.separator + "backgrounds");
File bgfile;
if (conversationUUID != null) {
bgfile = new File(activity.getFilesDir() + File.separator + "backgrounds" + File.separator + "bg_" + conversationUUID + ".jpg");
} else {
bgfile = new File(activity.getFilesDir() + File.separator + "backgrounds" + File.separator + "bg.jpg");
}
//create output directory if it doesn't exist
if (!bgfolder.exists()) {
bgfolder.mkdirs();
}
in = activity.getContentResolver().openInputStream(uri);
out = new FileOutputStream(bgfile);
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
in = null;
// write the output file
out.flush();
out.close();
out = null;
compressImage(activity, bgfile, uri, 0);
Toast.makeText(activity, R.string.custom_background_set,Toast.LENGTH_LONG).show();
} catch (IOException exception) {
Toast.makeText(activity,R.string.create_background_failed,Toast.LENGTH_LONG).show();
Log.d(Config.LOGTAG, "Could not create background" + exception);
}
}
}
private static void compressImage(Activity activity, File f, Uri image, int sampleSize) throws IOException {
InputStream is = null;
OutputStream os = null;
int IMAGE_QUALITY = 65;
int ImageSize = (int) (0.08 * 1024 * 1024);
try {
if (!f.exists() && !f.createNewFile()) {
throw new IOException(String.valueOf(R.string.error_unable_to_create_temporary_file));
}
is = activity.getContentResolver().openInputStream(image);
if (is == null) {
throw new IOException(String.valueOf(R.string.error_not_an_image_file));
}
final Bitmap originalBitmap;
final BitmapFactory.Options options = new BitmapFactory.Options();
final int inSampleSize = (int) Math.pow(2, sampleSize);
Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
options.inSampleSize = inSampleSize;
originalBitmap = BitmapFactory.decodeStream(is, null, options);
is.close();
if (originalBitmap == null) {
throw new IOException("Source file was not an image");
}
if (!"image/jpeg".equals(options.outMimeType) && hasAlpha(originalBitmap)) {
originalBitmap.recycle();
throw new IOException("Source file had alpha channel");
}
int size;
int resolution = 1920;
if (resolution == 0) {
int height = originalBitmap.getHeight();
int width = originalBitmap.getWidth();
size = height > width ? height : width;
} else {
size = resolution;
}
Bitmap scaledBitmap = resize(originalBitmap, size);
final int rotation = getRotation(activity, image);
scaledBitmap = rotate(scaledBitmap, rotation);
boolean targetSizeReached = false;
int quality = IMAGE_QUALITY;
while (!targetSizeReached) {
os = new FileOutputStream(f);
boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os);
if (!success) {
throw new IOException(String.valueOf(R.string.error_compressing_image));
}
os.flush();
targetSizeReached = (f.length() <= ImageSize && ImageSize != 0) || quality <= 50;
quality -= 5;
}
scaledBitmap.recycle();
} catch (final FileNotFoundException e) {
cleanup(f);
throw new IOException(String.valueOf(R.string.error_file_not_found));
} catch (final IOException e) {
cleanup(f);
throw new IOException(String.valueOf(R.string.error_io_exception));
} catch (SecurityException e) {
cleanup(f);
throw new IOException(String.valueOf(R.string.error_security_exception_during_image_copy));
} catch (final OutOfMemoryError e) {
++sampleSize;
if (sampleSize <= 3) {
compressImage(activity, f, image, sampleSize);
} else {
throw new IOException(String.valueOf(R.string.error_out_of_memory));
}
} finally {
close(os);
close(is);
}
}
private static int getRotation(Activity activity, final Uri image) {
try (final InputStream is = activity.getContentResolver().openInputStream(image)) {
return is == null ? 0 : getRotation(is);
} catch (final Exception e) {
return 0;
}
}
private static int getRotation(final InputStream inputStream) throws IOException {
final ExifInterface exif = new ExifInterface(inputStream);
final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default:
return 0;
}
}
private static Bitmap rotate(final Bitmap bitmap, final int degree) {
if (degree == 0) {
return bitmap;
}
final int w = bitmap.getWidth();
final int h = bitmap.getHeight();
final Matrix matrix = new Matrix();
matrix.postRotate(degree);
final Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true);
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
return result;
}
private static void close(final Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to close stream", e);
}
}
}
private static void cleanup(final File file) {
try {
file.delete();
} catch (Exception e) {
}
}
private static Bitmap resize(final Bitmap originalBitmap, int size) throws IOException {
int w = originalBitmap.getWidth();
int h = originalBitmap.getHeight();
if (w <= 0 || h <= 0) {
throw new IOException("Decoded bitmap reported bounds smaller 0");
} else if (Math.max(w, h) > size) {
int scalledW;
int scalledH;
if (w <= h) {
scalledW = Math.max((int) (w / ((double) h / size)), 1);
scalledH = size;
} else {
scalledW = size;
scalledH = Math.max((int) (h / ((double) w / size)), 1);
}
final Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true);
if (!originalBitmap.isRecycled()) {
originalBitmap.recycle();
}
return result;
} else {
return originalBitmap;
}
}
private static boolean hasAlpha(final Bitmap bitmap) {
final int w = bitmap.getWidth();
final int h = bitmap.getHeight();
final int yStep = Math.max(1, w / 100);
final int xStep = Math.max(1, h / 100);
for (int x = 0; x < w; x += xStep) {
for (int y = 0; y < h; y += yStep) {
if (Color.alpha(bitmap.getPixel(x, y)) < 255) {
return true;
}
}
}
return false;
}
}

View file

@ -7,6 +7,13 @@
android:layout_height="match_parent"
android:background="?attr/color_background_secondary">
<ImageView
android:id="@+id/background_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="@color/grey700_25"
android:scaleType="centerCrop" />
<eu.siacs.conversations.ui.widget.TabLayout
android:visibility="gone"
android:id="@+id/tab_layout"
@ -25,8 +32,7 @@
android:id="@+id/conversation_view_pager"
android:layout_below="@id/tab_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="?attr/color_background_secondary">
android:layout_height="fill_parent">
<RelativeLayout
android:layout_width="fill_parent"
@ -39,7 +45,6 @@
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:background="?attr/color_background_secondary"
android:divider="@null"
android:listSelector="@android:color/transparent"
android:stackFromBottom="true"
@ -284,7 +289,6 @@
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:background="?attr/color_background_secondary"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"></ListView>

View file

@ -147,6 +147,18 @@
android:orderInCategory="75"
android:title="@string/refresh_feature_discovery"
app:showAsAction="never" />
<item
android:id="@+id/action_set_custom_bg"
android:orderInCategory="76"
android:title="@string/custom_background"
app:showAsAction="never" />
<item
android:id="@+id/action_delete_custom_bg"
android:orderInCategory="77"
android:title="@string/delete_background"
app:showAsAction="never" />
</menu>
</item>

View file

@ -16,6 +16,7 @@
<color name="grey500">#ff9e9e9e</color>
<color name="grey700">#ff616161</color>
<color name="grey700_40">#66616161</color>
<color name="grey700_25">#40616161</color>
<color name="grey800">#ff424242</color>
<color name="grey900">#ff282828</color>
<color name="red500">#fff44336</color>

View file

@ -1090,4 +1090,13 @@
<string name="retract_message_alert_title">Do you really want to retract this message?</string>
<string name="chats">Chats</string>
<string name="accounts">Accounts</string>
<string name="custom_background">Custom background</string>
<string name="pref_chat_background_summary">Choose an own image file as chat background.</string>
<string name="delete_background">Remove chat background</string>
<string name="pref_delete_background_summary">Remove your custom background image from the chat</string>
<string name="create_background_failed">Failed to create background</string>
<string name="custom_background_set">Custom background set</string>
<string name="delete_background_failed">Couldn\'t remove background image</string>
<string name="delete_background_success">Background image removed</string>
<string name="no_background_set">No custom background set</string>
</resources>

View file

@ -232,6 +232,14 @@
android:key="font_size"
android:summary="@string/pref_font_size_summary"
android:title="@string/pref_font_size" />
<Preference
android:key="import_background"
android:summary="@string/pref_chat_background_summary"
android:title="@string/custom_background" />
<Preference
android:key="delete_background"
android:summary="@string/pref_delete_background_summary"
android:title="@string/delete_background" />
</PreferenceCategory>
<PreferenceCategory
android:key="backup_category"