BarcodeProvider.java

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