~singpolyma/cheogram-android

15692fbc038f4560caba8ecb9b77cf318737897e — Stephen Paul Weber 1 year, 2 months ago 78e6102 + 67646c6
Merge branch 'default-stickers'

* default-stickers:
  Default sticker download offer on first start
  Scan files so they show up under images
  Send SIMS with hashes and filename
  sha-1 is the standard name sha1 is just for bob
  Send cid with known URL without uploading
  Option to download default stickers and save their cid and url in database
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>