Skip to content

Commit c3adc33

Browse files
WIP
1 parent 6ed5e52 commit c3adc33

9 files changed

+353
-136
lines changed

app/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ dependencies {
9494
implementation("androidx.preference:preference:1.2.1")
9595
implementation("com.google.android.material:material:1.11.0")
9696
implementation("com.github.yalantis:ucrop:2.2.8")
97+
implementation("androidx.work:work-runtime:2.9.0")
9798
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
9899

99100
// Splash Screen

app/src/main/AndroidManifest.xml

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
1313

1414
<uses-permission android:name="android.permission.CAMERA" />
15+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
16+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1517
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23" />
1618

1719
<uses-feature
@@ -186,5 +188,6 @@
186188
<action android:name="android.service.controls.ControlsProviderService" />
187189
</intent-filter>
188190
</service>
191+
<service android:name=".importexport.ImportExportWorker"/>
189192
</application>
190193
</manifest>

app/src/main/java/protect/card_locker/ImportExportActivity.java

+73-134
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import android.content.ActivityNotFoundException;
44
import android.content.DialogInterface;
55
import android.content.Intent;
6+
import android.content.pm.PackageManager;
67
import android.net.Uri;
78
import android.os.Bundle;
89
import android.text.InputType;
@@ -17,9 +18,15 @@
1718

1819
import androidx.activity.result.ActivityResultLauncher;
1920
import androidx.activity.result.contract.ActivityResultContracts;
21+
import androidx.annotation.NonNull;
2022
import androidx.annotation.Nullable;
2123
import androidx.appcompat.app.AlertDialog;
2224
import androidx.appcompat.widget.Toolbar;
25+
import androidx.work.Data;
26+
import androidx.work.OneTimeWorkRequest;
27+
import androidx.work.OutOfQuotaPolicy;
28+
import androidx.work.WorkManager;
29+
import androidx.work.WorkRequest;
2330

2431
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
2532
import com.google.android.material.textfield.TextInputLayout;
@@ -28,20 +35,20 @@
2835
import java.io.InputStream;
2936
import java.io.OutputStream;
3037
import java.util.ArrayList;
38+
import java.util.Arrays;
3139
import java.util.List;
3240

3341
import protect.card_locker.async.TaskHandler;
3442
import protect.card_locker.databinding.ImportExportActivityBinding;
3543
import protect.card_locker.importexport.DataFormat;
3644
import protect.card_locker.importexport.ImportExportResult;
3745
import protect.card_locker.importexport.ImportExportResultType;
46+
import protect.card_locker.importexport.ImportExportWorker;
3847

3948
public class ImportExportActivity extends CatimaAppCompatActivity {
4049
private ImportExportActivityBinding binding;
4150
private static final String TAG = "Catima";
4251

43-
private ImportExportTask importExporter;
44-
4552
private String importAlertTitle;
4653
private String importAlertMessage;
4754
private DataFormat importDataFormat;
@@ -51,7 +58,10 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
5158
private ActivityResultLauncher<String> fileOpenLauncher;
5259
private ActivityResultLauncher<Intent> filePickerLauncher;
5360

54-
final private TaskHandler mTasks = new TaskHandler();
61+
private static final int PERMISSION_REQUEST_EXPORT = 100;
62+
private static final int PERMISSION_REQUEST_IMPORT = 101;
63+
64+
private WorkRequest mRequestedWorkRequest;
5565

5666
@Override
5767
protected void onCreate(Bundle savedInstanceState) {
@@ -80,15 +90,20 @@ protected void onCreate(Bundle savedInstanceState) {
8090
Log.e(TAG, "Activity returned NULL uri");
8191
return;
8292
}
83-
try {
84-
OutputStream writer = getContentResolver().openOutputStream(uri);
85-
Log.e(TAG, "Starting file export with: " + result.toString());
86-
startExport(writer, uri, exportPassword.toCharArray(), true);
87-
} catch (IOException e) {
88-
Log.e(TAG, "Failed to export file: " + result.toString(), e);
89-
onExportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, result.toString()), uri);
90-
}
9193

94+
Data exportRequestData = new Data.Builder()
95+
.putString(ImportExportWorker.INPUT_URI, uri.toString())
96+
.putString(ImportExportWorker.INPUT_ACTION, ImportExportWorker.ACTION_EXPORT)
97+
.putString(ImportExportWorker.INPUT_FORMAT, DataFormat.Catima.name())
98+
.putString(ImportExportWorker.INPUT_PASSWORD, exportPassword)
99+
.build();
100+
101+
mRequestedWorkRequest = new OneTimeWorkRequest.Builder(ImportExportWorker.class)
102+
.setInputData(exportRequestData)
103+
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
104+
.build();
105+
106+
PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_EXPORT);
92107
});
93108
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
94109
if (result == null) {
@@ -160,14 +175,19 @@ protected void onCreate(Bundle savedInstanceState) {
160175
}
161176

162177
private void openFileForImport(Uri uri, char[] password) {
163-
try {
164-
InputStream reader = getContentResolver().openInputStream(uri);
165-
Log.e(TAG, "Starting file import with: " + uri.toString());
166-
startImport(reader, uri, importDataFormat, password, true);
167-
} catch (IOException e) {
168-
Log.e(TAG, "Failed to import file: " + uri.toString(), e);
169-
onImportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, e.toString()), uri, importDataFormat);
170-
}
178+
Data importRequestData = new Data.Builder()
179+
.putString(ImportExportWorker.INPUT_URI, uri.toString())
180+
.putString(ImportExportWorker.INPUT_ACTION, ImportExportWorker.ACTION_IMPORT)
181+
.putString(ImportExportWorker.INPUT_FORMAT, importDataFormat.name())
182+
.putString(ImportExportWorker.INPUT_PASSWORD, Arrays.toString(password))
183+
.build();
184+
185+
mRequestedWorkRequest = new OneTimeWorkRequest.Builder(ImportExportWorker.class)
186+
.setInputData(importRequestData)
187+
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
188+
.build();
189+
190+
PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_IMPORT);
171191
}
172192

