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}