Browse Source

Significant work on push notifications

Signed-off-by: Mario Danic <mario@lovelyhq.com>
Mario Danic 7 years ago
parent
commit
0a6f54cedf
24 changed files with 794 additions and 27 deletions
  1. 9 1
      app/build.gradle
  2. 14 0
      app/src/main/AndroidManifest.xml
  3. 28 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  4. 11 0
      app/src/main/java/com/nextcloud/talk/api/helpers/api/ApiHelper.java
  5. 36 0
      app/src/main/java/com/nextcloud/talk/api/models/PushConfigurationState.java
  6. 44 0
      app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistration.java
  7. 37 0
      app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOCS.java
  8. 36 0
      app/src/main/java/com/nextcloud/talk/api/models/json/push/PushRegistrationOverall.java
  9. 9 0
      app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.java
  10. 1 1
      app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java
  11. 29 18
      app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java
  12. 41 0
      app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationJob.java
  13. 45 0
      app/src/main/java/com/nextcloud/talk/jobs/creator/MagicJobCreator.java
  14. 2 0
      app/src/main/java/com/nextcloud/talk/persistence/entities/User.java
  15. 52 0
      app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseInstanceIDService.java
  16. 34 0
      app/src/main/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.java
  17. 317 0
      app/src/main/java/com/nextcloud/talk/utils/PushUtils.java
  18. 30 1
      app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java
  19. 9 0
      app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
  20. 3 2
      app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt
  21. 1 1
      app/src/main/res/layout/controller_account_verification.xml
  22. 1 1
      app/src/main/res/layout/controller_generic_rv.xml
  23. 1 1
      app/src/main/res/layout/controller_server_selection.xml
  24. 4 1
      app/src/main/res/values/setup.xml

+ 9 - 1
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'

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

@@ -38,5 +38,19 @@
         </activity>
 
 
+        <service
+            android:name=".services.firebase.MagicFirebaseMessagingService">
+            <intent-filter>
+                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
+            </intent-filter>
+        </service>
+
+        <service
+            android:name=".services.firebase.MagicFirebaseInstanceIDService">
+            <intent-filter>
+                <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
+            </intent-filter>
+        </service>
+
     </application>
 </manifest>

+ 28 - 0
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<Status> 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<PushRegistrationOverall> registerDeviceForNotificationsWithNextcloud(@Header("Authorization")
+                                                                                            String authorization,
+                                                                                    @Url String url,
+                                                                                    @QueryMap Map<String,
+                                                                                            String> options);
+
+    @FormUrlEncoded
+    @POST
+    Observable<Void> registerDeviceForNotificationsWithProxy(@Header("Authorization") String authorization,
+                                                             @Url String url,
+                                                             @FieldMap Map<String, String> fields);
+
 }

+ 11 - 0
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";
+    }
 }

+ 36 - 0
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 <mario@lovelyhq.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.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;
+}

+ 44 - 0
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 <mario@lovelyhq.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.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;
+}
+

+ 37 - 0
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 <mario@lovelyhq.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.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;
+}

+ 36 - 0
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 <mario@lovelyhq.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.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;
+}

+ 9 - 0
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();
+
     }
 
 

+ 1 - 1
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 -> {

+ 29 - 18
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);
         }
     }
 

+ 41 - 0
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 <mario@lovelyhq.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.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;
+    }
+}

+ 45 - 0
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 <mario@lovelyhq.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.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;
+    }
+}

+ 2 - 0
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();
 }

+ 52 - 0
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 <mario@lovelyhq.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.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();
+    }
+}

+ 34 - 0
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 <mario@lovelyhq.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.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);
+    }
+
+}

+ 317 - 0
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 <mario@lovelyhq.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.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<String, String> 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<String, String> 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<Void>() {
+                                                    @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<Throwable>() {
+                                                    @Override
+                                                    public void accept(Throwable throwable) throws Exception {
+                                                        // something went wrong
+                                                    }
+                                                });
+
+
+                                    }, new Consumer<Throwable>() {
+                                        @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;
+    }
+
+}

+ 30 - 1
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<Persistable> dataStore;
 
     UserUtils(ReactiveEntityStore<Persistable> dataStore) {
@@ -46,6 +52,12 @@ public class UserUtils {
         return (dataStore.count(User.class).limit(1).get().value() > 0);
     }
 
+    public List<UserEntity> 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<UserEntity> 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)

+ 9 - 0
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();
 }

+ 3 - 2
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<String>? = null
         var cipherSuites: Array<String>? = 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
                 }

+ 1 - 1
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">
 
     <ProgressBar
         android:id="@+id/progress_bar"

+ 1 - 1
app/src/main/res/layout/controller_generic_rv.xml

@@ -24,7 +24,7 @@
                                               android:id="@+id/swipe_refresh_layout"
                                               android:layout_width="match_parent"
                                               android:layout_height="match_parent"
-                                              android:background="@color/background_color">
+                                              android:background="@color/nc_background_color">
 
     <android.support.v7.widget.RecyclerView
         android:id="@+id/recycler_view"

+ 1 - 1
app/src/main/res/layout/controller_server_selection.xml

@@ -23,7 +23,7 @@
                 xmlns:app="http://schemas.android.com/apk/res-auto"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
-                android:background="@color/background_color">
+                android:background="@color/nc_background_color">
 
     <studio.carbonylgroup.textfieldboxes.TextFieldBoxes
         android:id="@+id/text_field_boxes"

+ 4 - 1
app/src/main/res/values/setup.xml

@@ -7,5 +7,8 @@
     <string name="nc_app_name">Nextcloud Talk</string>
     <string name="nc_server_product_name">Nextcloud</string>
 
-    <color name="background_color">@color/per70white</color>
+    <color name="nc_background_color">@color/per70white</color>
+
+    <string name="nc_push_server_url">https://push-notifications.nextcloud.com</string>
+
 </resources>