173193
private void chooseImportType(boolean choosePicker,
@@ -232,20 +252,17 @@ private void chooseImportType(boolean choosePicker,
232252
new MaterialAlertDialogBuilder(this)
233253
.setTitle(importAlertTitle)
234254
.setMessage(importAlertMessage)
235-
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
236-
@Override
237-
public void onClick(DialogInterface dialog, int which) {
238-
try {
239-
if (choosePicker) {
240-
final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
241-
filePickerLauncher.launch(intentPickAction);
242-
} else {
243-
fileOpenLauncher.launch("*/*");
244-
}
245-
} catch (ActivityNotFoundException e) {
246-
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
247-
Log.e(TAG, "No activity found to handle intent", e);
255+
.setPositiveButton(R.string.ok, (dialog1, which1) -> {
256+
try {
257+
if (choosePicker) {
258+
final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
259+
filePickerLauncher.launch(intentPickAction);
260+
} else {
261+
fileOpenLauncher.launch("*/*");
248262
}
263+
} catch (ActivityNotFoundException e) {
264+
Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show();
265+
Log.e(TAG, "No activity found to handle intent", e);
249266
}
250267
})
251268
.setNegativeButton(R.string.cancel, null)
@@ -254,55 +271,6 @@ public void onClick(DialogInterface dialog, int which) {
254271
builder.show();
255272
}
256273

257-
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) {
258-
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
259-
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
260-
@Override
261-
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
262-
onImportComplete(result, targetUri, dataFormat);
263-
if (closeWhenDone) {
264-
try {
265-
target.close();
266-
} catch (IOException ioException) {
267-
ioException.printStackTrace();
268-
}
269-
}
270-
}
271-
};
272-
273-
importExporter = new ImportExportTask(ImportExportActivity.this,
274-
dataFormat, target, password, listener);
275-
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter);
276-
}
277-
278-
private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) {
279-
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
280-
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
281-
@Override
282-
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
283-
onExportComplete(result, targetUri);
284-
if (closeWhenDone) {
285-
try {
286-
target.close();
287-
} catch (IOException ioException) {
288-
ioException.printStackTrace();
289-
}
290-
}
291-
}
292-
};
293-
294-
importExporter = new ImportExportTask(ImportExportActivity.this,
295-
DataFormat.Catima, target, password, listener);
296-
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter);
297-
}
298-
299-
@Override
300-
protected void onDestroy() {
301-
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
302-
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
303-
super.onDestroy();
304-
}
305-
306274
@Override
307275
public boolean onOptionsItemSelected(MenuItem item) {
308276
int id = item.getItemId();
@@ -343,68 +311,39 @@ private void retryWithPassword(DataFormat dataFormat, Uri uri) {
343311
builder.show();
344312
}
345313

346-
private String buildResultDialogMessage(ImportExportResult result, boolean isImport) {
347-
int messageId;
348-
349-
if (result.resultType() == ImportExportResultType.Success) {
350-
messageId = isImport ? R.string.importSuccessful : R.string.exportSuccessful;
351-
} else {
352-
messageId = isImport ? R.string.importFailed : R.string.exportFailed;
353-
}
354-
355-
StringBuilder messageBuilder = new StringBuilder(getResources().getString(messageId));
356-
if (result.developerDetails() != null) {
357-
messageBuilder.append("\n\n");
358-
messageBuilder.append(getResources().getString(R.string.include_if_asking_support));
359-
messageBuilder.append("\n\n");
360-
messageBuilder.append(result.developerDetails());
361-
}
362-
363-
return messageBuilder.toString();
364-
}
365-
366-
private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) {
367-
ImportExportResultType resultType = result.resultType();
368-
369-
if (resultType == ImportExportResultType.BadPassword) {
370-
retryWithPassword(dataFormat, path);
371-
return;
372-
}
373-
374-
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
375-
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle);
376-
builder.setMessage(buildResultDialogMessage(result, true));
377-
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
314+
@Override
315+
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
316+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
378317

