diff --git a/src/cheogram/AndroidManifest.xml b/src/cheogram/AndroidManifest.xml index 82d1ab15712a1fdbc7272086c5df29dd6d5af71e..3df380045568a12df77b1c05277f2940e50985eb 100644 --- a/src/cheogram/AndroidManifest.xml +++ b/src/cheogram/AndroidManifest.xml @@ -7,6 +7,7 @@ + { + try { + download(); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to download stickers", e); + } + stopForeground(true); + RUNNING.set(false); + stopSelf(); + }).start(); + return START_STICKY; + } else { + Log.d(Config.LOGTAG, "DownloadDefaultStickers. ignoring start command because already running"); + } + return START_NOT_STICKY; + } + + private void oneSticker(JSONObject sticker) throws Exception { + Response r = http.newCall(new Request.Builder().url(sticker.getString("url")).build()).execute(); + File file = new File(mStickerDir.getAbsolutePath() + "/" + sticker.getString("pack") + "/" + sticker.getString("name") + "." + MimeUtils.guessExtensionFromMimeType(r.headers().get("content-type"))); + file.getParentFile().mkdirs(); + OutputStream os = new FileOutputStream(file); + ByteStreams.copy(r.body().byteStream(), os); + os.close(); + + JSONArray cids = sticker.getJSONArray("cids"); + for (int i = 0; i < cids.length(); i++) { + Cid cid = Cid.decode(cids.getString(i)); + mDatabaseBackend.saveCid(cid, file, sticker.getString("url")); + } + + MediaScannerConnection.scanFile( + getBaseContext(), + new String[] { file.getAbsolutePath() }, + null, + new MediaScannerConnection.MediaScannerConnectionClient() { + @Override + public void onMediaScannerConnected() {} + + @Override + public void onScanCompleted(String path, Uri uri) {} + } + ); + + try { + File copyright = new File(mStickerDir.getAbsolutePath() + "/" + sticker.getString("pack") + "/copyright.txt"); + OutputStreamWriter w = new OutputStreamWriter(new FileOutputStream(copyright, true), "utf-8"); + w.write(sticker.getString("pack")); + w.write('/'); + w.write(sticker.getString("name")); + w.write(": "); + w.write(sticker.getString("copyright")); + w.write('\n'); + w.close(); + } catch (final Exception e) { } + } + + private void download() throws Exception { + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + mBuilder.setContentTitle("Downloading Default Stickers") + .setSmallIcon(R.drawable.ic_archive_white_24dp) + .setProgress(1, 0, false); + startForeground(NOTIFICATION_ID, mBuilder.build()); + + Response r = http.newCall(new Request.Builder().url("https://stickers.cheogram.com/index.json").build()).execute(); + JSONArray stickers = new JSONArray(r.body().string()); + + final Progress progress = new Progress(mBuilder, 1, 0); + for (int i = 0; i < stickers.length(); i++) { + oneSticker(stickers.getJSONObject(i)); + + final int percentage = i * 100 / stickers.length(); + notificationManager.notify(NOTIFICATION_ID, progress.build(percentage)); + } + } + + private File stickerDir() { + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + final String dir = p.getString("sticker_directory", "Stickers"); + if (dir.startsWith("content://")) { + Uri uri = Uri.parse(dir); + uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); + return new File(FileUtils.getPath(getBaseContext(), uri)); + } else { + return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private static class Progress { + private final NotificationCompat.Builder builder; + private final int max; + private final int count; + + private Progress(NotificationCompat.Builder builder, int max, int count) { + this.builder = builder; + this.max = max; + this.count = count; + } + + private Notification build(int percentage) { + builder.setProgress(max * 100, count * 100 + percentage, false); + return builder.build(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index be3627bb2cf49ad921e39db9e607b7f5c1843ed0..40939cb4bcb4efd028467a693cc2a34d226aee74 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -1100,6 +1100,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable fileParams.sims = this.fileParams.sims; } this.fileParams = fileParams; + if (fileParams != null && getSims().isEmpty()) { + addPayload(fileParams.toSims()); + } } public synchronized FileParams getFileParams() { @@ -1229,6 +1232,21 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return file.findChildContent("name", file.getNamespace()); } + public void setName(final String name) { + if (sims == null) toSims(); + Element file = getFileElement(); + + for (Element child : file.getChildren()) { + if (child.getName().equals("name") && child.getNamespace().equals(file.getNamespace())) { + file.removeChild(child); + } + } + + if (name != null) { + file.addChild("name", file.getNamespace()).setContent(name); + } + } + public Element toSims() { if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0"); sims.setAttribute("type", "data"); @@ -1277,6 +1295,23 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return file; } + public void setCids(Iterable cids) throws NoSuchAlgorithmException { + if (sims == null) toSims(); + Element file = getFileElement(); + + for (Element child : file.getChildren()) { + if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) { + file.removeChild(child); + } + } + + for (Cid cid : cids) { + file.addChild("hash", "urn:xmpp:hashes:2") + .setAttribute("algo", CryptoHelper.multihashAlgo(cid.getType())) + .setContent(Base64.encodeToString(cid.getHash(), Base64.NO_WRAP)); + } + } + public List getCids() { List cids = new ArrayList<>(); Element file = getFileElement(); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 1ccc6efe1000a687e361934f0b3f55eb62d93624..8d5db0fea44831974669d1032a3ea89eb0680c6e 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -283,6 +283,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("PRAGMA cheogram.user_version = 6"); } + if(cheogramVersion < 7) { + db.execSQL( + "ALTER TABLE cheogram.cids " + + "ADD COLUMN url TEXT" + ); + db.execSQL("PRAGMA cheogram.user_version = 7"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -775,11 +783,27 @@ public class DatabaseBackend extends SQLiteOpenHelper { return f; } + public String getUrlForCid(Cid cid) { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query("cheogram.cids", new String[]{"url"}, "cid=?", new String[]{cid.toString()}, null, null, null); + String url = null; + if (cursor.moveToNext()) { + url = cursor.getString(0); + } + cursor.close(); + return url; + } + public void saveCid(Cid cid, File file) { + saveCid(cid, file, null); + } + + public void saveCid(Cid cid, File file, String url) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("cid", cid.toString()); cv.put("path", file.getAbsolutePath()); + cv.put("url", url); db.insertWithOnConflict("cheogram.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE); } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 4adf756a67b3075d9a6223ddeb3156e06668c64e..b2a368aa0fe50fe755d42027ef49a45696439e8f 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -654,6 +654,9 @@ public class FileBackend { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; try { + for (Cid cid : calculateCids(uri)) { + if (mXmppConnectionService.getUrlForCid(cid) != null) return true; + } final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(uri); BitmapFactory.decodeStream(inputStream, null, options); @@ -664,7 +667,7 @@ public class FileBackend { return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); - } catch (FileNotFoundException e) { + } catch (final IOException e) { Log.d(Config.LOGTAG, "unable to get image dimensions", e); return false; } @@ -927,6 +930,10 @@ public class FileBackend { } } + public Cid[] calculateCids(final Uri uri) throws IOException { + return calculateCids(mXmppConnectionService.getContentResolver().openInputStream(uri)); + } + public Cid[] calculateCids(final InputStream is) throws IOException { try { return CryptoHelper.cid(is, new String[]{"SHA-256", "SHA-1", "SHA-512"}); @@ -1727,7 +1734,7 @@ public class FileBackend { updateFileParams(message, url, true); } - public void updateFileParams(final Message message, final String url, boolean updateCids) { + public void updateFileParams(final Message message, String url, boolean updateCids) { final boolean encrypted = message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED; @@ -1739,9 +1746,23 @@ public class FileBackend { || (mime != null && mime.startsWith("image/")); Message.FileParams fileParams = message.getFileParams(); if (fileParams == null) fileParams = new Message.FileParams(); - if (url != null) { + Cid[] cids = new Cid[0]; + try { + cids = calculateCids(new FileInputStream(file)); + fileParams.setCids(List.of(cids)); + } catch (final IOException | NoSuchAlgorithmException e) { } + if (url == null) { + for (Cid cid : cids) { + url = mXmppConnectionService.getUrlForCid(cid); + if (url != null) { + fileParams.url = url; + break; + } + } + } else { fileParams.url = url; } + fileParams.setName(file.getName()); if (encrypted && !file.exists()) { Log.d(Config.LOGTAG, "skipping updateFileParams because file is encrypted"); final DownloadableFile encryptedFile = getFile(message, false); @@ -1801,11 +1822,10 @@ public class FileBackend { if (updateCids) { try { - Cid[] cids = calculateCids(new FileInputStream(getFile(message))); for (int i = 0; i < cids.length; i++) { - mXmppConnectionService.saveCid(cids[i], file); + mXmppConnectionService.saveCid(cids[i], file, url); } - } catch (final IOException | XmppConnectionService.BlockedMediaException e) { } + } catch (XmppConnectionService.BlockedMediaException e) { } } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 7056b4e1a0441e6c3ff62f9a9e91aede9608bba9..22e545295673823a424475519390573ec7fdbbed 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -561,11 +561,19 @@ public class XmppConnectionService extends Service { return this.databaseBackend.getFileForCid(cid); } + public String getUrlForCid(Cid cid) { + return this.databaseBackend.getUrlForCid(cid); + } + public void saveCid(Cid cid, File file) throws BlockedMediaException { + saveCid(cid, file, null); + } + + public void saveCid(Cid cid, File file, String url) throws BlockedMediaException { if (this.databaseBackend.isBlockedMedia(cid)) { throw new BlockedMediaException(); } - this.databaseBackend.saveCid(cid, file); + this.databaseBackend.saveCid(cid, file, url); } public void blockMedia(File f) { @@ -1610,6 +1618,9 @@ public class XmppConnectionService extends Service { final boolean inProgressJoin = isJoinInProgress(conversation); + if (message.getCounterpart() == null && !message.isPrivateMessage()) { + message.setCounterpart(message.getConversation().getJid().asBareJid()); + } if (account.isOnlineAndConnected() && !inProgressJoin) { switch (message.getEncryption()) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index bdfb4adfd8da123ee9b31e31ee592591b35f52ec..fe893ec81141ecc6f080261f2301929398a22f78 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -57,8 +57,11 @@ import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; +import com.cheogram.android.DownloadDefaultStickers; + import org.openintents.openpgp.util.OpenPgpApi; import java.util.Arrays; @@ -118,6 +121,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public static final int REQUEST_PLAY_PAUSE = 0x5432; public static final int REQUEST_MICROPHONE = 0x5432f; public static final int DIALLER_INTEGRATION = 0x5432ff; + public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702; //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment @@ -217,12 +221,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); if (fragment instanceof ConversationsOverviewFragment) { - if (ExceptionHelper.checkForCrash(this)) { - return; - } - if (!offerToSetupDiallerIntegration()) { - openBatteryOptimizationDialogIfNeeded(); - } + if (ExceptionHelper.checkForCrash(this)) return; + if (offerToSetupDiallerIntegration()) return; + if (offerToDownloadStickers()) return; + openBatteryOptimizationDialogIfNeeded(); } } @@ -259,6 +261,26 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } } + private boolean offerToDownloadStickers() { + int offered = getPreferences().getInt("default_stickers_offered", 0); + if (offered > 0) return false; + getPreferences().edit().putInt("default_stickers_offered", 1).apply(); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Download Stickers?"); + builder.setMessage("Would you like to download some default sticker packs?"); + builder.setPositiveButton(R.string.yes, (dialog, which) -> { + if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) { + downloadStickers(); + } + }); + builder.setNegativeButton(R.string.no, (dialog, which) -> { }); + final AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + return true; + } + private boolean offerToSetupDiallerIntegration() { if (mRequestCode == DIALLER_INTEGRATION) { mRequestCode = -1; @@ -353,11 +375,21 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio "com.android.server.telecom.settings.EnableAccountPreferenceActivity")); startActivityForResult(intent, DIALLER_INTEGRATION); break; + case REQUEST_DOWNLOAD_STICKERS: + downloadStickers(); + break; } } } } + private void downloadStickers() { + Intent intent = new Intent(this, DownloadDefaultStickers.class); + ContextCompat.startForegroundService(this, intent); + displayToast("Sticker download started"); + showDialogsIfMainIsOverview(); + } + @Override public void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index daa13ab7c629732d3160ed941f431d6c28fa6823..4e44d167616e3e688dd0537b279da73fed2f30fd 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -25,6 +25,8 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; +import com.cheogram.android.DownloadDefaultStickers; + import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -69,6 +71,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; public static final int REQUEST_CREATE_BACKUP = 0xbf8701; + public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702; private SettingsFragment mSettingsFragment; @@ -390,6 +393,17 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference } } + final Preference downloadDefaultStickers = mSettingsFragment.findPreference("download_default_stickers"); + if (downloadDefaultStickers != null) { + downloadDefaultStickers.setOnPreferenceClickListener( + preference -> { + if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) { + downloadStickers(); + } + return true; + }); + } + final Preference clearBlockedMedia = mSettingsFragment.findPreference("clear_blocked_media"); if (clearBlockedMedia != null) { clearBlockedMedia.setOnPreferenceClickListener((p) -> { @@ -587,6 +601,9 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference if (requestCode == REQUEST_CREATE_BACKUP) { createBackup(); } + if (requestCode == REQUEST_DOWNLOAD_STICKERS) { + downloadStickers(); + } } else { Toast.makeText( this, @@ -620,6 +637,12 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference builder.create().show(); } + private void downloadStickers() { + Intent intent = new Intent(this, DownloadDefaultStickers.class); + ContextCompat.startForegroundService(this, intent); + displayToast("Sticker download started"); + } + private void displayToast(final String msg) { runOnUiThread(() -> Toast.makeText(SettingsActivity.this, msg, Toast.LENGTH_LONG).show()); } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 77f6565fa23f53190def7bd173e5418f9f3ec8ee..b9aafbadd529abb981f4bcd51ce2c56f9269ee92 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -293,7 +293,7 @@ public final class CryptoHelper { public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException { switch(type) { case sha1: - return "sha1"; + return "sha-1"; case sha2_256: return "sha-256"; case sha2_512: diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 2f57ec695c3291292765b35b7c68d3f76c3cba6e..455e55f2a02e0449011ca26a058e16cb288b485a 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -370,6 +370,9 @@ +