Migrate contacts import job to new API

Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
This commit is contained in:
Chris Narkiewicz 2020-03-27 23:44:21 +00:00
parent 44bcecf98b
commit 5204ba7541
No known key found for this signature in database
GPG key ID: 30D28CA4CCC665C6
9 changed files with 270 additions and 167 deletions

View file

@ -191,6 +191,7 @@ android {
testOptions {
unitTests.returnDefaultValues = true
// comment out if you run androidTests in Android Studio experiencing test runner crashes
execution 'ANDROIDX_TEST_ORCHESTRATOR'
animationsDisabled true
}

View file

@ -67,6 +67,7 @@ import java.util.concurrent.TimeoutException
BackgroundJobManagerTest.ContentObserver::class,
BackgroundJobManagerTest.PeriodicContactsBackup::class,
BackgroundJobManagerTest.ImmediateContactsBackup::class,
BackgroundJobManagerTest.ImmediateContactsImport::class,
BackgroundJobManagerTest.Tags::class
)
class BackgroundJobManagerTest {
@ -307,6 +308,64 @@ class BackgroundJobManagerTest {
}
}
class ImmediateContactsImport : Fixture() {
private lateinit var workInfo: MutableLiveData<WorkInfo>
private lateinit var jobInfo: LiveData<JobInfo?>
private lateinit var request: OneTimeWorkRequest
@Before
fun setUp() {
val requestCaptor: KArgumentCaptor<OneTimeWorkRequest> = argumentCaptor()
workInfo = MutableLiveData()
whenever(workManager.getWorkInfoByIdLiveData(any())).thenReturn(workInfo)
jobInfo = backgroundJobManager.startImmediateContactsImport(
contactsAccountName = "name",
contactsAccountType = "type",
vCardFilePath = "/path/to/vcard/file",
selectedContacts = intArrayOf(1, 2, 3)
)
verify(workManager).enqueueUniqueWork(
any(),
any(),
requestCaptor.capture()
)
assertEquals(1, requestCaptor.allValues.size)
request = requestCaptor.firstValue
}
@Test
fun job_is_unique() {
verify(workManager).enqueueUniqueWork(
eq(BackgroundJobManagerImpl.JOB_IMMEDIATE_CONTACTS_IMPORT),
eq(ExistingWorkPolicy.KEEP),
argThat(IsOneTimeWorkRequest())
)
}
@Test
fun job_request_has_mandatory_tags() {
assertHasRequiredTags(request.tags, BackgroundJobManagerImpl.JOB_IMMEDIATE_CONTACTS_IMPORT)
}
@Test
@UiThreadTest
fun job_info_is_obtained_from_work_info() {
// GIVEN
// work info is available
workInfo.value = buildWorkInfo(0)
// WHEN
// job info has listener
jobInfo.observeForever {}
// THEN
// converted value is available
assertNotNull(jobInfo.value)
assertEquals(workInfo.value?.id, jobInfo.value?.id)
}
}
class Tags {
@Test
fun split_tag_key_and_value() {

View file

@ -31,6 +31,7 @@ import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.core.Clock
import com.nextcloud.client.device.DeviceInfo
import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.client.logger.Logger
import com.nextcloud.client.preferences.AppPreferences
import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.datamodel.SyncedFolderProvider
@ -38,10 +39,10 @@ import javax.inject.Inject
import javax.inject.Provider
/**
* This factory is responsible for creating all background jobs and for injecting
* all jobs dependencies.
* This factory is responsible for creating all background jobs and for injecting job dependencies.
*/
class BackgroundJobFactory @Inject constructor(
private val logger: Logger,
private val preferences: AppPreferences,
private val contentResolver: ContentResolver,
private val clock: Clock,
@ -68,6 +69,7 @@ class BackgroundJobFactory @Inject constructor(
return when (workerClass) {
ContentObserverWork::class -> createContentObserverJob(context, workerParameters, clock)
ContactsBackupWork::class -> createContactsBackupWork(context, workerParameters)
ContactsImportWork::class -> createContactsImportWork(context, workerParameters)
else -> null // falls back to default factory
}
}
@ -102,4 +104,13 @@ class BackgroundJobFactory @Inject constructor(
accountManager
)
}
private fun createContactsImportWork(context: Context, params: WorkerParameters): ContactsImportWork {
return ContactsImportWork(
context,
params,
logger,
contentResolver
)
}
}

View file

@ -66,10 +66,29 @@ interface BackgroundJobManager {
* Immediately start single contacts backup job.
* This job will launch independently from periodic contacts backup.
*
* @return Job info with current status, or null if job does not exist anymore
* @return Job info with current status; status is null if job does not exist
*/
fun startImmediateContactsBackup(user: User): LiveData<JobInfo?>
/**
* Immediately start contacts import job. Import job will be started only once.
* If new job is started while existing job is running - request will be ignored
* and currently running job will continue running.
*
* @param contactsAccountName Target contacts account name; null for local contacts
* @param contactsAccountType Target contacts account type; null for local contacts
* @param vCardFilePath Path to file containing all contact entries
* @param selectedContacts List of contact indices to import from [vCardFilePath] file
*
* @return Job info with current status; status is null if job does not exist
*/
fun startImmediateContactsImport(
contactsAccountName: String?,
contactsAccountType: String?,
vCardFilePath: String,
selectedContacts: IntArray
): LiveData<JobInfo?>
fun scheduleTestJob()
fun cancelTestJob()

View file

@ -66,6 +66,7 @@ internal class BackgroundJobManagerImpl(
const val JOB_CONTENT_OBSERVER = "content_observer"
const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup"
const val JOB_IMMEDIATE_CONTACTS_BACKUP = "immediate_contacts_backup"
const val JOB_IMMEDIATE_CONTACTS_IMPORT = "immediate_contacts_import"
const val JOB_TEST = "test_job"
const val MAX_CONTENT_TRIGGER_DELAY_MS = 1500L
@ -225,6 +226,34 @@ internal class BackgroundJobManagerImpl(
workManager.cancelJob(JOB_PERIODIC_CONTACTS_BACKUP, user)
}
override fun startImmediateContactsImport(
contactsAccountName: String?,
contactsAccountType: String?,
vCardFilePath: String,
selectedContacts: IntArray
): LiveData<JobInfo?> {
val data = Data.Builder()
.putString(ContactsImportWork.ACCOUNT_NAME, contactsAccountName)
.putString(ContactsImportWork.ACCOUNT_TYPE, contactsAccountType)
.putString(ContactsImportWork.VCARD_FILE_PATH, vCardFilePath)
.putIntArray(ContactsImportWork.SELECTED_CONTACTS_INDICES, selectedContacts)
.build()
val constraints = Constraints.Builder()
.setRequiresCharging(false)
.build()
val request = oneTimeRequestBuilder(ContactsImportWork::class, JOB_IMMEDIATE_CONTACTS_IMPORT)
.setInputData(data)
.setConstraints(constraints)
.build()
workManager.enqueueUniqueWork(JOB_IMMEDIATE_CONTACTS_IMPORT, ExistingWorkPolicy.KEEP, request)
return workManager.getJobInfo(request.id)
}
override fun startImmediateContactsBackup(user: User): LiveData<JobInfo?> {
val data = Data.Builder()
.putString(ContactsBackupWork.ACCOUNT, user.accountName)

View file

@ -0,0 +1,128 @@
/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2017 Tobias Kaminsky
* Copyright (C) 2017 Nextcloud GmbH.
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.jobs
import android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.ContactsContract
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.nextcloud.client.logger.Logger
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment.VCardComparator
import ezvcard.Ezvcard
import ezvcard.VCard
import third_parties.ezvcard_android.ContactOperations
import java.io.File
import java.io.IOException
import java.util.ArrayList
import java.util.Collections
import java.util.TreeMap
class ContactsImportWork(
appContext: Context,
params: WorkerParameters,
private val logger: Logger,
private val contentResolver: ContentResolver
) : Worker(appContext, params) {
companion object {
const val TAG = "ContactsImportWork"
const val ACCOUNT_TYPE = "account_type"
const val ACCOUNT_NAME = "account_name"
const val VCARD_FILE_PATH = "vcard_file_path"
const val SELECTED_CONTACTS_INDICES = "selected_contacts_indices"
}
override fun doWork(): Result {
val vCardFilePath = inputData.getString(VCARD_FILE_PATH) ?: ""
val contactsAccountName = inputData.getString(ACCOUNT_NAME)
val contactsAccountType = inputData.getString(ACCOUNT_TYPE)
val selectedContactsIndices = inputData.getIntArray(SELECTED_CONTACTS_INDICES) ?: IntArray(0)
val file = File(vCardFilePath)
val vCards = ArrayList<VCard>()
var cursor: Cursor? = null
try {
val operations = ContactOperations(applicationContext, contactsAccountName, contactsAccountType)
vCards.addAll(Ezvcard.parse(file).all())
Collections.sort(vCards, VCardComparator())
cursor = contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null
)
val ownContactMap = TreeMap<VCard, Long?>(VCardComparator())
if (cursor != null && cursor.count > 0) {
cursor.moveToFirst()
for (i in 0 until cursor.count) {
val vCard = getContactFromCursor(cursor)
if (vCard != null) {
ownContactMap[vCard] = cursor.getLong(cursor.getColumnIndex("NAME_RAW_CONTACT_ID"))
}
cursor.moveToNext()
}
}
for (contactIndex in selectedContactsIndices) {
val vCard = vCards[contactIndex]
if (ContactListFragment.getDisplayName(vCard).isEmpty()) {
if (!ownContactMap.containsKey(vCard)) {
operations.insertContact(vCard)
} else {
operations.updateContact(vCard, ownContactMap[vCard])
}
} else {
operations.insertContact(vCard) // Insert All the contacts without name
}
}
} catch (e: Exception) {
logger.e(TAG, "${e.message}")
} finally {
cursor?.close()
}
return Result.success()
}
private fun getContactFromCursor(cursor: Cursor): VCard? {
val lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY))
val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
var vCard: VCard? = null
try {
contentResolver.openInputStream(uri).use { inputStream ->
val vCardList = ArrayList<VCard>()
vCardList.addAll(Ezvcard.parse(inputStream).all())
if (vCardList.size > 0) {
vCard = vCardList[0]
}
}
} catch (e: IOException) {
logger.d(TAG, "${e.message}")
}
return vCard
}
}

View file

@ -1,132 +0,0 @@
/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2017 Tobias Kaminsky
* Copyright (C) 2017 Nextcloud GmbH.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.owncloud.android.jobs;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import com.evernote.android.job.Job;
import com.evernote.android.job.util.support.PersistableBundleCompat;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.fragment.contactsbackup.ContactListFragment;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.TreeMap;
import androidx.annotation.NonNull;
import ezvcard.Ezvcard;
import ezvcard.VCard;
import third_parties.ezvcard_android.ContactOperations;
/**
* Job to import contacts
*/
public class ContactsImportJob extends Job {
public static final String TAG = "ContactsImportJob";
public static final String ACCOUNT_TYPE = "account_type";
public static final String ACCOUNT_NAME = "account_name";
public static final String VCARD_FILE_PATH = "vcard_file_path";
public static final String CHECKED_ITEMS_ARRAY = "checked_items_array";
@NonNull
@Override
protected Result onRunJob(@NonNull Params params) {
PersistableBundleCompat bundle = params.getExtras();
String vCardFilePath = bundle.getString(VCARD_FILE_PATH, "");
String accountName = bundle.getString(ACCOUNT_NAME, "");
String accountType = bundle.getString(ACCOUNT_TYPE, "");
int[] intArray = bundle.getIntArray(CHECKED_ITEMS_ARRAY);
File file = new File(vCardFilePath);
ArrayList<VCard> vCards = new ArrayList<>();
Cursor cursor = null;
try {
ContactOperations operations = new ContactOperations(getContext(), accountName, accountType);
vCards.addAll(Ezvcard.parse(file).all());
Collections.sort(vCards, new ContactListFragment.VCardComparator());
cursor = getContext().getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null,
null, null, null);
TreeMap<VCard, Long> ownContactMap = new TreeMap<>(new ContactListFragment.VCardComparator());
if (cursor != null && cursor.getCount() > 0) {
cursor.moveToFirst();
for (int i = 0; i < cursor.getCount(); i++) {
VCard vCard = getContactFromCursor(cursor);
if (vCard != null) {
ownContactMap.put(vCard, cursor.getLong(cursor.getColumnIndex("NAME_RAW_CONTACT_ID")));
}
cursor.moveToNext();
}
}
for (int checkedItem : intArray) {
VCard vCard = vCards.get(checkedItem);
if (ContactListFragment.getDisplayName(vCard).length() != 0) {
if (!ownContactMap.containsKey(vCard)) {
operations.insertContact(vCard);
} else {
operations.updateContact(vCard, ownContactMap.get(vCard));
}
} else {
operations.insertContact(vCard); //Insert All the contacts without name
}
}
} catch (Exception e) {
Log_OC.e(TAG, e.getMessage());
} finally {
if (cursor != null) {
cursor.close();
}
}
return Result.SUCCESS;
}
private VCard getContactFromCursor(Cursor cursor) {
String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
VCard vCard = null;
try (InputStream inputStream = getContext().getContentResolver().openInputStream(uri)){
ArrayList<VCard> vCardList = new ArrayList<>();
vCardList.addAll(Ezvcard.parse(inputStream).all());
if (vCardList.size() > 0) {
vCard = vCardList.get(0);
}
} catch (IOException e) {
Log_OC.d(TAG, e.getMessage());
}
return vCard;
}
}

