From ad97abaee4f161f0a4f3f958c21776218ce9b7d2 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 12 Nov 2020 12:43:17 +0100 Subject: [PATCH] Add phone book integration Signed-off-by: tobiasKaminsky --- .idea/misc.xml | 12 +- .idea/modules.xml | 6 +- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 37 ++ .../nextcloud/talk/activities/MainActivity.kt | 103 +++++ .../java/com/nextcloud/talk/api/NcApi.java | 32 +- .../ConversationsListController.java | 61 +-- .../talk/controllers/SettingsController.java | 44 ++ .../talk/jobs/ContactAddressBookWorker.kt | 385 ++++++++++++++++++ .../json/search/ContactsByNumberOCS.java | 40 ++ .../json/search/ContactsByNumberOverall.java | 36 ++ .../com/nextcloud/talk/utils/ApiUtils.java | 13 +- .../talk/utils/AuthenticatorService.java | 104 +++++ .../com/nextcloud/talk/utils/SyncAdapter.java | 43 ++ .../com/nextcloud/talk/utils/SyncService.java | 50 +++ .../talk/utils/database/user/UserUtils.java | 10 +- .../utils/preferences/AppPreferences.java | 21 + .../main/res/layout/controller_settings.xml | 9 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/auth.xml | 26 ++ app/src/main/res/xml/contacts.xml | 27 ++ app/src/main/res/xml/syncadapter.xml | 25 ++ 22 files changed, 1043 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/SyncService.java create mode 100644 app/src/main/res/xml/auth.xml create mode 100644 app/src/main/res/xml/contacts.xml create mode 100644 app/src/main/res/xml/syncadapter.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index b36c7ce25..b3243728f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index dd4cd1d1b..61d732101 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,11 +2,7 @@ - - - - - + diff --git a/app/build.gradle b/app/build.gradle index 0599b80a6..5ff8c340f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,6 +242,8 @@ dependencies { implementation 'com.afollestad.material-dialogs:bottomsheets:3.1.0' implementation 'com.afollestad.material-dialogs:lifecycle:3.1.0' + implementation 'com.google.code.gson:gson:2.8.6' + testImplementation 'junit:junit:4.13' testImplementation 'org.mockito:mockito-core:3.0.0' testImplementation 'org.powermock:powermock-core:2.0.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f1d929427..494b5ab36 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,6 +51,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 75197b985..993174edd 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -25,6 +25,8 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle +import android.os.Handler +import android.provider.ContactsContract import android.text.TextUtils import android.view.ViewGroup import androidx.annotation.RequiresApi @@ -37,17 +39,31 @@ import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.controllers.* import com.nextcloud.talk.controllers.base.providers.ActionBarProvider +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ConductorRemapping +import com.nextcloud.talk.utils.ConductorRemapping.remapChatController import com.nextcloud.talk.utils.SecurityUtils import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.UserUtils +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers import io.requery.Persistable import io.requery.android.sqlcipher.SqlCipherDatabaseSource import io.requery.reactivex.ReactiveEntityStore +import org.parceler.Parcels import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -64,6 +80,8 @@ class MainActivity : BaseActivity(), ActionBarProvider { lateinit var dataStore: ReactiveEntityStore @Inject lateinit var sqlCipherDatabaseSource: SqlCipherDatabaseSource + @Inject + lateinit var ncApi: NcApi private var router: Router? = null @@ -132,8 +150,91 @@ class MainActivity : BaseActivity(), ActionBarProvider { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { checkIfWeAreSecure() } + + handleActionFromContact(intent) } + private fun handleActionFromContact(intent: Intent) { + if (intent.action == Intent.ACTION_VIEW && intent.data != null) { + + val cursor = contentResolver.query(intent.data!!, null, null, null, null) + + var userId = "" + if (cursor != null) { + if (cursor.moveToFirst()) { + // userId @ server + userId = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1)) + } + + cursor.close() + } + + when (intent.type) { + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" -> { + val user = userId.split("@")[0] + val baseUrl = userId.split("@")[1] + if (userUtils.currentUser?.baseUrl?.endsWith(baseUrl) == true) { + startConversation(user) + } else { + Snackbar.make(container, "Account not found", Snackbar.LENGTH_LONG).show() + } + } + } + } + } + + private fun startConversation(userId: String) { + val roomType = "1" + val currentUser = userUtils.currentUser ?: return + + val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.baseUrl, roomType, + userId, null) + ncApi.createRoom(credentials, + retrofitBucket.url, retrofitBucket.queryMap) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(roomOverall: RoomOverall) { + val conversationIntent = Intent(context, MagicCallActivity::class.java) + val bundle = Bundle() + bundle.putParcelable(KEY_USER_ENTITY, currentUser) + bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token) + bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId) + if (currentUser.hasSpreedFeatureCapability("chat-v2")) { + ncApi.getRoom(credentials, + ApiUtils.getRoom(currentUser.baseUrl, + roomOverall.ocs.data.token)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(roomOverall: RoomOverall) { + bundle.putParcelable(KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.ocs.data)) + remapChatController(router!!, currentUser.id, + roomOverall.ocs.data.token, bundle, true) + } + + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) + } else { + conversationIntent.putExtras(bundle) + startActivity(conversationIntent) + Handler().postDelayed({ + if (!isDestroyed) { + router!!.popCurrentController() + } + }, 100) + } + } + + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) + } @RequiresApi(api = Build.VERSION_CODES.M) fun checkIfWeAreSecure() { @@ -154,6 +255,8 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) + handleActionFromContact(intent) + if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) { router!!.pushController(RouterTransaction.with(CallNotificationController(intent.extras)) diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index cb073fef4..f9750f4ee 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -20,9 +20,10 @@ */ package com.nextcloud.talk.api; -import androidx.annotation.Nullable; import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall; import com.nextcloud.talk.models.json.chat.ChatOverall; +import com.nextcloud.talk.models.json.conversations.RoomOverall; +import com.nextcloud.talk.models.json.conversations.RoomsOverall; import com.nextcloud.talk.models.json.generic.GenericOverall; import com.nextcloud.talk.models.json.generic.Status; import com.nextcloud.talk.models.json.mention.MentionOverall; @@ -30,19 +31,32 @@ import com.nextcloud.talk.models.json.notifications.NotificationOverall; import com.nextcloud.talk.models.json.participants.AddParticipantOverall; import com.nextcloud.talk.models.json.participants.ParticipantsOverall; import com.nextcloud.talk.models.json.push.PushRegistrationOverall; -import com.nextcloud.talk.models.json.conversations.RoomOverall; -import com.nextcloud.talk.models.json.conversations.RoomsOverall; +import com.nextcloud.talk.models.json.search.ContactsByNumberOverall; import com.nextcloud.talk.models.json.signaling.SignalingOverall; import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; -import io.reactivex.Observable; -import okhttp3.ResponseBody; -import retrofit2.Response; -import retrofit2.http.*; import java.util.List; import java.util.Map; +import androidx.annotation.Nullable; +import io.reactivex.Observable; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Response; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FieldMap; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; +import retrofit2.http.Url; + public interface NcApi { /* @@ -327,4 +341,8 @@ public interface NcApi { @Url String url, @Field("state") Integer state, @Field("timer") Long timer); + @POST + Observable searchContactsByPhoneNumber(@Header("Authorization") String authorization, + @Url String url, + @Body RequestBody search); } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java index 782371bd9..98bb83d78 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java @@ -29,23 +29,16 @@ import android.os.Bundle; import android.os.Handler; import android.text.InputType; import android.text.TextUtils; -import android.view.*; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.ProgressBar; import android.widget.RelativeLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; -import androidx.core.view.MenuItemCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import autodagger.AutoInjector; -import butterknife.BindView; + import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat; @@ -74,10 +67,11 @@ import com.nextcloud.talk.events.BottomSheetLockEvent; import com.nextcloud.talk.events.EventStatus; import com.nextcloud.talk.events.MoreMenuClickEvent; import com.nextcloud.talk.interfaces.ConversationMenuInterface; +import com.nextcloud.talk.jobs.ContactAddressBookWorker; import com.nextcloud.talk.jobs.DeleteConversationWorker; import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.ConductorRemapping; import com.nextcloud.talk.utils.DisplayUtils; @@ -88,6 +82,32 @@ import com.nextcloud.talk.utils.database.user.UserUtils; import com.nextcloud.talk.utils.preferences.AppPreferences; import com.yarolegovich.lovelydialog.LovelySaveStateHandler; import com.yarolegovich.lovelydialog.LovelyStandardDialog; + +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; +import org.parceler.Parcels; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; +import androidx.core.view.MenuItemCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import autodagger.AutoInjector; +import butterknife.BindView; import eu.davidea.fastscroller.FastScroller; import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager; @@ -95,18 +115,8 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.apache.commons.lang3.builder.CompareToBuilder; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.parceler.Parcels; import retrofit2.HttpException; -import javax.inject.Inject; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - @AutoInjector(NextcloudTalkApplication.class) public class ConversationsListController extends BaseController implements SearchView.OnQueryTextListener, FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, FastScroller @@ -442,6 +452,7 @@ public class ConversationsListController extends BaseController implements Searc emptyLayoutView.setOnClickListener(v -> showNewConversationsScreen()); floatingActionButton.setOnClickListener(v -> { + ContactAddressBookWorker.Companion.run(context); showNewConversationsScreen(); }); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java index fdf801e31..07e57b46d 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java @@ -25,6 +25,7 @@ import android.animation.AnimatorListenerAdapter; import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -38,6 +39,7 @@ import android.view.WindowManager; import android.widget.Checkable; import android.widget.TextView; +import com.bluelinelabs.conductor.Controller; import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler; @@ -45,12 +47,14 @@ import com.bluelinelabs.logansquare.LoganSquare; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.view.SimpleDraweeView; +import com.google.android.material.snackbar.Snackbar; import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.R; import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.controllers.base.BaseController; import com.nextcloud.talk.jobs.AccountRemovalWorker; +import com.nextcloud.talk.jobs.ContactAddressBookWorker; import com.nextcloud.talk.models.RingtoneSettings; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.utils.ApiUtils; @@ -151,6 +155,8 @@ public class SettingsController extends BaseController { MaterialSwitchPreference screenLockSwitchPreference; @BindView(R.id.settings_screen_lock_timeout) MaterialChoicePreference screenLockTimeoutChoicePreference; + @BindView(R.id.settings_phone_book_integration) + MaterialSwitchPreference phoneBookIntegretationPreference; @BindView(R.id.message_text) TextView messageText; @@ -171,6 +177,7 @@ public class SettingsController extends BaseController { private OnPreferenceValueChangedListener screenLockChangeListener; private OnPreferenceValueChangedListener screenLockTimeoutChangeListener; private OnPreferenceValueChangedListener themeChangeListener; + private OnPreferenceValueChangedListener phoneBookIntegrationChangeListener; private Disposable profileQueryDisposable; private Disposable dbQueryDisposable; @@ -206,6 +213,8 @@ public class SettingsController extends BaseController { appPreferences.registerScreenLockListener(screenLockChangeListener = new ScreenLockListener()); appPreferences.registerScreenLockTimeoutListener(screenLockTimeoutChangeListener = new ScreenLockTimeoutListener()); appPreferences.registerThemeChangeListener(themeChangeListener = new ThemeChangeListener()); + appPreferences.registerPhoneBookIntegrationChangeListener( + phoneBookIntegrationChangeListener = new PhoneBookIntegrationChangeListener(this)); List listWithIntFields = new ArrayList<>(); listWithIntFields.add("proxy_port"); @@ -435,6 +444,7 @@ public class SettingsController extends BaseController { } ((Checkable) linkPreviewsSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getAreLinkPreviewsAllowed()); + ((Checkable) phoneBookIntegretationPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.isPhoneBookIntegrationEnabled()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); @@ -645,6 +655,7 @@ public class SettingsController extends BaseController { appPreferences.unregisterScreenLockListener(screenLockChangeListener); appPreferences.unregisterScreenLockTimeoutListener(screenLockTimeoutChangeListener); appPreferences.unregisterThemeChangeListener(themeChangeListener); + appPreferences.unregisterPhoneBookIntegrationChangeListener(phoneBookIntegrationChangeListener); } super.onDestroy(); } @@ -707,6 +718,24 @@ public class SettingsController extends BaseController { return getResources().getString(R.string.nc_settings); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == ContactAddressBookWorker.REQUEST_PERMISSION && + grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + WorkManager + .getInstance() + .enqueue(new OneTimeWorkRequest.Builder(ContactAddressBookWorker.class).build()); + } else { + appPreferences.setPhoneBookIntegration(false); + ((Checkable) phoneBookIntegretationPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.isPhoneBookIntegrationEnabled()); + Snackbar.make(getView(), + context.getResources().getString(R.string.no_phone_book_integration_due_to_permissions), + Snackbar.LENGTH_LONG) + .show(); + } + } + private class ScreenLockTimeoutListener implements OnPreferenceValueChangedListener { @Override @@ -797,4 +826,19 @@ public class SettingsController extends BaseController { NextcloudTalkApplication.Companion.setAppTheme(newValue); } } + + private class PhoneBookIntegrationChangeListener implements OnPreferenceValueChangedListener { + private final Controller controller; + + public PhoneBookIntegrationChangeListener(Controller controller) { + this.controller = controller; + } + + @Override + public void onChanged(Boolean newValue) { + if (newValue) { + ContactAddressBookWorker.Companion.checkPermission(controller, context); + } + } + } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt new file mode 100644 index 000000000..abd5c1e48 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt @@ -0,0 +1,385 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2020 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.jobs + +import android.Manifest +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderOperation +import android.content.Context +import android.content.OperationApplicationException +import android.content.pm.PackageManager +import android.net.Uri +import android.os.RemoteException +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.Phone.NUMBER +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.os.ConfigurationCompat +import androidx.work.* +import autodagger.AutoInjector +import com.bluelinelabs.conductor.Controller +import com.google.gson.Gson +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.models.json.search.ContactsByNumberOverall +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType +import okhttp3.RequestBody +import javax.inject.Inject +import com.nextcloud.talk.R + + +@AutoInjector(NextcloudTalkApplication::class) +class ContactAddressBookWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters) { + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userUtils: UserUtils + + @Inject + lateinit var appPreferences: AppPreferences + + override fun doWork(): Result { + sharedApplication!!.componentApplication.inject(this) + + val currentUser = userUtils.currentUser + + if (currentUser == null) { + Log.e(javaClass.simpleName, "No current user!") + return Result.failure() + } + // Check if run already at the date + val force = inputData.getBoolean(KEY_FORCE, false) + if (!force) { + if (System.currentTimeMillis() - appPreferences.getPhoneBookIntegrationLastRun(0L) < 24 * 60 * 60 * 1000) { + Log.d(TAG, "Already run within last 24h") + return Result.success() + } + } + + AccountManager.get(context).addAccountExplicitly(Account(ACCOUNT_NAME, ACCOUNT_TYPE), "", null) + + // collect all contacts with phone number + val contactsWithNumbers = collectPhoneNumbers() + + val currentLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0].country + + val map = mutableMapOf() + map["location"] = currentLocale + map["search"] = contactsWithNumbers + + val json = Gson().toJson(map) + + ncApi.searchContactsByPhoneNumber( + ApiUtils.getCredentials(currentUser.username, currentUser.token), + ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl), + RequestBody.create(MediaType.parse("application/json"), json)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onComplete() { + } + + override fun onSubscribe(d: Disposable) { + } + + override fun onNext(foundContacts: ContactsByNumberOverall) { + Log.d(javaClass.simpleName, "next") + + // todo update + up(foundContacts) + } + + override fun onError(e: Throwable) { + // TODO error handling + Log.d(javaClass.simpleName, "error") + } + + }) + + // store timestamp + appPreferences.setPhoneBookIntegrationLastRun(System.currentTimeMillis()) + + return Result.success() + } + + private fun collectPhoneNumbers(): MutableMap> { + val result: MutableMap> = mutableMapOf() + + val contactCursor = context.contentResolver.query( + ContactsContract.Contacts.CONTENT_URI, + null, + null, + null, + null + ) + + if (contactCursor != null) { + if (contactCursor.count > 0) { + contactCursor.moveToFirst() + for (i in 0 until contactCursor.count) { + val numbers: MutableList = mutableListOf() + + val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID)) + val lookup = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)) + + val phonesCursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + id, + null, + null) + + if (phonesCursor != null) { + while (phonesCursor.moveToNext()) { + numbers.add(phonesCursor.getString(phonesCursor.getColumnIndex(NUMBER))) + } + + result[lookup] = numbers + + phonesCursor.close() + } + + contactCursor.moveToNext() + } + } + + contactCursor.close() + } + + return result + } + + private fun up(foundContacts: ContactsByNumberOverall) { + val map = foundContacts.ocs.map + + // Delete all old associations (those that are associated on phone, but not in server response) + val rawContactUri = ContactsContract.Data.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, "Nextcloud Talk") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, "com.nextcloud.talk2") + .appendQueryParameter(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") + .build() + + // get all raw contacts + val rawContactsCursor = context.contentResolver.query( + rawContactUri, + null, + null, + null, + null + ) + + if (rawContactsCursor != null) { + if (rawContactsCursor.count > 0) { + while (rawContactsCursor.moveToNext()) { + val id = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.RawContacts._ID)) + val sync1 = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.SYNC1)) + val lookupKey = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)) + Log.d("Contact", "Found associated: $id") + + if (map == null || !map.containsKey(lookupKey)) { + if (sync1 != null) { + deleteAssociation(sync1) + } + } + } + } + + rawContactsCursor.close() + } + + // update / change found + if (map != null && map.isNotEmpty()) { + for (contact in foundContacts.ocs.map) { + val lookupKey = contact.key + val cloudId = contact.value + + update(lookupKey, cloudId) + } + } + } + + private fun update(uniqueId: String, cloudId: String) { + val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, uniqueId) + val lookupContactUri = ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri) + val contactCursor = context.contentResolver.query( + lookupContactUri, + null, + null, + null, + null) + + if (contactCursor != null) { + if (contactCursor.count > 0) { + contactCursor.moveToFirst() + + val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID)) + + val phonesCursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + ContactsContract.Data.CONTACT_ID + " = " + id, + null, + null) + + val numbers = mutableListOf() + if (phonesCursor != null) { + while (phonesCursor.moveToNext()) { + numbers.add(phonesCursor.getString(phonesCursor.getColumnIndex(NUMBER))) + } + + phonesCursor.close() + } + + var displayName: String? = null + + val whereName = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?" + val whereNameParams = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) + val nameCursor = context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + null, + whereName, + whereNameParams, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) + if (nameCursor != null) { + while (nameCursor.moveToNext()) { + displayName = nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)) + } + nameCursor.close() + } + + if (displayName == null) { + return + } + + // update entries + val ops = ArrayList() + val rawContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon() + .build() + val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon() + .build() + + ops.add(ContentProviderOperation + .newInsert(rawContactsUri) + .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, ACCOUNT_NAME) + .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, ACCOUNT_TYPE) + .withValue(ContactsContract.RawContacts.AGGREGATION_MODE, + ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT) + .withValue(ContactsContract.RawContacts.SYNC2, cloudId) + .build()) + ops.add(ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(NUMBER, numbers[0]) + .build()) + ops.add(ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .build()) + ops.add(ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat") + .withValue(ContactsContract.Data.DATA1, cloudId) + .withValue(ContactsContract.Data.DATA2, "Chat via " + context.resources.getString(R.string.nc_app_name)) + .build()) + + try { + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) + } catch (e: OperationApplicationException) { + e.printStackTrace() + } catch (e: RemoteException) { + e.printStackTrace() + } + } + + contactCursor.close() + } + } + + private fun deleteAssociation(id: String) { + Log.d("Contact", "Delete associated: $id") + + val rawContactUri = ContactsContract.RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, "Nextcloud Talk") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, "com.nextcloud.talk2") + .build() + + + val count = context.contentResolver.delete(rawContactUri, ContactsContract.RawContacts.SYNC2 + " LIKE \"" + id + "\"", null) + Log.d("Contact", "deleted $count for id $id") + } + + companion object { + const val TAG = "ContactAddressBook" + const val REQUEST_PERMISSION = 231 + const val KEY_FORCE = "KEY_FORCE" + const val ACCOUNT_TYPE = "com.nextcloud.talk2" + const val ACCOUNT_NAME = "Nextcloud Talk" + + fun run(context: Context) { + if (ContextCompat.checkSelfPermission(context, + Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, + Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { + WorkManager + .getInstance() + .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(KEY_FORCE, false).build()) + .build()) + } + } + + fun checkPermission(controller: Controller, context: Context) { + if (ContextCompat.checkSelfPermission(context, + Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, + Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + controller.requestPermissions(arrayOf(Manifest.permission.WRITE_CONTACTS, + Manifest.permission.READ_CONTACTS), REQUEST_PERMISSION) + } else { + WorkManager + .getInstance() + .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(KEY_FORCE, true).build()) + .build()) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.java new file mode 100644 index 000000000..e0f788555 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.java @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2020 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.models.json.search; + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; +import com.nextcloud.talk.models.json.generic.GenericOCS; + +import org.parceler.Parcel; + +import java.util.HashMap; +import java.util.Map; + +import lombok.Data; + +@Data +@Parcel +@JsonObject +public class ContactsByNumberOCS extends GenericOCS { + @JsonField(name = "data") + public Map map = new HashMap(); +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.java new file mode 100644 index 000000000..8ec10ebe1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.java @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2020 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.models.json.search; + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; + +import org.parceler.Parcel; + +import lombok.Data; + +@Data +@Parcel +@JsonObject +public class ContactsByNumberOverall { + @JsonField(name = "ocs") + public ContactsByNumberOCS ocs; +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 17ffc771b..4f0447727 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -21,17 +21,20 @@ package com.nextcloud.talk.utils; import android.net.Uri; import android.text.TextUtils; -import androidx.annotation.DimenRes; + import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.R; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.models.RetrofitBucket; -import okhttp3.Credentials; -import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; +import javax.annotation.Nullable; + +import androidx.annotation.DimenRes; +import okhttp3.Credentials; + public class ApiUtils { private static String ocsApiVersion = "/ocs/v2.php"; private static String spreedApiVersion = "/apps/spreed/api/v1"; @@ -276,4 +279,8 @@ public class ApiUtils { public static String getUrlForReadOnlyState(String baseUrl, String roomToken) { return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + roomToken + "/read-only"; } + + public static String getUrlForSearchByNumber(String baseUrl) { + return baseUrl + ocsApiVersion + "/cloud/users/search/by-phone"; + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java b/app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java new file mode 100644 index 000000000..aeb8872ec --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java @@ -0,0 +1,104 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2020 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public class AuthenticatorService extends Service { + + private static class Authenticator extends AbstractAccountAuthenticator { + public Authenticator(Context context) { + super(context); + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) { + return null; + } + + @Override + public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, Account account) throws NetworkErrorException { + return super.getAccountRemovalAllowed(response, account); + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) + throws NetworkErrorException { + return null; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, + Account account, String[] features) { + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) { + return null; + } + + } + + private static Authenticator authenticator = null; + + protected Authenticator getAuthenticator() { + if (authenticator == null) { + authenticator = new Authenticator(this); + } + return authenticator; + } + + + @Override + public IBinder onBind(Intent intent) { + if (intent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT)) { + return getAuthenticator().getIBinder(); + } else { + return null; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java b/app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java new file mode 100644 index 000000000..397b49d34 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java @@ -0,0 +1,43 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2020 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + +import android.accounts.Account; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.SyncResult; +import android.os.Bundle; +import android.util.Log; + +class SyncAdapter extends AbstractThreadedSyncAdapter { + + public SyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + Log.i("SyncAdapter", "Sync adapter created"); + } + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, + ContentProviderClient provider, SyncResult syncResult) { + Log.i("SyncAdapter", "Sync adapter called"); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/SyncService.java b/app/src/main/java/com/nextcloud/talk/utils/SyncService.java new file mode 100644 index 000000000..87be50d32 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/SyncService.java @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk application + * + * @author Tobias Kaminsky + * Copyright (C) 2020 Tobias Kaminsky + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +public class SyncService extends Service { + + private static final Object sSyncAdapterLock = new Object(); + + private static SyncAdapter sSyncAdapter = null; + + @Override + public void onCreate() { + Log.i("SyncService", "Sync service created"); + synchronized (sSyncAdapterLock) { + if (sSyncAdapter == null) { + sSyncAdapter = new SyncAdapter(getApplicationContext(), true); + } + } + } + + @Override + public IBinder onBind(Intent intent) { + Log.i("SyncService", "Sync service binded"); + return sSyncAdapter.getSyncAdapterBinder(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java index bf6b174ad..d0a7aeffb 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java @@ -21,9 +21,13 @@ package com.nextcloud.talk.utils.database.user; import android.text.TextUtils; -import androidx.annotation.Nullable; + import com.nextcloud.talk.models.database.User; import com.nextcloud.talk.models.database.UserEntity; + +import java.util.List; + +import androidx.annotation.Nullable; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -32,8 +36,6 @@ import io.requery.Persistable; import io.requery.query.Result; import io.requery.reactivex.ReactiveEntityStore; -import java.util.List; - public class UserUtils { private ReactiveEntityStore dataStore; @@ -76,7 +78,7 @@ public class UserUtils { return null; } - public UserEntity getCurrentUser() { + public @Nullable UserEntity getCurrentUser() { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.CURRENT.eq(true) .and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(true))) .limit(1).get(); diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index 455da289b..f6e4679eb 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -238,6 +238,13 @@ public interface AppPreferences { @KeyByString("link_previews") @DefaultValue(R.bool.value_true) boolean getAreLinkPreviewsAllowed(); + + @KeyByString("phone_book_integration") + @DefaultValue(R.bool.value_false) + boolean isPhoneBookIntegrationEnabled(); + + @KeyByString("phone_book_integration") + void setPhoneBookIntegration(boolean value); @KeyByString("link_previews") void setLinkPreviewsAllowed(boolean value); @@ -280,6 +287,20 @@ public interface AppPreferences { @KeyByResource(R.string.nc_settings_theme_key) @UnregisterChangeListenerMethod void unregisterThemeChangeListener(OnPreferenceValueChangedListener listener); + + @KeyByResource(R.string.nc_settings_phone_book_integration_key) + @RegisterChangeListenerMethod + void registerPhoneBookIntegrationChangeListener(OnPreferenceValueChangedListener listener); + + @KeyByResource(R.string.nc_settings_phone_book_integration_key) + @UnregisterChangeListenerMethod + void unregisterPhoneBookIntegrationChangeListener(OnPreferenceValueChangedListener listener); + + @KeyByString("phone_book_integration_last_run") + void setPhoneBookIntegrationLastRun(long currentTimeMillis); + + @KeyByString("phone_book_integration_last_run") + long getPhoneBookIntegrationLastRun(Long defaultValue); @ClearMethod void clear(); diff --git a/app/src/main/res/layout/controller_settings.xml b/app/src/main/res/layout/controller_settings.xml index 5fe68dde5..a7a09c086 100644 --- a/app/src/main/res/layout/controller_settings.xml +++ b/app/src/main/res/layout/controller_settings.xml @@ -217,6 +217,15 @@ apc:mp_key="@string/nc_settings_link_previews_key" apc:mp_summary="@string/nc_settings_link_previews_desc" apc:mp_title="@string/nc_settings_link_previews_title" /> + + M3.27,4.27L19.74,20.74 + phone_book_integration + Match contacts based on phone number to integrate Talk shortcut in phone book + Phone book integration + No phone book integration due to missing permissions diff --git a/app/src/main/res/xml/auth.xml b/app/src/main/res/xml/auth.xml new file mode 100644 index 000000000..7d60d273c --- /dev/null +++ b/app/src/main/res/xml/auth.xml @@ -0,0 +1,26 @@ + + + + diff --git a/app/src/main/res/xml/contacts.xml b/app/src/main/res/xml/contacts.xml new file mode 100644 index 000000000..830443fc1 --- /dev/null +++ b/app/src/main/res/xml/contacts.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/xml/syncadapter.xml b/app/src/main/res/xml/syncadapter.xml new file mode 100644 index 000000000..940c3419c --- /dev/null +++ b/app/src/main/res/xml/syncadapter.xml @@ -0,0 +1,25 @@ + + + +