diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 56ed5c762..c1baf42d2 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -204,6 +204,11 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + diff --git a/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java b/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java new file mode 100644 index 000000000..ccaa65c91 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java @@ -0,0 +1,226 @@ +package eu.siacs.conversations.services; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.aztec.AztecWriter; +import com.google.zxing.common.BitMatrix; + +import java.io.BufferedOutputStream; +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.util.Hashtable; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class BarcodeProvider extends ContentProvider implements ServiceConnection { + + private static final String AUTHORITY = "eu.siacs.conversations.barcodes"; + + private final Object lock = new Object(); + + private XmppConnectionService mXmppConnectionService; + + @Override + public boolean onCreate() { + File barcodeDirectory = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/"); + if (barcodeDirectory.exists() && barcodeDirectory.isDirectory()) { + for (File file : barcodeDirectory.listFiles()) { + if (file.isFile() && !file.isHidden()) { + Log.d(Config.LOGTAG, "deleting old barcode file " + file.getAbsolutePath()); + file.delete(); + } + } + } + return true; + } + + @Nullable + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return null; + } + + @Nullable + @Override + public String getType(Uri uri) { + return "image/png"; + } + + @Nullable + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + return openFile(uri, mode, null); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException { + Log.d(Config.LOGTAG, "opening file with uri (normal): " + uri.toString()); + String path = uri.getPath(); + if (path != null && path.endsWith(".png") && path.length() >= 5) { + String jid = path.substring(1).substring(0, path.length() - 4); + Log.d(Config.LOGTAG, "account:" + jid); + if (connectAndWait()) { + Log.d(Config.LOGTAG, "connected to background service"); + try { + Account account = mXmppConnectionService.findAccountByJid(Jid.fromString(jid)); + if (account != null) { + String shareableUri = account.getShareableUri(); + String hash = CryptoHelper.getFingerprint(shareableUri); + File file = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/" + hash); + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + Bitmap bitmap = createAztecBitmap(account.getShareableUri(), 1024); + OutputStream outputStream = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + outputStream.close(); + outputStream.flush(); + } + return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY); + } + } catch (Exception e) { + throw new FileNotFoundException(); + } + } + } + throw new FileNotFoundException(); + } + + private boolean connectAndWait() { + Intent intent = new Intent(getContext(), XmppConnectionService.class); + intent.setAction("contact_chooser"); + Context context = getContext(); + if (context != null) { + context.startService(intent); + context.bindService(intent, this, Context.BIND_AUTO_CREATE); + try { + waitForService(); + Log.d(Config.LOGTAG, "service initialized"); + return true; + } catch (InterruptedException e) { + return false; + } + } else { + Log.d(Config.LOGTAG, "context was null"); + return false; + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service; + mXmppConnectionService = binder.getService(); + synchronized (this.lock) { + lock.notifyAll(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mXmppConnectionService = null; + } + + private void waitForService() throws InterruptedException { + if (mXmppConnectionService == null) { + synchronized (this.lock) { + lock.wait(); + } + } + } + + public static Uri getUriForAccount(Account account) { + return Uri.parse("content://" + AUTHORITY + "/" + account.getJid().toBareJid() + ".png"); + } + + public static Bitmap createAztecBitmap(String input, int size) { + try { + final AztecWriter AZTEC_WRITER = new AztecWriter(); + final Hashtable hints = new Hashtable<>(); + hints.put(EncodeHintType.ERROR_CORRECTION, 10); + final BitMatrix result = AZTEC_WRITER.encode(input, BarcodeFormat.AZTEC, size, size, hints); + final int width = result.getWidth(); + final int height = result.getHeight(); + final int[] pixels = new int[width * height]; + for (int y = 0; y < height; y++) { + final int offset = y * width; + for (int x = 0; x < width; x++) { + pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT; + } + } + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, width, 0, 0, width, height); + return bitmap; + } catch (final Exception e) { + return null; + } + } + + static class TransferThread extends Thread { + InputStream in; + OutputStream out; + + TransferThread(InputStream in, OutputStream out) { + this.in = in; + this.out = out; + } + + @Override + public void run() { + byte[] buf = new byte[1024]; + int len; + + try { + while ((len = in.read(buf)) >= 0) { + out.write(buf, 0, len); + } + + in.close(); + out.flush(); + out.close(); + } catch (IOException e) { + Log.e(Config.LOGTAG, "Exception transferring file", e); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 693b7185b..14e3d9293 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -46,6 +46,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.BarcodeProvider; import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; @@ -703,6 +704,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat case R.id.action_server_info_show_more: changeMoreTableVisibility(!item.isChecked()); break; + case R.id.action_share_barcode: + shareBarcode(); + break; + case R.id.action_share_http: + shareLink(true); + break; + case R.id.action_share_uri: + shareLink(false); + break; case R.id.action_change_password_on_server: gotoChangePassword(null); break; @@ -722,6 +732,27 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat return super.onOptionsItemSelected(item); } + private void shareLink(boolean http) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + String text; + if (http) { + text = "https://conversations.im/i/"+mAccount.getJid().toBareJid().toString(); + } else { + text = mAccount.getShareableUri(); + } + intent.putExtra(Intent.EXTRA_TEXT,text); + startActivity(Intent.createChooser(intent, getText(R.string.share_with))); + } + + private void shareBarcode() { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM,BarcodeProvider.getUriForAccount(mAccount)); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType("image/png"); + startActivity(Intent.createChooser(intent, getText(R.string.share_with))); + } + private void changeMoreTableVisibility(boolean visible) { mMoreTable.setVisibility(visible ? View.VISIBLE : View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 5d0868072..583fab785 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -78,6 +78,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.services.BarcodeProvider; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.utils.CryptoHelper; @@ -1058,7 +1059,7 @@ public abstract class XmppActivity extends Activity { Point size = new Point(); getWindowManager().getDefaultDisplay().getSize(size); final int width = (size.x < size.y ? size.x : size.y); - Bitmap bitmap = createQrCodeBitmap(uri, width); + Bitmap bitmap = BarcodeProvider.createAztecBitmap(uri, width); ImageView view = new ImageView(this); view.setBackgroundColor(Color.WHITE); view.setImageBitmap(bitmap); @@ -1068,29 +1069,6 @@ public abstract class XmppActivity extends Activity { } } - protected Bitmap createQrCodeBitmap(String input, int size) { - try { - final AztecWriter AZTEC_WRITER = new AztecWriter(); - final Hashtable hints = new Hashtable<>(); - hints.put(EncodeHintType.ERROR_CORRECTION, 10); - final BitMatrix result = AZTEC_WRITER.encode(input, BarcodeFormat.AZTEC, size, size, hints); - final int width = result.getWidth(); - final int height = result.getHeight(); - final int[] pixels = new int[width * height]; - for (int y = 0; y < height; y++) { - final int offset = y * width; - for (int x = 0; x < width; x++) { - pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT; - } - } - final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - bitmap.setPixels(pixels, 0, width, 0, 0, width, height); - return bitmap; - } catch (final Exception e) { - return null; - } - } - protected Account extractAccount(Intent intent) { String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null; try { diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 38ebced10..f1a9d8c41 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -206,9 +206,13 @@ public final class CryptoHelper { } public static String getAccountFingerprint(Account account) { + return getFingerprint(account.getJid().toBareJid().toString()); + } + + public static String getFingerprint(String value) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); - return bytesToHex(md.digest(account.getJid().toBareJid().toString().getBytes("UTF-8"))); + return bytesToHex(md.digest(value.getBytes("UTF-8"))); } catch (Exception e) { return ""; } diff --git a/src/main/res/menu/editaccount.xml b/src/main/res/menu/editaccount.xml index 5df7ee5b2..c67e5c470 100644 --- a/src/main/res/menu/editaccount.xml +++ b/src/main/res/menu/editaccount.xml @@ -1,5 +1,21 @@ + + + + + + + + Verified fingerprints Use the camera to scan a contacts barcode Please wait for keys to be fetched + Share as Barcode + Share as XMPP URI + Share as HTTP link