Add phone book integration

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2020-11-12 12:43:17 +01:00
parent 8d53e71547
commit ad97abaee4
No known key found for this signature in database
GPG key ID: 0E00D4D47D0C5AF7
22 changed files with 1043 additions and 47 deletions

View file

@ -5,7 +5,7 @@
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" /> <option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myNullables"> <option name="myNullables">
<value> <value>
<list size="12"> <list size="15">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" /> <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="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" /> <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="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="10" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.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> </list>
</value> </value>
</option> </option>
<option name="myNotNulls"> <option name="myNotNulls">
<value> <value>
<list size="11"> <list size="14">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" /> <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="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.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="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="9" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.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> </list>
</value> </value>
</option> </option>
</component> </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" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

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

View file

@ -242,6 +242,8 @@ dependencies {
implementation 'com.afollestad.material-dialogs:bottomsheets:3.1.0' implementation 'com.afollestad.material-dialogs:bottomsheets:3.1.0'
implementation 'com.afollestad.material-dialogs:lifecycle: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 'junit:junit:4.13'
testImplementation 'org.mockito:mockito-core:3.0.0' testImplementation 'org.mockito:mockito-core:3.0.0'
testImplementation 'org.powermock:powermock-core:2.0.2' testImplementation 'org.powermock:powermock-core:2.0.2'

View file

@ -51,6 +51,13 @@
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <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 <uses-permission
android:name="android.permission.USE_CREDENTIALS" android:name="android.permission.USE_CREDENTIALS"
@ -87,6 +94,12 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </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>
<activity <activity
@ -100,6 +113,30 @@
</intent-filter> </intent-filter>
</receiver> </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 <service
android:name="com.novoda.merlin.MerlinService" android:name="com.novoda.merlin.MerlinService"
android:exported="false" /> android:exported="false" />

View file

@ -25,6 +25,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.provider.ContactsContract
import android.text.TextUtils import android.text.TextUtils
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -37,17 +39,31 @@ import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.controllers.* import com.nextcloud.talk.controllers.*
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider 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
import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
import com.nextcloud.talk.utils.SecurityUtils import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.bundle.BundleKeys 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 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.Persistable
import io.requery.android.sqlcipher.SqlCipherDatabaseSource import io.requery.android.sqlcipher.SqlCipherDatabaseSource
import io.requery.reactivex.ReactiveEntityStore import io.requery.reactivex.ReactiveEntityStore
import org.parceler.Parcels
import javax.inject.Inject import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -64,6 +80,8 @@ class MainActivity : BaseActivity(), ActionBarProvider {
lateinit var dataStore: ReactiveEntityStore<Persistable> lateinit var dataStore: ReactiveEntityStore<Persistable>
@Inject @Inject
lateinit var sqlCipherDatabaseSource: SqlCipherDatabaseSource lateinit var sqlCipherDatabaseSource: SqlCipherDatabaseSource
@Inject
lateinit var ncApi: NcApi
private var router: Router? = null private var router: Router? = null
@ -132,8 +150,91 @@ class MainActivity : BaseActivity(), ActionBarProvider {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkIfWeAreSecure() 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) @RequiresApi(api = Build.VERSION_CODES.M)
fun checkIfWeAreSecure() { fun checkIfWeAreSecure() {
@ -154,6 +255,8 @@ class MainActivity : BaseActivity(), ActionBarProvider {
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
handleActionFromContact(intent)
if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) { if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) {
router!!.pushController(RouterTransaction.with(CallNotificationController(intent.extras)) router!!.pushController(RouterTransaction.with(CallNotificationController(intent.extras))

View file

@ -20,9 +20,10 @@
*/ */
package com.nextcloud.talk.api; package com.nextcloud.talk.api;
import androidx.annotation.Nullable;
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall; import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
import com.nextcloud.talk.models.json.chat.ChatOverall; 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.GenericOverall;
import com.nextcloud.talk.models.json.generic.Status; import com.nextcloud.talk.models.json.generic.Status;
import com.nextcloud.talk.models.json.mention.MentionOverall; 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.AddParticipantOverall;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall; import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.models.json.push.PushRegistrationOverall; import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
import com.nextcloud.talk.models.json.conversations.RoomOverall; import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
import com.nextcloud.talk.models.json.conversations.RoomsOverall;
import com.nextcloud.talk.models.json.signaling.SignalingOverall; import com.nextcloud.talk.models.json.signaling.SignalingOverall;
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall; import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; 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.List;
import java.util.Map; 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 { public interface NcApi {
/* /*
@ -327,4 +341,8 @@ public interface NcApi {
@Url String url, @Field("state") Integer state, @Url String url, @Field("state") Integer state,
@Field("timer") Long timer); @Field("timer") Long timer);
@POST
Observable<ContactsByNumberOverall> searchContactsByPhoneNumber(@Header("Authorization") String authorization,
@Url String url,
@Body RequestBody search);
} }

View file

@ -29,23 +29,16 @@ import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils; 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.view.inputmethod.EditorInfo;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.RelativeLayout; 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.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat; 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.EventStatus;
import com.nextcloud.talk.events.MoreMenuClickEvent; import com.nextcloud.talk.events.MoreMenuClickEvent;
import com.nextcloud.talk.interfaces.ConversationMenuInterface; import com.nextcloud.talk.interfaces.ConversationMenuInterface;
import com.nextcloud.talk.jobs.ContactAddressBookWorker;
import com.nextcloud.talk.jobs.DeleteConversationWorker; import com.nextcloud.talk.jobs.DeleteConversationWorker;
import com.nextcloud.talk.models.database.UserEntity; 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.conversations.Conversation;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.ConductorRemapping; import com.nextcloud.talk.utils.ConductorRemapping;
import com.nextcloud.talk.utils.DisplayUtils; 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.nextcloud.talk.utils.preferences.AppPreferences;
import com.yarolegovich.lovelydialog.LovelySaveStateHandler; import com.yarolegovich.lovelydialog.LovelySaveStateHandler;
import com.yarolegovich.lovelydialog.LovelyStandardDialog; 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.fastscroller.FastScroller;
import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager; 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.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; 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 retrofit2.HttpException;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@AutoInjector(NextcloudTalkApplication.class) @AutoInjector(NextcloudTalkApplication.class)
public class ConversationsListController extends BaseController implements SearchView.OnQueryTextListener, public class ConversationsListController extends BaseController implements SearchView.OnQueryTextListener,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, FastScroller FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, FastScroller
@ -442,6 +452,7 @@ public class ConversationsListController extends BaseController implements Searc
emptyLayoutView.setOnClickListener(v -> showNewConversationsScreen()); emptyLayoutView.setOnClickListener(v -> showNewConversationsScreen());
floatingActionButton.setOnClickListener(v -> { floatingActionButton.setOnClickListener(v -> {
ContactAddressBookWorker.Companion.run(context);
showNewConversationsScreen(); showNewConversationsScreen();
}); });

View file

@ -25,6 +25,7 @@ import android.animation.AnimatorListenerAdapter;
import android.app.KeyguardManager; import android.app.KeyguardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -38,6 +39,7 @@ import android.view.WindowManager;
import android.widget.Checkable; import android.widget.Checkable;
import android.widget.TextView; import android.widget.TextView;
import com.bluelinelabs.conductor.Controller;
import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler; 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.backends.pipeline.Fresco;
import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.BuildConfig;
import com.nextcloud.talk.R; import com.nextcloud.talk.R;
import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.controllers.base.BaseController; import com.nextcloud.talk.controllers.base.BaseController;
import com.nextcloud.talk.jobs.AccountRemovalWorker; import com.nextcloud.talk.jobs.AccountRemovalWorker;
import com.nextcloud.talk.jobs.ContactAddressBookWorker;
import com.nextcloud.talk.models.RingtoneSettings; import com.nextcloud.talk.models.RingtoneSettings;
import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.ApiUtils;
@ -151,6 +155,8 @@ public class SettingsController extends BaseController {
MaterialSwitchPreference screenLockSwitchPreference; MaterialSwitchPreference screenLockSwitchPreference;
@BindView(R.id.settings_screen_lock_timeout) @BindView(R.id.settings_screen_lock_timeout)
MaterialChoicePreference screenLockTimeoutChoicePreference; MaterialChoicePreference screenLockTimeoutChoicePreference;
@BindView(R.id.settings_phone_book_integration)
MaterialSwitchPreference phoneBookIntegretationPreference;
@BindView(R.id.message_text) @BindView(R.id.message_text)
TextView messageText; TextView messageText;
@ -171,6 +177,7 @@ public class SettingsController extends BaseController {
private OnPreferenceValueChangedListener<Boolean> screenLockChangeListener; private OnPreferenceValueChangedListener<Boolean> screenLockChangeListener;
private OnPreferenceValueChangedListener<String> screenLockTimeoutChangeListener; private OnPreferenceValueChangedListener<String> screenLockTimeoutChangeListener;
private OnPreferenceValueChangedListener<String> themeChangeListener; private OnPreferenceValueChangedListener<String> themeChangeListener;
private OnPreferenceValueChangedListener<Boolean> phoneBookIntegrationChangeListener;
private Disposable profileQueryDisposable; private Disposable profileQueryDisposable;
private Disposable dbQueryDisposable; private Disposable dbQueryDisposable;
@ -206,6 +213,8 @@ public class SettingsController extends BaseController {
appPreferences.registerScreenLockListener(screenLockChangeListener = new ScreenLockListener()); appPreferences.registerScreenLockListener(screenLockChangeListener = new ScreenLockListener());
appPreferences.registerScreenLockTimeoutListener(screenLockTimeoutChangeListener = new ScreenLockTimeoutListener()); appPreferences.registerScreenLockTimeoutListener(screenLockTimeoutChangeListener = new ScreenLockTimeoutListener());
appPreferences.registerThemeChangeListener(themeChangeListener = new ThemeChangeListener()); appPreferences.registerThemeChangeListener(themeChangeListener = new ThemeChangeListener());
appPreferences.registerPhoneBookIntegrationChangeListener(
phoneBookIntegrationChangeListener = new PhoneBookIntegrationChangeListener(this));
List<String> listWithIntFields = new ArrayList<>(); List<String> listWithIntFields = new ArrayList<>();
listWithIntFields.add("proxy_port"); listWithIntFields.add("proxy_port");
@ -435,6 +444,7 @@ public class SettingsController extends BaseController {
} }
((Checkable) linkPreviewsSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getAreLinkPreviewsAllowed()); ((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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
@ -645,6 +655,7 @@ public class SettingsController extends BaseController {
appPreferences.unregisterScreenLockListener(screenLockChangeListener); appPreferences.unregisterScreenLockListener(screenLockChangeListener);
appPreferences.unregisterScreenLockTimeoutListener(screenLockTimeoutChangeListener); appPreferences.unregisterScreenLockTimeoutListener(screenLockTimeoutChangeListener);
appPreferences.unregisterThemeChangeListener(themeChangeListener); appPreferences.unregisterThemeChangeListener(themeChangeListener);
appPreferences.unregisterPhoneBookIntegrationChangeListener(phoneBookIntegrationChangeListener);
} }
super.onDestroy(); super.onDestroy();
} }
@ -707,6 +718,24 @@ public class SettingsController extends BaseController {
return getResources().getString(R.string.nc_settings); 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> { private class ScreenLockTimeoutListener implements OnPreferenceValueChangedListener<String> {
@Override @Override
@ -797,4 +826,19 @@ public class SettingsController extends BaseController {
NextcloudTalkApplication.Companion.setAppTheme(newValue); 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);
}
}
}
} }

View file

@ -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())
}
}
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -21,17 +21,20 @@ package com.nextcloud.talk.utils;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.DimenRes;
import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.BuildConfig;
import com.nextcloud.talk.R; import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.models.RetrofitBucket; import com.nextcloud.talk.models.RetrofitBucket;
import okhttp3.Credentials;
import javax.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable;
import androidx.annotation.DimenRes;
import okhttp3.Credentials;
public class ApiUtils { public class ApiUtils {
private static String ocsApiVersion = "/ocs/v2.php"; private static String ocsApiVersion = "/ocs/v2.php";
private static String spreedApiVersion = "/apps/spreed/api/v1"; private static String spreedApiVersion = "/apps/spreed/api/v1";
@ -276,4 +279,8 @@ public class ApiUtils {
public static String getUrlForReadOnlyState(String baseUrl, String roomToken) { public static String getUrlForReadOnlyState(String baseUrl, String roomToken) {
return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + roomToken + "/read-only"; return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + roomToken + "/read-only";
} }
public static String getUrlForSearchByNumber(String baseUrl) {
return baseUrl + ocsApiVersion + "/cloud/users/search/by-phone";
}
} }

View file

@ -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;
}
}
}

View file

@ -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");
}
}

View file

@ -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();
}
}

View file

@ -21,9 +21,13 @@
package com.nextcloud.talk.utils.database.user; package com.nextcloud.talk.utils.database.user;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.nextcloud.talk.models.database.User; import com.nextcloud.talk.models.database.User;
import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.database.UserEntity;
import java.util.List;
import androidx.annotation.Nullable;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
@ -32,8 +36,6 @@ import io.requery.Persistable;
import io.requery.query.Result; import io.requery.query.Result;
import io.requery.reactivex.ReactiveEntityStore; import io.requery.reactivex.ReactiveEntityStore;
import java.util.List;
public class UserUtils { public class UserUtils {
private ReactiveEntityStore<Persistable> dataStore; private ReactiveEntityStore<Persistable> dataStore;
@ -76,7 +78,7 @@ public class UserUtils {
return null; return null;
} }
public UserEntity getCurrentUser() { public @Nullable UserEntity getCurrentUser() {
Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.CURRENT.eq(true) Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.CURRENT.eq(true)
.and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(true))) .and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(true)))
.limit(1).get(); .limit(1).get();

View file

@ -239,6 +239,13 @@ public interface AppPreferences {
@DefaultValue(R.bool.value_true) @DefaultValue(R.bool.value_true)
boolean getAreLinkPreviewsAllowed(); boolean getAreLinkPreviewsAllowed();
@KeyByString("phone_book_integration")
@DefaultValue(R.bool.value_false)
boolean isPhoneBookIntegrationEnabled();
@KeyByString("phone_book_integration")
void setPhoneBookIntegration(boolean value);
@KeyByString("link_previews") @KeyByString("link_previews")
void setLinkPreviewsAllowed(boolean value); void setLinkPreviewsAllowed(boolean value);
@ -281,6 +288,20 @@ public interface AppPreferences {
@UnregisterChangeListenerMethod @UnregisterChangeListenerMethod
void unregisterThemeChangeListener(OnPreferenceValueChangedListener<String> listener); 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 @ClearMethod
void clear(); void clear();
} }

View file

@ -217,6 +217,15 @@
apc:mp_key="@string/nc_settings_link_previews_key" apc:mp_key="@string/nc_settings_link_previews_key"
apc:mp_summary="@string/nc_settings_link_previews_desc" apc:mp_summary="@string/nc_settings_link_previews_desc"
apc:mp_title="@string/nc_settings_link_previews_title" /> 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>
<com.yarolegovich.mp.MaterialPreferenceCategory <com.yarolegovich.mp.MaterialPreferenceCategory

View file

@ -322,4 +322,8 @@
<string name="path_password_strike_through" translatable="false" <string name="path_password_strike_through" translatable="false"
tools:override="true">M3.27,4.27L19.74,20.74</string> 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> </resources>

View file

@ -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"/>

View file

@ -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>

View file

@ -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" />