瀏覽代碼

Add phone book integration

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 4 年之前
父節點
當前提交
ad97abaee4

+ 9 - 3
.idea/misc.xml

@@ -5,7 +5,7 @@
     <option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
     <option name="myNullables">
       <value>
-        <list size="12">
+        <list size="15">
           <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
           <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
           <item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
@@ -18,12 +18,15 @@
           <item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
           <item index="10" class="java.lang.String" itemvalue="android.annotation.Nullable" />
           <item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
+          <item index="12" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
+          <item index="13" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
+          <item index="14" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
         </list>
       </value>
     </option>
     <option name="myNotNulls">
       <value>
-        <list size="11">
+        <list size="14">
           <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
           <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
           <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
@@ -35,11 +38,14 @@
           <item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
           <item index="9" class="java.lang.String" itemvalue="android.annotation.NonNull" />
           <item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
+          <item index="11" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
+          <item index="12" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
+          <item index="13" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
         </list>
       </value>
     </option>
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
   <component name="ProjectType">

+ 1 - 5
.idea/modules.xml

@@ -2,11 +2,7 @@
 <project version="4">
   <component name="ProjectModuleManager">
     <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/Projects-talk-android.iml" filepath="$PROJECT_DIR$/.idea/modules/Projects-talk-android.iml" group="talk-android" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Projects-talk-android-app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Projects-talk-android-app.iml" group="talk-android/app" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/app/app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/app.iml" group="talk-android/app" />
-      <module fileurl="file://$PROJECT_DIR$/talk-android.iml" filepath="$PROJECT_DIR$/talk-android.iml" group="talk-android" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/app/talk-android-app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/talk-android-app.iml" group="talk-android/app" />
+      <module fileurl="file://$PROJECT_DIR$/talk-android.iml" filepath="$PROJECT_DIR$/talk-android.iml" />
       <module fileurl="file://$PROJECT_DIR$/.idea/modules/app/talk-android.app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/talk-android.app.iml" />
     </modules>
   </component>

+ 2 - 0
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'

+ 37 - 0
app/src/main/AndroidManifest.xml

@@ -51,6 +51,13 @@
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
 
     <uses-permission
         android:name="android.permission.USE_CREDENTIALS"
@@ -87,6 +94,12 @@
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" />
+            </intent-filter>
         </activity>
 
         <activity
@@ -100,6 +113,30 @@
             </intent-filter>
         </receiver>
 
+        <service
+            android:name=".utils.SyncService"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.content.SyncAdapter"
+                android:resource="@xml/syncadapter" />
+            <meta-data
+                android:name="android.provider.CONTACTS_STRUCTURE"
+                android:resource="@xml/contacts" />
+        </service>
+
+        <service android:name=".utils.AuthenticatorService">
+            <intent-filter>
+                <action android:name="android.accounts.AccountAuthenticator" />
+            </intent-filter>
+            <meta-data
+                android:name="android.accounts.AccountAuthenticator"
+                android:resource="@xml/auth" />
+        </service>
+
         <service
             android:name="com.novoda.merlin.MerlinService"
             android:exported="false" />

+ 103 - 0
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<Persistable>
     @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<RoomOverall> {
+                    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<RoomOverall> {
+                                        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))

+ 25 - 7
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<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
+                                                                    @Url String url,
+                                                                    @Body RequestBody search);
 }

+ 35 - 24
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,25 +82,41 @@ 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 eu.davidea.fastscroller.FastScroller;
-import eu.davidea.flexibleadapter.FlexibleAdapter;
-import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
-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;
 
+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;
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import retrofit2.HttpException;
+
 @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();
         });
 

+ 44 - 0
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<Boolean> screenLockChangeListener;
     private OnPreferenceValueChangedListener<String> screenLockTimeoutChangeListener;
     private OnPreferenceValueChangedListener<String> themeChangeListener;
+    private OnPreferenceValueChangedListener<Boolean> 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<String> 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<String> {
 
         @Override
@@ -797,4 +826,19 @@ public class SettingsController extends BaseController {
             NextcloudTalkApplication.Companion.setAppTheme(newValue);
         }
     }
