M src/cheogram/AndroidManifest.xml => src/cheogram/AndroidManifest.xml +1 -0
@@ 7,6 7,7 @@
<application tools:ignore="GoogleAppIndexingWarning">
<!-- INSERT -->
+ <service android:name="com.cheogram.android.DownloadDefaultStickers" />
<service android:name="com.cheogram.android.ConnectionService"
android:label="Cheogram"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
M src/cheogram/java/com/cheogram/android/BobTransfer.java => src/cheogram/java/com/cheogram/android/BobTransfer.java +8 -1
@@ 13,6 13,7 @@ import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import io.ipfs.cid.Cid;
+import io.ipfs.multihash.Multihash;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ 57,7 58,13 @@ public class BobTransfer implements Transferable {
}
public static URI uri(Cid cid) throws NoSuchAlgorithmException, URISyntaxException {
- return new URI("cid", CryptoHelper.multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
+ return new URI("cid", multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
+ }
+
+ private static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
+ final String algo = CryptoHelper.multihashAlgo(type);
+ if (algo.equals("sha-1")) return "sha1";
+ return algo;
}
public BobTransfer(URI uri, Account account, Jid to, XmppConnectionService xmppConnectionService) {
A src/cheogram/java/com/cheogram/android/DownloadDefaultStickers.java => src/cheogram/java/com/cheogram/android/DownloadDefaultStickers.java +178 -0
@@ 0,0 1,178 @@
+package com.cheogram.android;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.IBinder;
+import android.provider.DocumentsContract;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+
+import com.google.common.io.ByteStreams;
+
+import io.ipfs.cid.Cid;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.utils.Compatibility;
+import eu.siacs.conversations.utils.FileUtils;
+import eu.siacs.conversations.utils.MimeUtils;
+
+public class DownloadDefaultStickers extends Service {
+
+ private static final int NOTIFICATION_ID = 20;
+ private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
+ private DatabaseBackend mDatabaseBackend;
+ private NotificationManager notificationManager;
+ private File mStickerDir;
+ private OkHttpClient http = new OkHttpClient();
+
+ @Override
+ public void onCreate() {
+ mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
+ notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ mStickerDir = stickerDir();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (RUNNING.compareAndSet(false, true)) {
+ new Thread(() -> {
+ 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();
+ }
+ }
+}
M src/main/java/eu/siacs/conversations/entities/Message.java => src/main/java/eu/siacs/conversations/entities/Message.java +35 -0
@@ 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<Cid> 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<Cid> getCids() {
List<Cid> cids = new ArrayList<>();
Element file = getFileElement();
M src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java => src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +24 -0
@@ 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);
}
M src/main/java/eu/siacs/conversations/persistance/FileBackend.java => src/main/java/eu/siacs/conversations/persistance/FileBackend.java +26 -6
@@ 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) { }
}
}
M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +12 -1
@@ 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()) {
M src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java => src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +38 -6
@@ 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);
M src/main/java/eu/siacs/conversations/ui/SettingsActivity.java => src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +23 -0
@@ 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());
}
M src/main/java/eu/siacs/conversations/utils/CryptoHelper.java => src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +1 -1
@@ 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:
M src/main/res/xml/preferences.xml => src/main/res/xml/preferences.xml +3 -0
@@ 371,6 371,9 @@
android:title="Change Stickers Location"
android:key="sticker_directory" />
<Preference
+ android:title="Update Default Stickers"
+ android:key="download_default_stickers" />
+ <Preference
android:title="Clear Blocked Media"
android:key="clear_blocked_media" />
</PreferenceCategory>