379-
builder.create().show();
318+
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
380319
}
381320

382-
private void onExportComplete(ImportExportResult result, final Uri path) {
383-
ImportExportResultType resultType = result.resultType();
321+
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
322+
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
323+
Integer failureReason = null;
384324

385-
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
386-
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.exportSuccessfulTitle : R.string.exportFailedTitle);
387-
builder.setMessage(buildResultDialogMessage(result, false));
388-
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
325+
if (requestCode == PERMISSION_REQUEST_EXPORT) {
326+
if (granted) {
327+
WorkManager.getInstance(this).enqueue(mRequestedWorkRequest);
389328

390-
if (resultType == ImportExportResultType.Success) {
391-
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
392-
393-
builder.setPositiveButton(sendLabel, (dialog, which) -> {
394-
Intent sendIntent = new Intent(Intent.ACTION_SEND);
395-
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
396-
sendIntent.setType("text/csv");
329+
Toast.makeText(this, R.string.exportStartedCheckNotifications, Toast.LENGTH_LONG).show();
330+
return;
331+
}
397332

398-
// set flag to give temporary permission to external app to use the FileProvider
399-
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
333+
failureReason = R.string.postNotificationsPermissionRequired;
334+
} else if (requestCode == PERMISSION_REQUEST_IMPORT) {
335+
if (granted) {
336+
WorkManager.getInstance(this).enqueue(mRequestedWorkRequest);
400337

401-
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
402-
sendLabel));
338+
Toast.makeText(this, R.string.importStartedCheckNotifications, Toast.LENGTH_LONG).show();
339+
return;
340+
}
403341

404-
dialog.dismiss();
405-
});
342+
failureReason = R.string.postNotificationsPermissionRequired;
406343
}
407344

408-
builder.create().show();
345+
if (failureReason != null) {
346+
Toast.makeText(this, failureReason, Toast.LENGTH_LONG).show();
347+
}
409348
}
410349
}

app/src/main/java/protect/card_locker/ImportExportTask.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public ImportExportResult call() {
136136
return doInBackground();
137137
}
138138

139-
interface TaskCompleteListener {
139+
public interface TaskCompleteListener {
140140
void onTaskComplete(ImportExportResult result, DataFormat format);
141141
}
142142

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package protect.card_locker;
2+
3+
import static android.content.Context.NOTIFICATION_SERVICE;
4+
5+
import android.app.Notification;
6+
import android.app.NotificationChannel;
7+
import android.app.NotificationManager;
8+
import android.content.Context;
9+
10+
import androidx.annotation.NonNull;
11+
import androidx.annotation.Nullable;
12+
13+
public class NotificationHelper {
14+
15+
// Do not change these IDs!
16+
public static final String CHANNEL_IMPORT = "import";
17+
18+
public static final String CHANNEL_EXPORT = "export";
19+
20+
public static final int IMPORT_ID = 100;
21+
public static final int IMPORT_PROGRESS_ID = 101;
22+
public static final int EXPORT_ID = 103;
23+
public static final int EXPORT_PROGRESS_ID = 104;
24+
25+
26+
public static Notification.Builder createNotificationBuilder(@NonNull Context context, @NonNull String channel, @NonNull int icon, @NonNull String title, @Nullable String message) {
27+
Notification.Builder notificationBuilder = new Notification.Builder(context)
28+
.setSmallIcon(icon)
29+
.setTicker(title)
30+
.setContentTitle(title);
31+
32+
if (message != null) {
33+
notificationBuilder.setContentText(message);
34+
}
35+
36+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
37+
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
38+
NotificationChannel notificationChannel = new NotificationChannel(channel, getChannelName(channel), NotificationManager.IMPORTANCE_DEFAULT);
39+
notificationManager.createNotificationChannel(notificationChannel);
40+
41+
notificationBuilder.setChannelId(channel);
42+
}
43+
44+
return notificationBuilder;
45+
}
46+
47+
public static void sendNotification(@NonNull Context context, @NonNull int notificationId, @NonNull Notification notification) {
48+
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
49+
50+
notificationManager.notify(notificationId, notification);
51+
}
52+
53+
private static String getChannelName(@NonNull String channel) {
54+
switch(channel) {
55+
case CHANNEL_IMPORT:
56+
return "Import";
57+
case CHANNEL_EXPORT:
58+
return "Export";
59+
default:
60+
throw new IllegalArgumentException("Unknown notification channel");
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)