View file

@ -81,8 +81,6 @@ public class NCJobCreator implements JobCreator {
@Override
public Job create(@NonNull String tag) {
switch (tag) {
case ContactsImportJob.TAG:
return new ContactsImportJob();
case AccountRemovalJob.TAG:
return new AccountRemovalJob(uploadsStorageManager,
accountManager,

View file

@ -55,17 +55,15 @@ import android.widget.Toast;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.evernote.android.job.JobRequest;
import com.evernote.android.job.util.support.PersistableBundleCompat;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.jobs.BackgroundJobManager;
import com.nextcloud.client.network.ClientFactory;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.jobs.ContactsImportJob;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.TextDrawable;
import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
@ -149,6 +147,7 @@ public class ContactListFragment extends FileFragment implements Injectable {
private OCFile ocFile;
@Inject UserAccountManager accountManager;
@Inject ClientFactory clientFactory;
@Inject BackgroundJobManager backgroundJobManager;
public static ContactListFragment newInstance(OCFile file, Account account) {
ContactListFragment frag = new ContactListFragment();
@ -360,20 +359,11 @@ public class ContactListFragment extends FileFragment implements Injectable {
}
}
private void importContacts(ContactAccount account) {
PersistableBundleCompat bundle = new PersistableBundleCompat();
bundle.putString(ContactsImportJob.ACCOUNT_NAME, account.name);
bundle.putString(ContactsImportJob.ACCOUNT_TYPE, account.type);
bundle.putString(ContactsImportJob.VCARD_FILE_PATH, getFile().getStoragePath());
bundle.putIntArray(ContactsImportJob.CHECKED_ITEMS_ARRAY, contactListAdapter.getCheckedIntArray());
new JobRequest.Builder(ContactsImportJob.TAG)
.setExtras(bundle)
.startNow()
.setRequiresCharging(false)
.setUpdateCurrent(false)
.build()
.schedule();
private void importContacts(ContactsAccount account) {
backgroundJobManager.startImmediateContactsImport(account.name,
account.type,
getFile().getStoragePath(),
contactListAdapter.getCheckedIntArray());
Snackbar.make(recyclerView, R.string.contacts_preferences_import_scheduled, Snackbar.LENGTH_LONG).show();
@ -391,10 +381,10 @@ public class ContactListFragment extends FileFragment implements Injectable {
}
private void getAccountForImport() {
final ArrayList<ContactAccount> accounts = new ArrayList<>();
final ArrayList<ContactsAccount> contactsAccounts = new ArrayList<>();
// add local one
accounts.add(new ContactAccount("Local contacts", null, null));
contactsAccounts.add(new ContactsAccount("Local contacts", null, null));
Cursor cursor = null;
try {
@ -409,10 +399,10 @@ public class ContactListFragment extends FileFragment implements Injectable {
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME));
String type = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE));
ContactAccount account = new ContactAccount(name, name, type);
ContactsAccount account = new ContactsAccount(name, name, type);
if (!accounts.contains(account)) {
accounts.add(account);
if (!contactsAccounts.contains(account)) {
contactsAccounts.add(account);
}
}
@ -426,16 +416,16 @@ public class ContactListFragment extends FileFragment implements Injectable {
}
}
if (accounts.size() == SINGLE_ACCOUNT) {
importContacts(accounts.get(0));
if (contactsAccounts.size() == SINGLE_ACCOUNT) {
importContacts(contactsAccounts.get(0));
} else {
ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, accounts);
ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, contactsAccounts);
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.contactlist_account_chooser_title)
.setAdapter(adapter, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
importContacts(accounts.get(which));
importContacts(contactsAccounts.get(which));
}
}).show();
}
@ -475,12 +465,12 @@ public class ContactListFragment extends FileFragment implements Injectable {
}
}
private class ContactAccount {
private class ContactsAccount {
private String displayName;
private String name;
private String type;
ContactAccount(String displayName, String name, String type) {
ContactsAccount(String displayName, String name, String type) {
this.displayName = displayName;
this.name = name;
this.type = type;
@ -488,8 +478,8 @@ public class ContactListFragment extends FileFragment implements Injectable {
@Override
public boolean equals(Object obj) {
if (obj instanceof ContactAccount) {
ContactAccount other = (ContactAccount) obj;
if (obj instanceof ContactsAccount) {
ContactsAccount other = (ContactsAccount) obj;
return this.name.equalsIgnoreCase(other.name) && this.type.equalsIgnoreCase(other.type);
} else {
return false;