1package eu.siacs.conversations.services;
  2
  3import android.content.ComponentName;
  4import android.content.ContentProvider;
  5import android.content.ContentValues;
  6import android.content.Context;
  7import android.content.Intent;
  8import android.content.ServiceConnection;
  9import android.database.Cursor;
 10import android.graphics.Bitmap;
 11import android.graphics.Color;
 12import android.net.Uri;
 13import android.os.CancellationSignal;
 14import android.os.IBinder;
 15import android.os.ParcelFileDescriptor;
 16import android.util.Log;
 17
 18import androidx.annotation.Nullable;
 19
 20import com.google.zxing.BarcodeFormat;
 21import com.google.zxing.EncodeHintType;
 22import com.google.zxing.common.BitMatrix;
 23import com.google.zxing.qrcode.QRCodeWriter;
 24import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
 25
 26import java.io.File;
 27import java.io.FileNotFoundException;
 28import java.io.FileOutputStream;
 29import java.io.OutputStream;
 30import java.util.Hashtable;
 31
 32import eu.siacs.conversations.Config;
 33import eu.siacs.conversations.entities.Account;
 34import eu.siacs.conversations.utils.CryptoHelper;
 35import eu.siacs.conversations.xmpp.Jid;
 36
 37public class BarcodeProvider extends ContentProvider implements ServiceConnection {
 38
 39	private static final String AUTHORITY = ".barcodes";
 40
 41	private final Object lock = new Object();
 42
 43	private XmppConnectionService mXmppConnectionService;
 44	private boolean mBindingInProcess = false;
 45
 46	public static Uri getUriForAccount(Context context, Account account) {
 47		final String packageId = context.getPackageName();
 48		return Uri.parse("content://" + packageId + AUTHORITY + "/" + account.getJid().asBareJid() + ".png");
 49	}
 50
 51	public static Bitmap create2dBarcodeBitmap(final String input, final int size) {
 52		return create2dBarcodeBitmap(input, size, Color.BLACK, Color.WHITE);
 53	}
 54
 55	public static Bitmap create2dBarcodeBitmap(final String input, final int size, final int black, final int white) {
 56		try {
 57			final QRCodeWriter barcodeWriter = new QRCodeWriter();
 58			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
 59			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
 60			hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
 61			final BitMatrix result = barcodeWriter.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
 62			final int width = result.getWidth();
 63			final int height = result.getHeight();
 64			final int[] pixels = new int[width * height];
 65			for (int y = 0; y < height; y++) {
 66				final int offset = y * width;
 67				for (int x = 0; x < width; x++) {
 68					pixels[offset + x] = result.get(x, y) ? black : white;
 69				}
 70			}
 71			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
 72			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
 73			return bitmap;
 74		} catch (final Exception e) {
 75			Log.e(Config.LOGTAG,"could not generate QR code image",e);
 76			return null;
 77		}
 78	}
 79
 80	@Override
 81	public boolean onCreate() {
 82		File barcodeDirectory = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/");
 83		if (barcodeDirectory.exists() && barcodeDirectory.isDirectory()) {
 84			for (File file : barcodeDirectory.listFiles()) {
 85				if (file.isFile() && !file.isHidden()) {
 86					if (file.delete()) {
 87						Log.d(Config.LOGTAG, "deleted old barcode file " + file.getAbsolutePath());
 88					}
 89				}
 90			}
 91		}
 92		return true;
 93	}
 94
 95	@Nullable
 96	@Override
 97	public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
 98		return null;
 99	}
100
101	@Nullable
102	@Override
103	public String getType(Uri uri) {
104		return "image/png";
105	}
106
107	@Nullable
108	@Override
109	public Uri insert(Uri uri, ContentValues values) {
110		return null;
111	}
112
113	@Override
114	public int delete(Uri uri, String selection, String[] selectionArgs) {
115		return 0;
116	}
117
118	@Override
119	public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
120		return 0;
121	}
122
123	@Override
124	public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
125		return openFile(uri, mode, null);
126	}
127
128	@Override
129	public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException {
130		Log.d(Config.LOGTAG, "opening file with uri (normal): " + uri.toString());
131		String path = uri.getPath();
132		if (path != null && path.endsWith(".png") && path.length() >= 5) {
133			String jid = path.substring(1).substring(0, path.length() - 4);
134			Log.d(Config.LOGTAG, "account:" + jid);
135			if (connectAndWait()) {
136				Log.d(Config.LOGTAG, "connected to background service");
137				try {
138					Account account = mXmppConnectionService.findAccountByJid(Jid.of(jid));
139					if (account != null) {
140						String shareableUri = account.getShareableUri();
141						String hash = CryptoHelper.getFingerprint(shareableUri);
142						File file = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/" + hash);
143						if (!file.exists()) {
144							file.getParentFile().mkdirs();
145							file.createNewFile();
146							Bitmap bitmap = create2dBarcodeBitmap(account.getShareableUri(), 1024);
147							OutputStream outputStream = new FileOutputStream(file);
148							bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
149							outputStream.close();
150							outputStream.flush();
151						}
152						return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
153					}
154				} catch (Exception e) {
155					throw new FileNotFoundException();
156				}
157			}
158		}
159		throw new FileNotFoundException();
160	}
161
162	private boolean connectAndWait() {
163		Intent intent = new Intent(getContext(), XmppConnectionService.class);
164		intent.setAction(this.getClass().getSimpleName());
165		Context context = getContext();
166		if (context != null) {
167			synchronized (this) {
168				if (mXmppConnectionService == null && !mBindingInProcess) {
169					Log.d(Config.LOGTAG, "calling to bind service");
170					context.bindService(intent, this, Context.BIND_AUTO_CREATE);
171					this.mBindingInProcess = true;
172				}
173			}
174			try {
175				waitForService();
176				return true;
177			} catch (InterruptedException e) {
178				return false;
179			}
180		} else {
181			Log.d(Config.LOGTAG, "context was null");
182			return false;
183		}
184	}
185
186	@Override
187	public void onServiceConnected(ComponentName name, IBinder service) {
188		synchronized (this) {
189			XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
190			mXmppConnectionService = binder.getService();
191			mBindingInProcess = false;
192			synchronized (this.lock) {
193				lock.notifyAll();
194			}
195		}
196	}
197
198	@Override
199	public void onServiceDisconnected(ComponentName name) {
200		synchronized (this) {
201			mXmppConnectionService = null;
202		}
203	}
204
205	private void waitForService() throws InterruptedException {
206		if (mXmppConnectionService == null) {
207			synchronized (this.lock) {
208				lock.wait();
209			}
210		} else {
211			Log.d(Config.LOGTAG, "not waiting for service because already initialized");
212		}
213	}
214}