+    
+    private class PhoneBookIntegrationChangeListener implements OnPreferenceValueChangedListener<Boolean> {
+        private final Controller controller;
+        
+        public PhoneBookIntegrationChangeListener(Controller controller) {
+            this.controller = controller;
+        }
+        
+        @Override
+        public void onChanged(Boolean newValue) {
+            if (newValue) {
+                ContactAddressBookWorker.Companion.checkPermission(controller, context);
+            }
+        }
+    }
 }

+ 385 - 0
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 <tobias.kaminsky@nextcloud.com>
+ *  
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String, Any>()
+        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<ContactsByNumberOverall> {
+                    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<String, List<String>> {
+        val result: MutableMap<String, List<String>> = 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<String> = 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<String>()
+                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<ContentProviderOperation>()
+                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())
+            }
+        }
+    }
+}

+ 40 - 0
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 <tobias.kaminsky@nextcloud.com>
+ *  
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String, String> map = new HashMap();
+}

+ 36 - 0
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 <tobias.kaminsky@nextcloud.com>
+ *  
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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;
+}

+ 10 - 3
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";
+    }
 }

+ 104 - 0
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 <tobias.kaminsky@nextcloud.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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;
+        }
+    }
+}

+ 43 - 0
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 <tobias.kaminsky@nextcloud.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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");
+    }
+}

+ 50 - 0
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 <tobias.kaminsky@nextcloud.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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();
+    }
+}

+ 6 - 4
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<Persistable> 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();

+ 21 - 0
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<String> listener);
+    
+    @KeyByResource(R.string.nc_settings_phone_book_integration_key)
+    @RegisterChangeListenerMethod
+    void registerPhoneBookIntegrationChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
+
+    @KeyByResource(R.string.nc_settings_phone_book_integration_key)
+    @UnregisterChangeListenerMethod
+    void unregisterPhoneBookIntegrationChangeListener(OnPreferenceValueChangedListener<Boolean> listener);
+
+    @KeyByString("phone_book_integration_last_run")
+    void setPhoneBookIntegrationLastRun(long currentTimeMillis);
+
+    @KeyByString("phone_book_integration_last_run")
+    long getPhoneBookIntegrationLastRun(Long defaultValue);
 
     @ClearMethod
     void clear();

+ 9 - 0
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" />
+        
+        <com.yarolegovich.mp.MaterialSwitchPreference
+            android:id="@+id/settings_phone_book_integration"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            apc:mp_default_value="@bool/value_false"
+            apc:mp_key="@string/nc_settings_phone_book_integration_key"
+            apc:mp_summary="@string/nc_settings_phone_book_integration_desc"
+            apc:mp_title="@string/nc_settings_phone_book_integration_title" />
     </com.yarolegovich.mp.MaterialPreferenceCategory>
 
     <com.yarolegovich.mp.MaterialPreferenceCategory

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -322,4 +322,8 @@
 
     <string name="path_password_strike_through" translatable="false"
         tools:override="true">M3.27,4.27L19.74,20.74</string>
+    <string name="nc_settings_phone_book_integration_key" translatable="false">phone_book_integration</string>
+    <string name="nc_settings_phone_book_integration_desc">Match contacts based on phone number to integrate Talk shortcut in phone book</string>
+    <string name="nc_settings_phone_book_integration_title">Phone book integration</string>
+    <string name="no_phone_book_integration_due_to_permissions">No phone book integration due to missing permissions</string>
 </resources>

+ 26 - 0
app/src/main/res/xml/auth.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~  
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2020 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+  ~  
+  ~ 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 <http://www.gnu.org/licenses/>.
+  -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="com.nextcloud.talk2"
+    android:icon="@mipmap/ic_launcher"
+    android:smallIcon="@mipmap/ic_launcher"
+    android:label="Nextcloud Talk"/>

+ 27 - 0
app/src/main/res/xml/contacts.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~  
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2020 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+  ~  
+  ~ 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 <http://www.gnu.org/licenses/>.
+  -->
+
+<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
+    <ContactsDataKind
+        android:mimeType="vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat"
+        android:icon="@mipmap/ic_launcher"
+        android:detailColumn="data2"/>
+</ContactsAccountType>

+ 25 - 0
app/src/main/res/xml/syncadapter.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~  
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2020 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+  ~  
+  ~ 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 <http://www.gnu.org/licenses/>.
+  -->
+
+<sync-adapter
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="com.nextcloud.talk2"
+    android:contentAuthority="com.android.contacts" />