From 0a6f54cedf702b4f229e24775a4c7058f1f04475 Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Sat, 28 Oct 2017 20:20:52 +0200 Subject: [PATCH] Significant work on push notifications Signed-off-by: Mario Danic --- app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 14 + .../java/com/nextcloud/talk/api/NcApi.java | 28 ++ .../talk/api/helpers/api/ApiHelper.java | 11 + .../api/models/PushConfigurationState.java | 36 ++ .../models/json/push/PushRegistration.java | 44 +++ .../models/json/push/PushRegistrationOCS.java | 37 ++ .../json/push/PushRegistrationOverall.java | 36 ++ .../application/NextcloudTalkApplication.java | 9 + .../AccountVerificationController.java | 2 +- .../controllers/WebViewLoginController.java | 47 ++- .../talk/jobs/PushRegistrationJob.java | 41 +++ .../talk/jobs/creator/MagicJobCreator.java | 45 +++ .../talk/persistence/entities/User.java | 2 + .../MagicFirebaseInstanceIDService.java | 52 +++ .../MagicFirebaseMessagingService.java | 34 ++ .../com/nextcloud/talk/utils/PushUtils.java | 317 ++++++++++++++++++ .../talk/utils/database/user/UserUtils.java | 31 +- .../utils/preferences/AppPreferences.java | 9 + .../talk/utils/ssl/SSLSocketFactoryCompat.kt | 5 +- .../controller_account_verification.xml | 2 +- .../main/res/layout/controller_generic_rv.xml | 2 +- .../layout/controller_server_selection.xml | 2 +- app/src/main/res/values/setup.xml | 5 +- 24 files changed, 794 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/api/models/PushConfigurationState.java create mode 100644 app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistration.java create mode 100644 app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOCS.java create mode 100644 app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOverall.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationJob.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/creator/MagicJobCreator.java create mode 100644 app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseInstanceIDService.java create mode 100644 app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/PushUtils.java diff --git a/app/build.gradle b/app/build.gradle index a3b74dd9d..1556e3bff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,6 +55,7 @@ android { ext { supportLibraryVersion = '26.1.0' + googleLibraryVersion = '11.4.2' } dependencies { @@ -69,7 +70,7 @@ dependencies { implementation 'com.bluelinelabs:conductor:2.1.4' implementation 'com.bluelinelabs:conductor-support:2.1.4' - implementation 'com.squareup.okhttp3:okhttp:3.8.1' + implementation 'com.squareup.okhttp3:okhttp:3.9.0' implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.6.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.6.0' @@ -121,6 +122,13 @@ dependencies { implementation 'org.webrtc:google-webrtc:1.0.+' implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" + implementation 'com.evernote:android-job:1.2.0' + + implementation "com.google.firebase:firebase-messaging:${googleLibraryVersion}" + implementation "com.google.android.gms:play-services-base:${googleLibraryVersion}" + implementation "com.google.android.gms:play-services-gcm:${googleLibraryVersion}" + implementation "com.google.firebase:firebase-core:${googleLibraryVersion}" + testImplementation 'junit:junit:4.12' androidTestImplementation ('com.android.support.test.espresso:espresso-core:3.0.1', { exclude group: 'com.android.support', module: 'support-annotations' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9adb78439..d2c87f5f4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,5 +38,19 @@ + + + + + + + + + + + + diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index bad72474a..54e3537e6 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -24,6 +24,7 @@ import com.nextcloud.talk.api.models.json.call.CallOverall; import com.nextcloud.talk.api.models.json.generic.Status; import com.nextcloud.talk.api.models.json.participants.AddParticipantOverall; import com.nextcloud.talk.api.models.json.participants.ParticipantsOverall; +import com.nextcloud.talk.api.models.json.push.PushRegistrationOverall; import com.nextcloud.talk.api.models.json.rooms.RoomOverall; import com.nextcloud.talk.api.models.json.rooms.RoomsOverall; import com.nextcloud.talk.api.models.json.sharees.ShareesOverall; @@ -33,6 +34,8 @@ import java.util.Map; import io.reactivex.Observable; import retrofit2.http.DELETE; +import retrofit2.http.FieldMap; +import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.POST; @@ -176,4 +179,29 @@ public interface NcApi { */ @GET Observable getServerStatus(@Url String url); + + + /* + QueryMap items are as follows: + - "format" : "json" + - "pushTokenHash" : "" + - "devicePublicKey" : "" + - "proxyServer" : "" + + Server URL is: baseUrl + ocsApiVersion + "/apps/notifications/api/v2/push + */ + + @POST + Observable registerDeviceForNotificationsWithNextcloud(@Header("Authorization") + String authorization, + @Url String url, + @QueryMap Map options); + + @FormUrlEncoded + @POST + Observable registerDeviceForNotificationsWithProxy(@Header("Authorization") String authorization, + @Url String url, + @FieldMap Map fields); + } diff --git a/app/src/main/java/com/nextcloud/talk/api/helpers/api/ApiHelper.java b/app/src/main/java/com/nextcloud/talk/api/helpers/api/ApiHelper.java index 7637ce4ec..eb746f17e 100644 --- a/app/src/main/java/com/nextcloud/talk/api/helpers/api/ApiHelper.java +++ b/app/src/main/java/com/nextcloud/talk/api/helpers/api/ApiHelper.java @@ -22,7 +22,9 @@ package com.nextcloud.talk.api.helpers.api; import android.net.Uri; import com.nextcloud.talk.BuildConfig; +import com.nextcloud.talk.R; import com.nextcloud.talk.api.models.RetrofitBucket; +import com.nextcloud.talk.application.NextcloudTalkApplication; import java.util.HashMap; import java.util.Map; @@ -138,4 +140,13 @@ public class ApiHelper { public static String getCredentials(String username, String token) { return Credentials.basic(username, token); } + + public static String getUrlNextcloudPush(String baseUrl) { + return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/push"; + } + + public static String getUrlPushProxy(String baseUrl) { + return NextcloudTalkApplication.getSharedApplication(). + getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices"; + } } diff --git a/app/src/main/java/com/nextcloud/talk/api/models/PushConfigurationState.java b/app/src/main/java/com/nextcloud/talk/api/models/PushConfigurationState.java new file mode 100644 index 000000000..034dcbf16 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/api/models/PushConfigurationState.java @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.api.models; + +import org.parceler.Parcel; + +import lombok.Data; + +@Parcel +@Data +public class PushConfigurationState { + String pushToken; + String deviceIdentifier; + String deviceIdentifierSignature; + String userPublicKey; + boolean shouldBeDeleted; + boolean usesRegularPass; +} diff --git a/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistration.java b/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistration.java new file mode 100644 index 000000000..2e8547a86 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistration.java @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.api.models.json.push; + + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; + +import org.parceler.Parcel; + +import lombok.Data; + +@Data +@Parcel +@JsonObject +public class PushRegistration { + @JsonField(name = "publicKey") + String publicKey; + + @JsonField(name = "deviceIdentifier") + String deviceIdentifier; + + @JsonField(name = "signature") + String signature; +} + diff --git a/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOCS.java b/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOCS.java new file mode 100644 index 000000000..3387493d7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOCS.java @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.api.models.json.push; + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; +import com.nextcloud.talk.api.models.json.generic.GenericOCS; + +import org.parceler.Parcel; + +import lombok.Data; + +@Data +@Parcel +@JsonObject +public class PushRegistrationOCS extends GenericOCS { + @JsonField(name = "data") + PushRegistration data; +} diff --git a/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOverall.java b/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOverall.java new file mode 100644 index 000000000..7ebc4f1a0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOverall.java @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.api.models.json.push; + +import com.bluelinelabs.logansquare.annotation.JsonField; +import com.bluelinelabs.logansquare.annotation.JsonObject; + +import org.parceler.Parcel; + +import lombok.Data; + +@Data +@Parcel +@JsonObject +public class PushRegistrationOverall { + @JsonField(name = "ocs") + PushRegistrationOCS ocs; +} diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.java b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.java index b6b702879..8a0333ebe 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.java +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.java @@ -24,11 +24,15 @@ import android.content.Context; import android.support.multidex.MultiDex; import android.support.multidex.MultiDexApplication; +import com.evernote.android.job.JobManager; +import com.evernote.android.job.JobRequest; import com.nextcloud.talk.BuildConfig; import com.nextcloud.talk.dagger.modules.BusModule; import com.nextcloud.talk.dagger.modules.ContextModule; import com.nextcloud.talk.dagger.modules.DatabaseModule; import com.nextcloud.talk.dagger.modules.RestModule; +import com.nextcloud.talk.jobs.PushRegistrationJob; +import com.nextcloud.talk.jobs.creator.MagicJobCreator; import com.nextcloud.talk.utils.database.cache.CacheModule; import com.nextcloud.talk.utils.database.user.UserModule; import com.squareup.leakcanary.LeakCanary; @@ -76,6 +80,8 @@ public class NextcloudTalkApplication extends MultiDexApplication { @Override public void onCreate() { super.onCreate(); + JobManager.create(this).addJobCreator(new MagicJobCreator()); + sharedApplication = this; try { @@ -88,6 +94,9 @@ public class NextcloudTalkApplication extends MultiDexApplication { componentApplication.inject(this); refWatcher = LeakCanary.install(this); + + new JobRequest.Builder(PushRegistrationJob.TAG).setUpdateCurrent(true).startNow(); + } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java index d424a0794..db5da55c7 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java @@ -132,7 +132,7 @@ public class AccountVerificationController extends BaseController { if (!TextUtils.isEmpty(displayName)) { dbQueryDisposable = userUtils.createOrUpdateUser(username, token, - baseUrl, displayName) + baseUrl, displayName, null) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(userEntity -> { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java index 9694f6163..1337ea910 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java @@ -41,6 +41,7 @@ import com.nextcloud.talk.api.helpers.api.ApiHelper; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.controllers.base.BaseController; import com.nextcloud.talk.models.LoginData; +import com.nextcloud.talk.persistence.entities.UserEntity; import com.nextcloud.talk.utils.bundle.BundleBuilder; import com.nextcloud.talk.utils.bundle.BundleKeys; import com.nextcloud.talk.utils.database.user.UserUtils; @@ -134,6 +135,7 @@ public class WebViewLoginController extends BaseController { webView.setWebViewClient(new WebViewClient() { private boolean basePageLoaded; + @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith(assembledPrefix)) { @@ -160,7 +162,7 @@ public class WebViewLoginController extends BaseController { SslCertificate sslCertificate = error.getCertificate(); Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate"); f.setAccessible(true); - X509Certificate cert = (X509Certificate)f.get(sslCertificate); + X509Certificate cert = (X509Certificate) f.get(sslCertificate); if (cert == null) { handler.cancel(); @@ -201,25 +203,34 @@ public class WebViewLoginController extends BaseController { if (loginData != null) { dispose(); + UserEntity currentUser = userUtils.getCurrentUser(); + + String displayName = null; + String pushConfiguration = null; + + if (currentUser != null) { + displayName = currentUser.getDisplayName(); + pushConfiguration = currentUser.getPushConfigurationState(); + } // We use the URL user entered because one provided by the server is NOT reliable userQueryDisposable = userUtils.createOrUpdateUser(loginData.getUsername(), loginData.getToken(), - baseUrl, null).subscribe(userEntity -> { - if (!isPasswordUpdate) { - - BundleBuilder bundleBuilder = new BundleBuilder(new Bundle()); - bundleBuilder.putString(BundleKeys.KEY_USERNAME, userEntity.getUsername()); - bundleBuilder.putString(BundleKeys.KEY_TOKEN, userEntity.getToken()); - bundleBuilder.putString(BundleKeys.KEY_BASE_URL, userEntity.getBaseUrl()); - getRouter().pushController(RouterTransaction.with(new AccountVerificationController - (bundleBuilder.build())).pushChangeHandler(new HorizontalChangeHandler()) - .popChangeHandler(new HorizontalChangeHandler())); - } else { - getRouter().setRoot(RouterTransaction.with(new BottomNavigationController(R.menu.menu_navigation)) - .pushChangeHandler(new HorizontalChangeHandler()) - .popChangeHandler(new HorizontalChangeHandler())); - } - }, throwable -> dispose(), - this::dispose); + baseUrl, displayName, pushConfiguration). + subscribe(userEntity -> { + if (!isPasswordUpdate) { + BundleBuilder bundleBuilder = new BundleBuilder(new Bundle()); + bundleBuilder.putString(BundleKeys.KEY_USERNAME, userEntity.getUsername()); + bundleBuilder.putString(BundleKeys.KEY_TOKEN, userEntity.getToken()); + bundleBuilder.putString(BundleKeys.KEY_BASE_URL, userEntity.getBaseUrl()); + getRouter().pushController(RouterTransaction.with(new AccountVerificationController + (bundleBuilder.build())).pushChangeHandler(new HorizontalChangeHandler()) + .popChangeHandler(new HorizontalChangeHandler())); + } else { + getRouter().setRoot(RouterTransaction.with(new BottomNavigationController(R.menu.menu_navigation)) + .pushChangeHandler(new HorizontalChangeHandler()) + .popChangeHandler(new HorizontalChangeHandler())); + } + }, throwable -> dispose(), + this::dispose); } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationJob.java b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationJob.java new file mode 100644 index 000000000..d989226bb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationJob.java @@ -0,0 +1,41 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.jobs; + +import android.support.annotation.NonNull; + +import com.evernote.android.job.Job; +import com.nextcloud.talk.utils.PushUtils; + +public class PushRegistrationJob extends Job { + public static final String TAG = "PushRegistrationJob"; + + @NonNull + @Override + protected Result onRunJob(Params params) { + PushUtils pushUtils = new PushUtils(); + + pushUtils.generateRsa2048KeyPair(); + pushUtils.pushRegistrationToServer(); + + return Result.SUCCESS; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/creator/MagicJobCreator.java b/app/src/main/java/com/nextcloud/talk/jobs/creator/MagicJobCreator.java new file mode 100644 index 000000000..f2bb82c75 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/creator/MagicJobCreator.java @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.jobs.creator; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.evernote.android.job.Job; +import com.evernote.android.job.JobCreator; +import com.nextcloud.talk.jobs.PushRegistrationJob; + +public class MagicJobCreator implements JobCreator { + private static final String TAG = "MagicJobCreator"; + + @Nullable + @Override + public Job create(@NonNull String tag) { + switch (tag) { + case PushRegistrationJob.TAG: + break; + default: + return null; + } + + return null; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/persistence/entities/User.java b/app/src/main/java/com/nextcloud/talk/persistence/entities/User.java index 6a3967d1f..4e39e6c16 100644 --- a/app/src/main/java/com/nextcloud/talk/persistence/entities/User.java +++ b/app/src/main/java/com/nextcloud/talk/persistence/entities/User.java @@ -42,4 +42,6 @@ public interface User extends Parcelable, Persistable, Serializable { String getToken(); String getDisplayName(); + + String getPushConfigurationState(); } diff --git a/app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseInstanceIDService.java b/app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseInstanceIDService.java new file mode 100644 index 000000000..6298f5868 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseInstanceIDService.java @@ -0,0 +1,52 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.services.firebase; + +import com.evernote.android.job.JobRequest; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.FirebaseInstanceIdService; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.jobs.PushRegistrationJob; +import com.nextcloud.talk.utils.preferences.AppPreferences; + +import javax.inject.Inject; + +import autodagger.AutoInjector; + +@AutoInjector(NextcloudTalkApplication.class) +public class MagicFirebaseInstanceIDService extends FirebaseInstanceIdService { + private static final String TAG = "MagicFirebaseInstanceIDService"; + + @Inject + AppPreferences appPreferences; + + public MagicFirebaseInstanceIDService() { + super(); + NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this); + } + + @Override + public void onTokenRefresh() { + appPreferences.setPushtoken(FirebaseInstanceId.getInstance().getToken()); + + new JobRequest.Builder(PushRegistrationJob.TAG).setUpdateCurrent(true).startNow(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.java b/app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.java new file mode 100644 index 000000000..1b32c4b7a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.java @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.services.firebase; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +public class MagicFirebaseMessagingService extends FirebaseMessagingService { + private static final String TAG = "MagicFirebaseMessagingService"; + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + super.onMessageReceived(remoteMessage); + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java new file mode 100644 index 000000000..e67d6fb10 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java @@ -0,0 +1,317 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import com.bluelinelabs.logansquare.LoganSquare; +import com.nextcloud.talk.R; +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.api.helpers.api.ApiHelper; +import com.nextcloud.talk.api.models.PushConfigurationState; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.persistence.entities.UserEntity; +import com.nextcloud.talk.utils.database.user.UserUtils; +import com.nextcloud.talk.utils.preferences.AppPreferences; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +import autodagger.AutoInjector; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +@AutoInjector(NextcloudTalkApplication.class) +public class PushUtils { + private static final String TAG = "PushUtils"; + + @Inject + UserUtils userUtils; + + @Inject + AppPreferences appPreferences; + + @Inject + NcApi ncApi; + + private File keysFile; + private File publicKeyFile; + private File privateKeyFile; + + private String proxyServer; + + public PushUtils() { + NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this); + + keysFile = NextcloudTalkApplication.getSharedApplication().getDir("PushKeyStore", Context.MODE_PRIVATE); + publicKeyFile = new File(NextcloudTalkApplication.getSharedApplication().getDir("PushKeystore", + Context.MODE_PRIVATE), "push_key.pub"); + privateKeyFile = new File(NextcloudTalkApplication.getSharedApplication().getDir("PushKeystore", + Context.MODE_PRIVATE), "push_key.priv"); + proxyServer = NextcloudTalkApplication.getSharedApplication().getResources(). + getString(R.string.nc_push_server_url); + + } + + private static int saveKeyToFile(Key key, String path) { + byte[] encoded = key.getEncoded(); + FileOutputStream keyFileOutputStream = null; + try { + if (!new File(path).exists()) { + new File(path).createNewFile(); + } + keyFileOutputStream = new FileOutputStream(path); + keyFileOutputStream.write(encoded); + keyFileOutputStream.close(); + return 0; + } catch (FileNotFoundException e) { + Log.d(TAG, "Failed to save key to file"); + } catch (IOException e) { + Log.d(TAG, "Failed to save key to file via IOException"); + } + + return -1; + } + + public String generateSHA512Hash(String pushToken) { + MessageDigest messageDigest = null; + try { + messageDigest = MessageDigest.getInstance("SHA-512"); + messageDigest.update(pushToken.getBytes()); + return bytesToHex(messageDigest.digest()); + } catch (NoSuchAlgorithmException e) { + Log.d(TAG, "SHA-512 algorithm not supported"); + } + return ""; + } + + public String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte individualByte : bytes) { + result.append(Integer.toString((individualByte & 0xff) + 0x100, 16) + .substring(1)); + } + return result.toString(); + } + + private void deleteRegistrationForAccount(UserEntity userEntity) { + } + + public int generateRsa2048KeyPair() { + if (!publicKeyFile.exists() && !privateKeyFile.exists()) { + if (!keysFile.exists()) { + keysFile.mkdirs(); + } + + KeyPairGenerator keyGen = null; + try { + keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + + KeyPair pair = keyGen.generateKeyPair(); + int statusPrivate = saveKeyToFile(pair.getPrivate(), privateKeyFile.getAbsolutePath()); + int statusPublic = saveKeyToFile(pair.getPublic(), publicKeyFile.getAbsolutePath()); + + if (statusPrivate == 0 && statusPublic == 0) { + // all went well + return 0; + } else { + return -2; + } + + } catch (NoSuchAlgorithmException e) { + Log.d(TAG, "RSA algorithm not supported"); + } + } else { + // We already have the key + return -1; + } + + // we failed to generate the key + return -2; + } + + public void pushRegistrationToServer() { + String token = appPreferences.getPushToken(); + + if (!TextUtils.isEmpty(token)) { + String pushTokenHash = generateSHA512Hash(token).toLowerCase(); + PublicKey devicePublicKey = (PublicKey) readKeyFromFile(true); + if (devicePublicKey != null) { + byte[] publicKeyBytes = Base64.encode(devicePublicKey.getEncoded(), Base64.NO_WRAP); + String publicKey = new String(publicKeyBytes); + publicKey = publicKey.replaceAll("(.{64})", "$1\n"); + + publicKey = "-----BEGIN PUBLIC KEY-----\n" + publicKey + "\n-----END PUBLIC KEY-----\n"; + + if (userUtils.anyUserExists()) { + String providerValue; + PushConfigurationState accountPushData = null; + for (UserEntity userEntity : userUtils.getUsers()) { + providerValue = userEntity.getPushConfigurationState(); + if (!TextUtils.isEmpty(providerValue)) { + try { + accountPushData = LoganSquare.parse(providerValue, PushConfigurationState.class); + } catch (IOException e) { + Log.d(TAG, "Failed to parse account push data"); + accountPushData = null; + } + } else { + accountPushData = null; + } + + if (accountPushData != null && !accountPushData.getPushToken().equals(token) && + !accountPushData.isShouldBeDeleted() || + TextUtils.isEmpty(providerValue)) { + + + Map queryMap = new HashMap<>(); + queryMap.put("format", "json"); + queryMap.put("pushTokenHash", pushTokenHash); + queryMap.put("devicePublicKey", publicKey); + queryMap.put("proxyServer", proxyServer); + + ncApi.registerDeviceForNotificationsWithNextcloud( + ApiHelper.getCredentials(userEntity.getUsername(), userEntity.getToken()), + ApiHelper.getUrlNextcloudPush(userEntity.getBaseUrl()), queryMap) + .subscribeOn(Schedulers.newThread()) + .subscribe(pushRegistrationOverall -> { + Map proxyMap = new HashMap<>(); + proxyMap.put("pushToken", token); + proxyMap.put("deviceIdentifier", pushRegistrationOverall.getOcs().getData(). + getDeviceIdentifier()); + proxyMap.put("deviceIdentifierSignature", pushRegistrationOverall.getOcs() + .getData().getSignature()); + proxyMap.put("userPublicKey", pushRegistrationOverall.getOcs() + .getData().getPublicKey()); + + + ncApi.registerDeviceForNotificationsWithProxy(ApiHelper.getCredentials + (userEntity.getUsername(), userEntity.getToken()), + ApiHelper.getUrlPushProxy(userEntity.getBaseUrl()), proxyMap) + .subscribeOn(Schedulers.newThread()) + .subscribe(new Consumer() { + @Override + public void accept(Void aVoid) throws Exception { + + PushConfigurationState pushConfigurationState = + new PushConfigurationState(); + pushConfigurationState.setPushToken(token); + pushConfigurationState.setDeviceIdentifier( + pushRegistrationOverall.getOcs() + .getData().getDeviceIdentifier()); + pushConfigurationState.setDeviceIdentifierSignature( + pushRegistrationOverall + .getOcs().getData().getSignature()); + pushConfigurationState.setUserPublicKey( + pushRegistrationOverall.getOcs() + .getData().getPublicKey()); + pushConfigurationState.setShouldBeDeleted(false); + pushConfigurationState.setUsesRegularPass(false); + + userUtils.createOrUpdateUser(userEntity.getUsername(), + userEntity.getToken(), userEntity.getBaseUrl(), + userEntity.getDisplayName(), + LoganSquare.serialize(pushConfigurationState)); + + } + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + // something went wrong + } + }); + + + }, new Consumer() { + @Override + public void accept(Throwable throwable) throws Exception { + // TODO: If 400, we're using regular token + } + }); + + } + } + } + } + } + } + + private Key readKeyFromFile(boolean readPublicKey) { + String path; + + if (readPublicKey) { + path = publicKeyFile.getAbsolutePath(); + } else { + path = privateKeyFile.getAbsolutePath(); + } + + FileInputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(path); + byte[] bytes = new byte[fileInputStream.available()]; + fileInputStream.read(bytes); + fileInputStream.close(); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + if (readPublicKey) { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes); + return keyFactory.generatePublic(keySpec); + } else { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); + return keyFactory.generatePrivate(keySpec); + } + + } catch (FileNotFoundException e) { + Log.d(TAG, "Failed to find path while reading the Key"); + } catch (IOException e) { + Log.d(TAG, "IOException while reading the key"); + } catch (InvalidKeySpecException e) { + Log.d(TAG, "InvalidKeySpecException while reading the key"); + } catch (NoSuchAlgorithmException e) { + Log.d(TAG, "RSA algorithm not supported"); + } + + return null; + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java index a485c798a..bbdc8fefc 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java @@ -22,10 +22,15 @@ package com.nextcloud.talk.utils.database.user; import android.support.annotation.Nullable; import android.text.TextUtils; +import android.util.Log; +import com.bluelinelabs.logansquare.LoganSquare; import com.nextcloud.talk.persistence.entities.User; import com.nextcloud.talk.persistence.entities.UserEntity; +import java.io.IOException; +import java.util.List; + import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -35,6 +40,7 @@ import io.requery.query.Result; import io.requery.reactivex.ReactiveEntityStore; public class UserUtils { + private static final String TAG = "UserUtils"; private ReactiveEntityStore dataStore; UserUtils(ReactiveEntityStore dataStore) { @@ -46,6 +52,12 @@ public class UserUtils { return (dataStore.count(User.class).limit(1).get().value() > 0); } + public List getUsers() { + Result findUsersQueryResult = dataStore.select(User.class).get(); + + return findUsersQueryResult.toList(); + } + // temporary method while we only support 1 user public UserEntity getCurrentUser() { Result findUserQueryResult = dataStore.select(User.class).limit(1).get(); @@ -66,7 +78,8 @@ public class UserUtils { } public Observable createOrUpdateUser(String username, String token, String serverUrl, - @Nullable String displayName) { + @Nullable String displayName, + @Nullable String pushConfigurationState) { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.USERNAME.eq(username). and(UserEntity.BASE_URL.eq(serverUrl.toLowerCase()))).limit(1).get(); @@ -82,6 +95,14 @@ public class UserUtils { user.setDisplayName(displayName); } + if (pushConfigurationState != null) { + try { + user.setPushConfigurationState(LoganSquare.serialize(pushConfigurationState)); + } catch (IOException e) { + Log.d(TAG, "Failed to serialize push configuration state"); + } + } + } else { if (!token.equals(user.getToken())) { user.setToken(token); @@ -90,6 +111,14 @@ public class UserUtils { if (!TextUtils.isEmpty(displayName) && !displayName.equals(user.getDisplayName())) { user.setDisplayName(displayName); } + + if (pushConfigurationState != null && !pushConfigurationState.equals(user.getPushConfigurationState())) { + try { + user.setPushConfigurationState(LoganSquare.serialize(pushConfigurationState)); + } catch (IOException e) { + Log.d(TAG, "Failed to serialize push configuration state"); + } + } } return dataStore.upsert(user) diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index 2d8476a80..114adc9b7 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -45,6 +45,15 @@ public interface AppPreferences { @RemoveMethod void removeProxyServer(); + @KeyByString("push_token") + String getPushToken(); + + @KeyByString("push_token") + void setPushtoken(String pushToken); + + @KeyByString("push_token") + void removePushToken(); + @ClearMethod void clear(); } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt index 282bba40d..4e91374ae 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt @@ -19,7 +19,7 @@ import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager -class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory() { +class SSLSocketFactoryCompat(trustManager: X509TrustManager) : SSLSocketFactory() { private var delegate: SSLSocketFactory @@ -29,6 +29,7 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory() // https://developer.android.com/reference/javax/net/ssl/SSLSocket.html var protocols: Array? = null var cipherSuites: Array? = null + init { if (Build.VERSION.SDK_INT >= 23) { // Since Android 6.0 (API level 23), @@ -87,7 +88,7 @@ class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory() cipherSuites = _cipherSuites.toTypedArray() } - } catch(e: IOException) { + } catch (e: IOException) { } finally { socket?.close() // doesn't implement Closeable on all supported Android versions } diff --git a/app/src/main/res/layout/controller_account_verification.xml b/app/src/main/res/layout/controller_account_verification.xml index cde4fc930..850ff4949 100644 --- a/app/src/main/res/layout/controller_account_verification.xml +++ b/app/src/main/res/layout/controller_account_verification.xml @@ -23,7 +23,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/background_color"> + android:background="@color/nc_background_color"> + android:background="@color/nc_background_color"> + android:background="@color/nc_background_color"> Nextcloud Talk Nextcloud - @color/per70white + @color/per70white + + https://push-notifications.nextcloud.com + \ No newline at end of file