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}