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